mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-01-20 13:40:19 +00:00
Compare commits
23 Commits
pr-testing
...
release-81
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9cd6e7798e | ||
|
|
8fb280063b | ||
|
|
1620e1e9f4 | ||
|
|
9cb34964d2 | ||
|
|
950a5d9dec | ||
|
|
7ddfc182db | ||
|
|
236e30c307 | ||
|
|
f629fc5545 | ||
|
|
830b458a9c | ||
|
|
fcd09bcb2f | ||
|
|
398cc6eca4 | ||
|
|
a8333f3fb8 | ||
|
|
aff5ccddd0 | ||
|
|
fe1f2299cf | ||
|
|
b6c78a528d | ||
|
|
ab6ea18b2f | ||
|
|
b1d65a4af3 | ||
|
|
a26a085345 | ||
|
|
5dfaabb7d4 | ||
|
|
9bc7347d1e | ||
|
|
68803853fc | ||
|
|
d55c065420 | ||
|
|
8541427604 |
@@ -559,10 +559,10 @@ export default {
|
||||
* If prejoin page is enabled open an new connection in the background
|
||||
* and create local tracks.
|
||||
*
|
||||
* @param {{ roomName: string }} options
|
||||
* @param {{ roomName: string, shouldDispatchConnect }} options
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async init({ roomName }) {
|
||||
async init({ roomName, shouldDispatchConnect }) {
|
||||
const state = APP.store.getState();
|
||||
const initialOptions = {
|
||||
startAudioOnly: config.startAudioOnly,
|
||||
@@ -606,30 +606,50 @@ export default {
|
||||
const { dispatch, getState } = APP.store;
|
||||
const { tryCreateLocalTracks, errors } = this.createInitialLocalTracks(initialOptions);
|
||||
|
||||
dispatch(setInitialGUMPromise(tryCreateLocalTracks.then(async tr => {
|
||||
tryCreateLocalTracks.then(async tr => {
|
||||
const tracks = handleInitialTracks(initialOptions, tr);
|
||||
|
||||
this._initDeviceList(true);
|
||||
|
||||
const { initialGUMPromise } = getState()['features/base/media'];
|
||||
|
||||
if (isPrejoinPageVisible(getState())) {
|
||||
dispatch(gumPending([ MEDIA_TYPE.AUDIO, MEDIA_TYPE.VIDEO ], IGUMPendingState.NONE));
|
||||
dispatch(setInitialGUMPromise());
|
||||
|
||||
// Note: Not sure if initPrejoin needs to be async. But let's wait for it just to be sure the
|
||||
// tracks are added.
|
||||
// Since the conference is not yet created in redux this function will execute synchronous
|
||||
// which will guarantee us that the local tracks are added to redux before we proceed.
|
||||
initPrejoin(tracks, errors, dispatch);
|
||||
|
||||
// resolve the initialGUMPromise in case connect have finished so that we can proceed to join.
|
||||
if (initialGUMPromise) {
|
||||
logger.debug('Resolving the initialGUM promise! (prejoinVisible=true)');
|
||||
initialGUMPromise.resolve({
|
||||
tracks,
|
||||
errors
|
||||
});
|
||||
}
|
||||
|
||||
logger.debug('Clear the initialGUM promise! (prejoinVisible=true)');
|
||||
|
||||
// For prejoin we don't need the initial GUM promise since the tracks are already added to the store
|
||||
// via initPrejoin
|
||||
dispatch(setInitialGUMPromise());
|
||||
} else {
|
||||
APP.store.dispatch(displayErrorsForCreateInitialLocalTracks(errors));
|
||||
setGUMPendingStateOnFailedTracks(tracks, APP.store.dispatch);
|
||||
|
||||
if (initialGUMPromise) {
|
||||
logger.debug('Resolving the initialGUM promise!');
|
||||
initialGUMPromise.resolve({
|
||||
tracks,
|
||||
errors
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
tracks,
|
||||
errors
|
||||
};
|
||||
})));
|
||||
|
||||
if (!isPrejoinPageVisible(getState())) {
|
||||
if (shouldDispatchConnect) {
|
||||
logger.info('Dispatching connect from init since prejoin is not visible.');
|
||||
dispatch(connect());
|
||||
}
|
||||
},
|
||||
@@ -2034,8 +2054,9 @@ export default {
|
||||
const { dispatch } = APP.store;
|
||||
|
||||
return dispatch(getAvailableDevices())
|
||||
.then(devices => {
|
||||
APP.UI.onAvailableDevicesChanged(devices);
|
||||
.then(() => {
|
||||
this.updateAudioIconEnabled();
|
||||
this.updateVideoIconEnabled();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2200,7 +2221,8 @@ export default {
|
||||
|
||||
return Promise.all(promises)
|
||||
.then(() => {
|
||||
APP.UI.onAvailableDevicesChanged(filteredDevices);
|
||||
this.updateAudioIconEnabled();
|
||||
this.updateVideoIconEnabled();
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -1426,6 +1426,13 @@ var config = {
|
||||
*/
|
||||
// dynamicBrandingUrl: '',
|
||||
|
||||
// A list of allowed URL domains for shared video.
|
||||
//
|
||||
// NOTE:
|
||||
// '*' is allowed value and it will allow any URL to be used for shared video. We do not recommend using '*',
|
||||
// use it at your own risk!
|
||||
// sharedVideoAllowedURLDomains: [ ],
|
||||
|
||||
// Options related to the participants pane.
|
||||
// participantsPane: {
|
||||
// // Enables feature
|
||||
@@ -1756,8 +1763,6 @@ var config = {
|
||||
// tileTime: 5000,
|
||||
// // Limit results by rating: g, pg, pg-13, r. Default value: g.
|
||||
// rating: 'pg',
|
||||
// // The proxy server url for giphy requests in the web app.
|
||||
// proxyUrl: 'https://giphy-proxy.example.com',
|
||||
// },
|
||||
|
||||
// Logging
|
||||
|
||||
@@ -439,11 +439,14 @@
|
||||
"shareScreenWarningD2": "you need to stop audio sharing, start screen sharing and check the \"share audio\" option.",
|
||||
"shareScreenWarningH1": "If you want to share just your screen:",
|
||||
"shareScreenWarningTitle": "You need to stop audio sharing before sharing your screen",
|
||||
"shareVideoLinkError": "Please provide a correct video link.",
|
||||
"shareVideoConfirmPlay": "You’re about to open an external website. Do you want to continue?",
|
||||
"shareVideoConfirmPlayTitle": "{{name}} has shared a video with you.",
|
||||
"shareVideoLinkError": "Oops, this video cannot be played.",
|
||||
"shareVideoLinkStopped": "The video from {{name}} was stopped.",
|
||||
"shareVideoTitle": "Share video",
|
||||
"shareYourScreen": "Share your screen",
|
||||
"shareYourScreenDisabled": "Screen sharing disabled.",
|
||||
"sharedVideoDialogError": "Error: Invalid URL",
|
||||
"sharedVideoDialogError": "Error: Invalid or forbidden URL",
|
||||
"sharedVideoLinkPlaceholder": "YouTube link or direct video link",
|
||||
"show": "Show",
|
||||
"start": "Start ",
|
||||
@@ -1092,6 +1095,7 @@
|
||||
"desktopShareWarning": "You need to restart the screen share for the new settings to take effect.",
|
||||
"devices": "Devices",
|
||||
"followMe": "Everyone follows me",
|
||||
"followMeRecorder": "Recorder follows me",
|
||||
"framesPerSecond": "frames-per-second",
|
||||
"incomingMessage": "Incoming message",
|
||||
"language": "Language",
|
||||
@@ -1503,7 +1507,6 @@
|
||||
"labelTooltip": "Number of visitors: {{count}}",
|
||||
"notification": {
|
||||
"demoteDescription": "Sent here by {{actor}}, raise your hand to participate",
|
||||
"description": "To participate raise your hand",
|
||||
"noMainParticipantsDescription": "A participant needs to start the meeting. Please try again in a bit.",
|
||||
"noMainParticipantsTitle": "This meeting hasn’t started yet.",
|
||||
"noVisitorLobby": "You cannot join while there is a lobby enabled for the meeting.",
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
sendTones,
|
||||
setAssumedBandwidthBps,
|
||||
setFollowMe,
|
||||
setFollowMeRecorder,
|
||||
setLocalSubject,
|
||||
setPassword,
|
||||
setSubject
|
||||
@@ -322,15 +323,18 @@ function initCommands() {
|
||||
|
||||
APP.store.dispatch(setAssumedBandwidthBps(value));
|
||||
},
|
||||
'set-follow-me': value => {
|
||||
|
||||
'set-follow-me': (value, recorderOnly) => {
|
||||
if (value) {
|
||||
sendAnalytics(createApiEvent('follow.me.set'));
|
||||
sendAnalytics(createApiEvent('follow.me.set', {
|
||||
recorderOnly
|
||||
}));
|
||||
} else {
|
||||
sendAnalytics(createApiEvent('follow.me.unset'));
|
||||
sendAnalytics(createApiEvent('follow.me.unset', {
|
||||
recorderOnly
|
||||
}));
|
||||
}
|
||||
|
||||
APP.store.dispatch(setFollowMe(value));
|
||||
APP.store.dispatch(recorderOnly ? setFollowMeRecorder(value) : setFollowMe(value));
|
||||
},
|
||||
'set-large-video-participant': (participantId, videoType) => {
|
||||
const { getState, dispatch } = APP.store;
|
||||
|
||||
@@ -211,14 +211,6 @@ UI.handleLastNEndpoints = function(leavingIds, enteringIds) {
|
||||
*/
|
||||
UI.setAudioLevel = (id, lvl) => VideoLayout.setAudioLevel(id, lvl);
|
||||
|
||||
/**
|
||||
* Update list of available physical devices.
|
||||
*/
|
||||
UI.onAvailableDevicesChanged = function() {
|
||||
APP.conference.updateAudioIconEnabled();
|
||||
APP.conference.updateVideoIconEnabled();
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the id of the current video shown on large.
|
||||
* Currently used by tests (torture).
|
||||
|
||||
9
package-lock.json
generated
9
package-lock.json
generated
@@ -61,7 +61,7 @@
|
||||
"js-md5": "0.6.1",
|
||||
"js-sha512": "0.8.0",
|
||||
"jwt-decode": "2.2.0",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1845.0.0+515a927c/lib-jitsi-meet.tgz",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet#release-8100",
|
||||
"lodash": "4.17.21",
|
||||
"moment": "2.29.4",
|
||||
"moment-duration-format": "2.2.2",
|
||||
@@ -12489,8 +12489,7 @@
|
||||
},
|
||||
"node_modules/lib-jitsi-meet": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1845.0.0+515a927c/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-UzT6skfD8wqs2695oMi3B8SxcImhG2SIYwc1xM+z9pVlJ4MAOWsLc7flCBXSnpnUTK3b5AjCOot3cRqeMibNcQ==",
|
||||
"resolved": "git+ssh://git@github.com/jitsi/lib-jitsi-meet.git#8dfcb42fea0a58d653469d6bb7e2d84e33b566d5",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@@ -27986,8 +27985,8 @@
|
||||
}
|
||||
},
|
||||
"lib-jitsi-meet": {
|
||||
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1845.0.0+515a927c/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-UzT6skfD8wqs2695oMi3B8SxcImhG2SIYwc1xM+z9pVlJ4MAOWsLc7flCBXSnpnUTK3b5AjCOot3cRqeMibNcQ==",
|
||||
"version": "git+ssh://git@github.com/jitsi/lib-jitsi-meet.git#8dfcb42fea0a58d653469d6bb7e2d84e33b566d5",
|
||||
"from": "lib-jitsi-meet@https://github.com/jitsi/lib-jitsi-meet#release-8100",
|
||||
"requires": {
|
||||
"@jitsi/js-utils": "2.2.1",
|
||||
"@jitsi/logger": "2.0.2",
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
"js-md5": "0.6.1",
|
||||
"js-sha512": "0.8.0",
|
||||
"jwt-decode": "2.2.0",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1845.0.0+515a927c/lib-jitsi-meet.tgz",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet#release-8100",
|
||||
"lodash": "4.17.21",
|
||||
"moment": "2.29.4",
|
||||
"moment-duration-format": "2.2.2",
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
authenticateAndUpgradeRole,
|
||||
cancelLogin
|
||||
} from '../../actions.web';
|
||||
import logger from '../../logger';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link LoginDialog}.
|
||||
@@ -134,6 +135,7 @@ class LoginDialog extends Component<IProps, IState> {
|
||||
if (conference) {
|
||||
dispatch(authenticateAndUpgradeRole(jid, password, conference));
|
||||
} else {
|
||||
logger.info('Dispatching connect from LoginDialog.');
|
||||
dispatch(connect(jid, password));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ export default class BaseApp<P> extends Component<P, IState> {
|
||||
* @see {@link #_initStorage}
|
||||
* @type {Promise}
|
||||
*/
|
||||
this._init = createDeferred();
|
||||
this._init = createDeferred<void>();
|
||||
|
||||
try {
|
||||
await this._initStorage();
|
||||
|
||||
@@ -56,7 +56,7 @@ export const CONFERENCE_LEFT = 'CONFERENCE_LEFT';
|
||||
/**
|
||||
* The type of (redux) action which signals that the conference is out of focus.
|
||||
* For example, if the user navigates to the Chat screen.
|
||||
*
|
||||
*
|
||||
* {
|
||||
* type: CONFERENCE_BLURRED,
|
||||
* }
|
||||
@@ -65,7 +65,7 @@ export const CONFERENCE_BLURRED = 'CONFERENCE_BLURRED';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which signals that the conference is in focus.
|
||||
*
|
||||
*
|
||||
* {
|
||||
* type: CONFERENCE_FOCUSED,
|
||||
* }
|
||||
@@ -258,6 +258,17 @@ export const SEND_TONES = 'SEND_TONES';
|
||||
*/
|
||||
export const SET_FOLLOW_ME = 'SET_FOLLOW_ME';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which updates the current known status of the
|
||||
* Follow Me feature that is used only by the recorder.
|
||||
*
|
||||
* {
|
||||
* type: SET_FOLLOW_ME_RECORDER,
|
||||
* enabled: boolean
|
||||
* }
|
||||
*/
|
||||
export const SET_FOLLOW_ME_RECORDER = 'SET_FOLLOW_ME_RECORDER';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which sets the obfuscated room name.
|
||||
*
|
||||
@@ -338,7 +349,7 @@ export const SET_START_MUTED_POLICY = 'SET_START_MUTED_POLICY';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which updates the assumed bandwidth bps.
|
||||
*
|
||||
*
|
||||
* {
|
||||
* type: SET_ASSUMED_BANDWIDTH_BPS,
|
||||
* assumedBandwidthBps: number
|
||||
|
||||
@@ -61,6 +61,7 @@ import {
|
||||
SEND_TONES,
|
||||
SET_ASSUMED_BANDWIDTH_BPS,
|
||||
SET_FOLLOW_ME,
|
||||
SET_FOLLOW_ME_RECORDER,
|
||||
SET_OBFUSCATED_ROOM,
|
||||
SET_PASSWORD,
|
||||
SET_PASSWORD_FAILED,
|
||||
@@ -840,6 +841,22 @@ export function setFollowMe(enabled: boolean) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables or disables the Follow Me feature used only for the recorder.
|
||||
*
|
||||
* @param {boolean} enabled - Whether Follow Me should be enabled and used only by the recorder.
|
||||
* @returns {{
|
||||
* type: SET_FOLLOW_ME_RECORDER,
|
||||
* enabled: boolean
|
||||
* }}
|
||||
*/
|
||||
export function setFollowMeRecorder(enabled: boolean) {
|
||||
return {
|
||||
type: SET_FOLLOW_ME_RECORDER,
|
||||
enabled
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables or disables the Mute reaction sounds feature.
|
||||
*
|
||||
@@ -1069,6 +1086,7 @@ export function redirect(vnode: string, focusJid: string, username: string) {
|
||||
})
|
||||
.then(() => {
|
||||
dispatch(conferenceWillInit());
|
||||
logger.info(`Dispatching connect from redirect (visitor = ${Boolean(vnode)}).`);
|
||||
|
||||
return dispatch(connect());
|
||||
})
|
||||
|
||||
@@ -614,37 +614,6 @@ function _setRoom({ dispatch, getState }: IStore, next: Function, action: AnyAct
|
||||
return next(action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronizes local tracks from state with local tracks in JitsiConference
|
||||
* instance.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @param {Object} action - Action object.
|
||||
* @private
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function _syncConferenceLocalTracksWithState({ getState }: IStore, action: AnyAction) {
|
||||
const state = getState();
|
||||
const conference = getCurrentConference(state);
|
||||
let promise;
|
||||
|
||||
if (conference) {
|
||||
const track = action.track.jitsiTrack;
|
||||
|
||||
if (action.type === TRACK_ADDED) {
|
||||
// If gUM is slow and tracks are created after the user has already joined the conference, avoid
|
||||
// adding the tracks to the conference if the user is a visitor.
|
||||
if (!iAmVisitor(state)) {
|
||||
promise = _addLocalTracksToConference(conference, [ track ]);
|
||||
}
|
||||
} else {
|
||||
promise = _removeLocalTracksFromConference(conference, [ track ]);
|
||||
}
|
||||
}
|
||||
|
||||
return promise || Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the feature base/conference that the action {@code TRACK_ADDED}
|
||||
* or {@code TRACK_REMOVED} is being dispatched within a specific redux store.
|
||||
@@ -664,9 +633,28 @@ function _trackAddedOrRemoved(store: IStore, next: Function, action: AnyAction)
|
||||
|
||||
// TODO All track swapping should happen here instead of conference.js.
|
||||
if (track?.local) {
|
||||
return (
|
||||
_syncConferenceLocalTracksWithState(store, action)
|
||||
.then(() => next(action)));
|
||||
const { getState } = store;
|
||||
const state = getState();
|
||||
const conference = getCurrentConference(state);
|
||||
let promise;
|
||||
|
||||
if (conference) {
|
||||
const jitsiTrack = action.track.jitsiTrack;
|
||||
|
||||
if (action.type === TRACK_ADDED) {
|
||||
// If gUM is slow and tracks are created after the user has already joined the conference, avoid
|
||||
// adding the tracks to the conference if the user is a visitor.
|
||||
if (!iAmVisitor(state)) {
|
||||
promise = _addLocalTracksToConference(conference, [ jitsiTrack ]);
|
||||
}
|
||||
} else {
|
||||
promise = _removeLocalTracksFromConference(conference, [ jitsiTrack ]);
|
||||
}
|
||||
|
||||
if (promise) {
|
||||
return promise.then(() => next(action));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return next(action);
|
||||
|
||||
@@ -149,12 +149,15 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
break;
|
||||
}
|
||||
case CONNECTION_ESTABLISHED: {
|
||||
if (isPrejoinPageVisible(getState())) {
|
||||
let { initialGUMPromise } = getState()['features/base/media'];
|
||||
const { initialGUMPromise } = getState()['features/base/media'];
|
||||
const promise = initialGUMPromise ? initialGUMPromise.promise : Promise.resolve({ tracks: [] });
|
||||
const prejoinVisible = isPrejoinPageVisible(getState());
|
||||
|
||||
initialGUMPromise = initialGUMPromise || Promise.resolve({ tracks: [] });
|
||||
logger.debug(`On connection established: prejoinVisible: ${prejoinVisible}, initialGUMPromiseExists=${
|
||||
Boolean(initialGUMPromise)}, promiseExists=${Boolean(promise)}`);
|
||||
|
||||
initialGUMPromise.then(() => {
|
||||
if (prejoinVisible) {
|
||||
promise.then(() => {
|
||||
const state = getState();
|
||||
let localTracks = getLocalTracks(state['features/base/tracks']);
|
||||
const trackReplacePromises = [];
|
||||
@@ -186,11 +189,7 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
});
|
||||
});
|
||||
} else {
|
||||
let { initialGUMPromise } = getState()['features/base/media'];
|
||||
|
||||
initialGUMPromise = initialGUMPromise || Promise.resolve({ tracks: [] });
|
||||
|
||||
initialGUMPromise.then(({ tracks }) => {
|
||||
promise.then(({ tracks }) => {
|
||||
let tracksToUse = tracks ?? [];
|
||||
|
||||
if (iAmVisitor(getState())) {
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
P2P_STATUS_CHANGED,
|
||||
SET_ASSUMED_BANDWIDTH_BPS,
|
||||
SET_FOLLOW_ME,
|
||||
SET_FOLLOW_ME_RECORDER,
|
||||
SET_OBFUSCATED_ROOM,
|
||||
SET_PASSWORD,
|
||||
SET_PENDING_SUBJECT_CHANGE,
|
||||
@@ -159,6 +160,7 @@ export interface IConferenceState {
|
||||
e2eeSupported?: boolean;
|
||||
error?: Error;
|
||||
followMeEnabled?: boolean;
|
||||
followMeRecorderEnabled?: boolean;
|
||||
joining?: IJitsiConference;
|
||||
leaving?: IJitsiConference;
|
||||
lobbyWaitingForHost?: boolean;
|
||||
@@ -251,6 +253,12 @@ ReducerRegistry.register<IConferenceState>('features/base/conference',
|
||||
case SET_FOLLOW_ME:
|
||||
return set(state, 'followMeEnabled', action.enabled);
|
||||
|
||||
case SET_FOLLOW_ME_RECORDER:
|
||||
return { ...state,
|
||||
followMeRecorderEnabled: action.enabled,
|
||||
followMeEnabled: action.enabled
|
||||
};
|
||||
|
||||
case SET_START_REACTIONS_MUTED:
|
||||
return set(state, 'startReactionsMuted', action.muted);
|
||||
|
||||
|
||||
@@ -375,7 +375,6 @@ export interface IConfig {
|
||||
giphy?: {
|
||||
displayMode?: 'all' | 'tile' | 'chat';
|
||||
enabled?: boolean;
|
||||
proxyUrl?: string;
|
||||
rating?: 'g' | 'pg' | 'pg-13' | 'r';
|
||||
sdkKey?: string;
|
||||
tileTime?: number;
|
||||
@@ -526,6 +525,7 @@ export interface IConfig {
|
||||
hideLobbyButton?: boolean;
|
||||
};
|
||||
serviceUrl?: string;
|
||||
sharedVideoAllowedURLDomains?: Array<string>;
|
||||
sipInviteUrl?: string;
|
||||
speakerStats?: {
|
||||
disableSearch?: boolean;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { AnyAction, combineReducers } from 'redux';
|
||||
import { CONFERENCE_FAILED, CONFERENCE_LEFT } from '../conference/actionTypes';
|
||||
import ReducerRegistry from '../redux/ReducerRegistry';
|
||||
import { TRACK_REMOVED } from '../tracks/actionTypes';
|
||||
import { DefferedPromise, createDeferred } from '../util/helpers';
|
||||
|
||||
import {
|
||||
GUM_PENDING,
|
||||
@@ -88,6 +89,12 @@ function _audio(state: IAudioState = _AUDIO_INITIAL_MEDIA_STATE, action: AnyActi
|
||||
}
|
||||
}
|
||||
|
||||
// Using a deferred promise here to make sure that once the connection is established even if conference.init and the
|
||||
// initial track creation haven't been started we would wait for it to finish before starting to join the room.
|
||||
// NOTE: The previous implementation was using the GUM promise from conference.init. But it turned out that connect
|
||||
// may finish even before conference.init is executed.
|
||||
const DEFAULT_INITIAL_PROMISE_STATE = createDeferred<IInitialGUMPromiseResult>();
|
||||
|
||||
/**
|
||||
* Reducer fot the common properties in media state.
|
||||
*
|
||||
@@ -96,7 +103,8 @@ function _audio(state: IAudioState = _AUDIO_INITIAL_MEDIA_STATE, action: AnyActi
|
||||
* @param {string} action.type - Type of action.
|
||||
* @returns {ICommonState}
|
||||
*/
|
||||
function _initialGUMPromise(state: initialGUMPromise | null = null, action: AnyAction) {
|
||||
function _initialGUMPromise(state: DefferedPromise<IInitialGUMPromiseResult> | null = DEFAULT_INITIAL_PROMISE_STATE,
|
||||
action: AnyAction) {
|
||||
if (action.type === SET_INITIAL_GUM_PROMISE) {
|
||||
return action.promise ?? null;
|
||||
}
|
||||
@@ -264,10 +272,10 @@ interface IAudioState {
|
||||
unmuteBlocked: boolean;
|
||||
}
|
||||
|
||||
type initialGUMPromise = Promise<{
|
||||
errors?: any;
|
||||
tracks: Array<any>;
|
||||
}> | null;
|
||||
interface IInitialGUMPromiseResult {
|
||||
errors?: any;
|
||||
tracks: Array<any>;
|
||||
}
|
||||
|
||||
interface IScreenshareState {
|
||||
available: boolean;
|
||||
@@ -286,7 +294,7 @@ interface IVideoState {
|
||||
|
||||
export interface IMediaState {
|
||||
audio: IAudioState;
|
||||
initialGUMPromise: initialGUMPromise;
|
||||
initialGUMPromise: DefferedPromise<IInitialGUMPromiseResult> | null;
|
||||
screenshare: IScreenshareState;
|
||||
video: IVideoState;
|
||||
}
|
||||
|
||||
@@ -547,7 +547,7 @@ export function participantKicked(kicker: any, kicked: any) {
|
||||
const state = getState();
|
||||
const localParticipant = getLocalParticipant(state);
|
||||
const kickedId = kicked.getId();
|
||||
const kickerId = kicker.getId();
|
||||
const kickerId = kicker?.getId();
|
||||
|
||||
dispatch({
|
||||
type: PARTICIPANT_KICKED,
|
||||
@@ -555,7 +555,7 @@ export function participantKicked(kicker: any, kicked: any) {
|
||||
kicker: kickerId
|
||||
});
|
||||
|
||||
if (kicked.isReplaced?.() || kickerId === localParticipant?.id) {
|
||||
if (kicked.isReplaced?.() || !kickerId || kickerId === localParticipant?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
isTrackStreamingStatusInactive
|
||||
} from '../../../connection-indicator/functions';
|
||||
import SharedVideo from '../../../shared-video/components/native/SharedVideo';
|
||||
import { isSharedVideoEnabled } from '../../../shared-video/functions';
|
||||
import { IStateful } from '../../app/types';
|
||||
import Avatar from '../../avatar/components/Avatar';
|
||||
import { translate } from '../../i18n/functions';
|
||||
@@ -236,7 +237,8 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
|
||||
_isConnectionInactive: isTrackStreamingStatusInactive(videoTrack),
|
||||
_isSharedVideoParticipant: isSharedVideoParticipant(participant),
|
||||
_participantName: getParticipantDisplayName(state, participantId),
|
||||
_renderVideo: shouldRenderParticipantVideo(state, participantId) && !disableVideo,
|
||||
_renderVideo: shouldRenderParticipantVideo(state, participantId) && !disableVideo
|
||||
&& isSharedVideoEnabled(state),
|
||||
_videoTrack: videoTrack
|
||||
};
|
||||
}
|
||||
|
||||
@@ -33,7 +33,8 @@ StateListenerRegistry.register(
|
||||
StateListenerRegistry.register(
|
||||
/* selector */ state => state['features/base/participants'].raisedHandsQueue,
|
||||
/* listener */ (raisedHandsQueue, store) => {
|
||||
if (raisedHandsQueue.length && isNextToSpeak(store.getState()) && !hasBeenNotified(store.getState())) {
|
||||
if (raisedHandsQueue.length && isNextToSpeak(store.getState()) && !hasBeenNotified(store.getState())
|
||||
&& !store.getState()['features/visitors'].iAmVisitor) { // visitors raise hand to be promoted
|
||||
_notifyNextSpeakerInRaisedHandQueue(store);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
import React, { Component, ReactNode } from 'react';
|
||||
import { toArray } from 'react-emoji-render';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../../app/types';
|
||||
import GifMessage from '../../../../chat/components/web/GifMessage';
|
||||
import { GIF_PREFIX } from '../../../../gifs/constants';
|
||||
import { isGifMessage } from '../../../../gifs/functions.web';
|
||||
import { isGifEnabled, isGifMessage } from '../../../../gifs/functions.web';
|
||||
|
||||
import Linkify from './Linkify';
|
||||
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* Whether the gifs are enabled or not.
|
||||
*/
|
||||
gifEnabled: boolean;
|
||||
|
||||
/**
|
||||
* The body of the message.
|
||||
*/
|
||||
@@ -43,11 +50,11 @@ class Message extends Component<IProps> {
|
||||
|
||||
// Tokenize the text in order to avoid emoji substitution for URLs
|
||||
const tokens = text ? text.split(' ') : [];
|
||||
|
||||
const content = [];
|
||||
const { gifEnabled } = this.props;
|
||||
|
||||
// check if the message is a GIF
|
||||
if (isGifMessage(text)) {
|
||||
if (gifEnabled && isGifMessage(text)) {
|
||||
const url = text.substring(GIF_PREFIX.length, text.length - 1);
|
||||
|
||||
content.push(<GifMessage
|
||||
@@ -93,4 +100,16 @@ class Message extends Component<IProps> {
|
||||
}
|
||||
}
|
||||
|
||||
export default Message;
|
||||
/**
|
||||
* Maps part of the redux state to the props of this component.
|
||||
*
|
||||
* @param {IReduxState} state - The Redux state.
|
||||
* @returns {IProps}
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState) {
|
||||
return {
|
||||
gifEnabled: isGifEnabled(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps)(Message);
|
||||
|
||||
@@ -178,18 +178,20 @@ function _getUserSelectedDeviceId(options: {
|
||||
replacement = ''
|
||||
} = options;
|
||||
|
||||
// If there is no label at all, there is no need to fall back to checking
|
||||
// the label for a fuzzy match.
|
||||
if (!userSelectedDeviceLabel || !userSelectedDeviceId) {
|
||||
return userSelectedDeviceId;
|
||||
if (userSelectedDeviceId) {
|
||||
const foundMatchingBasedonDeviceId = availableDevices?.find(
|
||||
candidate => candidate.deviceId === userSelectedDeviceId);
|
||||
|
||||
// Prioritize matching the deviceId
|
||||
if (foundMatchingBasedonDeviceId) {
|
||||
return userSelectedDeviceId;
|
||||
}
|
||||
}
|
||||
|
||||
const foundMatchingBasedonDeviceId = availableDevices?.find(
|
||||
candidate => candidate.deviceId === userSelectedDeviceId);
|
||||
|
||||
// Prioritize matching the deviceId
|
||||
if (foundMatchingBasedonDeviceId) {
|
||||
return userSelectedDeviceId;
|
||||
// If there is no label at all, there is no need to fall back to checking
|
||||
// the label for a fuzzy match.
|
||||
if (!userSelectedDeviceLabel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const strippedDeviceLabel
|
||||
|
||||
@@ -22,16 +22,21 @@ export function assignIfDefined(target: Object, source: Object) {
|
||||
return to;
|
||||
}
|
||||
|
||||
export type DefferedPromise<T> = {
|
||||
promise: Promise<T>;
|
||||
reject: (reason?: any) => void;
|
||||
resolve: (value: T) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a deferred object.
|
||||
*
|
||||
* @returns {{promise, resolve, reject}}
|
||||
*/
|
||||
export function createDeferred() {
|
||||
const deferred: any = {};
|
||||
export function createDeferred<T>() {
|
||||
const deferred = {} as DefferedPromise<T>;
|
||||
|
||||
deferred.promise = new Promise((resolve, reject) => {
|
||||
deferred.promise = new Promise<T>((resolve, reject) => {
|
||||
deferred.resolve = resolve;
|
||||
deferred.reject = reject;
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ import { IReduxState } from '../../../app/types';
|
||||
import Avatar from '../../../base/avatar/components/Avatar';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import Linkify from '../../../base/react/components/native/Linkify';
|
||||
import { isGifMessage } from '../../../gifs/functions.native';
|
||||
import { isGifEnabled, isGifMessage } from '../../../gifs/functions.native';
|
||||
import { MESSAGE_TYPE_ERROR, MESSAGE_TYPE_LOCAL } from '../../constants';
|
||||
import {
|
||||
getCanReplyToMessage,
|
||||
@@ -32,7 +32,7 @@ class ChatMessage extends Component<IChatMessageProps> {
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const { message, knocking } = this.props;
|
||||
const { gifEnabled, message, knocking } = this.props;
|
||||
const localMessage = message.messageType === MESSAGE_TYPE_LOCAL;
|
||||
const { privateMessage, lobbyChat } = message;
|
||||
|
||||
@@ -82,7 +82,7 @@ class ChatMessage extends Component<IChatMessageProps> {
|
||||
<View style = { messageBubbleStyle }>
|
||||
<View style = { styles.textWrapper as ViewStyle } >
|
||||
{ this._renderDisplayName() }
|
||||
{ isGifMessage(messageText)
|
||||
{ gifEnabled && isGifMessage(messageText)
|
||||
? <GifMessage message = { messageText } />
|
||||
: (
|
||||
<Linkify
|
||||
@@ -210,6 +210,7 @@ class ChatMessage extends Component<IChatMessageProps> {
|
||||
function _mapStateToProps(state: IReduxState, { message }: IChatMessageProps) {
|
||||
return {
|
||||
canReply: getCanReplyToMessage(state, message),
|
||||
gifEnabled: isGifEnabled(state),
|
||||
knocking: state['features/lobby'].knocking
|
||||
};
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ import StateListenerRegistry from '../base/redux/StateListenerRegistry';
|
||||
import { playSound, registerSound, unregisterSound } from '../base/sounds/actions';
|
||||
import { addGif } from '../gifs/actions';
|
||||
import { GIF_PREFIX } from '../gifs/constants';
|
||||
import { getGifDisplayMode, isGifMessage } from '../gifs/function.any';
|
||||
import { getGifDisplayMode, isGifEnabled, isGifMessage } from '../gifs/function.any';
|
||||
import { showMessageNotification } from '../notifications/actions';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE } from '../notifications/constants';
|
||||
import { resetNbUnreadPollsMessages } from '../polls/actions';
|
||||
@@ -314,7 +314,7 @@ function _addChatMsgListener(conference: IJitsiConference, store: IStore) {
|
||||
function _onConferenceMessageReceived(store: IStore, { displayName, id, isGuest, message, timestamp, privateMessage }: {
|
||||
displayName?: string; id: string; isGuest?: boolean;
|
||||
message: string; privateMessage: boolean; timestamp: number; }) {
|
||||
const isGif = isGifMessage(message);
|
||||
const isGif = isGifEnabled(store.getState()) && isGifMessage(message);
|
||||
|
||||
if (isGif) {
|
||||
_handleGifMessageReceived(store, id, message);
|
||||
|
||||
@@ -44,6 +44,11 @@ export interface IChatMessageProps extends WithTranslation {
|
||||
*/
|
||||
canReply?: boolean;
|
||||
|
||||
/**
|
||||
* Whether gifs are enabled or not.
|
||||
*/
|
||||
gifEnabled?: boolean;
|
||||
|
||||
/**
|
||||
* Whether current participant is currently knocking in the lobby room.
|
||||
*/
|
||||
|
||||
@@ -49,9 +49,11 @@ export function setupInitialDevices() {
|
||||
/**
|
||||
* Init.
|
||||
*
|
||||
* @param {boolean} shouldDispatchConnect - Whether or not connect should be dispatched. This should be false only when
|
||||
* prejoin is enabled.
|
||||
* @returns {Promise<JitsiConnection>}
|
||||
*/
|
||||
export function init() {
|
||||
export function init(shouldDispatchConnect: boolean) {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const room = getBackendSafeRoomName(getState()['features/base/conference'].room);
|
||||
|
||||
@@ -59,7 +61,8 @@ export function init() {
|
||||
// from the old app (at the moment of writing).
|
||||
return dispatch(setupInitialDevices()).then(
|
||||
() => APP.conference.init({
|
||||
roomName: room
|
||||
roomName: room,
|
||||
shouldDispatchConnect
|
||||
}).catch((error: Error) => {
|
||||
APP.API.notifyConferenceLeft(APP.conference.roomName);
|
||||
logger.error(error);
|
||||
|
||||
@@ -104,12 +104,24 @@ interface IProps extends AbstractProps, WithTranslation {
|
||||
|
||||
/**
|
||||
* If visitors queue page is visible or not.
|
||||
* NOTE: This should be set to true once we received an error on connect. Before the first connect this will always
|
||||
* be false.
|
||||
*/
|
||||
_showVisitorsQueue: boolean;
|
||||
|
||||
dispatch: IStore['dispatch'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the prejoin screen should be displayed and false otherwise.
|
||||
*
|
||||
* @param {IProps} props - The props object.
|
||||
* @returns {boolean} - True if the prejoin screen should be displayed and false otherwise.
|
||||
*/
|
||||
function shouldShowPrejoin({ _showPrejoin, _showVisitorsQueue }: IProps) {
|
||||
return _showPrejoin && !_showVisitorsQueue;
|
||||
}
|
||||
|
||||
/**
|
||||
* The conference page of the Web application.
|
||||
*/
|
||||
@@ -265,7 +277,7 @@ class Conference extends AbstractConference<IProps, any> {
|
||||
|
||||
<CalleeInfoContainer />
|
||||
|
||||
{ (_showPrejoin && !_showVisitorsQueue) && <Prejoin />}
|
||||
{ shouldShowPrejoin(this.props) && <Prejoin />}
|
||||
{ (_showLobby && !_showVisitorsQueue) && <LobbyScreen />}
|
||||
{ _showVisitorsQueue && <VisitorsQueue />}
|
||||
</div>
|
||||
@@ -384,7 +396,9 @@ class Conference extends AbstractConference<IProps, any> {
|
||||
|
||||
const { dispatch, t } = this.props;
|
||||
|
||||
dispatch(init());
|
||||
// if we will be showing prejoin we don't want to call connect from init.
|
||||
// Connect will be dispatched from prejoin screen.
|
||||
dispatch(init(!shouldShowPrejoin(this.props)));
|
||||
|
||||
maybeShowSuboptimalExperienceNotification(dispatch, t);
|
||||
}
|
||||
|
||||
@@ -157,6 +157,7 @@ export interface IDynamicBrandingState {
|
||||
logoImageUrl: string;
|
||||
muiBrandedTheme?: boolean;
|
||||
premeetingBackground: string;
|
||||
sharedVideoAllowedURLDomains?: Array<string>;
|
||||
showGiphyIntegration?: boolean;
|
||||
useDynamicBrandingData: boolean;
|
||||
virtualBackgrounds: Array<Image>;
|
||||
@@ -181,6 +182,7 @@ ReducerRegistry.register<IDynamicBrandingState>(STORE_NAME, (state = DEFAULT_STA
|
||||
logoImageUrl,
|
||||
muiBrandedTheme,
|
||||
premeetingBackground,
|
||||
sharedVideoAllowedURLDomains,
|
||||
showGiphyIntegration,
|
||||
virtualBackgrounds
|
||||
} = action.value;
|
||||
@@ -198,6 +200,7 @@ ReducerRegistry.register<IDynamicBrandingState>(STORE_NAME, (state = DEFAULT_STA
|
||||
logoImageUrl,
|
||||
muiBrandedTheme,
|
||||
premeetingBackground,
|
||||
sharedVideoAllowedURLDomains,
|
||||
showGiphyIntegration,
|
||||
customizationFailed: false,
|
||||
customizationReady: true,
|
||||
|
||||
@@ -193,8 +193,8 @@ class FaceLandmarksDetector {
|
||||
const state = getState();
|
||||
const localVideoTrack = track || getLocalVideoTrack(state['features/base/tracks']);
|
||||
|
||||
if (localVideoTrack === undefined) {
|
||||
logger.warn('Face landmarks detection is disabled due to missing local track.');
|
||||
if (!localVideoTrack || localVideoTrack.jitsiTrack?.isMuted()) {
|
||||
logger.debug('Face landmarks detection is disabled due to missing local track.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -7,15 +7,18 @@ import {
|
||||
* Sets the current moderator id or clears it.
|
||||
*
|
||||
* @param {?string} id - The Follow Me moderator participant id.
|
||||
* @param {?boolean} forRecorder - Whether this is command only for recorder.
|
||||
* @returns {{
|
||||
* type: SET_FOLLOW_ME_MODERATOR,
|
||||
* id, string
|
||||
* id: string,
|
||||
* forRecorder: boolean
|
||||
* }}
|
||||
*/
|
||||
export function setFollowMeModerator(id?: string) {
|
||||
export function setFollowMeModerator(id?: string, forRecorder?: boolean) {
|
||||
return {
|
||||
type: SET_FOLLOW_ME_MODERATOR,
|
||||
id
|
||||
id,
|
||||
forRecorder
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -13,3 +13,16 @@ export function isFollowMeActive(stateful: IStateful) {
|
||||
|
||||
return Boolean(state['features/follow-me'].moderator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if follow me is active only for the recorder and false otherwise.
|
||||
*
|
||||
* @param {Object|Function} stateful - Object or function that can be resolved
|
||||
* to the Redux state.
|
||||
* @returns {boolean} - True if follow me is active and false otherwise.
|
||||
*/
|
||||
export function isFollowMeRecorderActive(stateful: IStateful) {
|
||||
const state = toState(stateful);
|
||||
|
||||
return Boolean(state['features/follow-me'].recorder);
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ function _onFollowMeCommand(attributes: any = {}, id: string, store: IStore) {
|
||||
}
|
||||
|
||||
if (!isFollowMeActive(state)) {
|
||||
store.dispatch(setFollowMeModerator(id));
|
||||
store.dispatch(setFollowMeModerator(id, attributes.recorder));
|
||||
}
|
||||
|
||||
// just a command that follow me was turned off
|
||||
@@ -138,6 +138,11 @@ function _onFollowMeCommand(attributes: any = {}, id: string, store: IStore) {
|
||||
return;
|
||||
}
|
||||
|
||||
// when recorder flag is on, follow me is handled only on recorder side
|
||||
if (attributes.recorder && !store.getState()['features/base/config'].iAmRecorder) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldState = state['features/follow-me'].state || {};
|
||||
|
||||
store.dispatch(setFollowMeState(attributes));
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
|
||||
export interface IFollowMeState {
|
||||
moderator?: string;
|
||||
recorder?: boolean;
|
||||
state?: {
|
||||
[key: string]: string;
|
||||
};
|
||||
@@ -24,9 +25,12 @@ ReducerRegistry.register<IFollowMeState>(
|
||||
case SET_FOLLOW_ME_MODERATOR: {
|
||||
let newState = set(state, 'moderator', action.id);
|
||||
|
||||
if (!action.id) {
|
||||
if (action.id) {
|
||||
newState = set(newState, 'recorder', action.forRecorder);
|
||||
} else {
|
||||
// clear the state if feature becomes disabled
|
||||
newState = set(newState, 'state', undefined);
|
||||
newState = set(newState, 'recorder', undefined);
|
||||
}
|
||||
|
||||
return newState;
|
||||
|
||||
@@ -88,6 +88,7 @@ function _getFollowMeState(state: IReduxState) {
|
||||
const stageFilmstrip = isStageFilmstripEnabled(state);
|
||||
|
||||
return {
|
||||
recorder: state['features/base/conference'].followMeRecorderEnabled,
|
||||
filmstripVisible: state['features/filmstrip'].visible,
|
||||
maxStageParticipants: stageFilmstrip ? state['features/base/settings'].maxStageParticipants : undefined,
|
||||
nextOnStage: pinnedParticipant?.id,
|
||||
|
||||
@@ -9,7 +9,7 @@ import JitsiScreen from '../../../base/modal/components/JitsiScreen';
|
||||
import Input from '../../../base/ui/components/native/Input';
|
||||
import { sendMessage } from '../../../chat/actions.any';
|
||||
import { goBack } from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
|
||||
import { formatGifUrlMessage, getGifRating, getGifUrl, getGiphyProxyUrl } from '../../functions.native';
|
||||
import { formatGifUrlMessage, getGifRating, getGifUrl } from '../../functions.native';
|
||||
|
||||
import GifsMenuFooter from './GifsMenuFooter';
|
||||
import styles from './styles';
|
||||
@@ -19,8 +19,6 @@ const GifsMenu = () => {
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const rating = useSelector(getGifRating) as GiphyRating;
|
||||
const proxyUrl = useSelector(getGiphyProxyUrl);
|
||||
|
||||
const options = {
|
||||
mediaType: GiphyMediaType.Gif,
|
||||
limit: 20,
|
||||
@@ -35,7 +33,7 @@ const GifsMenu = () => {
|
||||
});
|
||||
|
||||
const sendGif = useCallback(e => {
|
||||
const url = getGifUrl(e.nativeEvent.media, proxyUrl);
|
||||
const url = getGifUrl(e.nativeEvent.media);
|
||||
|
||||
sendAnalytics(createGifSentEvent());
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { GiphyFetch, TrendingOptions, setServerUrl } from '@giphy/js-fetch-api';
|
||||
import { GiphyFetch, TrendingOptions } from '@giphy/js-fetch-api';
|
||||
import { Grid } from '@giphy/react-components';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -20,8 +20,7 @@ import {
|
||||
formatGifUrlMessage,
|
||||
getGifAPIKey,
|
||||
getGifRating,
|
||||
getGifUrl,
|
||||
getGiphyProxyUrl
|
||||
getGifUrl
|
||||
} from '../../function.any';
|
||||
|
||||
const OVERFLOW_DRAWER_PADDING = 16;
|
||||
@@ -104,7 +103,6 @@ function GifsMenu({ columns = 2, parent }: IProps) {
|
||||
= parent === IReactionsMenuParent.OverflowDrawer || parent === IReactionsMenuParent.OverflowMenu;
|
||||
const { clientWidth } = useSelector((state: IReduxState) => state['features/base/responsive-ui']);
|
||||
const rating = useSelector(getGifRating);
|
||||
const proxyUrl = useSelector(getGiphyProxyUrl);
|
||||
|
||||
const fetchGifs = useCallback(async (offset = 0) => {
|
||||
const options: TrendingOptions = {
|
||||
@@ -126,7 +124,7 @@ function GifsMenu({ columns = 2, parent }: IProps) {
|
||||
|
||||
const handleGifClick = useCallback((gif, e) => {
|
||||
e?.stopPropagation();
|
||||
const url = getGifUrl(gif, proxyUrl);
|
||||
const url = getGifUrl(gif);
|
||||
|
||||
sendAnalytics(createGifSentEvent());
|
||||
batch(() => {
|
||||
@@ -189,12 +187,6 @@ function GifsMenu({ columns = 2, parent }: IProps) {
|
||||
// This fixes that.
|
||||
useEffect(() => setSearchKey(''), []);
|
||||
|
||||
useEffect(() => {
|
||||
if (proxyUrl) {
|
||||
setServerUrl(proxyUrl);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onInputKeyPress = useCallback((e: React.KeyboardEvent) => {
|
||||
e.stopPropagation();
|
||||
}, []);
|
||||
|
||||
@@ -33,15 +33,6 @@ export function getGifRating(state: IReduxState) {
|
||||
return getGifConfig(state).rating || GIF_DEFAULT_RATING;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Giphy proxy url.
|
||||
*
|
||||
* @param {IReduxState} state - Redux state.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getGiphyProxyUrl(state: IReduxState) {
|
||||
return getGifConfig(state).proxyUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the URL of the GIF for the given participant or null if there's none.
|
||||
@@ -54,6 +45,26 @@ export function getGifForParticipant(state: IReduxState, participantId: string):
|
||||
return isGifEnabled(state) ? state['features/gifs'].gifList.get(participantId) || {} : {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a given URL is allowed to be rendered as gif and false otherwise.
|
||||
*
|
||||
* @param {string} url - The URL to be validated.
|
||||
* @returns {boolean} - True if a given URL is allowed to be rendered as gif and false otherwise.
|
||||
*/
|
||||
export function isGifUrlAllowed(url: string) {
|
||||
let hostname: string | undefined;
|
||||
|
||||
try {
|
||||
const urlObject = new URL(url);
|
||||
|
||||
hostname = urlObject?.hostname;
|
||||
} catch (_error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return hostname === 'i.giphy.com';
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the message is a GIF message.
|
||||
*
|
||||
@@ -61,26 +72,23 @@ export function getGifForParticipant(state: IReduxState, participantId: string):
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isGifMessage(message: string) {
|
||||
const url = message.substring(GIF_PREFIX.length, message.length - 1);
|
||||
|
||||
return message.trim().toLowerCase()
|
||||
.startsWith(GIF_PREFIX);
|
||||
.startsWith(GIF_PREFIX) && isGifUrlAllowed(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the url of the gif selected in the gifs menu.
|
||||
*
|
||||
* @param {Object} gif - The gif data.
|
||||
* @param {string} proxyUrl - The proxy server url.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function getGifUrl(gif?: { data?: { embed_url: string; }; embed_url?: string; }, proxyUrl?: string) {
|
||||
export function getGifUrl(gif?: { data?: { embed_url: string; }; embed_url?: string; }) {
|
||||
const embedUrl = gif?.embed_url || gif?.data?.embed_url || '';
|
||||
const idx = embedUrl.lastIndexOf('/');
|
||||
const id = embedUrl.substr(idx + 1);
|
||||
|
||||
if (proxyUrl) {
|
||||
return `${proxyUrl}gifs/id/${id}`;
|
||||
}
|
||||
|
||||
return `https://i.giphy.com/media/${id}/giphy.gif`;
|
||||
}
|
||||
|
||||
|
||||
@@ -206,6 +206,8 @@ export function startKnocking() {
|
||||
const state = getState();
|
||||
const { membersOnly } = state['features/base/conference'];
|
||||
|
||||
logger.info(`Lobby starting knocking (membersOnly = ${membersOnly})`);
|
||||
|
||||
if (!membersOnly) {
|
||||
|
||||
// no membersOnly, this means we got lobby screen shown as someone
|
||||
|
||||
@@ -280,7 +280,7 @@ function MeetingParticipantItem({
|
||||
</>
|
||||
}
|
||||
|
||||
{!overflowDrawer && (_localVideoOwner || _participant?.fakeParticipant) && (
|
||||
{!overflowDrawer && (_localVideoOwner && _participant?.fakeParticipant) && (
|
||||
<ParticipantActionEllipsis
|
||||
accessibilityLabel = { participantActionEllipsisLabel }
|
||||
onClick = { onContextMenu } />
|
||||
|
||||
@@ -209,6 +209,7 @@ export function joinConference(options?: Object, ignoreJoiningInProgress = false
|
||||
|
||||
options && dispatch(updateConfig(options));
|
||||
|
||||
logger.info('Dispatching connect from joinConference.');
|
||||
dispatch(connect(jid, password))
|
||||
.catch(() => {
|
||||
// There is nothing to do here. This is handled and dispatched in base/connection/actions.
|
||||
@@ -257,6 +258,8 @@ export function joinConferenceWithoutAudio() {
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('Dispatching joinConference action with startSilent=true from joinConferenceWithoutAudio.');
|
||||
|
||||
dispatch(joinConference({
|
||||
startSilent: true
|
||||
}, true));
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
isJoinByPhoneDialogVisible,
|
||||
isPrejoinDisplayNameVisible
|
||||
} from '../../functions';
|
||||
import logger from '../../logger';
|
||||
import { hasDisplayName } from '../../utils';
|
||||
|
||||
import JoinByPhoneDialog from './dialogs/JoinByPhoneDialog';
|
||||
@@ -256,6 +257,9 @@ const Prejoin = ({
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info('Prejoin join button clicked.');
|
||||
|
||||
joinConference();
|
||||
};
|
||||
|
||||
@@ -337,6 +341,7 @@ const Prejoin = ({
|
||||
&& (e.key === ' '
|
||||
|| e.key === 'Enter')) {
|
||||
e.preventDefault();
|
||||
logger.info('Prejoin joinConferenceWithoutAudio dispatched on a key pressed.');
|
||||
joinConferenceWithoutAudio();
|
||||
}
|
||||
};
|
||||
@@ -352,7 +357,10 @@ const Prejoin = ({
|
||||
testId: 'prejoin.joinWithoutAudio',
|
||||
icon: IconVolumeOff,
|
||||
label: t('prejoin.joinWithoutAudio'),
|
||||
onClick: joinConferenceWithoutAudio,
|
||||
onClick: () => {
|
||||
logger.info('Prejoin join conference without audio pressed.');
|
||||
joinConferenceWithoutAudio();
|
||||
},
|
||||
onKeyPress: onJoinConferenceWithoutAudioKeyPress
|
||||
};
|
||||
|
||||
@@ -379,6 +387,7 @@ const Prejoin = ({
|
||||
*/
|
||||
const onInputKeyPress = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
logger.info('Dispatching join conference on Enter key press from the prejoin screen.');
|
||||
joinConference();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ import { setTokenAuthUrlSuccess } from '../authentication/actions.web';
|
||||
import { isTokenAuthEnabled } from '../authentication/functions';
|
||||
import {
|
||||
setFollowMe,
|
||||
setFollowMeRecorder,
|
||||
setStartMutedPolicy,
|
||||
setStartReactionsMuted
|
||||
} from '../base/conference/actions';
|
||||
@@ -165,6 +166,10 @@ export function submitModeratorTab(newState: any) {
|
||||
dispatch(setFollowMe(newState.followMeEnabled));
|
||||
}
|
||||
|
||||
if (newState.followMeRecorderEnabled !== currentState.followMeRecorderEnabled) {
|
||||
dispatch(setFollowMeRecorder(newState.followMeRecorderEnabled));
|
||||
}
|
||||
|
||||
if (newState.startReactionsMuted !== currentState.startReactionsMuted) {
|
||||
batch(() => {
|
||||
// updating settings we want to update and backend (notify the rest of the participants)
|
||||
|
||||
@@ -2,7 +2,12 @@ import React, { useCallback, useMemo } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { setFollowMe, setStartMutedPolicy, setStartReactionsMuted } from '../../../base/conference/actions';
|
||||
import {
|
||||
setFollowMe,
|
||||
setFollowMeRecorder,
|
||||
setStartMutedPolicy,
|
||||
setStartReactionsMuted
|
||||
} from '../../../base/conference/actions';
|
||||
import { updateSettings } from '../../../base/settings/actions';
|
||||
import Switch from '../../../base/ui/components/native/Switch';
|
||||
import { getModeratorTabProps } from '../../functions.native';
|
||||
@@ -13,7 +18,10 @@ import FormSection from './FormSection';
|
||||
const ModeratorSection = () => {
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
followMeActive,
|
||||
followMeEnabled,
|
||||
followMeRecorderActive,
|
||||
followMeRecorderEnabled,
|
||||
startAudioMuted,
|
||||
startVideoMuted,
|
||||
startReactionsMuted
|
||||
@@ -35,29 +43,45 @@ const ModeratorSection = () => {
|
||||
dispatch(setFollowMe(Boolean(enabled)));
|
||||
}, [ dispatch, setFollowMe ]);
|
||||
|
||||
const onFollowMeRecorderToggled = useCallback((enabled?: boolean) => {
|
||||
dispatch(setFollowMeRecorder(Boolean(enabled)));
|
||||
}, [ dispatch, setFollowMeRecorder ]);
|
||||
|
||||
const onStartReactionsMutedToggled = useCallback((enabled?: boolean) => {
|
||||
dispatch(setStartReactionsMuted(Boolean(enabled), true));
|
||||
dispatch(updateSettings({ soundsReactions: enabled }));
|
||||
}, [ dispatch, updateSettings, setStartReactionsMuted ]);
|
||||
|
||||
const followMeRecorderChecked = followMeRecorderEnabled && !followMeRecorderActive;
|
||||
|
||||
const moderationSettings = useMemo(() => {
|
||||
const moderation = [
|
||||
{
|
||||
disabled: false,
|
||||
label: 'settings.startAudioMuted',
|
||||
state: startAudioMuted,
|
||||
onChange: onStartAudioMutedToggled
|
||||
},
|
||||
{
|
||||
disabled: false,
|
||||
label: 'settings.startVideoMuted',
|
||||
state: startVideoMuted,
|
||||
onChange: onStartVideoMutedToggled
|
||||
},
|
||||
{
|
||||
disabled: followMeActive || followMeRecorderActive,
|
||||
label: 'settings.followMe',
|
||||
state: followMeEnabled,
|
||||
state: followMeEnabled && !followMeActive && !followMeRecorderChecked,
|
||||
onChange: onFollowMeToggled
|
||||
},
|
||||
{
|
||||
disabled: followMeRecorderActive || followMeActive,
|
||||
label: 'settings.followMeRecorder',
|
||||
state: followMeRecorderChecked,
|
||||
onChange: onFollowMeRecorderToggled
|
||||
},
|
||||
{
|
||||
disabled: false,
|
||||
label: 'settings.startReactionsMuted',
|
||||
state: startReactionsMuted,
|
||||
onChange: onStartReactionsMutedToggled
|
||||
@@ -72,19 +96,26 @@ const ModeratorSection = () => {
|
||||
}, [ startAudioMuted,
|
||||
startVideoMuted,
|
||||
followMeEnabled,
|
||||
followMeRecorderEnabled,
|
||||
disableReactionsModeration,
|
||||
onStartAudioMutedToggled,
|
||||
onStartVideoMutedToggled,
|
||||
onFollowMeToggled,
|
||||
onFollowMeRecorderToggled,
|
||||
onStartReactionsMutedToggled,
|
||||
startReactionsMuted ]);
|
||||
|
||||
return (
|
||||
<FormSection
|
||||
label = 'settings.playSounds'>
|
||||
{
|
||||
moderationSettings.map(({ label, state, onChange }) => (
|
||||
moderationSettings.map(({ label, state, onChange, disabled }) => (
|
||||
<FormRow
|
||||
key = { label }
|
||||
label = { label }>
|
||||
<Switch
|
||||
checked = { Boolean(state) }
|
||||
disabled = { disabled }
|
||||
onChange = { onChange } />
|
||||
</FormRow>
|
||||
))
|
||||
|
||||
@@ -34,6 +34,16 @@ export interface IProps extends AbstractDialogTabProps, WithTranslation {
|
||||
*/
|
||||
followMeEnabled: boolean;
|
||||
|
||||
/**
|
||||
* Whether follow me for recorder is currently active (enabled by some other participant).
|
||||
*/
|
||||
followMeRecorderActive: boolean;
|
||||
|
||||
/**
|
||||
* Whether the user has selected the Follow Me Recorder feature to be enabled.
|
||||
*/
|
||||
followMeRecorderEnabled: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not the user has selected the Start Audio Muted feature to be
|
||||
* enabled.
|
||||
@@ -92,6 +102,7 @@ class ModeratorTab extends AbstractDialogTab<IProps, any> {
|
||||
this._onStartVideoMutedChanged = this._onStartVideoMutedChanged.bind(this);
|
||||
this._onStartReactionsMutedChanged = this._onStartReactionsMutedChanged.bind(this);
|
||||
this._onFollowMeEnabledChanged = this._onFollowMeEnabledChanged.bind(this);
|
||||
this._onFollowMeRecorderEnabledChanged = this._onFollowMeRecorderEnabledChanged.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -139,7 +150,24 @@ class ModeratorTab extends AbstractDialogTab<IProps, any> {
|
||||
* @returns {void}
|
||||
*/
|
||||
_onFollowMeEnabledChanged({ target: { checked } }: React.ChangeEvent<HTMLInputElement>) {
|
||||
super._onChange({ followMeEnabled: checked });
|
||||
super._onChange({
|
||||
followMeEnabled: checked,
|
||||
followMeRecorderEnabled: checked ? false : undefined
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked to select if follow-me for recorder mode should be activated.
|
||||
*
|
||||
* @param {Object} e - The key event to handle.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onFollowMeRecorderEnabledChanged({ target: { checked } }: React.ChangeEvent<HTMLInputElement>) {
|
||||
super._onChange({
|
||||
followMeEnabled: checked ? false : undefined,
|
||||
followMeRecorderEnabled: checked
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -153,6 +181,8 @@ class ModeratorTab extends AbstractDialogTab<IProps, any> {
|
||||
disableReactionsModeration,
|
||||
followMeActive,
|
||||
followMeEnabled,
|
||||
followMeRecorderActive,
|
||||
followMeRecorderEnabled,
|
||||
startAudioMuted,
|
||||
startVideoMuted,
|
||||
startReactionsMuted,
|
||||
@@ -160,6 +190,8 @@ class ModeratorTab extends AbstractDialogTab<IProps, any> {
|
||||
} = this.props;
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
|
||||
const followMeRecorderChecked = followMeRecorderEnabled && !followMeRecorderActive;
|
||||
|
||||
return (
|
||||
<div
|
||||
className = { `moderator-tab ${classes.container}` }
|
||||
@@ -180,12 +212,19 @@ class ModeratorTab extends AbstractDialogTab<IProps, any> {
|
||||
name = 'start-video-muted'
|
||||
onChange = { this._onStartVideoMutedChanged } />
|
||||
<Checkbox
|
||||
checked = { followMeEnabled && !followMeActive }
|
||||
checked = { followMeEnabled && !followMeActive && !followMeRecorderChecked }
|
||||
className = { classes.checkbox }
|
||||
disabled = { followMeActive }
|
||||
disabled = { followMeActive || followMeRecorderActive }
|
||||
label = { t('settings.followMe') }
|
||||
name = 'follow-me'
|
||||
onChange = { this._onFollowMeEnabledChanged } />
|
||||
<Checkbox
|
||||
checked = { followMeRecorderChecked }
|
||||
className = { classes.checkbox }
|
||||
disabled = { followMeRecorderActive || followMeActive }
|
||||
label = { t('settings.followMeRecorder') }
|
||||
name = 'follow-me-recorder'
|
||||
onChange = { this._onFollowMeRecorderEnabledChanged } />
|
||||
{ !disableReactionsModeration
|
||||
&& <Checkbox
|
||||
checked = { startReactionsMuted }
|
||||
|
||||
@@ -253,6 +253,7 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
|
||||
return {
|
||||
...newProps,
|
||||
followMeEnabled: tabState?.followMeEnabled,
|
||||
followMeRecorderEnabled: tabState?.followMeRecorderEnabled,
|
||||
startAudioMuted: tabState?.startAudioMuted,
|
||||
startVideoMuted: tabState?.startVideoMuted,
|
||||
startReactionsMuted: tabState?.startReactionsMuted
|
||||
|
||||
@@ -9,7 +9,7 @@ import { toState } from '../base/redux/functions';
|
||||
import { getHideSelfView } from '../base/settings/functions.any';
|
||||
import { parseStandardURIString } from '../base/util/uri';
|
||||
import { isStageFilmstripEnabled } from '../filmstrip/functions';
|
||||
import { isFollowMeActive } from '../follow-me/functions';
|
||||
import { isFollowMeActive, isFollowMeRecorderActive } from '../follow-me/functions';
|
||||
import { isPrejoinEnabledInConfig } from '../prejoin/functions';
|
||||
import { isReactionsEnabled } from '../reactions/functions.any';
|
||||
import { iAmVisitor } from '../visitors/functions';
|
||||
@@ -133,12 +133,14 @@ export function getModeratorTabProps(stateful: IStateful) {
|
||||
const {
|
||||
conference,
|
||||
followMeEnabled,
|
||||
followMeRecorderEnabled,
|
||||
startAudioMutedPolicy,
|
||||
startVideoMutedPolicy,
|
||||
startReactionsMuted
|
||||
} = state['features/base/conference'];
|
||||
const { disableReactionsModeration } = state['features/base/config'];
|
||||
const followMeActive = isFollowMeActive(state);
|
||||
const followMeRecorderActive = isFollowMeRecorderActive(state);
|
||||
const showModeratorSettings = shouldShowModeratorSettings(state);
|
||||
|
||||
// The settings sections to display.
|
||||
@@ -147,6 +149,8 @@ export function getModeratorTabProps(stateful: IStateful) {
|
||||
disableReactionsModeration: Boolean(disableReactionsModeration),
|
||||
followMeActive: Boolean(conference && followMeActive),
|
||||
followMeEnabled: Boolean(conference && followMeEnabled),
|
||||
followMeRecorderActive: Boolean(conference && followMeRecorderActive),
|
||||
followMeRecorderEnabled: Boolean(conference && followMeRecorderEnabled),
|
||||
startReactionsMuted: Boolean(conference && startReactionsMuted),
|
||||
startAudioMuted: Boolean(conference && startAudioMutedPolicy),
|
||||
startVideoMuted: Boolean(conference && startVideoMutedPolicy)
|
||||
|
||||
@@ -19,6 +19,16 @@ export const SET_SHARED_VIDEO_STATUS = 'SET_SHARED_VIDEO_STATUS';
|
||||
*/
|
||||
export const RESET_SHARED_VIDEO_STATUS = 'RESET_SHARED_VIDEO_STATUS';
|
||||
|
||||
/**
|
||||
* The type of the action which marks that the user had confirmed to play video.
|
||||
*
|
||||
* {
|
||||
* type: SET_CONFIRM_SHOW_VIDEO
|
||||
* }
|
||||
*/
|
||||
export const SET_CONFIRM_SHOW_VIDEO = 'SET_CONFIRM_SHOW_VIDEO';
|
||||
|
||||
|
||||
/**
|
||||
* The type of the action which signals to disable or enable the shared video
|
||||
* button.
|
||||
@@ -28,3 +38,12 @@ export const RESET_SHARED_VIDEO_STATUS = 'RESET_SHARED_VIDEO_STATUS';
|
||||
* }
|
||||
*/
|
||||
export const SET_DISABLE_BUTTON = 'SET_DISABLE_BUTTON';
|
||||
|
||||
/**
|
||||
* The type of the action which sets an array of whitelisted urls.
|
||||
*
|
||||
* {
|
||||
* type: SET_ALLOWED_URL_DOMAINS
|
||||
* }
|
||||
*/
|
||||
export const SET_ALLOWED_URL_DOMAINS = 'SET_ALLOWED_URL_DOMAINS';
|
||||
|
||||
@@ -1,10 +1,33 @@
|
||||
import { IStore } from '../app/types';
|
||||
import { getCurrentConference } from '../base/conference/functions';
|
||||
import { openDialog } from '../base/dialog/actions';
|
||||
import { hideDialog, openDialog } from '../base/dialog/actions';
|
||||
import { getLocalParticipant } from '../base/participants/functions';
|
||||
|
||||
import { RESET_SHARED_VIDEO_STATUS, SET_SHARED_VIDEO_STATUS } from './actionTypes';
|
||||
import { SharedVideoDialog } from './components';
|
||||
import {
|
||||
RESET_SHARED_VIDEO_STATUS,
|
||||
SET_ALLOWED_URL_DOMAINS,
|
||||
SET_CONFIRM_SHOW_VIDEO,
|
||||
SET_SHARED_VIDEO_STATUS
|
||||
} from './actionTypes';
|
||||
import { ShareVideoConfirmDialog, SharedVideoDialog } from './components';
|
||||
import { PLAYBACK_START, PLAYBACK_STATUSES } from './constants';
|
||||
import { isSharedVideoEnabled, sendShareVideoCommand } from './functions';
|
||||
|
||||
|
||||
/**
|
||||
* Marks that user confirmed or not to play video.
|
||||
*
|
||||
* @param {boolean} value - The value to set.
|
||||
* @returns {{
|
||||
* type: SET_CONFIRM_SHOW_VIDEO,
|
||||
* }}
|
||||
*/
|
||||
export function setConfirmShowVideo(value: boolean) {
|
||||
return {
|
||||
type: SET_CONFIRM_SHOW_VIDEO,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the status of the shared video.
|
||||
@@ -89,17 +112,23 @@ export function stopSharedVideo() {
|
||||
*/
|
||||
export function playSharedVideo(videoUrl: string) {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
if (!isSharedVideoEnabled(getState())) {
|
||||
return;
|
||||
}
|
||||
const conference = getCurrentConference(getState());
|
||||
|
||||
if (conference) {
|
||||
const localParticipant = getLocalParticipant(getState());
|
||||
|
||||
dispatch(setSharedVideoStatus({
|
||||
videoUrl,
|
||||
status: 'start',
|
||||
time: 0,
|
||||
ownerId: localParticipant?.id
|
||||
}));
|
||||
// we will send the command and will create local video fake participant
|
||||
// and start playing once we receive ourselves the command
|
||||
sendShareVideoCommand({
|
||||
conference,
|
||||
id: videoUrl,
|
||||
localParticipantId: localParticipant?.id,
|
||||
status: PLAYBACK_START,
|
||||
time: 0
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -115,10 +144,60 @@ export function toggleSharedVideo() {
|
||||
const state = getState();
|
||||
const { status = '' } = state['features/shared-video'];
|
||||
|
||||
if ([ 'playing', 'start', 'pause' ].includes(status)) {
|
||||
if ([ PLAYBACK_STATUSES.PLAYING, PLAYBACK_START, PLAYBACK_STATUSES.PAUSED ].includes(status)) {
|
||||
dispatch(stopSharedVideo());
|
||||
} else {
|
||||
dispatch(showSharedVideoDialog((id: string) => dispatch(playSharedVideo(id))));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the allowed URL domains of the shared video.
|
||||
*
|
||||
* @param {Array<string>} allowedUrlDomains - The new whitelist to be set.
|
||||
* @returns {{
|
||||
* type: SET_ALLOWED_URL_DOMAINS,
|
||||
* allowedUrlDomains: Array<string>
|
||||
* }}
|
||||
*/
|
||||
export function setAllowedUrlDomians(allowedUrlDomains: Array<string>) {
|
||||
return {
|
||||
type: SET_ALLOWED_URL_DOMAINS,
|
||||
allowedUrlDomains
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a confirmation dialog whether to play the external video link.
|
||||
*
|
||||
* @param {string} actor - The actor's name.
|
||||
* @param {Function} onSubmit - The function to execute when confirmed.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function showConfirmPlayingDialog(actor: String, onSubmit: Function) {
|
||||
return (dispatch: IStore['dispatch']) => {
|
||||
// shows only one dialog at a time
|
||||
dispatch(setConfirmShowVideo(false));
|
||||
|
||||
dispatch(openDialog(ShareVideoConfirmDialog, {
|
||||
actorName: actor,
|
||||
onSubmit: () => {
|
||||
dispatch(setConfirmShowVideo(true));
|
||||
onSubmit();
|
||||
}
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the video play confirmation dialog.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function hideConfirmPlayingDialog() {
|
||||
return (dispatch: IStore['dispatch']) => {
|
||||
dispatch(hideDialog(ShareVideoConfirmDialog));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,6 +10,11 @@ import { extractYoutubeIdOrURL } from '../functions';
|
||||
*/
|
||||
export interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* The allowed URL domains for shared video.
|
||||
*/
|
||||
_allowedUrlDomains: Array<string>;
|
||||
|
||||
/**
|
||||
* Invoked to update the shared video link.
|
||||
*/
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
// @ts-ignore
|
||||
export { default as SharedVideoDialog } from './native/SharedVideoDialog';
|
||||
export { default as SharedVideoButton } from './native/SharedVideoButton';
|
||||
export { default as ShareVideoConfirmDialog } from './native/ShareVideoConfirmDialog';
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
export { default as SharedVideoDialog } from './web/SharedVideoDialog';
|
||||
export { default as SharedVideoButton } from './web/SharedVideoButton';
|
||||
export { default as ShareVideoConfirmDialog } from './web/ShareVideoConfirmDialog';
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import ConfirmDialog from '../../../base/dialog/components/native/ConfirmDialog';
|
||||
import { DialogProps } from '../../../base/dialog/constants';
|
||||
|
||||
interface IProps extends DialogProps {
|
||||
|
||||
/**
|
||||
* The name of the remote participant that shared the video.
|
||||
*/
|
||||
actorName: string;
|
||||
|
||||
/**
|
||||
* The function to execute when confirmed.
|
||||
*/
|
||||
onSubmit: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dialog to confirm playing a video shared from a remote participant.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
export default function ShareVideoConfirmDialog({ actorName, onSubmit }: IProps): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ConfirmDialog
|
||||
cancelLabel = 'dialog.Cancel'
|
||||
confirmLabel = 'dialog.Ok'
|
||||
descriptionKey = 'dialog.shareVideoConfirmPlay'
|
||||
onSubmit = { onSubmit }
|
||||
title = { t('dialog.shareVideoConfirmPlayTitle', {
|
||||
name: actorName
|
||||
}) } />
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import InputDialog from '../../../base/dialog/components/native/InputDialog';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import AbstractSharedVideoDialog, { IProps } from '../AbstractSharedVideoDialog';
|
||||
@@ -67,4 +68,19 @@ class SharedVideoDialog extends AbstractSharedVideoDialog<IState> {
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(connect()(SharedVideoDialog));
|
||||
/**
|
||||
* Maps part of the Redux state to the props of this component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {IProps}
|
||||
*/
|
||||
function mapStateToProps(state: IReduxState) {
|
||||
const { allowedUrlDomains } = state['features/shared-video'];
|
||||
|
||||
return {
|
||||
_allowedUrlDomains: allowedUrlDomains
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(mapStateToProps)(SharedVideoDialog));
|
||||
|
||||
@@ -130,7 +130,7 @@ class YoutubeVideoManager extends AbstractVideoManager<IState> {
|
||||
});
|
||||
}
|
||||
|
||||
if (event === 'playing') {
|
||||
if (event === PLAYBACK_STATUSES.PLAYING) {
|
||||
this.setState({
|
||||
paused: false
|
||||
}, () => {
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { DialogProps } from '../../../base/dialog/constants';
|
||||
import Dialog from '../../../base/ui/components/web/Dialog';
|
||||
|
||||
interface IProps extends DialogProps {
|
||||
|
||||
/**
|
||||
* The name of the remote participant that shared the video.
|
||||
*/
|
||||
actorName: string;
|
||||
|
||||
/**
|
||||
* The function to execute when confirmed.
|
||||
*/
|
||||
onSubmit: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dialog to confirm playing a video shared from a remote participant.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
export default function ShareVideoConfirmDialog({ actorName, onSubmit }: IProps): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
onSubmit = { onSubmit }
|
||||
title = { t('dialog.shareVideoConfirmPlayTitle', {
|
||||
name: actorName
|
||||
}) }>
|
||||
<div>
|
||||
{ t('dialog.shareVideoConfirmPlay') }
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { IReduxState } from '../../../app/types';
|
||||
import { getLocalParticipant } from '../../../base/participants/functions';
|
||||
import { getVerticalViewMaxWidth } from '../../../filmstrip/functions.web';
|
||||
import { getToolboxHeight } from '../../../toolbox/functions.web';
|
||||
import { isSharedVideoEnabled } from '../../functions';
|
||||
|
||||
import VideoManager from './VideoManager';
|
||||
import YoutubeVideoManager from './YoutubeVideoManager';
|
||||
@@ -33,6 +34,11 @@ interface IProps {
|
||||
*/
|
||||
filmstripWidth: number;
|
||||
|
||||
/**
|
||||
* Whether the shared video is enabled or not.
|
||||
*/
|
||||
isEnabled: boolean;
|
||||
|
||||
/**
|
||||
* Is the video shared by the local user.
|
||||
*/
|
||||
@@ -118,7 +124,12 @@ class SharedVideo extends Component<IProps> {
|
||||
* @returns {React$Element}
|
||||
*/
|
||||
render() {
|
||||
const { isOwner, isResizing } = this.props;
|
||||
const { isEnabled, isOwner, isResizing } = this.props;
|
||||
|
||||
if (!isEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const className = !isResizing && isOwner ? '' : 'disable-pointer';
|
||||
|
||||
return (
|
||||
@@ -152,6 +163,7 @@ function _mapStateToProps(state: IReduxState) {
|
||||
clientWidth,
|
||||
filmstripVisible: visible,
|
||||
filmstripWidth: getVerticalViewMaxWidth(state),
|
||||
isEnabled: isSharedVideoEnabled(state),
|
||||
isOwner: ownerId === localParticipant?.id,
|
||||
isResizing,
|
||||
videoUrl
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { hideDialog } from '../../../base/dialog/actions';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import Dialog from '../../../base/ui/components/web/Dialog';
|
||||
@@ -99,4 +100,19 @@ class SharedVideoDialog extends AbstractSharedVideoDialog<any> {
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(connect()(SharedVideoDialog));
|
||||
/**
|
||||
* Maps part of the Redux state to the props of this component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {IProps}
|
||||
*/
|
||||
function mapStateToProps(state: IReduxState) {
|
||||
const { allowedUrlDomains } = state['features/shared-video'];
|
||||
|
||||
return {
|
||||
_allowedUrlDomains: allowedUrlDomains
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(mapStateToProps)(SharedVideoDialog));
|
||||
|
||||
@@ -28,3 +28,23 @@ export const PLAYBACK_STATUSES = {
|
||||
PAUSED: 'pause',
|
||||
STOPPED: 'stop'
|
||||
};
|
||||
|
||||
/**
|
||||
* Playback start state.
|
||||
*/
|
||||
export const PLAYBACK_START = 'start';
|
||||
|
||||
/**
|
||||
* The domain for youtube URLs.
|
||||
*/
|
||||
export const YOUTUBE_URL_DOMAIN = 'youtube.com';
|
||||
|
||||
/**
|
||||
* The constant to allow URL domains.
|
||||
*/
|
||||
export const ALLOW_ALL_URL_DOMAINS = '*';
|
||||
|
||||
/**
|
||||
* The default white listed domains for shared video.
|
||||
*/
|
||||
export const DEFAULT_ALLOWED_URL_DOMAINS = [ YOUTUBE_URL_DOMAIN ];
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
import { IStateful } from '../base/app/types';
|
||||
import { IJitsiConference } from '../base/conference/reducer';
|
||||
import { getFakeParticipants } from '../base/participants/functions';
|
||||
import { toState } from '../base/redux/functions';
|
||||
|
||||
import { VIDEO_PLAYER_PARTICIPANT_NAME, YOUTUBE_PLAYER_PARTICIPANT_NAME } from './constants';
|
||||
import {
|
||||
ALLOW_ALL_URL_DOMAINS,
|
||||
PLAYBACK_START,
|
||||
PLAYBACK_STATUSES,
|
||||
SHARED_VIDEO,
|
||||
VIDEO_PLAYER_PARTICIPANT_NAME,
|
||||
YOUTUBE_PLAYER_PARTICIPANT_NAME,
|
||||
YOUTUBE_URL_DOMAIN
|
||||
} from './constants';
|
||||
|
||||
/**
|
||||
* Validates the entered video url.
|
||||
@@ -29,7 +39,7 @@ function getYoutubeId(url: string) {
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isSharingStatus(status: string) {
|
||||
return [ 'playing', 'pause', 'start' ].includes(status);
|
||||
return [ PLAYBACK_STATUSES.PLAYING, PLAYBACK_STATUSES.PAUSED, PLAYBACK_START ].includes(status);
|
||||
}
|
||||
|
||||
|
||||
@@ -87,3 +97,80 @@ export function extractYoutubeIdOrURL(input: string) {
|
||||
return trimmedLink;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if shared video functionality is enabled and false otherwise.
|
||||
*
|
||||
* @param {IStateful} stateful - - The redux store or {@code getState} function.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isSharedVideoEnabled(stateful: IStateful) {
|
||||
const state = toState(stateful);
|
||||
|
||||
const { disableThirdPartyRequests = false } = state['features/base/config'];
|
||||
|
||||
return !disableThirdPartyRequests;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if you youtube URLs should be allowed for shared videos.
|
||||
*
|
||||
* @param {Array<string>} allowedUrlDomains - The allowed URL domains for shared video.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function areYoutubeURLsAllowedForSharedVideo(allowedUrlDomains?: Array<string>) {
|
||||
return Boolean(allowedUrlDomains?.includes(YOUTUBE_URL_DOMAIN));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the passed url is allowed to be used for shared video or not.
|
||||
*
|
||||
* @param {string} url - The URL.
|
||||
* @param {Array<string>} allowedUrlDomains - The allowed url domains.
|
||||
* @param {boolean} considerNonURLsAllowedForYoututbe - If true, the invalid URLs will be considered youtube IDs
|
||||
* and if youtube is allowed the function will return true.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isURLAllowedForSharedVideo(url: string,
|
||||
allowedUrlDomains: Array<string> = [], considerNonURLsAllowedForYoututbe = false) {
|
||||
if (!url) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const urlObject = new URL(url);
|
||||
|
||||
if ([ 'http:', 'https:' ].includes(urlObject?.protocol?.toLowerCase())) {
|
||||
return allowedUrlDomains.includes(ALLOW_ALL_URL_DOMAINS) || allowedUrlDomains.includes(urlObject?.hostname);
|
||||
}
|
||||
} catch (_e) { // it should be YouTube id.
|
||||
return considerNonURLsAllowedForYoututbe && allowedUrlDomains.includes(YOUTUBE_URL_DOMAIN);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends SHARED_VIDEO command.
|
||||
*
|
||||
* @param {string} id - The id of the video.
|
||||
* @param {string} status - The status of the shared video.
|
||||
* @param {JitsiConference} conference - The current conference.
|
||||
* @param {string} localParticipantId - The id of the local participant.
|
||||
* @param {string} time - The seek position of the video.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function sendShareVideoCommand({ id, status, conference, localParticipantId = '', time, muted, volume }: {
|
||||
conference?: IJitsiConference; id: string; localParticipantId?: string; muted?: boolean;
|
||||
status: string; time: number; volume?: number;
|
||||
}) {
|
||||
conference?.sendCommandOnce(SHARED_VIDEO, {
|
||||
value: id,
|
||||
attributes: {
|
||||
from: localParticipantId,
|
||||
muted,
|
||||
state: status,
|
||||
time,
|
||||
volume
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
24
react/features/shared-video/hooks.ts
Normal file
24
react/features/shared-video/hooks.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { SharedVideoButton } from './components';
|
||||
import { isSharedVideoEnabled } from './functions';
|
||||
|
||||
const shareVideo = {
|
||||
key: 'sharedvideo',
|
||||
Content: SharedVideoButton,
|
||||
group: 3
|
||||
};
|
||||
|
||||
/**
|
||||
* A hook that returns the shared video button if it is enabled and undefined otherwise.
|
||||
*
|
||||
* @returns {Object | undefined}
|
||||
*/
|
||||
export function useSharedVideoButton() {
|
||||
const sharedVideoEnabled = useSelector(isSharedVideoEnabled);
|
||||
|
||||
if (sharedVideoEnabled) {
|
||||
return shareVideo;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,20 +4,33 @@ import { IStore } from '../app/types';
|
||||
import { CONFERENCE_JOIN_IN_PROGRESS, CONFERENCE_LEFT } from '../base/conference/actionTypes';
|
||||
import { getCurrentConference } from '../base/conference/functions';
|
||||
import { IJitsiConference } from '../base/conference/reducer';
|
||||
import { SET_CONFIG } from '../base/config/actionTypes';
|
||||
import { MEDIA_TYPE } from '../base/media/constants';
|
||||
import { PARTICIPANT_LEFT } from '../base/participants/actionTypes';
|
||||
import { participantJoined, participantLeft, pinParticipant } from '../base/participants/actions';
|
||||
import { getLocalParticipant, getParticipantById } from '../base/participants/functions';
|
||||
import { getLocalParticipant, getParticipantById, getParticipantDisplayName } from '../base/participants/functions';
|
||||
import { FakeParticipant } from '../base/participants/types';
|
||||
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
||||
import { SET_DYNAMIC_BRANDING_DATA } from '../dynamic-branding/actionTypes';
|
||||
import { showWarningNotification } from '../notifications/actions';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE } from '../notifications/constants';
|
||||
|
||||
import { RESET_SHARED_VIDEO_STATUS, SET_SHARED_VIDEO_STATUS } from './actionTypes';
|
||||
import {
|
||||
hideConfirmPlayingDialog,
|
||||
resetSharedVideoStatus,
|
||||
setSharedVideoStatus
|
||||
setAllowedUrlDomians,
|
||||
setSharedVideoStatus,
|
||||
showConfirmPlayingDialog
|
||||
} from './actions.any';
|
||||
import { PLAYBACK_STATUSES, SHARED_VIDEO, VIDEO_PLAYER_PARTICIPANT_NAME } from './constants';
|
||||
import { isSharingStatus } from './functions';
|
||||
import {
|
||||
DEFAULT_ALLOWED_URL_DOMAINS,
|
||||
PLAYBACK_START,
|
||||
PLAYBACK_STATUSES,
|
||||
SHARED_VIDEO,
|
||||
VIDEO_PLAYER_PARTICIPANT_NAME
|
||||
} from './constants';
|
||||
import { isSharedVideoEnabled, isSharingStatus, isURLAllowedForSharedVideo, sendShareVideoCommand } from './functions';
|
||||
import logger from './logger';
|
||||
|
||||
|
||||
@@ -30,25 +43,61 @@ import logger from './logger';
|
||||
*/
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
const { dispatch, getState } = store;
|
||||
const state = getState();
|
||||
|
||||
if (!isSharedVideoEnabled(getState())) {
|
||||
return next(action);
|
||||
}
|
||||
|
||||
switch (action.type) {
|
||||
case CONFERENCE_JOIN_IN_PROGRESS: {
|
||||
const { conference } = action;
|
||||
const localParticipantId = getLocalParticipant(state)?.id;
|
||||
const localParticipantId = getLocalParticipant(getState())?.id;
|
||||
|
||||
conference.addCommandListener(SHARED_VIDEO,
|
||||
({ value, attributes }: { attributes: {
|
||||
from: string; muted: string; state: string; time: string; }; value: string; }) => {
|
||||
const state = getState();
|
||||
|
||||
const { from } = attributes;
|
||||
const sharedVideoStatus = attributes.state;
|
||||
|
||||
if (isSharingStatus(sharedVideoStatus)) {
|
||||
handleSharingVideoStatus(store, value, attributes, conference);
|
||||
} else if (sharedVideoStatus === 'stop') {
|
||||
// confirmShowVideo is undefined the first time we receive
|
||||
// when confirmShowVideo is false we ignore everything except stop that resets it
|
||||
if (getState()['features/shared-video'].confirmShowVideo === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isURLAllowedForSharedVideo(value, getState()['features/shared-video'].allowedUrlDomains, true)
|
||||
|| localParticipantId === from
|
||||
|| getState()['features/shared-video'].confirmShowVideo) { // if confirmed skip asking again
|
||||
handleSharingVideoStatus(store, value, attributes, conference);
|
||||
} else {
|
||||
dispatch(showConfirmPlayingDialog(getParticipantDisplayName(getState(), from), () => {
|
||||
|
||||
handleSharingVideoStatus(store, value, attributes, conference);
|
||||
|
||||
return true; // on mobile this is used to close the dialog
|
||||
}));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (sharedVideoStatus === 'stop') {
|
||||
const videoParticipant = getParticipantById(state, value);
|
||||
|
||||
if (getState()['features/shared-video'].confirmShowVideo === false) {
|
||||
dispatch(showWarningNotification({
|
||||
titleKey: 'dialog.shareVideoLinkStopped',
|
||||
titleArguments: {
|
||||
name: getParticipantDisplayName(getState(), from)
|
||||
}
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
|
||||
}
|
||||
|
||||
dispatch(hideConfirmPlayingDialog());
|
||||
|
||||
dispatch(participantLeft(value, conference, {
|
||||
fakeParticipant: videoParticipant?.fakeParticipant
|
||||
}));
|
||||
@@ -62,9 +111,11 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
break;
|
||||
}
|
||||
case CONFERENCE_LEFT:
|
||||
dispatch(setAllowedUrlDomians(DEFAULT_ALLOWED_URL_DOMAINS));
|
||||
dispatch(resetSharedVideoStatus());
|
||||
break;
|
||||
case PARTICIPANT_LEFT: {
|
||||
const state = getState();
|
||||
const conference = getCurrentConference(state);
|
||||
const { ownerId: stateOwnerId, videoUrl: statevideoUrl } = state['features/shared-video'];
|
||||
|
||||
@@ -76,7 +127,23 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SET_CONFIG:
|
||||
case SET_DYNAMIC_BRANDING_DATA: {
|
||||
const result = next(action);
|
||||
const state = getState();
|
||||
const { sharedVideoAllowedURLDomains: allowedURLDomainsFromConfig = [] } = state['features/base/config'];
|
||||
const { sharedVideoAllowedURLDomains: allowedURLDomainsFromBranding = [] } = state['features/dynamic-branding'];
|
||||
|
||||
dispatch(setAllowedUrlDomians([
|
||||
...DEFAULT_ALLOWED_URL_DOMAINS,
|
||||
...allowedURLDomainsFromBranding,
|
||||
...allowedURLDomainsFromConfig
|
||||
]));
|
||||
|
||||
return result;
|
||||
}
|
||||
case SET_SHARED_VIDEO_STATUS: {
|
||||
const state = getState();
|
||||
const conference = getCurrentConference(state);
|
||||
const localParticipantId = getLocalParticipant(state)?.id;
|
||||
const { videoUrl, status, ownerId, time, muted, volume } = action;
|
||||
@@ -88,6 +155,14 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
APP.API.notifyAudioOrVideoSharingToggled(MEDIA_TYPE.VIDEO, status, ownerId);
|
||||
}
|
||||
|
||||
// when setting status we need to send the command for that, but not do it for the start command
|
||||
// as we are sending the command in playSharedVideo and setting the start status once
|
||||
// we receive the response, this way we will start the video at the same time when remote participants
|
||||
// start it, on receiving the command
|
||||
if (status === 'start') {
|
||||
break;
|
||||
}
|
||||
|
||||
if (localParticipantId === ownerId) {
|
||||
sendShareVideoCommand({
|
||||
conference,
|
||||
@@ -102,6 +177,7 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
break;
|
||||
}
|
||||
case RESET_SHARED_VIDEO_STATUS: {
|
||||
const state = getState();
|
||||
const localParticipantId = getLocalParticipant(state)?.id;
|
||||
const { ownerId: stateOwnerId, videoUrl: statevideoUrl } = state['features/shared-video'];
|
||||
|
||||
@@ -152,8 +228,19 @@ function handleSharingVideoStatus(store: IStore, videoUrl: string,
|
||||
const { dispatch, getState } = store;
|
||||
const localParticipantId = getLocalParticipant(getState())?.id;
|
||||
const oldStatus = getState()['features/shared-video']?.status ?? '';
|
||||
const oldVideoUrl = getState()['features/shared-video'].videoUrl;
|
||||
|
||||
if (state === 'start' || ![ 'playing', 'pause', 'start' ].includes(oldStatus)) {
|
||||
if (oldVideoUrl && oldVideoUrl !== videoUrl) {
|
||||
logger.warn(
|
||||
`User with id: ${from} sent videoUrl: ${videoUrl} while we are playing: ${oldVideoUrl}`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// If the video was not started (no participant) we want to create the participant
|
||||
// this can be triggered by start, but also by paused or playing
|
||||
// commands (joining late) and getting the current state
|
||||
if (state === PLAYBACK_START || !isSharingStatus(oldStatus)) {
|
||||
const youtubeId = videoUrl.match(/http/) ? false : videoUrl;
|
||||
const avatarURL = youtubeId ? `https://img.youtube.com/vi/${youtubeId}/0.jpg` : '';
|
||||
|
||||
@@ -166,6 +253,15 @@ function handleSharingVideoStatus(store: IStore, videoUrl: string,
|
||||
}));
|
||||
|
||||
dispatch(pinParticipant(videoUrl));
|
||||
|
||||
if (localParticipantId === from) {
|
||||
dispatch(setSharedVideoStatus({
|
||||
videoUrl,
|
||||
status: state,
|
||||
time: Number(time),
|
||||
ownerId: localParticipantId
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
if (localParticipantId !== from) {
|
||||
@@ -178,31 +274,3 @@ function handleSharingVideoStatus(store: IStore, videoUrl: string,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/* eslint-disable max-params */
|
||||
|
||||
/**
|
||||
* Sends SHARED_VIDEO command.
|
||||
*
|
||||
* @param {string} id - The id of the video.
|
||||
* @param {string} status - The status of the shared video.
|
||||
* @param {JitsiConference} conference - The current conference.
|
||||
* @param {string} localParticipantId - The id of the local participant.
|
||||
* @param {string} time - The seek position of the video.
|
||||
* @returns {void}
|
||||
*/
|
||||
function sendShareVideoCommand({ id, status, conference, localParticipantId = '', time, muted, volume }: {
|
||||
conference?: IJitsiConference; id: string; localParticipantId?: string; muted: boolean;
|
||||
status: string; time: number; volume: number;
|
||||
}) {
|
||||
conference?.sendCommandOnce(SHARED_VIDEO, {
|
||||
value: id,
|
||||
attributes: {
|
||||
from: localParticipantId,
|
||||
muted,
|
||||
state: status,
|
||||
time,
|
||||
volume
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@ import { getLocalParticipant } from '../base/participants/functions';
|
||||
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
||||
|
||||
import { setDisableButton } from './actions.web';
|
||||
import { SHARED_VIDEO } from './constants';
|
||||
import { PLAYBACK_STATUSES, SHARED_VIDEO } from './constants';
|
||||
import { isSharedVideoEnabled } from './functions';
|
||||
|
||||
import './middleware.any';
|
||||
|
||||
@@ -13,6 +14,10 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
|
||||
switch (action.type) {
|
||||
case CONFERENCE_JOIN_IN_PROGRESS: {
|
||||
if (!isSharedVideoEnabled(state)) {
|
||||
break;
|
||||
}
|
||||
|
||||
const { conference } = action;
|
||||
|
||||
conference.addCommandListener(SHARED_VIDEO, ({ attributes }: { attributes:
|
||||
@@ -20,7 +25,7 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
const { from } = attributes;
|
||||
const status = attributes.state;
|
||||
|
||||
if (status === 'playing') {
|
||||
if (status === PLAYBACK_STATUSES.PLAYING) {
|
||||
if (localParticipantId !== from) {
|
||||
dispatch(setDisableButton(true));
|
||||
}
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
import ReducerRegistry from '../base/redux/ReducerRegistry';
|
||||
|
||||
import { RESET_SHARED_VIDEO_STATUS, SET_DISABLE_BUTTON, SET_SHARED_VIDEO_STATUS } from './actionTypes';
|
||||
import {
|
||||
RESET_SHARED_VIDEO_STATUS,
|
||||
SET_ALLOWED_URL_DOMAINS,
|
||||
SET_CONFIRM_SHOW_VIDEO,
|
||||
SET_DISABLE_BUTTON,
|
||||
SET_SHARED_VIDEO_STATUS
|
||||
} from './actionTypes';
|
||||
import { DEFAULT_ALLOWED_URL_DOMAINS } from './constants';
|
||||
|
||||
const initialState = {};
|
||||
const initialState = {
|
||||
allowedUrlDomains: DEFAULT_ALLOWED_URL_DOMAINS
|
||||
};
|
||||
|
||||
export interface ISharedVideoState {
|
||||
allowedUrlDomains: Array<string>;
|
||||
confirmShowVideo?: boolean;
|
||||
disabled?: boolean;
|
||||
muted?: boolean;
|
||||
ownerId?: string;
|
||||
@@ -23,7 +34,16 @@ ReducerRegistry.register<ISharedVideoState>('features/shared-video',
|
||||
|
||||
switch (action.type) {
|
||||
case RESET_SHARED_VIDEO_STATUS:
|
||||
return initialState;
|
||||
return {
|
||||
...initialState,
|
||||
allowedUrlDomains: state.allowedUrlDomains
|
||||
};
|
||||
case SET_CONFIRM_SHOW_VIDEO: {
|
||||
return {
|
||||
...state,
|
||||
confirmShowVideo: action.value
|
||||
};
|
||||
}
|
||||
case SET_SHARED_VIDEO_STATUS:
|
||||
return {
|
||||
...state,
|
||||
@@ -41,6 +61,13 @@ ReducerRegistry.register<ISharedVideoState>('features/shared-video',
|
||||
disabled
|
||||
};
|
||||
|
||||
case SET_ALLOWED_URL_DOMAINS: {
|
||||
return {
|
||||
...state,
|
||||
allowedUrlDomains: action.allowedUrlDomains
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import RecordButton from '../../../recording/components/Recording/native/RecordB
|
||||
import SecurityDialogButton
|
||||
from '../../../security/components/security-dialog/native/SecurityDialogButton';
|
||||
import SharedVideoButton from '../../../shared-video/components/native/SharedVideoButton';
|
||||
import { isSharedVideoEnabled } from '../../../shared-video/functions';
|
||||
import SpeakerStatsButton from '../../../speaker-stats/components/native/SpeakerStatsButton';
|
||||
import { isSpeakerStatsDisabled } from '../../../speaker-stats/functions';
|
||||
import ClosedCaptionButton from '../../../subtitles/components/native/ClosedCaptionButton';
|
||||
@@ -55,6 +56,11 @@ interface IProps {
|
||||
*/
|
||||
_isOpen: boolean;
|
||||
|
||||
/**
|
||||
* Whether the shared video is enabled or not.
|
||||
*/
|
||||
_isSharedVideoEnabled: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not speaker stats is disable.
|
||||
*/
|
||||
@@ -121,6 +127,7 @@ class OverflowMenu extends PureComponent<IProps, IState> {
|
||||
const {
|
||||
_isBreakoutRoomsSupported,
|
||||
_isSpeakerStatsDisabled,
|
||||
_isSharedVideoEnabled,
|
||||
_shouldDisplayReactionsButtons,
|
||||
_width,
|
||||
dispatch
|
||||
@@ -168,7 +175,7 @@ class OverflowMenu extends PureComponent<IProps, IState> {
|
||||
<WhiteboardButton { ...buttonProps } />
|
||||
{/* @ts-ignore */}
|
||||
<Divider style = { styles.divider as ViewStyle } />
|
||||
<SharedVideoButton { ...buttonProps } />
|
||||
{_isSharedVideoEnabled && <SharedVideoButton { ...buttonProps } />}
|
||||
{!toolbarButtons.has('screensharing') && <ScreenSharingButton { ...buttonProps } />}
|
||||
{!_isSpeakerStatsDisabled && <SpeakerStatsButton { ...buttonProps } />}
|
||||
{!toolbarButtons.has('tileview') && <TileViewButton { ...buttonProps } />}
|
||||
@@ -255,6 +262,7 @@ function _mapStateToProps(state: IReduxState) {
|
||||
return {
|
||||
_customToolbarButtons: customToolbarButtons,
|
||||
_isBreakoutRoomsSupported: conference?.getBreakoutRooms()?.isSupported(),
|
||||
_isSharedVideoEnabled: isSharedVideoEnabled(state),
|
||||
_isSpeakerStatsDisabled: isSpeakerStatsDisabled(state),
|
||||
_shouldDisplayReactionsButtons: shouldDisplayReactionsButtons(state),
|
||||
_width: state['features/base/responsive-ui'].clientWidth
|
||||
|
||||
@@ -45,7 +45,7 @@ import ShareAudioButton from '../screen-share/components/web/ShareAudioButton';
|
||||
import { isScreenAudioSupported, isScreenVideoShared } from '../screen-share/functions';
|
||||
import { useSecurityDialogButton } from '../security/hooks';
|
||||
import SettingsButton from '../settings/components/web/SettingsButton';
|
||||
import SharedVideoButton from '../shared-video/components/web/SharedVideoButton';
|
||||
import { useSharedVideoButton } from '../shared-video/hooks';
|
||||
import SpeakerStats from '../speaker-stats/components/web/SpeakerStats';
|
||||
import { isSpeakerStatsDisabled } from '../speaker-stats/functions';
|
||||
import { useSpeakerStatsButton } from '../speaker-stats/hooks.web';
|
||||
@@ -142,12 +142,6 @@ const linkToSalesforce = {
|
||||
group: 2
|
||||
};
|
||||
|
||||
const shareVideo = {
|
||||
key: 'sharedvideo',
|
||||
Content: SharedVideoButton,
|
||||
group: 3
|
||||
};
|
||||
|
||||
const shareAudio = {
|
||||
key: 'shareaudio',
|
||||
Content: ShareAudioButton,
|
||||
@@ -288,6 +282,7 @@ export function useToolboxButtons(
|
||||
const liveStreaming = useLiveStreamingButton();
|
||||
const linktosalesforce = useLinkToSalesforceButton();
|
||||
const shareaudio = getShareAudioButton();
|
||||
const shareVideo = useSharedVideoButton();
|
||||
const whiteboard = useWhiteboardButton();
|
||||
const etherpad = useEtherpadButton();
|
||||
const virtualBackground = useVirtualBackgroundButton();
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
UPDATE_VISITORS_IN_QUEUE_COUNT,
|
||||
VISITOR_PROMOTION_REQUEST
|
||||
} from './actionTypes';
|
||||
import logger from './logger';
|
||||
import { IPromotionRequest } from './types';
|
||||
|
||||
/**
|
||||
@@ -95,6 +96,7 @@ export function demoteRequest(id: string) {
|
||||
dispatch(disconnect(true))
|
||||
.then(() => {
|
||||
dispatch(setPreferVisitor(true));
|
||||
logger.info('Dispatching connect to demote the local participant.');
|
||||
|
||||
return dispatch(connect());
|
||||
});
|
||||
|
||||
@@ -3,7 +3,6 @@ import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { IconArrowUp } from '../../../base/icons/svg';
|
||||
import ToolboxButtonWithPopup from '../../../base/toolbox/components/web/ToolboxButtonWithPopup';
|
||||
import Dialog from '../../../base/ui/components/web/Dialog';
|
||||
import { RaiseHandButton } from '../../../reactions/components/web/RaiseHandButton';
|
||||
@@ -55,8 +54,6 @@ export default function JoinMeetingDialog() {
|
||||
<p className = { classes.raiseHandTooltip }>{t('visitors.joinMeeting.raiseHand')}</p>
|
||||
<div className = { classes.raiseHandButton }>
|
||||
<ToolboxButtonWithPopup
|
||||
icon = { IconArrowUp }
|
||||
iconDisabled = { false }
|
||||
onPopoverClose = { noop }
|
||||
onPopoverOpen = { noop }
|
||||
popoverContent = { null }
|
||||
|
||||
@@ -72,26 +72,26 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
const { conference } = action;
|
||||
|
||||
if (getState()['features/visitors'].iAmVisitor) {
|
||||
dispatch(openDialog(JoinMeetingDialog));
|
||||
|
||||
const { demoteActorDisplayName } = getState()['features/visitors'];
|
||||
|
||||
dispatch(setVisitorDemoteActor(undefined));
|
||||
|
||||
const notificationParams: INotificationProps = {
|
||||
titleKey: 'visitors.notification.title',
|
||||
descriptionKey: 'visitors.notification.description'
|
||||
};
|
||||
|
||||
if (demoteActorDisplayName) {
|
||||
notificationParams.descriptionKey = 'visitors.notification.demoteDescription';
|
||||
notificationParams.descriptionArguments = {
|
||||
actor: demoteActorDisplayName
|
||||
const notificationParams: INotificationProps = {
|
||||
titleKey: 'visitors.notification.title',
|
||||
descriptionKey: 'visitors.notification.demoteDescription',
|
||||
descriptionArguments: {
|
||||
actor: demoteActorDisplayName
|
||||
}
|
||||
};
|
||||
|
||||
batch(() => {
|
||||
dispatch(showNotification(notificationParams, NOTIFICATION_TIMEOUT_TYPE.STICKY));
|
||||
dispatch(setVisitorDemoteActor(undefined));
|
||||
});
|
||||
} else {
|
||||
dispatch(openDialog(JoinMeetingDialog));
|
||||
}
|
||||
|
||||
// check for demote actor and update notification
|
||||
dispatch(showNotification(notificationParams, NOTIFICATION_TIMEOUT_TYPE.STICKY));
|
||||
} else {
|
||||
dispatch(setVisitorsSupported(conference.isVisitorsSupported()));
|
||||
conference.on(JitsiConferenceEvents.VISITORS_SUPPORTED_CHANGED, (value: boolean) => {
|
||||
@@ -118,6 +118,8 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
dispatch(setVisitorDemoteActor(participantById.name));
|
||||
}
|
||||
|
||||
logger.info('Dispatching connect on demote request visitor message for local participant.');
|
||||
|
||||
return dispatch(connect());
|
||||
});
|
||||
}
|
||||
@@ -198,6 +200,7 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
logger.info('Dispatching joinConference on conference live event.');
|
||||
dispatch(joinConference());
|
||||
dispatch(setInVisitorsQueue(false));
|
||||
}, Math.random() * delay);
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
server {
|
||||
listen 443 ssl;
|
||||
|
||||
server_name giphy-proxy.example.com;
|
||||
|
||||
ssl_certificate /etc/nginx/ssl/giphy-proxy.example.com.crt;
|
||||
ssl_certificate_key /etc/nginx/ssl/giphy-proxy.example.com.key;
|
||||
|
||||
ssl_stapling on;
|
||||
ssl_stapling_verify on;
|
||||
resolver 8.8.4.4 8.8.8.8 valid=300s;
|
||||
resolver_timeout 10s;
|
||||
|
||||
location / {
|
||||
if ($request_method = 'OPTIONS') {
|
||||
add_header 'Access-Control-Allow-Origin' '*';
|
||||
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
|
||||
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
|
||||
|
||||
add_header 'Access-Control-Max-Age' 1728000;
|
||||
add_header 'Content-Type' 'text/plain; charset=utf-8';
|
||||
add_header 'Content-Length' 0;
|
||||
return 204;
|
||||
}
|
||||
if ($request_method = 'POST') {
|
||||
add_header 'Access-Control-Allow-Origin' '*';
|
||||
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
|
||||
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
|
||||
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
|
||||
}
|
||||
if ($request_method = 'GET') {
|
||||
add_header 'Access-Control-Allow-Origin' '*';
|
||||
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
|
||||
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
|
||||
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
|
||||
}
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
}
|
||||
|
||||
location /gifs/trending {
|
||||
proxy_pass https://api.giphy.com/v1/gifs/trending;
|
||||
}
|
||||
|
||||
location /gifs/search {
|
||||
proxy_pass https://api.giphy.com/v1/gifs/search;
|
||||
}
|
||||
|
||||
location ~ /gifs/id/(.*) {
|
||||
proxy_pass https://i.giphy.com/media/$1/giphy.gif;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user