mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-02-04 04:50:20 +00:00
Compare commits
30 Commits
dependabot
...
release-87
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a5039e006 | ||
|
|
78dcc1fd4c | ||
|
|
aabfe88ad1 | ||
|
|
535455fd5c | ||
|
|
fe72123415 | ||
|
|
54be267b59 | ||
|
|
27e0f31207 | ||
|
|
206eb1d2d4 | ||
|
|
5f57185c3f | ||
|
|
4dede8d81e | ||
|
|
a4531495ad | ||
|
|
5f5fa5a2f5 | ||
|
|
0db9f1b7b4 | ||
|
|
c1257e70e3 | ||
|
|
858361b344 | ||
|
|
9e79494487 | ||
|
|
d1764ea4a0 | ||
|
|
e2ce8f803b | ||
|
|
127cfeb28d | ||
|
|
6fab1a3346 | ||
|
|
53c1f3791c | ||
|
|
f9084661e4 | ||
|
|
513dfb0fca | ||
|
|
3109d0a3f9 | ||
|
|
111ff5d10b | ||
|
|
40ce690b4b | ||
|
|
91eeadf25e | ||
|
|
1ccd5dc57c | ||
|
|
90c4e052d9 | ||
|
|
c6851807a1 |
@@ -50,7 +50,8 @@ import {
|
||||
commonUserJoinedHandling,
|
||||
commonUserLeftHandling,
|
||||
getConferenceOptions,
|
||||
sendLocalParticipant
|
||||
sendLocalParticipant,
|
||||
updateTrackMuteState
|
||||
} from './react/features/base/conference/functions';
|
||||
import { getReplaceParticipant, getSsrcRewritingFeatureFlag } from './react/features/base/config/functions';
|
||||
import { connect } from './react/features/base/connection/actions.web';
|
||||
@@ -1663,8 +1664,12 @@ export default {
|
||||
room.on(
|
||||
JitsiConferenceEvents.START_MUTED_POLICY_CHANGED,
|
||||
({ audio, video }) => {
|
||||
APP.store.dispatch(
|
||||
onStartMutedPolicyChanged(audio, video));
|
||||
APP.store.dispatch(onStartMutedPolicyChanged(audio, video));
|
||||
|
||||
const state = APP.store.getState();
|
||||
|
||||
updateTrackMuteState(state, APP.store.dispatch, true);
|
||||
updateTrackMuteState(state, APP.store.dispatch, false);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -375,6 +375,7 @@
|
||||
"micTimeoutError": "Could not start audio source. Timeout occurred!",
|
||||
"micUnknownError": "Cannot use microphone for an unknown reason.",
|
||||
"moderationAudioLabel": "Allow attendees to unmute themselves",
|
||||
"moderationDesktopLabel": "Allow non-moderators to share their screen",
|
||||
"moderationVideoLabel": "Allow non-moderators to start their video",
|
||||
"muteEveryoneDialog": "The participants can unmute themselves at any time.",
|
||||
"muteEveryoneDialogModerationOn": "The participants can send a request to speak at any time.",
|
||||
@@ -387,6 +388,9 @@
|
||||
"muteEveryoneSelf": "yourself",
|
||||
"muteEveryoneStartMuted": "Everyone starts muted from now on",
|
||||
"muteEveryoneTitle": "Mute everyone?",
|
||||
"muteEveryonesDesktopDialog": "The participants can share their screen at any time.",
|
||||
"muteEveryonesDesktopDialogModerationOn": "The participants can send a request to share their screen at any time.",
|
||||
"muteEveryonesDesktopTitle": "Stop everyone's screen share?",
|
||||
"muteEveryonesVideoDialog": "The participants can turn on their video at any time.",
|
||||
"muteEveryonesVideoDialogModerationOn": "The participants can send a request to turn on their video at any time.",
|
||||
"muteEveryonesVideoDialogOk": "Disable",
|
||||
|
||||
9
package-lock.json
generated
9
package-lock.json
generated
@@ -64,7 +64,7 @@
|
||||
"js-md5": "0.6.1",
|
||||
"js-sha512": "0.8.0",
|
||||
"jwt-decode": "2.2.0",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2030.0.0+b225c920/lib-jitsi-meet.tgz",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet#release-8717",
|
||||
"lodash-es": "4.17.21",
|
||||
"null-loader": "4.0.1",
|
||||
"optional-require": "1.0.3",
|
||||
@@ -18090,8 +18090,7 @@
|
||||
},
|
||||
"node_modules/lib-jitsi-meet": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2030.0.0+b225c920/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-UjQElh+1XA7sk9Q8fADLoAuyE3/CDE0Oc0jbqFjeSzhu9LLbbARZEyP9FoeYQ1miTNf7hI3/QVGRkpBNCdqGKA==",
|
||||
"resolved": "git+ssh://git@github.com/jitsi/lib-jitsi-meet.git#ff978008229b898c4aa61e68199eeb9f3cd7cefb",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@jitsi/js-utils": "2.4.6",
|
||||
@@ -39348,8 +39347,8 @@
|
||||
}
|
||||
},
|
||||
"lib-jitsi-meet": {
|
||||
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2030.0.0+b225c920/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-UjQElh+1XA7sk9Q8fADLoAuyE3/CDE0Oc0jbqFjeSzhu9LLbbARZEyP9FoeYQ1miTNf7hI3/QVGRkpBNCdqGKA==",
|
||||
"version": "git+ssh://git@github.com/jitsi/lib-jitsi-meet.git#ff978008229b898c4aa61e68199eeb9f3cd7cefb",
|
||||
"from": "lib-jitsi-meet@https://github.com/jitsi/lib-jitsi-meet#release-8717",
|
||||
"requires": {
|
||||
"@jitsi/js-utils": "2.4.6",
|
||||
"@jitsi/logger": "2.0.2",
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
"js-md5": "0.6.1",
|
||||
"js-sha512": "0.8.0",
|
||||
"jwt-decode": "2.2.0",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2030.0.0+b225c920/lib-jitsi-meet.tgz",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet#release-8717",
|
||||
"lodash-es": "4.17.21",
|
||||
"null-loader": "4.0.1",
|
||||
"optional-require": "1.0.3",
|
||||
|
||||
@@ -35,3 +35,12 @@ export function fixDeviceID(amplitude: Types.BrowserClient) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the amplitude shared deviceId.
|
||||
*
|
||||
* @returns {string} - The amplitude deviceId.
|
||||
*/
|
||||
export function getDeviceID() {
|
||||
return jitsiLocalStorage.getItem(DEVICE_ID_KEY);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { IStateful } from '../base/app/types';
|
||||
import { toState } from '../base/redux/functions';
|
||||
import { getServerURL } from '../base/settings/functions.web';
|
||||
import { getJitsiMeetGlobalNS } from '../base/util/helpers';
|
||||
|
||||
export * from './functions.any';
|
||||
import logger from './logger';
|
||||
|
||||
/**
|
||||
* Retrieves the default URL for the app. This can either come from a prop to
|
||||
@@ -31,3 +33,27 @@ export function getDefaultURL(stateful: IStateful) {
|
||||
export function getName() {
|
||||
return interfaceConfig.APP_NAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a handler function after the window load event has been received.
|
||||
* If the app has already loaded, the handler is executed immediately.
|
||||
* Otherwise, the handler is registered as a 'load' event listener.
|
||||
*
|
||||
* @param {Function} handler - The callback function to execute.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function executeAfterLoad(handler: () => void) {
|
||||
const safeHandler = () => {
|
||||
try {
|
||||
handler();
|
||||
} catch (error) {
|
||||
logger.error('Error executing handler after load:', error);
|
||||
}
|
||||
};
|
||||
|
||||
if (getJitsiMeetGlobalNS()?.hasLoaded) {
|
||||
safeHandler();
|
||||
} else {
|
||||
window.addEventListener('load', safeHandler);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ import '../calendar-sync/middleware';
|
||||
import '../chat/middleware';
|
||||
import '../conference/middleware';
|
||||
import '../connection-indicator/middleware';
|
||||
import '../deep-linking/middleware';
|
||||
import '../device-selection/middleware';
|
||||
import '../display-name/middleware';
|
||||
import '../dynamic-branding/middleware';
|
||||
|
||||
@@ -2,6 +2,7 @@ import '../base/app/middleware';
|
||||
import '../base/connection/middleware';
|
||||
import '../base/devices/middleware';
|
||||
import '../base/media/middleware';
|
||||
import '../deep-linking/middleware.web';
|
||||
import '../dynamic-branding/middleware';
|
||||
import '../e2ee/middleware';
|
||||
import '../external-api/middleware';
|
||||
|
||||
@@ -83,7 +83,8 @@ import {
|
||||
getConferenceState,
|
||||
getCurrentConference,
|
||||
getVisitorOptions,
|
||||
sendLocalParticipant
|
||||
sendLocalParticipant,
|
||||
updateTrackMuteState
|
||||
} from './functions';
|
||||
import logger from './logger';
|
||||
import { IConferenceMetadata, IJitsiConference } from './reducer';
|
||||
@@ -186,6 +187,15 @@ function _addConferenceListeners(conference: IJitsiConference, dispatch: IStore[
|
||||
(disableVideoMuteChange: boolean) => {
|
||||
dispatch(setVideoUnmutePermissions(disableVideoMuteChange));
|
||||
});
|
||||
conference.on(
|
||||
JitsiConferenceEvents.START_MUTED_POLICY_CHANGED,
|
||||
({ audio, video }: { audio: boolean; video: boolean; }) => {
|
||||
dispatch(onStartMutedPolicyChanged(audio, video));
|
||||
|
||||
updateTrackMuteState(state, dispatch, true);
|
||||
updateTrackMuteState(state, dispatch, false);
|
||||
}
|
||||
);
|
||||
|
||||
// Dispatches into features/base/tracks follow:
|
||||
|
||||
@@ -1013,6 +1023,8 @@ export function setStartMutedPolicy(
|
||||
audio: startAudioMuted,
|
||||
video: startVideoMuted
|
||||
});
|
||||
|
||||
dispatch(onStartMutedPolicyChanged(startAudioMuted, startVideoMuted));
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -40,3 +40,8 @@ export const CONFERENCE_LEAVE_REASONS = {
|
||||
SWITCH_ROOM: 'switch_room',
|
||||
UNRECOVERABLE_ERROR: 'unrecoverable_error'
|
||||
};
|
||||
|
||||
/**
|
||||
* The ID of the notification that is shown when the user is muted by focus.
|
||||
*/
|
||||
export const START_MUTED_NOTIFICATION_ID = 'start-muted';
|
||||
|
||||
@@ -3,9 +3,13 @@ import { upperFirst, words } from 'lodash-es';
|
||||
|
||||
import { getName } from '../../app/functions';
|
||||
import { IReduxState, IStore } from '../../app/types';
|
||||
import { showNotification } from '../../notifications/actions';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE } from '../../notifications/constants';
|
||||
import { determineTranscriptionLanguage } from '../../transcribing/functions';
|
||||
import { IStateful } from '../app/types';
|
||||
import { JitsiTrackErrors } from '../lib-jitsi-meet';
|
||||
import { setAudioMuted, setVideoMuted } from '../media/actions';
|
||||
import { VIDEO_MUTISM_AUTHORITY } from '../media/constants';
|
||||
import {
|
||||
participantJoined,
|
||||
participantLeft
|
||||
@@ -22,7 +26,8 @@ import { setObfuscatedRoom } from './actions';
|
||||
import {
|
||||
AVATAR_URL_COMMAND,
|
||||
EMAIL_COMMAND,
|
||||
JITSI_CONFERENCE_URL_KEY
|
||||
JITSI_CONFERENCE_URL_KEY,
|
||||
START_MUTED_NOTIFICATION_ID
|
||||
} from './constants';
|
||||
import logger from './logger';
|
||||
import { IJitsiConference } from './reducer';
|
||||
@@ -574,3 +579,42 @@ function safeStartCase(s = '') {
|
||||
(result, word, index) => result + (index ? ' ' : '') + upperFirst(word)
|
||||
, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the mute state of the track based on the start muted policy.
|
||||
*
|
||||
* @param {Object|Function} stateful - Either the whole Redux state object or the Redux store's {@code getState} method.
|
||||
* @param {Function} dispatch - Redux dispatch function.
|
||||
* @param {boolean} isAudio - Whether the track is audio or video.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function updateTrackMuteState(stateful: IStateful, dispatch: IStore['dispatch'], isAudio: boolean) {
|
||||
const state = toState(stateful);
|
||||
const mutedPolicyKey = isAudio ? 'startAudioMutedPolicy' : 'startVideoMutedPolicy';
|
||||
const mutedPolicyValue = state['features/base/conference'][mutedPolicyKey];
|
||||
|
||||
// Currently, the policy only supports force muting others, not unmuting them.
|
||||
if (!mutedPolicyValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
let muteStateUpdated = false;
|
||||
const { muted } = isAudio ? state['features/base/media'].audio : state['features/base/media'].video;
|
||||
|
||||
if (isAudio && !Boolean(muted)) {
|
||||
dispatch(setAudioMuted(mutedPolicyValue, true));
|
||||
muteStateUpdated = true;
|
||||
} else if (!isAudio && !Boolean(muted)) {
|
||||
// TODO: Add a new authority for video mutism for the moderator case.
|
||||
dispatch(setVideoMuted(mutedPolicyValue, VIDEO_MUTISM_AUTHORITY.USER, true));
|
||||
muteStateUpdated = true;
|
||||
}
|
||||
|
||||
if (muteStateUpdated) {
|
||||
dispatch(showNotification({
|
||||
titleKey: 'notify.mutedTitle',
|
||||
descriptionKey: 'notify.muted',
|
||||
uid: START_MUTED_NOTIFICATION_ID // use the same id, to make sure we show one notification
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.SHORT));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +72,6 @@ import {
|
||||
} from './functions';
|
||||
import logger from './logger';
|
||||
import { IConferenceMetadata } from './reducer';
|
||||
import './subscriber';
|
||||
|
||||
/**
|
||||
* Handler for before unload event.
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import { IStore } from '../../app/types';
|
||||
import { showNotification } from '../../notifications/actions';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE } from '../../notifications/constants';
|
||||
import { setAudioMuted, setVideoMuted } from '../media/actions';
|
||||
import { VIDEO_MUTISM_AUTHORITY } from '../media/constants';
|
||||
import StateListenerRegistry from '../redux/StateListenerRegistry';
|
||||
|
||||
let hasShownNotification = false;
|
||||
|
||||
/**
|
||||
* Handles changes in the start muted policy for audio and video tracks in the meta data set for the conference.
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
/* selector */ state => state['features/base/conference'].startAudioMutedPolicy,
|
||||
/* listener */ (startAudioMutedPolicy, store) => {
|
||||
_updateTrackMuteState(store, true);
|
||||
});
|
||||
|
||||
StateListenerRegistry.register(
|
||||
/* selector */ state => state['features/base/conference'].startVideoMutedPolicy,
|
||||
/* listener */(startVideoMutedPolicy, store) => {
|
||||
_updateTrackMuteState(store, false);
|
||||
});
|
||||
|
||||
/**
|
||||
* Updates the mute state of the track based on the start muted policy.
|
||||
*
|
||||
* @param {IStore} store - The redux store.
|
||||
* @param {boolean} isAudio - Whether the track is audio or video.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _updateTrackMuteState(store: IStore, isAudio: boolean) {
|
||||
const { dispatch, getState } = store;
|
||||
const mutedPolicyKey = isAudio ? 'startAudioMutedPolicy' : 'startVideoMutedPolicy';
|
||||
const mutedPolicyValue = getState()['features/base/conference'][mutedPolicyKey];
|
||||
|
||||
// Currently, the policy only supports force muting others, not unmuting them.
|
||||
if (!mutedPolicyValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
let muteStateUpdated = false;
|
||||
const { muted } = isAudio ? getState()['features/base/media'].audio : getState()['features/base/media'].video;
|
||||
|
||||
if (isAudio && !Boolean(muted)) {
|
||||
dispatch(setAudioMuted(mutedPolicyValue, true));
|
||||
muteStateUpdated = true;
|
||||
} else if (!isAudio && !Boolean(muted)) {
|
||||
// TODO: Add a new authority for video mutism for the moderator case.
|
||||
dispatch(setVideoMuted(mutedPolicyValue, VIDEO_MUTISM_AUTHORITY.USER, true));
|
||||
muteStateUpdated = true;
|
||||
}
|
||||
|
||||
if (!hasShownNotification && muteStateUpdated) {
|
||||
hasShownNotification = true;
|
||||
dispatch(showNotification({
|
||||
titleKey: 'notify.mutedTitle',
|
||||
descriptionKey: 'notify.muted'
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.SHORT));
|
||||
}
|
||||
}
|
||||
@@ -553,7 +553,7 @@ export interface IConfig {
|
||||
disableDemote?: boolean;
|
||||
disableGrantModerator?: boolean;
|
||||
disableKick?: boolean;
|
||||
disablePrivateChat?: 'all' | 'allow-moderator-chat';
|
||||
disablePrivateChat?: 'all' | 'allow-moderator-chat' | 'disable-visitor-chat';
|
||||
disabled?: boolean;
|
||||
};
|
||||
replaceParticipant?: string;
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
import { getGravatarURL } from '@jitsi/js-utils/avatar';
|
||||
|
||||
import { IReduxState, IStore } from '../../app/types';
|
||||
import { isVisitorChatParticipant } from '../../chat/functions';
|
||||
import { isStageFilmstripAvailable } from '../../filmstrip/functions';
|
||||
import { isAddPeopleEnabled, isDialOutEnabled } from '../../invite/functions';
|
||||
import { toggleShareDialog } from '../../share-room/actions';
|
||||
import { iAmVisitor } from '../../visitors/functions';
|
||||
import { IVisitorChatParticipant } from '../../visitors/types';
|
||||
import { IStateful } from '../app/types';
|
||||
import { GRAVATAR_BASE_URL } from '../avatar/constants';
|
||||
import { isCORSAvatarURL } from '../avatar/functions';
|
||||
@@ -827,18 +829,28 @@ export const setShareDialogVisiblity = (addPeopleFeatureEnabled: boolean, dispat
|
||||
/**
|
||||
* Checks if private chat is enabled for the given participant.
|
||||
*
|
||||
* @param {IParticipant|undefined} participant - The participant to check.
|
||||
* @param {IParticipant|IVisitorChatParticipant|undefined} participant - The participant to check.
|
||||
* @param {IReduxState} state - The Redux state.
|
||||
* @returns {boolean} - True if private chat is enabled, false otherwise.
|
||||
*/
|
||||
export function isPrivateChatEnabled(participant: IParticipant | undefined, state: IReduxState) {
|
||||
export function isPrivateChatEnabled(participant: IParticipant | IVisitorChatParticipant | undefined, state: IReduxState) {
|
||||
const { remoteVideoMenu = {} } = state['features/base/config'];
|
||||
const { disablePrivateChat } = remoteVideoMenu;
|
||||
|
||||
if (participant?.local || state['features/visitors'].iAmVisitor || disablePrivateChat === 'all') {
|
||||
if ((!isVisitorChatParticipant(participant) && participant?.local) || disablePrivateChat === 'all') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (disablePrivateChat === 'disable-visitor-chat') {
|
||||
// Block if the participant we're trying to message is a visitor
|
||||
// OR if the local user is a visitor
|
||||
if (isVisitorChatParticipant(participant) || iAmVisitor(state)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true; // should allow private chat for other participants
|
||||
}
|
||||
|
||||
if (disablePrivateChat === 'allow-moderator-chat') {
|
||||
return isLocalParticipantModerator(state) || isParticipantModerator(participant);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { WithTranslation } from 'react-i18next';
|
||||
import { IReduxState, IStore } from '../../app/types';
|
||||
import { getParticipantById } from '../../base/participants/functions';
|
||||
import { IParticipant } from '../../base/participants/types';
|
||||
import { IVisitorChatParticipant } from '../../visitors/types';
|
||||
import { sendMessage, setPrivateMessageRecipient } from '../actions';
|
||||
|
||||
interface IProps extends WithTranslation {
|
||||
@@ -23,6 +24,16 @@ interface IProps extends WithTranslation {
|
||||
*/
|
||||
_participant?: IParticipant;
|
||||
|
||||
/**
|
||||
* The display name of the visitor (if applicable).
|
||||
*/
|
||||
displayName?: string;
|
||||
|
||||
/**
|
||||
* Whether the message is from a visitor.
|
||||
*/
|
||||
isFromVisitor?: boolean;
|
||||
|
||||
/**
|
||||
* The message that is about to be sent.
|
||||
*/
|
||||
@@ -67,9 +78,21 @@ export class AbstractChatPrivacyDialog extends PureComponent<IProps> {
|
||||
* @returns {void}
|
||||
*/
|
||||
_onSendPrivateMessage() {
|
||||
const { message, _onSendMessage, _onSetMessageRecipient, _participant } = this.props;
|
||||
const { message, _onSendMessage, _onSetMessageRecipient, _participant, isFromVisitor, displayName, participantID } = this.props;
|
||||
|
||||
if (isFromVisitor) {
|
||||
// For visitors, create a participant object since they don't exist in the main participant list
|
||||
const visitorParticipant = {
|
||||
id: participantID,
|
||||
name: displayName,
|
||||
isVisitor: true
|
||||
};
|
||||
|
||||
_onSetMessageRecipient(visitorParticipant);
|
||||
} else {
|
||||
_onSetMessageRecipient(_participant);
|
||||
}
|
||||
|
||||
_onSetMessageRecipient(_participant);
|
||||
_onSendMessage(message);
|
||||
|
||||
return true;
|
||||
@@ -88,7 +111,7 @@ export function _mapDispatchToProps(dispatch: IStore['dispatch']) {
|
||||
dispatch(sendMessage(message, true));
|
||||
},
|
||||
|
||||
_onSetMessageRecipient: (participant: IParticipant) => {
|
||||
_onSetMessageRecipient: (participant: IParticipant | IVisitorChatParticipant) => {
|
||||
dispatch(setPrivateMessageRecipient(participant));
|
||||
}
|
||||
};
|
||||
@@ -103,6 +126,6 @@ export function _mapDispatchToProps(dispatch: IStore['dispatch']) {
|
||||
*/
|
||||
export function _mapStateToProps(state: IReduxState, ownProps: IProps) {
|
||||
return {
|
||||
_participant: getParticipantById(state, ownProps.participantID)
|
||||
_participant: ownProps.isFromVisitor ? undefined : getParticipantById(state, ownProps.participantID)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@ import { WithTranslation } from 'react-i18next';
|
||||
|
||||
import { IReduxState, IStore } from '../../app/types';
|
||||
import { getParticipantDisplayName, isLocalParticipantModerator } from '../../base/participants/functions';
|
||||
import { getVisitorDisplayName } from '../../visitors/functions';
|
||||
import { setLobbyChatActiveState, setPrivateMessageRecipient } from '../actions.any';
|
||||
import { isVisitorChatParticipant } from '../functions';
|
||||
|
||||
|
||||
export interface IProps extends WithTranslation {
|
||||
@@ -13,6 +15,11 @@ export interface IProps extends WithTranslation {
|
||||
*/
|
||||
_isLobbyChatActive: boolean;
|
||||
|
||||
/**
|
||||
* Whether the private message recipient is a visitor.
|
||||
*/
|
||||
_isVisitor?: boolean;
|
||||
|
||||
/**
|
||||
* The name of the lobby message recipient, if any.
|
||||
*/
|
||||
@@ -72,10 +79,18 @@ export function _mapDispatchToProps(dispatch: IStore['dispatch']) {
|
||||
*/
|
||||
export function _mapStateToProps(state: IReduxState, _ownProps: any) {
|
||||
const { privateMessageRecipient, lobbyMessageRecipient, isLobbyChatActive } = state['features/chat'];
|
||||
let _privateMessageRecipient;
|
||||
const _isVisitor = isVisitorChatParticipant(privateMessageRecipient);
|
||||
|
||||
if (privateMessageRecipient) {
|
||||
_privateMessageRecipient = _isVisitor
|
||||
? getVisitorDisplayName(state, privateMessageRecipient.name)
|
||||
: getParticipantDisplayName(state, privateMessageRecipient.id);
|
||||
}
|
||||
|
||||
return {
|
||||
_privateMessageRecipient:
|
||||
privateMessageRecipient ? getParticipantDisplayName(state, privateMessageRecipient.id) : undefined,
|
||||
_privateMessageRecipient,
|
||||
_isVisitor,
|
||||
_isLobbyChatActive: isLobbyChatActive,
|
||||
_lobbyMessageRecipient:
|
||||
isLobbyChatActive && lobbyMessageRecipient ? lobbyMessageRecipient.name : undefined,
|
||||
|
||||
@@ -122,15 +122,17 @@ class ChatMessage extends Component<IChatMessageProps> {
|
||||
* @returns {React.ReactElement<*> | null}
|
||||
*/
|
||||
_renderDisplayName() {
|
||||
const { message, showDisplayName } = this.props;
|
||||
const { message, showDisplayName, t } = this.props;
|
||||
|
||||
if (!showDisplayName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { displayName, isFromVisitor } = message;
|
||||
|
||||
return (
|
||||
<Text style = { styles.senderDisplayName }>
|
||||
{ message.displayName }
|
||||
{ `${displayName}${isFromVisitor ? ` ${t('visitors.chatIndicator')}` : ''}` }
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,8 @@ import {
|
||||
} from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
|
||||
import { setLobbyChatActiveState, setPrivateMessageRecipient } from '../../actions.any';
|
||||
import AbstractMessageRecipient, {
|
||||
IProps as AbstractProps
|
||||
IProps as AbstractProps,
|
||||
_mapStateToProps as _mapStateToPropsAbstract
|
||||
} from '../AbstractMessageRecipient';
|
||||
|
||||
import styles from './styles';
|
||||
@@ -36,11 +37,6 @@ interface IProps extends AbstractProps {
|
||||
id: string;
|
||||
name: string;
|
||||
} | ILocalParticipant;
|
||||
|
||||
/**
|
||||
* The participant object set for private messaging.
|
||||
*/
|
||||
privateMessageRecipient: { name: string; };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -96,7 +92,8 @@ class MessageRecipient extends AbstractMessageRecipient<IProps> {
|
||||
const {
|
||||
isLobbyChatActive,
|
||||
lobbyMessageRecipient,
|
||||
privateMessageRecipient,
|
||||
_privateMessageRecipient,
|
||||
_isVisitor,
|
||||
t
|
||||
} = this.props;
|
||||
|
||||
@@ -120,7 +117,7 @@ class MessageRecipient extends AbstractMessageRecipient<IProps> {
|
||||
);
|
||||
}
|
||||
|
||||
if (!privateMessageRecipient) {
|
||||
if (!_privateMessageRecipient) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -130,7 +127,7 @@ class MessageRecipient extends AbstractMessageRecipient<IProps> {
|
||||
style = { styles.messageRecipientContainer as ViewStyle }>
|
||||
<Text style = { styles.messageRecipientText }>
|
||||
{ t('chat.messageTo', {
|
||||
recipient: privateMessageRecipient.name
|
||||
recipient: `${_privateMessageRecipient}${_isVisitor ? ` ${t('visitors.chatIndicator')}` : ''}`
|
||||
}) }
|
||||
</Text>
|
||||
<TouchableHighlight
|
||||
@@ -157,6 +154,7 @@ function _mapStateToProps(state: IReduxState, _ownProps: any) {
|
||||
const { lobbyMessageRecipient, isLobbyChatActive } = state['features/chat'];
|
||||
|
||||
return {
|
||||
..._mapStateToPropsAbstract(state, _ownProps),
|
||||
isLobbyChatActive,
|
||||
lobbyMessageRecipient
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ import { getParticipantById, getParticipantDisplayName, isPrivateChatEnabled } f
|
||||
import Popover from '../../../base/popover/components/Popover.web';
|
||||
import Message from '../../../base/react/components/web/Message';
|
||||
import { withPixelLineHeight } from '../../../base/styles/functions.web';
|
||||
import { MESSAGE_TYPE_LOCAL } from '../../constants';
|
||||
import { getFormattedTimestamp, getMessageText, getPrivateNoticeMessage } from '../../functions';
|
||||
import { IChatMessageProps } from '../../types';
|
||||
|
||||
@@ -16,9 +17,10 @@ import MessageMenu from './MessageMenu';
|
||||
import ReactButton from './ReactButton';
|
||||
|
||||
interface IProps extends IChatMessageProps {
|
||||
shouldDisplayChatMessageMenu: boolean;
|
||||
className?: string;
|
||||
enablePrivateChat?: boolean;
|
||||
shouldDisplayMenuOnRight?: boolean;
|
||||
state?: IReduxState;
|
||||
type: string;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()((theme: Theme) => {
|
||||
@@ -190,11 +192,12 @@ const useStyles = makeStyles()((theme: Theme) => {
|
||||
});
|
||||
|
||||
const ChatMessage = ({
|
||||
className = '',
|
||||
message,
|
||||
state,
|
||||
showDisplayName,
|
||||
type,
|
||||
shouldDisplayChatMessageMenu,
|
||||
shouldDisplayMenuOnRight,
|
||||
enablePrivateChat,
|
||||
knocking,
|
||||
t
|
||||
}: IProps) => {
|
||||
@@ -224,11 +227,13 @@ const ChatMessage = ({
|
||||
* @returns {React$Element<*>}
|
||||
*/
|
||||
function _renderDisplayName() {
|
||||
const { displayName, isFromVisitor = false } = message;
|
||||
|
||||
return (
|
||||
<div
|
||||
aria-hidden = { true }
|
||||
className = { cx('display-name', classes.displayName) }>
|
||||
{message.displayName}
|
||||
{`${displayName}${isFromVisitor ? ` ${t('visitors.chatIndicator')}` : ''}`}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -329,26 +334,28 @@ const ChatMessage = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className = { cx(classes.chatMessageWrapper, type) }
|
||||
className = { cx(classes.chatMessageWrapper, className) }
|
||||
id = { message.messageId }
|
||||
onMouseEnter = { handleMouseEnter }
|
||||
onMouseLeave = { handleMouseLeave }
|
||||
tabIndex = { -1 }>
|
||||
<div className = { classes.sideBySideContainer }>
|
||||
{!shouldDisplayChatMessageMenu && (
|
||||
{!shouldDisplayMenuOnRight && (
|
||||
<div className = { classes.optionsButtonContainer }>
|
||||
{isHovered && <MessageMenu
|
||||
displayName = { message.displayName }
|
||||
enablePrivateChat = { Boolean(enablePrivateChat) }
|
||||
isFromVisitor = { message.isFromVisitor }
|
||||
isLobbyMessage = { message.lobbyChat }
|
||||
message = { message.message }
|
||||
participantId = { message.participantId }
|
||||
shouldDisplayChatMessageMenu = { shouldDisplayChatMessageMenu } />}
|
||||
participantId = { message.participantId } />}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className = { cx(
|
||||
'chatmessage',
|
||||
classes.chatMessage,
|
||||
type,
|
||||
className,
|
||||
message.privateMessage && 'privatemessage',
|
||||
message.lobbyChat && !knocking && 'lobbymessage'
|
||||
) }>
|
||||
@@ -379,7 +386,7 @@ const ChatMessage = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{shouldDisplayChatMessageMenu && (
|
||||
{shouldDisplayMenuOnRight && (
|
||||
<div className = { classes.sideBySideContainer }>
|
||||
{!message.privateMessage && !message.lobbyChat && <div>
|
||||
<div className = { classes.optionsButtonContainer }>
|
||||
@@ -391,10 +398,12 @@ const ChatMessage = ({
|
||||
<div>
|
||||
<div className = { classes.optionsButtonContainer }>
|
||||
{isHovered && <MessageMenu
|
||||
displayName = { message.displayName }
|
||||
enablePrivateChat = { Boolean(enablePrivateChat) }
|
||||
isFromVisitor = { message.isFromVisitor }
|
||||
isLobbyMessage = { message.lobbyChat }
|
||||
message = { message.message }
|
||||
participantId = { message.participantId }
|
||||
shouldDisplayChatMessageMenu = { shouldDisplayChatMessageMenu } />}
|
||||
participantId = { message.participantId } />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -414,10 +423,23 @@ function _mapStateToProps(state: IReduxState, { message }: IProps) {
|
||||
const { knocking } = state['features/lobby'];
|
||||
|
||||
const participant = getParticipantById(state, message.participantId);
|
||||
const enablePrivateChat = isPrivateChatEnabled(participant, state);
|
||||
|
||||
// For visitor private messages, participant will be undefined but we should still allow private chat
|
||||
// Create a visitor participant object for visitor messages to pass to isPrivateChatEnabled
|
||||
const participantForCheck = message.isFromVisitor
|
||||
? { id: message.participantId, name: message.displayName, isVisitor: true as const }
|
||||
: participant;
|
||||
|
||||
const enablePrivateChat = (!message.isFromVisitor || message.privateMessage)
|
||||
&& isPrivateChatEnabled(participantForCheck, state);
|
||||
|
||||
// Only the local messages appear on the right side of the chat therefore only for them the menu has to be on the
|
||||
// left side.
|
||||
const shouldDisplayMenuOnRight = message.messageType !== MESSAGE_TYPE_LOCAL;
|
||||
|
||||
return {
|
||||
shouldDisplayChatMessageMenu: enablePrivateChat,
|
||||
shouldDisplayMenuOnRight,
|
||||
enablePrivateChat,
|
||||
knocking,
|
||||
state
|
||||
};
|
||||
|
||||
@@ -71,12 +71,11 @@ const ChatMessageGroup = ({ className = '', messages }: IProps) => {
|
||||
<div className = { `${classes.messageGroup} chat-message-group ${className}` }>
|
||||
{messages.map((message, i) => (
|
||||
<ChatMessage
|
||||
className = { className }
|
||||
key = { i }
|
||||
message = { message }
|
||||
shouldDisplayChatMessageMenu = { false }
|
||||
showDisplayName = { i === 0 }
|
||||
showTimestamp = { i === messages.length - 1 }
|
||||
type = { className } />
|
||||
showTimestamp = { i === messages.length - 1 } />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,10 +15,12 @@ import { handleLobbyChatInitialized, openChat } from '../../actions.web';
|
||||
|
||||
export interface IProps {
|
||||
className?: string;
|
||||
displayName?: string;
|
||||
enablePrivateChat: boolean;
|
||||
isFromVisitor?: boolean;
|
||||
isLobbyMessage: boolean;
|
||||
message: string;
|
||||
participantId: string;
|
||||
shouldDisplayChatMessageMenu: boolean;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
@@ -58,7 +60,7 @@ const useStyles = makeStyles()(theme => {
|
||||
};
|
||||
});
|
||||
|
||||
const MessageMenu = ({ message, participantId, isLobbyMessage, shouldDisplayChatMessageMenu }: IProps) => {
|
||||
const MessageMenu = ({ message, participantId, isFromVisitor, isLobbyMessage, enablePrivateChat, displayName }: IProps) => {
|
||||
const dispatch = useDispatch();
|
||||
const { classes, cx } = useStyles();
|
||||
const { t } = useTranslation();
|
||||
@@ -82,10 +84,23 @@ const MessageMenu = ({ message, participantId, isLobbyMessage, shouldDisplayChat
|
||||
if (isLobbyMessage) {
|
||||
dispatch(handleLobbyChatInitialized(participantId));
|
||||
} else {
|
||||
dispatch(openChat(participant));
|
||||
// For visitor messages, participant will be undefined but we can still open chat
|
||||
// using the participantId which contains the visitor's original JID
|
||||
if (isFromVisitor) {
|
||||
// Handle visitor participant that doesn't exist in main participant list
|
||||
const visitorParticipant = {
|
||||
id: participantId,
|
||||
name: displayName,
|
||||
isVisitor: true
|
||||
};
|
||||
|
||||
dispatch(openChat(visitorParticipant));
|
||||
} else {
|
||||
dispatch(openChat(participant));
|
||||
}
|
||||
}
|
||||
handleClose();
|
||||
}, [ dispatch, isLobbyMessage, participant, participantId ]);
|
||||
}, [ dispatch, isLobbyMessage, participant, participantId, displayName ]);
|
||||
|
||||
const handleCopyClick = useCallback(() => {
|
||||
copyText(message)
|
||||
@@ -115,7 +130,7 @@ const MessageMenu = ({ message, participantId, isLobbyMessage, shouldDisplayChat
|
||||
|
||||
const popoverContent = (
|
||||
<div className = { classes.menuPanel }>
|
||||
{shouldDisplayChatMessageMenu && (
|
||||
{enablePrivateChat && (
|
||||
<div
|
||||
className = { classes.menuItem }
|
||||
onClick = { handlePrivateClick }>
|
||||
|
||||
@@ -48,6 +48,7 @@ const useStyles = makeStyles()(theme => {
|
||||
const MessageRecipient = ({
|
||||
_privateMessageRecipient,
|
||||
_isLobbyChatActive,
|
||||
_isVisitor,
|
||||
_lobbyMessageRecipient,
|
||||
_onRemovePrivateMessageRecipient,
|
||||
_onHideLobbyChatRecipient,
|
||||
@@ -80,9 +81,9 @@ const MessageRecipient = ({
|
||||
id = 'chat-recipient'
|
||||
role = 'alert'>
|
||||
<span className = { classes.text }>
|
||||
{t(_isLobbyChatActive ? 'chat.lobbyChatMessageTo' : 'chat.messageTo', {
|
||||
recipient: _isLobbyChatActive ? _lobbyMessageRecipient : _privateMessageRecipient
|
||||
})}
|
||||
{ _isLobbyChatActive
|
||||
? t('chat.lobbyChatMessageTo', { recipient: _lobbyMessageRecipient })
|
||||
: t('chat.messageTo', { recipient: `${_privateMessageRecipient}${_isVisitor ? ` ${t('visitors.chatIndicator')}` : ''}` }) }
|
||||
</span>
|
||||
<Button
|
||||
accessibilityLabel = { t('dialog.close') }
|
||||
|
||||
@@ -9,10 +9,12 @@ import { getLocalizedDateFormatter } from '../base/i18n/dateUtil';
|
||||
import i18next from '../base/i18n/i18next';
|
||||
import { MEET_FEATURES } from '../base/jwt/constants';
|
||||
import { isJwtFeatureEnabled } from '../base/jwt/functions';
|
||||
import { getParticipantById } from '../base/participants/functions';
|
||||
import { getParticipantById, isPrivateChatEnabled } from '../base/participants/functions';
|
||||
import { IParticipant } from '../base/participants/types';
|
||||
import { escapeRegexp } from '../base/util/helpers';
|
||||
import { getParticipantsPaneWidth } from '../participants-pane/functions';
|
||||
import { VIDEO_SPACE_MIN_SIZE } from '../video-layout/constants';
|
||||
import { IVisitorChatParticipant } from '../visitors/types';
|
||||
|
||||
import { MESSAGE_TYPE_ERROR, MESSAGE_TYPE_LOCAL, TIMESTAMP_FORMAT } from './constants';
|
||||
import { IMessage } from './types';
|
||||
@@ -178,9 +180,24 @@ export function getCanReplyToMessage(state: IReduxState, message: IMessage) {
|
||||
const { knocking } = state['features/lobby'];
|
||||
const participant = getParticipantById(state, message.participantId);
|
||||
|
||||
return Boolean(participant)
|
||||
// Check if basic reply conditions are met
|
||||
const basicCanReply = (Boolean(participant) || message.isFromVisitor)
|
||||
&& (message.privateMessage || (message.lobbyChat && !knocking))
|
||||
&& message.messageType !== MESSAGE_TYPE_LOCAL;
|
||||
|
||||
if (!basicCanReply) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check private chat configuration for visitor messages
|
||||
if (message.isFromVisitor) {
|
||||
const visitorParticipant = { id: message.participantId, name: message.displayName, isVisitor: true as const };
|
||||
|
||||
return isPrivateChatEnabled(visitorParticipant, state);
|
||||
}
|
||||
|
||||
// For non-visitor messages, use the regular participant
|
||||
return isPrivateChatEnabled(participant, state);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -190,8 +207,19 @@ export function getCanReplyToMessage(state: IReduxState, message: IMessage) {
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getPrivateNoticeMessage(message: IMessage) {
|
||||
let recipient;
|
||||
|
||||
if (message.messageType === MESSAGE_TYPE_LOCAL) {
|
||||
// For messages sent by local user, show the recipient name
|
||||
// For visitor messages, use the visitor's display name with indicator
|
||||
recipient = message.sentToVisitor ? `${message.recipient} ${i18next.t('visitors.chatIndicator')}` : message.recipient;
|
||||
} else {
|
||||
// For messages received from others, show "you"
|
||||
recipient = i18next.t('chat.you');
|
||||
}
|
||||
|
||||
return i18next.t('chat.privateNotice', {
|
||||
recipient: message.messageType === MESSAGE_TYPE_LOCAL ? message.recipient : i18next.t('chat.you')
|
||||
recipient
|
||||
});
|
||||
}
|
||||
|
||||
@@ -225,3 +253,15 @@ export function getChatMaxSize(state: IReduxState) {
|
||||
|
||||
return Math.max(clientWidth - getParticipantsPaneWidth(state) - VIDEO_SPACE_MIN_SIZE, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if a participant is a visitor chat participant.
|
||||
*
|
||||
* @param {IParticipant | IVisitorChatParticipant | undefined} participant - The participant to check.
|
||||
* @returns {boolean} - True if the participant is a visitor chat participant.
|
||||
*/
|
||||
export function isVisitorChatParticipant(
|
||||
participant?: IParticipant | IVisitorChatParticipant
|
||||
): participant is IVisitorChatParticipant {
|
||||
return Boolean(participant && 'isVisitor' in participant && participant.isVisitor === true);
|
||||
}
|
||||
|
||||
@@ -33,8 +33,7 @@ import { pushReactions } from '../reactions/actions.any';
|
||||
import { ENDPOINT_REACTION_NAME } from '../reactions/constants';
|
||||
import { getReactionMessageFromBuffer, isReactionsEnabled } from '../reactions/functions.any';
|
||||
import { showToolbox } from '../toolbox/actions';
|
||||
|
||||
import './subscriber';
|
||||
import { getVisitorDisplayName } from '../visitors/functions';
|
||||
|
||||
import {
|
||||
ADD_MESSAGE,
|
||||
@@ -55,8 +54,9 @@ import {
|
||||
MESSAGE_TYPE_REMOTE,
|
||||
MESSAGE_TYPE_SYSTEM
|
||||
} from './constants';
|
||||
import { getUnreadCount, isSendGroupChatDisabled } from './functions';
|
||||
import { getUnreadCount, isSendGroupChatDisabled, isVisitorChatParticipant } from './functions';
|
||||
import { INCOMING_MSG_SOUND_FILE } from './sounds';
|
||||
import './subscriber';
|
||||
|
||||
/**
|
||||
* Timeout for when to show the privacy notice after a private message was received.
|
||||
@@ -186,6 +186,9 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
|
||||
if (participant) {
|
||||
action.participant = participant;
|
||||
} else if (isVisitorChatParticipant(privateMessageRecipient)) {
|
||||
// Handle visitor participants that don't exist in the main participant list
|
||||
action.participant = privateMessageRecipient;
|
||||
}
|
||||
}
|
||||
} else if (focusedTab === ChatTabs.POLLS) {
|
||||
@@ -204,14 +207,17 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
// recipient. This logic tries to mitigate this risk.
|
||||
const shouldSendPrivateMessageTo = _shouldSendPrivateMessageTo(state, action);
|
||||
|
||||
const participantExists = shouldSendPrivateMessageTo
|
||||
&& getParticipantById(state, shouldSendPrivateMessageTo);
|
||||
if (shouldSendPrivateMessageTo) {
|
||||
const participantExists = getParticipantById(state, shouldSendPrivateMessageTo.id);
|
||||
|
||||
if (shouldSendPrivateMessageTo && participantExists) {
|
||||
dispatch(openDialog(ChatPrivacyDialog, {
|
||||
message: action.message,
|
||||
participantID: shouldSendPrivateMessageTo
|
||||
}));
|
||||
if (participantExists || shouldSendPrivateMessageTo.isFromVisitor) {
|
||||
dispatch(openDialog(ChatPrivacyDialog, {
|
||||
message: action.message,
|
||||
participantID: shouldSendPrivateMessageTo.id,
|
||||
isFromVisitor: shouldSendPrivateMessageTo.isFromVisitor,
|
||||
displayName: shouldSendPrivateMessageTo.name
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
// Sending the message if privacy notice doesn't need to be shown.
|
||||
|
||||
@@ -227,10 +233,10 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
type: LOBBY_CHAT_MESSAGE,
|
||||
message: action.message
|
||||
}, lobbyMessageRecipient.id);
|
||||
_persistSentPrivateMessage(store, lobbyMessageRecipient.id, action.message, true);
|
||||
_persistSentPrivateMessage(store, lobbyMessageRecipient, action.message, true);
|
||||
} else if (privateMessageRecipient) {
|
||||
conference.sendPrivateTextMessage(privateMessageRecipient.id, action.message);
|
||||
_persistSentPrivateMessage(store, privateMessageRecipient.id, action.message);
|
||||
conference.sendPrivateTextMessage(privateMessageRecipient.id, action.message, 'body', isVisitorChatParticipant(privateMessageRecipient));
|
||||
_persistSentPrivateMessage(store, privateMessageRecipient, action.message);
|
||||
} else {
|
||||
conference.sendTextMessage(action.message);
|
||||
}
|
||||
@@ -317,7 +323,7 @@ function _addChatMsgListener(conference: IJitsiConference, store: IStore) {
|
||||
JitsiConferenceEvents.MESSAGE_RECEIVED,
|
||||
/* eslint-disable max-params */
|
||||
(participantId: string, message: string, timestamp: number,
|
||||
displayName: string, isGuest: boolean, messageId: string) => {
|
||||
displayName: string, isFromVisitor: boolean, messageId: string) => {
|
||||
/* eslint-enable max-params */
|
||||
_onConferenceMessageReceived(store, {
|
||||
// in case of messages coming from visitors we can have unknown id
|
||||
@@ -325,7 +331,7 @@ function _addChatMsgListener(conference: IJitsiConference, store: IStore) {
|
||||
message,
|
||||
timestamp,
|
||||
displayName,
|
||||
isGuest,
|
||||
isFromVisitor,
|
||||
messageId,
|
||||
privateMessage: false });
|
||||
|
||||
@@ -350,13 +356,15 @@ function _addChatMsgListener(conference: IJitsiConference, store: IStore) {
|
||||
|
||||
conference.on(
|
||||
JitsiConferenceEvents.PRIVATE_MESSAGE_RECEIVED,
|
||||
(participantId: string, message: string, timestamp: number, messageId: string) => {
|
||||
(participantId: string, message: string, timestamp: number, messageId: string, displayName?: string, isFromVisitor?: boolean) => {
|
||||
_onConferenceMessageReceived(store, {
|
||||
participantId,
|
||||
message,
|
||||
timestamp,
|
||||
displayName,
|
||||
messageId,
|
||||
privateMessage: true
|
||||
privateMessage: true,
|
||||
isFromVisitor
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -375,8 +383,8 @@ function _addChatMsgListener(conference: IJitsiConference, store: IStore) {
|
||||
* @returns {void}
|
||||
*/
|
||||
function _onConferenceMessageReceived(store: IStore,
|
||||
{ displayName, isGuest, message, messageId, participantId, privateMessage, timestamp }: {
|
||||
displayName?: string; isGuest?: boolean; message: string; messageId?: string;
|
||||
{ displayName, isFromVisitor, message, messageId, participantId, privateMessage, timestamp }: {
|
||||
displayName?: string; isFromVisitor?: boolean; message: string; messageId?: string;
|
||||
participantId: string; privateMessage: boolean; timestamp: number; }
|
||||
) {
|
||||
|
||||
@@ -390,7 +398,7 @@ function _onConferenceMessageReceived(store: IStore,
|
||||
}
|
||||
_handleReceivedMessage(store, {
|
||||
displayName,
|
||||
isGuest,
|
||||
isFromVisitor,
|
||||
participantId,
|
||||
message,
|
||||
privateMessage,
|
||||
@@ -505,8 +513,8 @@ function getLobbyChatDisplayName(state: IReduxState, participantId: string) {
|
||||
* @returns {void}
|
||||
*/
|
||||
function _handleReceivedMessage({ dispatch, getState }: IStore,
|
||||
{ displayName, isGuest, lobbyChat, message, messageId, participantId, privateMessage, timestamp }: {
|
||||
displayName?: string; isGuest?: boolean; lobbyChat: boolean; message: string;
|
||||
{ displayName, isFromVisitor, lobbyChat, message, messageId, participantId, privateMessage, timestamp }: {
|
||||
displayName?: string; isFromVisitor?: boolean; lobbyChat: boolean; message: string;
|
||||
messageId?: string; participantId: string; privateMessage: boolean; timestamp: number; },
|
||||
shouldPlaySound = true,
|
||||
isReaction = false
|
||||
@@ -525,9 +533,17 @@ function _handleReceivedMessage({ dispatch, getState }: IStore,
|
||||
const participant = getParticipantById(state, participantId) || { local: undefined };
|
||||
|
||||
const localParticipant = getLocalParticipant(getState);
|
||||
let displayNameToShow = lobbyChat
|
||||
? getLobbyChatDisplayName(state, participantId)
|
||||
: displayName || getParticipantDisplayName(state, participantId);
|
||||
let _displayName, displayNameToShow;
|
||||
|
||||
if (lobbyChat) {
|
||||
displayNameToShow = _displayName = getLobbyChatDisplayName(state, participantId);
|
||||
} else if (isFromVisitor) {
|
||||
_displayName = getVisitorDisplayName(state, displayName);
|
||||
displayNameToShow = `${_displayName} ${i18next.t('visitors.chatIndicator')}`;
|
||||
} else {
|
||||
displayNameToShow = _displayName = getParticipantDisplayName(state, participantId);
|
||||
}
|
||||
|
||||
const hasRead = participant.local || isChatOpen;
|
||||
const timestampToDate = timestamp ? new Date(timestamp) : new Date();
|
||||
const millisecondsTimestamp = timestampToDate.getTime();
|
||||
@@ -536,12 +552,8 @@ function _handleReceivedMessage({ dispatch, getState }: IStore,
|
||||
const shouldShowNotification = userSelectedNotifications?.['notify.chatMessages']
|
||||
&& !hasRead && !isReaction && (!timestamp || lobbyChat);
|
||||
|
||||
if (isGuest) {
|
||||
displayNameToShow = `${displayNameToShow} ${i18next.t('visitors.chatIndicator')}`;
|
||||
}
|
||||
|
||||
dispatch(addMessage({
|
||||
displayName: displayNameToShow,
|
||||
displayName: _displayName,
|
||||
hasRead,
|
||||
participantId,
|
||||
messageType: participant.local ? MESSAGE_TYPE_LOCAL : MESSAGE_TYPE_REMOTE,
|
||||
@@ -551,7 +563,8 @@ function _handleReceivedMessage({ dispatch, getState }: IStore,
|
||||
recipient: getParticipantDisplayName(state, localParticipant?.id ?? ''),
|
||||
timestamp: millisecondsTimestamp,
|
||||
messageId,
|
||||
isReaction
|
||||
isReaction,
|
||||
isFromVisitor
|
||||
}));
|
||||
|
||||
if (shouldShowNotification) {
|
||||
@@ -574,6 +587,15 @@ function _handleReceivedMessage({ dispatch, getState }: IStore,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for recipient objects used in private messaging.
|
||||
*/
|
||||
interface IRecipient {
|
||||
id: string;
|
||||
isVisitor?: boolean;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists the sent private messages as if they were received over the muc.
|
||||
*
|
||||
@@ -582,12 +604,12 @@ function _handleReceivedMessage({ dispatch, getState }: IStore,
|
||||
* But those messages should be in the store as well, otherwise they don't appear in the chat window.
|
||||
*
|
||||
* @param {Store} store - The Redux store.
|
||||
* @param {string} recipientID - The ID of the recipient the private message was sent to.
|
||||
* @param {IRecipient} recipient - The recipient the private message was sent to.
|
||||
* @param {string} message - The sent message.
|
||||
* @param {boolean} isLobbyPrivateMessage - Is a lobby message.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _persistSentPrivateMessage({ dispatch, getState }: IStore, recipientID: string,
|
||||
function _persistSentPrivateMessage({ dispatch, getState }: IStore, recipient: IRecipient,
|
||||
message: string, isLobbyPrivateMessage = false) {
|
||||
const state = getState();
|
||||
const localParticipant = getLocalParticipant(state);
|
||||
@@ -598,6 +620,13 @@ function _persistSentPrivateMessage({ dispatch, getState }: IStore, recipientID:
|
||||
const displayName = getParticipantDisplayName(state, localParticipant.id);
|
||||
const { lobbyMessageRecipient } = state['features/chat'];
|
||||
|
||||
const recipientName
|
||||
= recipient.isVisitor
|
||||
? getVisitorDisplayName(state, recipient.name)
|
||||
: (isLobbyPrivateMessage
|
||||
? lobbyMessageRecipient?.name
|
||||
: getParticipantDisplayName(getState, recipient?.id));
|
||||
|
||||
dispatch(addMessage({
|
||||
displayName,
|
||||
hasRead: true,
|
||||
@@ -606,20 +635,19 @@ function _persistSentPrivateMessage({ dispatch, getState }: IStore, recipientID:
|
||||
message,
|
||||
privateMessage: !isLobbyPrivateMessage,
|
||||
lobbyChat: isLobbyPrivateMessage,
|
||||
recipient: isLobbyPrivateMessage
|
||||
? lobbyMessageRecipient?.name
|
||||
: getParticipantDisplayName(getState, recipientID),
|
||||
recipient: recipientName,
|
||||
sentToVisitor: recipient.isVisitor,
|
||||
timestamp: Date.now()
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ID of the participant who we may have wanted to send the message
|
||||
* Returns the participant info for who we may have wanted to send the message
|
||||
* that we're about to send.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @param {Object} action - The action being dispatched now.
|
||||
* @returns {string?}
|
||||
* @returns {IRecipient?} - The recipient info or undefined if no notice should be shown.
|
||||
*/
|
||||
function _shouldSendPrivateMessageTo(state: IReduxState, action: AnyAction) {
|
||||
if (action.ignorePrivacy) {
|
||||
@@ -651,7 +679,11 @@ function _shouldSendPrivateMessageTo(state: IReduxState, action: AnyAction) {
|
||||
|
||||
if (lastMessage.privateMessage) {
|
||||
// We show the notice if the last received message was private.
|
||||
return lastMessage.participantId;
|
||||
return {
|
||||
id: lastMessage.participantId,
|
||||
isFromVisitor: Boolean(lastMessage.isFromVisitor),
|
||||
name: lastMessage.displayName
|
||||
};
|
||||
}
|
||||
|
||||
// But messages may come rapidly, we want to protect our users from mis-sending a message
|
||||
@@ -666,7 +698,11 @@ function _shouldSendPrivateMessageTo(state: IReduxState, action: AnyAction) {
|
||||
? recentPrivateMessages[0] : recentPrivateMessages[recentPrivateMessages.length - 1];
|
||||
|
||||
if (recentPrivateMessage) {
|
||||
return recentPrivateMessage.participantId;
|
||||
return {
|
||||
id: recentPrivateMessage.participantId,
|
||||
isFromVisitor: Boolean(recentPrivateMessage.isFromVisitor),
|
||||
name: recentPrivateMessage.displayName
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { UPDATE_CONFERENCE_METADATA } from '../base/conference/actionTypes';
|
||||
import { ILocalParticipant, IParticipant } from '../base/participants/types';
|
||||
import ReducerRegistry from '../base/redux/ReducerRegistry';
|
||||
import { IVisitorChatParticipant } from '../visitors/types';
|
||||
|
||||
import {
|
||||
ADD_MESSAGE,
|
||||
@@ -51,7 +52,7 @@ export interface IChatState {
|
||||
} | ILocalParticipant;
|
||||
messages: IMessage[];
|
||||
nbUnreadMessages: number;
|
||||
privateMessageRecipient?: IParticipant;
|
||||
privateMessageRecipient?: IParticipant | IVisitorChatParticipant;
|
||||
width: {
|
||||
current: number;
|
||||
userSet: number | null;
|
||||
@@ -64,6 +65,7 @@ ReducerRegistry.register<IChatState>('features/chat', (state = DEFAULT_STATE, ac
|
||||
const newMessage: IMessage = {
|
||||
displayName: action.displayName,
|
||||
error: action.error,
|
||||
isFromVisitor: Boolean(action.isFromVisitor),
|
||||
participantId: action.participantId,
|
||||
isReaction: action.isReaction,
|
||||
messageId: action.messageId,
|
||||
@@ -73,6 +75,7 @@ ReducerRegistry.register<IChatState>('features/chat', (state = DEFAULT_STATE, ac
|
||||
privateMessage: action.privateMessage,
|
||||
lobbyChat: action.lobbyChat,
|
||||
recipient: action.recipient,
|
||||
sentToVisitor: Boolean(action.sentToVisitor),
|
||||
timestamp: action.timestamp
|
||||
};
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { IStore } from '../app/types';
|
||||
export interface IMessage {
|
||||
displayName: string;
|
||||
error?: Object;
|
||||
isFromVisitor?: boolean;
|
||||
isReaction: boolean;
|
||||
lobbyChat: boolean;
|
||||
message: string;
|
||||
@@ -14,6 +15,7 @@ export interface IMessage {
|
||||
privateMessage: boolean;
|
||||
reactions: Map<string, Set<string>>;
|
||||
recipient: string;
|
||||
sentToVisitor?: boolean;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
@@ -60,11 +62,6 @@ export interface IChatMessageProps extends WithTranslation {
|
||||
*/
|
||||
message: IMessage;
|
||||
|
||||
/**
|
||||
* Whether the chat message menu is visible or not.
|
||||
*/
|
||||
shouldDisplayChatMessageMenu?: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not the avatar image of the participant which sent the message
|
||||
* should be displayed.
|
||||
|
||||
@@ -40,7 +40,6 @@ import {
|
||||
NOTIFICATION_TIMEOUT_TYPE
|
||||
} from '../notifications/constants';
|
||||
import { showStartRecordingNotification } from '../recording/actions';
|
||||
import { showSalesforceNotification } from '../salesforce/actions';
|
||||
import { setToolboxEnabled } from '../toolbox/actions.any';
|
||||
|
||||
import { DISMISS_CALENDAR_NOTIFICATION } from './actionTypes';
|
||||
@@ -164,7 +163,6 @@ function _conferenceJoined({ dispatch, getState }: IStore) {
|
||||
}), 10 * 1000);
|
||||
}
|
||||
|
||||
dispatch(showSalesforceNotification());
|
||||
dispatch(showStartRecordingNotification());
|
||||
|
||||
_checkIframe(getState(), dispatch);
|
||||
|
||||
@@ -8,7 +8,7 @@ import { isVpaasMeeting } from '../jaas/functions';
|
||||
import DeepLinkingDesktopPage from './components/DeepLinkingDesktopPage';
|
||||
import DeepLinkingMobilePage from './components/DeepLinkingMobilePage';
|
||||
import NoMobileApp from './components/NoMobileApp';
|
||||
import { _openDesktopApp } from './openDesktopApp';
|
||||
import { _openDesktopApp } from './openDesktopApp.web';
|
||||
|
||||
/**
|
||||
* Generates a deep linking URL based on the current window URL.
|
||||
@@ -1,7 +1,7 @@
|
||||
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
||||
|
||||
import { OPEN_DESKTOP_APP } from './actionTypes';
|
||||
import { openDesktopApp } from './functions';
|
||||
import { openDesktopApp } from './functions.web';
|
||||
|
||||
/**
|
||||
* Implements the middleware of the deep linking feature.
|
||||
@@ -1,3 +1,4 @@
|
||||
import { executeAfterLoad } from '../app/functions.web';
|
||||
import { IReduxState } from '../app/types';
|
||||
import { URI_PROTOCOL_PATTERN } from '../base/util/uri';
|
||||
|
||||
@@ -16,7 +17,10 @@ export function _openDesktopApp(_state: Object) {
|
||||
const { appScheme } = deeplinkingDesktop;
|
||||
const regex = new RegExp(URI_PROTOCOL_PATTERN, 'gi');
|
||||
|
||||
window.location.href = window.location.href.replace(regex, `${appScheme}:`);
|
||||
// This is needed to workaround https://issues.chromium.org/issues/41398687
|
||||
executeAfterLoad(() => {
|
||||
window.location.href = window.location.href.replace(regex, `${appScheme}:`);
|
||||
});
|
||||
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import { FixedSizeList } from 'react-window';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import Icon from '../../../base/icons/components/Icon';
|
||||
import { IconArrowDown, IconArrowUp } from '../../../base/icons/svg';
|
||||
import { withPixelLineHeight } from '../../../base/styles/functions.web';
|
||||
@@ -78,6 +79,7 @@ export default function CurrentVisitorsList({ searchString }: IProps) {
|
||||
const visitors = useSelector(getVisitorsList);
|
||||
const featureEnabled = useSelector(isVisitorsListEnabled);
|
||||
const shouldDisplayList = useSelector(shouldDisplayCurrentVisitorsList);
|
||||
const { defaultRemoteDisplayName } = useSelector((state: IReduxState) => state['features/base/config']);
|
||||
const { t } = useTranslation();
|
||||
const { classes } = useStyles();
|
||||
const dispatch = useDispatch();
|
||||
@@ -109,9 +111,11 @@ export default function CurrentVisitorsList({ searchString }: IProps) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const filtered = visitors.filter(v =>
|
||||
normalizeAccents(v.name).toLowerCase().includes(normalizeAccents(searchString).toLowerCase())
|
||||
);
|
||||
const filtered = visitors.filter(v => {
|
||||
const displayName = v.name || defaultRemoteDisplayName || 'Fellow Jitster';
|
||||
|
||||
return normalizeAccents(displayName).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.
|
||||
@@ -125,7 +129,7 @@ export default function CurrentVisitorsList({ searchString }: IProps) {
|
||||
<ParticipantItem
|
||||
actionsTrigger = { ACTION_TRIGGER.HOVER }
|
||||
audioMediaState = { MEDIA_STATE.NONE }
|
||||
displayName = { v.name }
|
||||
displayName = { v.name || defaultRemoteDisplayName || 'Fellow Jitster' }
|
||||
participantID = { v.id }
|
||||
videoMediaState = { MEDIA_STATE.NONE } />
|
||||
</div>
|
||||
|
||||
@@ -71,6 +71,7 @@ const useStyles = makeStyles<IStylesProps>()((theme, { isChatOpen }) => {
|
||||
container: {
|
||||
boxSizing: 'border-box',
|
||||
flex: 1,
|
||||
overflowY: 'auto',
|
||||
position: 'relative',
|
||||
padding: `0 ${participantsPaneTheme.panePadding}px`,
|
||||
display: 'flex',
|
||||
@@ -78,6 +79,21 @@ const useStyles = makeStyles<IStylesProps>()((theme, { isChatOpen }) => {
|
||||
|
||||
'&::-webkit-scrollbar': {
|
||||
display: 'none'
|
||||
},
|
||||
|
||||
// Temporary fix: Limit context menu width to prevent clipping
|
||||
// TODO: Long-term fix would be to portal context menus outside the scrollable container
|
||||
'& [class*="contextMenu"]': {
|
||||
maxWidth: '285px',
|
||||
|
||||
'& [class*="contextMenuItem"]': {
|
||||
whiteSpace: 'normal',
|
||||
|
||||
'& span': {
|
||||
whiteSpace: 'normal',
|
||||
wordBreak: 'break-word'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -91,6 +91,23 @@ export function getNotificationsMap(stateful: IStateful): { [key: string]: boole
|
||||
}, {});
|
||||
}
|
||||
|
||||
function normalizeCurrentLanguage(language: string) {
|
||||
if (!language) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [ country, lang ] = language.split('-');
|
||||
const jitsiNormalized = `${country}${lang ?? ''}`;
|
||||
|
||||
if (LANGUAGES.includes(jitsiNormalized)) {
|
||||
return jitsiNormalized;
|
||||
}
|
||||
|
||||
if (LANGUAGES.includes(country)) {
|
||||
return country;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the properties for the "More" tab from settings dialog from Redux
|
||||
* state.
|
||||
@@ -102,7 +119,7 @@ export function getNotificationsMap(stateful: IStateful): { [key: string]: boole
|
||||
export function getMoreTabProps(stateful: IStateful) {
|
||||
const state = toState(stateful);
|
||||
const stageFilmstripEnabled = isStageFilmstripEnabled(state);
|
||||
const language = i18next.language || DEFAULT_LANGUAGE;
|
||||
const language = normalizeCurrentLanguage(i18next.language) || DEFAULT_LANGUAGE;
|
||||
const configuredTabs: string[] = interfaceConfig.SETTINGS_SECTIONS || [];
|
||||
|
||||
// when self view is controlled by the config we hide the settings
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Client } from '@stomp/stompjs';
|
||||
import { Client, StompSubscription } from '@stomp/stompjs';
|
||||
|
||||
import logger from './logger';
|
||||
import { WebsocketClient } from './websocket-client';
|
||||
@@ -10,6 +10,9 @@ import { WebsocketClient } from './websocket-client';
|
||||
export class VisitorsListWebsocketClient extends WebsocketClient {
|
||||
private static client: VisitorsListWebsocketClient;
|
||||
|
||||
private _topicSubscription: StompSubscription | undefined;
|
||||
private _queueSubscription: StompSubscription | undefined;
|
||||
|
||||
/**
|
||||
* Creates a new instance of the VisitorsListWebsocketClient.
|
||||
*
|
||||
@@ -87,7 +90,7 @@ export class VisitorsListWebsocketClient extends WebsocketClient {
|
||||
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 => {
|
||||
this._topicSubscription = this.stompClient.subscribe(topicEndpoint, deltaMessage => {
|
||||
try {
|
||||
const updates: Array<{ n: string; r: string; s: string; }> = JSON.parse(deltaMessage.body);
|
||||
|
||||
@@ -102,7 +105,7 @@ export class VisitorsListWebsocketClient extends WebsocketClient {
|
||||
});
|
||||
|
||||
// Subscribe for the initial list after topic subscription is active
|
||||
const queueSubscription = this.stompClient.subscribe(queueEndpoint, message => {
|
||||
this._queueSubscription = this.stompClient.subscribe(queueEndpoint, message => {
|
||||
try {
|
||||
const visitors: Array<{ n: string; r: string; }> = JSON.parse(message.body);
|
||||
|
||||
@@ -110,7 +113,11 @@ export class VisitorsListWebsocketClient extends WebsocketClient {
|
||||
initialReceived = true;
|
||||
initialCallback(visitors);
|
||||
|
||||
queueSubscription.unsubscribe();
|
||||
// Unsubscribe from queue after receiving initial list
|
||||
if (this._queueSubscription) {
|
||||
this._queueSubscription.unsubscribe();
|
||||
this._queueSubscription = undefined;
|
||||
}
|
||||
|
||||
if (cachedDeltas.length) {
|
||||
deltaCallback(cachedDeltas);
|
||||
@@ -124,4 +131,45 @@ export class VisitorsListWebsocketClient extends WebsocketClient {
|
||||
|
||||
this.stompClient.activate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribes from both topic and queue subscriptions.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
override unsubscribe(): void {
|
||||
if (this._topicSubscription) {
|
||||
this._topicSubscription.unsubscribe();
|
||||
logger.debug('Unsubscribed from visitors list topic');
|
||||
this._topicSubscription = undefined;
|
||||
}
|
||||
|
||||
if (this._queueSubscription) {
|
||||
this._queueSubscription.unsubscribe();
|
||||
logger.debug('Unsubscribed from visitors list queue');
|
||||
this._queueSubscription = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects the current stomp client instance and clears it.
|
||||
* Unsubscribes from any active subscriptions first.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
override disconnect(): Promise<any> {
|
||||
if (!this.stompClient) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const url = this.stompClient.brokerURL;
|
||||
|
||||
// Unsubscribe first (synchronous), then disconnect
|
||||
this.unsubscribe();
|
||||
|
||||
return this.stompClient.deactivate().then(() => {
|
||||
logger.debug(`disconnected from: ${url}`);
|
||||
this.stompClient = undefined;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,3 +139,21 @@ export function shouldDisplayCurrentVisitorsList(stateful: IStateful): boolean {
|
||||
|
||||
return isVisitorsListEnabled(state) && getVisitorsCount(state) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param state
|
||||
* @param displayName
|
||||
* @returns
|
||||
*/
|
||||
/**
|
||||
* Returns visitor's display name, falling back to the default remote display name
|
||||
* from config, or 'Fellow Jitster' if neither is available.
|
||||
*
|
||||
* @param {IReduxState} state - The Redux state.
|
||||
* @param {string} [displayName] - Optional display name to use if available.
|
||||
* @returns {string} - The display name for a visitor.
|
||||
*/
|
||||
export function getVisitorDisplayName(state: IReduxState, displayName?: string): string {
|
||||
return displayName || state['features/base/config'].defaultRemoteDisplayName || 'Fellow Jitster';
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
ENDPOINT_MESSAGE_RECEIVED,
|
||||
UPDATE_CONFERENCE_METADATA
|
||||
} from '../base/conference/actionTypes';
|
||||
import { IConferenceMetadata } from '../base/conference/reducer';
|
||||
import { SET_CONFIG } from '../base/config/actionTypes';
|
||||
import { CONNECTION_FAILED } from '../base/connection/actionTypes';
|
||||
import { connect, setPreferVisitor } from '../base/connection/actions';
|
||||
@@ -176,7 +177,11 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
|
||||
// let's subscribe for visitor waiting queue
|
||||
const { room } = getState()['features/base/conference'];
|
||||
const { disableBeforeUnloadHandlers = false } = getState()['features/base/config'];
|
||||
const conferenceJid = `${room}@${hosts?.muc}`;
|
||||
const beforeUnloadHandler = () => {
|
||||
WebsocketClient.getInstance().disconnect();
|
||||
};
|
||||
|
||||
WebsocketClient.getInstance()
|
||||
.connect(`wss://${visitorsConfig?.queueService}/visitor/websocket`,
|
||||
@@ -185,8 +190,12 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
if ('status' in msg && msg.status === 'live') {
|
||||
logger.info('The conference is now live!');
|
||||
|
||||
|
||||
WebsocketClient.getInstance().disconnect()
|
||||
.then(() => {
|
||||
window.removeEventListener(
|
||||
disableBeforeUnloadHandlers ? 'unload' : 'beforeunload',
|
||||
beforeUnloadHandler);
|
||||
let delay = 0;
|
||||
|
||||
// now let's connect to meeting
|
||||
@@ -213,20 +222,18 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
dispatch(setInVisitorsQueue(true));
|
||||
});
|
||||
|
||||
/**
|
||||
* Disconnecting the WebSocket client when the user closes the page.
|
||||
*/
|
||||
window.addEventListener(disableBeforeUnloadHandlers ? 'unload' : 'beforeunload', beforeUnloadHandler);
|
||||
|
||||
|
||||
break;
|
||||
}
|
||||
case PARTICIPANT_UPDATED: {
|
||||
const { visitors: visitorsConfig } = toState(getState)['features/base/config'];
|
||||
const { metadata } = getState()['features/base/conference'];
|
||||
|
||||
if (visitorsConfig?.queueService && isLocalParticipantModerator(getState)) {
|
||||
const { metadata } = getState()['features/base/conference'];
|
||||
|
||||
if (metadata?.visitors?.live === false && !WebsocketClient.getInstance().isActive()) {
|
||||
// when go live is available and false, we should subscribe
|
||||
// to the service if available to listen for waiting visitors
|
||||
_subscribeQueueStats(getState(), dispatch);
|
||||
}
|
||||
}
|
||||
_handleQueueAndNotification(dispatch, getState, metadata);
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -242,26 +249,8 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
}
|
||||
case UPDATE_CONFERENCE_METADATA: {
|
||||
const { metadata } = action;
|
||||
const { visitors: visitorsConfig } = toState(getState)['features/base/config'];
|
||||
|
||||
if (!visitorsConfig?.queueService) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (isLocalParticipantModerator(getState)) {
|
||||
if (metadata?.visitors?.live === false) {
|
||||
if (!WebsocketClient.getInstance().isActive()) {
|
||||
// if metadata go live changes to goLive false and local is moderator
|
||||
// we should subscribe to the service if available to listen for waiting visitors
|
||||
_subscribeQueueStats(getState(), dispatch);
|
||||
}
|
||||
|
||||
_showNotLiveNotification(dispatch, getVisitorsInQueueCount(getState));
|
||||
} else if (metadata?.visitors?.live) {
|
||||
dispatch(hideNotification(VISITORS_NOT_LIVE_NOTIFICATION_ID));
|
||||
WebsocketClient.getInstance().disconnect();
|
||||
}
|
||||
}
|
||||
_handleQueueAndNotification(dispatch, getState, metadata);
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -275,6 +264,38 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
return next(action);
|
||||
});
|
||||
|
||||
/**
|
||||
* Handles the queue connection and notification for visitors if needed.
|
||||
*
|
||||
* @param {IStore.dispatch} dispatch - The Redux dispatch function.
|
||||
* @param {IStore.getState} getState - The Redux getState function.
|
||||
* @param {IConferenceMetadata} metadata - The conference metadata.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _handleQueueAndNotification(
|
||||
dispatch: IStore['dispatch'],
|
||||
getState: IStore['getState'],
|
||||
metadata: IConferenceMetadata | undefined): void {
|
||||
const { visitors: visitorsConfig } = toState(getState)['features/base/config'];
|
||||
|
||||
if (!(visitorsConfig?.queueService && isLocalParticipantModerator(getState))) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (metadata?.visitors?.live === false) {
|
||||
if (!WebsocketClient.getInstance().isActive()) {
|
||||
// if metadata go live changes to goLive false and local is moderator
|
||||
// we should subscribe to the service if available to listen for waiting visitors
|
||||
_subscribeQueueStats(getState(), dispatch);
|
||||
}
|
||||
|
||||
_showNotLiveNotification(dispatch, getVisitorsInQueueCount(getState));
|
||||
} else if (metadata?.visitors?.live) {
|
||||
dispatch(hideNotification(VISITORS_NOT_LIVE_NOTIFICATION_ID));
|
||||
WebsocketClient.getInstance().disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a notification that the meeting is not live.
|
||||
*
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
UPDATE_VISITORS_LIST,
|
||||
VISITOR_PROMOTION_REQUEST
|
||||
} from './actionTypes';
|
||||
import { IPromotionRequest, IVisitor } from './types';
|
||||
import { IPromotionRequest, IVisitorListParticipant } from './types';
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
count: 0,
|
||||
@@ -22,7 +22,7 @@ const DEFAULT_STATE = {
|
||||
showNotification: false,
|
||||
supported: false,
|
||||
promotionRequests: [],
|
||||
visitors: [] as IVisitor[],
|
||||
visitors: [] as IVisitorListParticipant[],
|
||||
visitorsListSubscribed: false
|
||||
};
|
||||
|
||||
@@ -34,7 +34,7 @@ export interface IVisitorsState {
|
||||
inQueueCount?: number;
|
||||
promotionRequests: IPromotionRequest[];
|
||||
supported: boolean;
|
||||
visitors: IVisitor[];
|
||||
visitors: IVisitorListParticipant[];
|
||||
visitorsListSubscribed: boolean;
|
||||
}
|
||||
ReducerRegistry.register<IVisitorsState>('features/visitors', (state = DEFAULT_STATE, action): IVisitorsState => {
|
||||
@@ -63,7 +63,7 @@ ReducerRegistry.register<IVisitorsState>('features/visitors', (state = DEFAULT_S
|
||||
};
|
||||
}
|
||||
case UPDATE_VISITORS_IN_QUEUE_COUNT: {
|
||||
if (state.count === action.count) {
|
||||
if (state.inQueueCount === action.count) {
|
||||
return state;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,13 @@ export interface IPromotionRequest {
|
||||
nick: string;
|
||||
}
|
||||
|
||||
export interface IVisitor {
|
||||
export interface IVisitorListParticipant {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface IVisitorChatParticipant {
|
||||
id: string;
|
||||
isVisitor: true;
|
||||
name: string;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
import { Client } from '@stomp/stompjs';
|
||||
import { Client, StompSubscription } from '@stomp/stompjs';
|
||||
|
||||
import logger from './logger';
|
||||
|
||||
@@ -28,6 +28,8 @@ export class WebsocketClient {
|
||||
|
||||
private _connectCount = 0;
|
||||
|
||||
private _subscription: StompSubscription | undefined;
|
||||
|
||||
/**
|
||||
* WebsocketClient getInstance.
|
||||
*
|
||||
@@ -100,7 +102,7 @@ export class WebsocketClient {
|
||||
this._connectCount++;
|
||||
connectCallback?.();
|
||||
|
||||
this.stompClient.subscribe(endpoint, message => {
|
||||
this._subscription = this.stompClient.subscribe(endpoint, message => {
|
||||
try {
|
||||
callback(JSON.parse(message.body));
|
||||
} catch (e) {
|
||||
@@ -113,7 +115,21 @@ export class WebsocketClient {
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects the current stomp client instance and clears it.
|
||||
* Unsubscribes from the current subscription.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
unsubscribe(): void {
|
||||
if (this._subscription) {
|
||||
this._subscription.unsubscribe();
|
||||
logger.debug('Unsubscribed from WebSocket topic');
|
||||
this._subscription = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects the current stomp client instance and clears it.
|
||||
* Unsubscribes from any active subscriptions first if available.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
@@ -124,8 +140,11 @@ export class WebsocketClient {
|
||||
|
||||
const url = this.stompClient.brokerURL;
|
||||
|
||||
// Unsubscribe first (synchronous), then disconnect
|
||||
this.unsubscribe();
|
||||
|
||||
return this.stompClient.deactivate().then(() => {
|
||||
logger.info(`disconnected from: ${url}`);
|
||||
logger.debug(`disconnected from: ${url}`);
|
||||
this.stompClient = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -47,11 +47,15 @@ if (Platform.OS === 'ios') {
|
||||
const globalNS = getJitsiMeetGlobalNS();
|
||||
const connectionTimes = getJitsiMeetGlobalNSConnectionTimes();
|
||||
|
||||
// Used to check if the load event has been fired.
|
||||
globalNS.hasLoaded = false;
|
||||
|
||||
// Used for automated performance tests.
|
||||
connectionTimes['index.loaded'] = window.indexLoadedTime;
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
connectionTimes['window.loaded'] = window.loadedEventTime;
|
||||
globalNS.hasLoaded = true;
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
@@ -157,7 +157,14 @@ function start_av_moderation(room, mediaType, occupant)
|
||||
room.av_moderation = {};
|
||||
room.av_moderation_actors = {};
|
||||
end
|
||||
room.av_moderation[mediaType] = array{};
|
||||
room.av_moderation[mediaType] = array();
|
||||
|
||||
-- add all current moderators to the new whitelist
|
||||
for _, room_occupant in room:each_occupant() do
|
||||
if room_occupant.role == 'moderator' and not ends_with(room_occupant.nick, '/focus') then
|
||||
room.av_moderation[mediaType]:push(internal_room_jid_match_rewrite(room_occupant.nick));
|
||||
end
|
||||
end
|
||||
|
||||
-- We want to set startMuted policy in metadata, in case of new participants are joining to respect
|
||||
-- it, that will be enforced by jicofo
|
||||
@@ -166,7 +173,7 @@ function start_av_moderation(room, mediaType, occupant)
|
||||
-- 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];
|
||||
av_moderation_startMuted_restore[mediaType] = startMutedMetadata[mediaType];
|
||||
room.av_moderation_startMuted_restore = av_moderation_startMuted_restore;
|
||||
|
||||
startMutedMetadata[mediaType] = true;
|
||||
@@ -262,6 +269,12 @@ function on_message(event)
|
||||
|
||||
-- send message to all occupants
|
||||
notify_occupants_enable(nil, enabled, room, occupant.nick, mediaType);
|
||||
|
||||
if enabled then
|
||||
-- inform all moderators for the newly created whitelist
|
||||
notify_whitelist_change(nil, true, room, mediaType);
|
||||
end
|
||||
|
||||
return true;
|
||||
elseif moderation_command.attr.jidToWhitelist then
|
||||
local occupant_jid = moderation_command.attr.jidToWhitelist;
|
||||
@@ -357,9 +370,13 @@ function occupant_joined(event)
|
||||
-- NOTE for some reason event.occupant.role is not reflecting the actual occupant role (when changed
|
||||
-- from allowners module) but iterating over room occupants returns the correct role
|
||||
for _, room_occupant in room:each_occupant() do
|
||||
-- if moderator send the whitelist
|
||||
if room_occupant.nick == occupant.nick and room_occupant.role == 'moderator' then
|
||||
notify_whitelist_change(room_occupant.jid, false, room);
|
||||
-- if it is a moderator, send the whitelist to every moderator
|
||||
if room_occupant.nick == occupant.nick and room_occupant.role == 'moderator' then
|
||||
for _,mediaType in pairs({'audio', 'video', 'desktop'}) do
|
||||
if room.av_moderation[mediaType] then
|
||||
notify_whitelist_change(nil, true, room, mediaType);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -367,14 +384,30 @@ end
|
||||
|
||||
-- when a occupant was granted moderator we need to update him with the whitelist
|
||||
function occupant_affiliation_changed(event)
|
||||
local room = event.room;
|
||||
if not room.av_moderation or is_healthcheck_room(room.jid) or is_admin(event.jid)
|
||||
or event.affiliation ~= 'owner' then
|
||||
return;
|
||||
end
|
||||
|
||||
-- in any enabled media type add the new moderator to the whitelist
|
||||
for _, room_occupant in room:each_occupant() do
|
||||
if room_occupant.bare_jid == event.jid then
|
||||
for _,mediaType in pairs({'audio', 'video', 'desktop'}) do
|
||||
if room.av_moderation[mediaType] then
|
||||
room.av_moderation[mediaType]:push(internal_room_jid_match_rewrite(room_occupant.nick));
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- the actor can be nil if is coming from allowners or similar module we want to skip it here
|
||||
-- as we will handle it in occupant_joined
|
||||
if event.actor and event.affiliation == 'owner' and event.room.av_moderation then
|
||||
local room = event.room;
|
||||
-- event.jid is the bare jid of participant
|
||||
for _, occupant in room:each_occupant() do
|
||||
if occupant.bare_jid == event.jid then
|
||||
notify_whitelist_change(occupant.jid, false, room);
|
||||
if event.actor and event.affiliation == 'owner' then
|
||||
-- notify all moderators for the new grant moderator and the change in whitelists
|
||||
for _,mediaType in pairs({'audio', 'video', 'desktop'}) do
|
||||
if room.av_moderation[mediaType] then
|
||||
notify_whitelist_change(nil, true, room, mediaType);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -530,12 +530,56 @@ module:hook('muc-occupant-groupchat', function(event)
|
||||
return true;
|
||||
end, 55); -- prosody check for visitor's chat is prio 50, we want to override it
|
||||
|
||||
-- Private messaging support for visitors
|
||||
module:hook('muc-private-message', function(event)
|
||||
-- private messaging is forbidden
|
||||
event.origin.send(st.error_reply(event.stanza, 'auth', 'forbidden',
|
||||
'Private messaging is disabled on visitor nodes'));
|
||||
return true;
|
||||
end, 10);
|
||||
local room, stanza = event.room, event.stanza;
|
||||
local from = stanza.attr.from;
|
||||
local to = stanza.attr.to;
|
||||
local recipient_occupant = room:get_occupant_by_nick(to);
|
||||
local recipient_domain = recipient_occupant and jid.host(recipient_occupant.bare_jid) or nil;
|
||||
local sender_occupant = room:get_occupant_by_nick(from);
|
||||
local sender_domain = sender_occupant and jid.host(sender_occupant.bare_jid) or nil;
|
||||
|
||||
if sender_domain == nil or recipient_domain == nil then
|
||||
return false;
|
||||
end
|
||||
|
||||
-- If both sender and recipient are local (on this vnode)
|
||||
if sender_domain == local_domain and recipient_domain == local_domain then
|
||||
event.origin.send(st.error_reply(event.stanza, 'auth', 'forbidden',
|
||||
'Private messaging between visitors is disabled on visitor nodes'));
|
||||
return false; -- Prevent sending the original message and stop further processing
|
||||
end
|
||||
|
||||
-- If sender is local (visitor node) and recipient is from main prosody, forward to main prosody
|
||||
if sender_domain == local_domain and recipient_domain == main_domain then
|
||||
local original_to = stanza.attr.to;
|
||||
local original_from = stanza.attr.from;
|
||||
|
||||
-- Add nick element for visitor identification
|
||||
-- remove existing nick to avoid forgery
|
||||
stanza:remove_children('nick', NICK_NS);
|
||||
local nick_element = sender_occupant:get_presence():get_child('nick', NICK_NS);
|
||||
if nick_element then
|
||||
stanza:add_child(nick_element);
|
||||
else
|
||||
stanza:tag('nick', { xmlns = NICK_NS }):text('anonymous'):up();
|
||||
end
|
||||
|
||||
-- Forward to main prosody, preserving the resource and original from
|
||||
stanza.attr.to = jid.join(jid.node(room.jid), muc_domain_prefix..'.'..main_domain, jid.resource(to));
|
||||
module:send(stanza);
|
||||
return false; -- Prevent sending the original message and stop further processing
|
||||
end
|
||||
|
||||
-- For main->visitor messages, let the default MUC handler process it
|
||||
-- We don't need to do anything special
|
||||
if sender_domain == main_domain and recipient_domain == local_domain then
|
||||
return; -- Return nothing, let other handlers continue. The default MUC handler will process it.
|
||||
end
|
||||
|
||||
return false; -- Prevent sending the original message and stop further processing
|
||||
end, 100); -- Lower priority to run after other handlers
|
||||
|
||||
-- we calculate the stats on the configured interval (60 seconds by default)
|
||||
module:hook_global('stats-update', function ()
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
--- This module removes identity information from presence stanzas when the
|
||||
--- hideDisplayNameForAll or hideDisplayNameForGuests options are enabled
|
||||
--- for a room.
|
||||
|
||||
--- To be enabled under the main muc component
|
||||
local filters = require 'util.filters';
|
||||
local st = require 'util.stanza';
|
||||
|
||||
local util = module:require 'util';
|
||||
local filter_identity_from_presence = util.filter_identity_from_presence;
|
||||
local get_room_by_name_and_subdomain = util.get_room_by_name_and_subdomain;
|
||||
local is_admin = util.is_admin;
|
||||
local ends_with = util.ends_with;
|
||||
local internal_room_jid_match_rewrite = util.internal_room_jid_match_rewrite;
|
||||
|
||||
local NICK_NS = 'http://jabber.org/protocol/nick';
|
||||
|
||||
-- we need to get the shared resource for joining moderators, as participants are marked as moderators
|
||||
-- after joining which is after the filter for stanza/out, but we need to know will this participant be a moderator
|
||||
local joining_moderator_participants = module:shared('moderators/joining_moderator_participants');
|
||||
|
||||
--- Filter presence sent to non-moderator members of a room when the hideDisplayNameForGuests option is set.
|
||||
function filter_stanza_out(stanza, session)
|
||||
if stanza.name ~= 'presence' or stanza.attr.type == 'error'
|
||||
or stanza.attr.type == 'unavailable' or ends_with(stanza.attr.from, '/focus') then
|
||||
@@ -21,48 +25,26 @@ function filter_stanza_out(stanza, session)
|
||||
end
|
||||
|
||||
local room = get_room_by_name_and_subdomain(session.jitsi_web_query_room, session.jitsi_web_query_prefix);
|
||||
local shouldFilter = false;
|
||||
|
||||
if not room or room._data.hideDisplayNameForGuests ~= true then
|
||||
return stanza;
|
||||
end
|
||||
|
||||
local occupant = room:get_occupant_by_real_jid(stanza.attr.to);
|
||||
if occupant then
|
||||
if stanza.attr.from == internal_room_jid_match_rewrite(occupant.nick) then
|
||||
-- we ignore self-presences, in this case and role will not be correct
|
||||
return stanza;
|
||||
end
|
||||
|
||||
if occupant.role ~= 'moderator' and not joining_moderator_participants[occupant.bare_jid] then
|
||||
local st_clone = st.clone(stanza);
|
||||
st_clone:remove_children('nick', NICK_NS);
|
||||
return st_clone;
|
||||
if room and (room._data.hideDisplayNameForGuests == true or room._data.hideDisplayNameForAll == true) then
|
||||
local occupant = room:get_occupant_by_real_jid(stanza.attr.to);
|
||||
-- don't touch self-presence
|
||||
if occupant and stanza.attr.from ~= internal_room_jid_match_rewrite(occupant.nick) then
|
||||
local isModerator = (occupant.role == 'moderator' or joining_moderator_participants[occupant.bare_jid]);
|
||||
shouldFilter = room._data.hideDisplayNameForAll or not isModerator;
|
||||
end
|
||||
end
|
||||
|
||||
return stanza;
|
||||
end
|
||||
|
||||
function filter_stanza_in(stanza, session)
|
||||
if stanza.name ~= 'presence' or stanza.attr.type == 'error' or stanza.attr.type == 'unavailable' then
|
||||
if shouldFilter then
|
||||
return filter_identity_from_presence(stanza);
|
||||
else
|
||||
return stanza;
|
||||
end
|
||||
|
||||
local room = get_room_by_name_and_subdomain(session.jitsi_web_query_room, session.jitsi_web_query_prefix);
|
||||
|
||||
-- if hideDisplayNameForAll we want to drop any display name from the presence stanza
|
||||
if not room or room._data.hideDisplayNameForAll ~= true then
|
||||
return stanza;
|
||||
end
|
||||
|
||||
stanza:remove_children('nick', NICK_NS);
|
||||
|
||||
return stanza;
|
||||
end
|
||||
|
||||
function filter_session(session)
|
||||
filters.add_filter(session, 'stanzas/out', filter_stanza_out, -100);
|
||||
filters.add_filter(session, 'stanzas/in', filter_stanza_in, -100);
|
||||
end
|
||||
|
||||
function module.load()
|
||||
|
||||
@@ -19,6 +19,7 @@ local internal_room_jid_match_rewrite = util.internal_room_jid_match_rewrite;
|
||||
local process_host_module = util.process_host_module;
|
||||
local table_shallow_copy = util.table_shallow_copy;
|
||||
local table_add = util.table_add;
|
||||
local table_equals = util.table_equals;
|
||||
|
||||
local MUC_NS = 'http://jabber.org/protocol/muc';
|
||||
local COMPONENT_IDENTITY_TYPE = 'room_metadata';
|
||||
@@ -188,12 +189,15 @@ function on_message(event)
|
||||
jsonData.data = res;
|
||||
end
|
||||
|
||||
room.jitsiMetadata[jsonData.key] = jsonData.data;
|
||||
local old_value = room.jitsiMetadata[jsonData.key];
|
||||
if not table_equals(old_value, jsonData.data) then
|
||||
room.jitsiMetadata[jsonData.key] = jsonData.data;
|
||||
|
||||
broadcastMetadata(room);
|
||||
broadcastMetadata(room);
|
||||
|
||||
-- fire and event for the change
|
||||
main_muc_module:fire_event('jitsi-metadata-updated', { room = room; actor = occupant; key = jsonData.key; });
|
||||
-- fire and event for the change
|
||||
main_muc_module:fire_event('jitsi-metadata-updated', { room = room; actor = occupant; key = jsonData.key; });
|
||||
end
|
||||
|
||||
return true;
|
||||
end
|
||||
@@ -238,12 +242,35 @@ function process_main_muc_loaded(main_muc, host_module)
|
||||
|
||||
local startMutedMetadata = room.jitsiMetadata.startMuted or {};
|
||||
|
||||
startMutedMetadata.audio = startMuted.attr.audio == 'true';
|
||||
startMutedMetadata.video = startMuted.attr.video == 'true';
|
||||
local audioNewValue = startMuted.attr.audio == 'true';
|
||||
local videoNewValue = startMuted.attr.video == 'true';
|
||||
local send_update = false;
|
||||
|
||||
room.jitsiMetadata.startMuted = startMutedMetadata;
|
||||
if startMutedMetadata.audio ~= audioNewValue then
|
||||
startMutedMetadata.audio = audioNewValue;
|
||||
send_update = true;
|
||||
end
|
||||
if startMutedMetadata.video ~= videoNewValue then
|
||||
startMutedMetadata.video = videoNewValue;
|
||||
send_update = true;
|
||||
end
|
||||
|
||||
host_module:fire_event('room-metadata-changed', { room = room; });
|
||||
if send_update then
|
||||
room.jitsiMetadata.startMuted = startMutedMetadata;
|
||||
|
||||
host_module:fire_event('room-metadata-changed', { room = room; });
|
||||
end
|
||||
end);
|
||||
|
||||
-- The the connection jid for authenticated users (like jicofo) stays the same,
|
||||
-- so leaving and re-joining will result not sending metatadata again.
|
||||
-- Make sure we clear the sent_initial_metadata entry for the occupant on leave.
|
||||
host_module:hook("muc-occupant-left", function(event)
|
||||
local room, occupant = event.room, event.occupant;
|
||||
|
||||
if room.sent_initial_metadata then
|
||||
room.sent_initial_metadata[jid.bare(event.occupant.jid)] = nil;
|
||||
end
|
||||
end);
|
||||
end
|
||||
|
||||
@@ -290,8 +317,7 @@ end
|
||||
|
||||
-- Send a message update for metadata before sending the first self presence
|
||||
function filter_stanza(stanza, session)
|
||||
if not stanza.attr or not stanza.attr.to or stanza.name ~= 'presence'
|
||||
or stanza.attr.type == 'unavailable' or ends_with(stanza.attr.from, '/focus') then
|
||||
if not stanza.attr or not stanza.attr.to or stanza.name ~= 'presence' or stanza.attr.type == 'unavailable' then
|
||||
return stanza;
|
||||
end
|
||||
|
||||
|
||||
@@ -12,12 +12,16 @@ local st = require 'util.stanza';
|
||||
local jid = require 'util.jid';
|
||||
local new_id = require 'util.id'.medium;
|
||||
local util = module:require 'util';
|
||||
local filter_identity_from_presence = util.filter_identity_from_presence;
|
||||
local is_admin = util.is_admin;
|
||||
local presence_check_status = util.presence_check_status;
|
||||
local process_host_module = util.process_host_module;
|
||||
local is_transcriber_jigasi = util.is_transcriber_jigasi;
|
||||
local json = require 'cjson.safe';
|
||||
|
||||
-- Debug flag
|
||||
local DEBUG = false;
|
||||
|
||||
local MUC_NS = 'http://jabber.org/protocol/muc';
|
||||
|
||||
-- required parameter for custom muc component prefix, defaults to 'conference'
|
||||
@@ -97,6 +101,25 @@ local function send_visitors_iq(conference_service, room, type)
|
||||
module:send(visitors_iq);
|
||||
end
|
||||
|
||||
-- Filter out identity information (nick name, email, etc) from a presence stanza,
|
||||
-- if the hideDisplayNameForGuests option for the room is set (note that the
|
||||
-- hideDisplayNameForAll option is implemented in a diffrent way and does not
|
||||
-- require filtering here)
|
||||
-- This is applied to presence of main room participants before it is sent out to
|
||||
-- vnodes.
|
||||
local function filter_stanza_nick_if_needed(stanza, room)
|
||||
if not stanza or stanza.name ~= 'presence' or stanza.attr.type == 'error' or stanza.attr.type == 'unavailable' then
|
||||
return stanza;
|
||||
end
|
||||
|
||||
-- if hideDisplayNameForGuests we want to drop any display name from the presence stanza
|
||||
if room and (room._data.hideDisplayNameForGuests or room._data.hideDisplayNameForAll) then
|
||||
return filter_identity_from_presence(stanza);
|
||||
end
|
||||
|
||||
return stanza;
|
||||
end
|
||||
|
||||
-- an event received from visitors component, which receives iqs from jicofo
|
||||
local function connect_vnode(event)
|
||||
local room, vnode = event.room, event.vnode;
|
||||
@@ -123,7 +146,7 @@ local function connect_vnode(event)
|
||||
|
||||
for _, o in room:each_occupant() do
|
||||
if not is_admin(o.bare_jid) then
|
||||
local fmuc_pr = st.clone(o:get_presence());
|
||||
local fmuc_pr = filter_stanza_nick_if_needed(st.clone(o:get_presence()), room);
|
||||
local user, _, res = jid.split(o.nick);
|
||||
fmuc_pr.attr.to = jid.join(user, conference_service , res);
|
||||
fmuc_pr.attr.from = o.jid;
|
||||
@@ -206,7 +229,8 @@ end, 900);
|
||||
process_host_module(main_muc_component_config, function(host_module, host)
|
||||
-- detects presence change in a main participant and propagate it to the used visitor nodes
|
||||
host_module:hook('muc-occupant-pre-change', function (event)
|
||||
local room, stanza, occupant = event.room, event.stanza, event.dest_occupant;
|
||||
local room, stanzaEv, occupant = event.room, event.stanza, event.dest_occupant;
|
||||
local stanza = filter_stanza_nick_if_needed(stanzaEv, room);
|
||||
|
||||
-- filter focus and configured domains (used for jibri and transcribers)
|
||||
if is_admin(stanza.attr.from) or visitors_nodes[room.jid] == nil
|
||||
@@ -227,7 +251,8 @@ process_host_module(main_muc_component_config, function(host_module, host)
|
||||
|
||||
-- when a main participant leaves inform the visitor nodes
|
||||
host_module:hook('muc-occupant-left', function (event)
|
||||
local room, stanza, occupant = event.room, event.stanza, event.occupant;
|
||||
local room, stanzaEv, occupant = event.room, event.stanza, event.occupant;
|
||||
local stanza = filter_stanza_nick_if_needed(stanzaEv, room);
|
||||
|
||||
-- ignore configured domains (jibri and transcribers)
|
||||
if is_admin(occupant.bare_jid) or visitors_nodes[room.jid] == nil or visitors_nodes[room.jid].nodes == nil
|
||||
@@ -270,7 +295,8 @@ process_host_module(main_muc_component_config, function(host_module, host)
|
||||
|
||||
-- detects new participants joining main room and sending them to the visitor nodes
|
||||
host_module:hook('muc-occupant-joined', function (event)
|
||||
local room, stanza, occupant = event.room, event.stanza, event.occupant;
|
||||
local room, stanzaEv, occupant = event.room, event.stanza, event.occupant;
|
||||
local stanza = filter_stanza_nick_if_needed(stanzaEv, room);
|
||||
|
||||
-- filter focus, ignore configured domains (jibri and transcribers)
|
||||
if is_admin(stanza.attr.from) or visitors_nodes[room.jid] == nil
|
||||
@@ -294,7 +320,8 @@ process_host_module(main_muc_component_config, function(host_module, host)
|
||||
end);
|
||||
-- forwards messages from main participants to vnodes
|
||||
host_module:hook('muc-occupant-groupchat', function(event)
|
||||
local room, stanza, occupant = event.room, event.stanza, event.occupant;
|
||||
local room, stanzaEv, occupant = event.room, event.stanza, event.occupant;
|
||||
local stanza = filter_stanza_nick_if_needed(stanzaEv, room);
|
||||
|
||||
-- filter sending messages from transcribers/jibris to visitors
|
||||
if not visitors_nodes[room.jid] then
|
||||
@@ -314,7 +341,8 @@ process_host_module(main_muc_component_config, function(host_module, host)
|
||||
-- receiving messages from visitor nodes and forward them to local main participants
|
||||
-- and forward them to the rest of visitor nodes
|
||||
host_module:hook('muc-occupant-groupchat', function(event)
|
||||
local occupant, room, stanza = event.occupant, event.room, event.stanza;
|
||||
local occupant, room, stanzaEv = event.occupant, event.room, event.stanza;
|
||||
local stanza = filter_stanza_nick_if_needed(stanzaEv, room);
|
||||
local to = stanza.attr.to;
|
||||
local from = stanza.attr.from;
|
||||
local from_vnode = jid.host(from);
|
||||
@@ -352,6 +380,73 @@ process_host_module(main_muc_component_config, function(host_module, host)
|
||||
return true;
|
||||
end, 55); -- prosody check for unknown participant chat is prio 50, we want to override it
|
||||
|
||||
-- Handle private messages from visitor nodes to main participants
|
||||
-- This routes forwarded private messages through the proper MUC system
|
||||
host_module:hook('message/full', function(event)
|
||||
local stanza = event.stanza;
|
||||
|
||||
-- Only handle chat messages (private messages)
|
||||
if stanza.attr.type ~= 'chat' then
|
||||
return; -- Let other handlers process non-chat messages
|
||||
end
|
||||
|
||||
local to = stanza.attr.to;
|
||||
|
||||
-- Early return if this is not targeted at our MUC component
|
||||
if jid.host(to) ~= main_muc_component_config then
|
||||
return; -- Not for our MUC component, let other handlers process
|
||||
end
|
||||
|
||||
local from = stanza.attr.from;
|
||||
local from_host = jid.host(from);
|
||||
local to_node = jid.node(to);
|
||||
local to_resource = jid.resource(to);
|
||||
|
||||
-- Check if this is a private message from a known visitor node
|
||||
local target_room_jid = jid.bare(to);
|
||||
|
||||
-- Early return if we don't have any visitor nodes for this room
|
||||
if not (visitors_nodes[target_room_jid] and visitors_nodes[target_room_jid].nodes) then
|
||||
return; -- No visitor nodes for this room, let default MUC handle it
|
||||
end
|
||||
|
||||
-- Early return if the from_host is not a known visitor node
|
||||
if not visitors_nodes[target_room_jid].nodes[from_host] then
|
||||
-- This could be a main->visitor message, let it go through s2s
|
||||
return; -- Not from a known visitor node, let default MUC handle it
|
||||
end
|
||||
|
||||
-- At this point we know it's a visitor message, handle it
|
||||
local room = prosody.hosts[main_muc_component_config].modules.muc.get_room_from_jid(target_room_jid);
|
||||
if room then
|
||||
-- Find the occupant
|
||||
local occupant = room:get_occupant_by_nick(to);
|
||||
if occupant then
|
||||
-- Add addresses element (XEP-0033) to store original visitor JID for reply functionality
|
||||
stanza:tag('addresses', { xmlns = 'http://jabber.org/protocol/address' })
|
||||
:tag('address', { type = 'ofrom', jid = stanza.attr.from }):up()
|
||||
:up();
|
||||
|
||||
-- Change from to be the main domain equivalent for proper client recognition
|
||||
-- Use bare JID without resource
|
||||
stanza.attr.from = jid.join(to_node, main_muc_component_config);
|
||||
|
||||
room:route_to_occupant(occupant, stanza);
|
||||
|
||||
return true;
|
||||
else
|
||||
module:log('warn', 'VISITOR PRIVATE MESSAGE: Occupant not found for %s', to);
|
||||
end
|
||||
else
|
||||
module:log('warn', 'VISITOR PRIVATE MESSAGE: Room not found for %s', to);
|
||||
end
|
||||
|
||||
return false;
|
||||
end, 10); -- Normal priority since we're in the right place now
|
||||
|
||||
-- Main->visitor private messages work via s2s routing automatically
|
||||
-- No special handling needed!
|
||||
|
||||
host_module:hook('muc-config-submitted/muc#roomconfig_roomsecret', function(event)
|
||||
if event.status_codes['104'] then
|
||||
local room = event.room;
|
||||
|
||||
@@ -620,11 +620,14 @@ end
|
||||
local function table_compare(old_table, new_table)
|
||||
local removed = {}
|
||||
local added = {}
|
||||
local modified = {}
|
||||
|
||||
-- Find removed items (in old but not in new)
|
||||
for id, _ in pairs(old_table) do
|
||||
for id, value in pairs(old_table) do
|
||||
if new_table[id] == nil then
|
||||
table.insert(removed, id)
|
||||
elseif new_table[id] ~= value then
|
||||
table.insert(modified, id)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -635,7 +638,20 @@ local function table_compare(old_table, new_table)
|
||||
end
|
||||
end
|
||||
|
||||
return removed, added
|
||||
return removed, added, modified
|
||||
end
|
||||
|
||||
local function table_equals(t1, t2)
|
||||
if t1 == nil then
|
||||
return t2 == nil;
|
||||
end
|
||||
if t2 == nil then
|
||||
return t1 == nil;
|
||||
end
|
||||
|
||||
local removed, added, modified = table_compare(t1, t2);
|
||||
|
||||
return next(removed) == nil and next(added) == nil and next(modified) == nil
|
||||
end
|
||||
|
||||
-- Splits a string using delimiter
|
||||
@@ -685,11 +701,35 @@ local function is_admin(_jid)
|
||||
return false;
|
||||
end
|
||||
|
||||
-- Filter out identity information (nick name, email, etc) from a presence stanza.
|
||||
local function filter_identity_from_presence(orig_stanza)
|
||||
local stanza = st.clone(orig_stanza);
|
||||
|
||||
stanza:remove_children('nick', 'http://jabber.org/protocol/nick');
|
||||
stanza:remove_children('email');
|
||||
stanza:remove_children('stats-id');
|
||||
local identity = stanza:get_child('identity');
|
||||
if identity then
|
||||
local user = identity:get_child('user');
|
||||
local name = identity:get_child('name');
|
||||
if user then
|
||||
user:remove_children('email');
|
||||
user:remove_children('name');
|
||||
end
|
||||
if name then
|
||||
name:remove_children('name'); -- Remove name with no namespace
|
||||
end
|
||||
end
|
||||
|
||||
return stanza;
|
||||
end
|
||||
|
||||
return {
|
||||
OUTBOUND_SIP_JIBRI_PREFIXES = OUTBOUND_SIP_JIBRI_PREFIXES;
|
||||
INBOUND_SIP_JIBRI_PREFIXES = INBOUND_SIP_JIBRI_PREFIXES;
|
||||
RECORDER_PREFIXES = RECORDER_PREFIXES;
|
||||
extract_subdomain = extract_subdomain;
|
||||
filter_identity_from_presence = filter_identity_from_presence;
|
||||
is_admin = is_admin;
|
||||
is_feature_allowed = is_feature_allowed;
|
||||
is_jibri = is_jibri;
|
||||
@@ -722,4 +762,5 @@ return {
|
||||
table_compare = table_compare;
|
||||
table_shallow_copy = table_shallow_copy;
|
||||
table_find = table_find;
|
||||
table_equals = table_equals;
|
||||
};
|
||||
|
||||
@@ -107,7 +107,7 @@ export default class WebhookProxy {
|
||||
* @param eventType
|
||||
* @param timeout
|
||||
*/
|
||||
async waitForEvent(eventType: string, timeout = 4000): Promise<any> {
|
||||
async waitForEvent(eventType: string, timeout = 120000): Promise<any> {
|
||||
// we create the error here so we have a meaningful stack trace
|
||||
const error = new Error(`Timeout waiting for event:${eventType}`);
|
||||
|
||||
|
||||
@@ -140,7 +140,7 @@ describe('Chat', () => {
|
||||
preAuthenticatedLink: string;
|
||||
};
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('CHAT_UPLOADED', 20000);
|
||||
} = await webhooksProxy.waitForEvent('CHAT_UPLOADED');
|
||||
|
||||
expect('CHAT_UPLOADED').toBe(event.eventType);
|
||||
expect(event.data.preAuthenticatedLink).toBeDefined();
|
||||
|
||||
@@ -235,6 +235,8 @@ describe('Participants presence', () => {
|
||||
|
||||
await p1.getIframeAPI().addEventListener('participantKickedOut');
|
||||
await p2.getIframeAPI().addEventListener('participantKickedOut');
|
||||
|
||||
await p2.getIframeAPI().clearEventResults('videoConferenceLeft');
|
||||
await p2.getIframeAPI().addEventListener('videoConferenceLeft');
|
||||
|
||||
await p1.getIframeAPI().executeCommand('kickParticipant', p2EpId);
|
||||
|
||||
@@ -140,7 +140,7 @@ describe('Transcriptions', () => {
|
||||
|
||||
// sometimes events are not immediately received,
|
||||
// let's wait for destroy event before waiting for those that depends on it
|
||||
await webhooksProxy.waitForEvent('ROOM_DESTROYED', 10000);
|
||||
await webhooksProxy.waitForEvent('ROOM_DESTROYED');
|
||||
|
||||
if (webhooksProxy) {
|
||||
const event: {
|
||||
@@ -148,7 +148,7 @@ describe('Transcriptions', () => {
|
||||
preAuthenticatedLink: string;
|
||||
};
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('TRANSCRIPTION_UPLOADED', 20000);
|
||||
} = await webhooksProxy.waitForEvent('TRANSCRIPTION_UPLOADED');
|
||||
|
||||
expect('TRANSCRIPTION_UPLOADED').toBe(event.eventType);
|
||||
expect(event.data.preAuthenticatedLink).toBeDefined();
|
||||
@@ -186,7 +186,7 @@ async function checkReceivingChunks(p1: Participant, p2: Participant, webhooksPr
|
||||
stable: string;
|
||||
};
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('TRANSCRIPTION_CHUNK_RECEIVED', 60000);
|
||||
} = await webhooksProxy.waitForEvent('TRANSCRIPTION_CHUNK_RECEIVED');
|
||||
|
||||
expect('TRANSCRIPTION_CHUNK_RECEIVED').toBe(event.eventType);
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ describe('Recording', () => {
|
||||
const liveStreamEvent: {
|
||||
customerId: string;
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('LIVE_STREAM_STARTED', 15000);
|
||||
} = await webhooksProxy.waitForEvent('LIVE_STREAM_STARTED');
|
||||
|
||||
expect('LIVE_STREAM_STARTED').toBe(liveStreamEvent.eventType);
|
||||
expect(liveStreamEvent.customerId).toBe(customerId);
|
||||
@@ -88,7 +88,7 @@ describe('Recording', () => {
|
||||
const liveStreamEvent: {
|
||||
customerId: string;
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('LIVE_STREAM_ENDED', 20000);
|
||||
} = await webhooksProxy.waitForEvent('LIVE_STREAM_ENDED');
|
||||
|
||||
expect('LIVE_STREAM_ENDED').toBe(liveStreamEvent.eventType);
|
||||
expect(liveStreamEvent.customerId).toBe(customerId);
|
||||
@@ -127,7 +127,7 @@ async function testRecordingStarted(command: boolean) {
|
||||
const recordingEvent: {
|
||||
customerId: string;
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('RECORDING_STARTED', 15000);
|
||||
} = await webhooksProxy.waitForEvent('RECORDING_STARTED');
|
||||
|
||||
expect('RECORDING_STARTED').toBe(recordingEvent.eventType);
|
||||
expect(recordingEvent.customerId).toBe(customerId);
|
||||
@@ -166,7 +166,7 @@ async function testRecordingStopped(command: boolean) {
|
||||
const liveStreamEvent: {
|
||||
customerId: string;
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('RECORDING_ENDED', 20000);
|
||||
} = await webhooksProxy.waitForEvent('RECORDING_ENDED');
|
||||
|
||||
expect('RECORDING_ENDED').toBe(liveStreamEvent.eventType);
|
||||
expect(liveStreamEvent.customerId).toBe(customerId);
|
||||
@@ -178,7 +178,7 @@ async function testRecordingStopped(command: boolean) {
|
||||
participants: Array<string>;
|
||||
};
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('RECORDING_UPLOADED', 20000);
|
||||
} = await webhooksProxy.waitForEvent('RECORDING_UPLOADED');
|
||||
|
||||
const jwtPayload = ctx.data[`${p1.name}-jwt-payload`];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user