Compare commits

...

32 Commits

Author SHA1 Message Date
damencho
b378d972e0 debug: try out stuff split tests. 2025-11-12 08:50:54 -06:00
damencho
99a5d7eaa9 debug: drop some tests. 2025-11-12 08:31:11 -06:00
Hristo Terezov
dad4fb9e06 Revert "fix(large-video): Prevents unnecessary updates when container is hidden"
This reverts commit 6deb0a6385.
2025-11-11 12:48:16 -06:00
Vishal Malyan
3772b9a5ae feat(toolbar): implement toolbar background color via configOverwrite for web and mobile
* Change toolbar background color from IFrame API #16468 fixed

* fix(toolbar #16468): implement toolbar background color via configOverwrite for web and mobile

* keep toolbarConfig defaults commented in config.js

* add trailing comma to commented toolbarConfig.backgroundColor

* fix: resolve linting errors

Fixes #16468
2025-11-11 07:02:28 -06:00
bgrozev
89b9c75242 test: Default sort by test order in allure report. (#16636) 2025-11-10 14:07:23 -06:00
Дамян Минков
b24b60b735 fix(tests): Wait for transcriptions to be off via an event. (#16635) 2025-11-10 12:55:35 -06:00
Дамян Минков
486a1f6511 fix(tests): Avoids being blocked by notification when clicking toolbar buttons
* fix(tests): Avoids clicking UI buttons to avoid being blocked by notification.

In AV moderation tests sometimes clicking mute/unmute buttons is blocked by askedToUnmute notification.

* squash: fix waiting for button.

* squash: adds some docs.
2025-11-10 11:04:36 -06:00
Werner Fleischer
80b3f1d7d4 fix(mod_jitsi_permissions): Use correct session on moderator revocation
In the `process_set_affiliation` function, an undefined `session` variable was used when revoking moderator privileges. This prevented the `jitsi_meet_context_features` from being cleared for the occupant.
2025-11-10 05:07:47 -06:00
dependabot[bot]
421b21edeb chore(deps-dev): bump @react-native-community/cli from 15.0.1 to 17.0.1
Bumps [@react-native-community/cli](https://github.com/react-native-community/cli/tree/HEAD/packages/cli) from 15.0.1 to 17.0.1.
- [Release notes](https://github.com/react-native-community/cli/releases)
- [Changelog](https://github.com/react-native-community/cli/blob/main/packages/cli/CHANGELOG.md)
- [Commits](https://github.com/react-native-community/cli/commits/v17.0.1/packages/cli)

---
updated-dependencies:
- dependency-name: "@react-native-community/cli"
  dependency-version: 17.0.1
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-07 09:06:19 +01:00
Дамян Минков
a58b0d9a85 fix(tests): Update lobby test. (#16618)
* fix(tests): Update lobby test.
* test: Include name in "hangup" logs.
---------

Co-authored-by: Boris Grozev <boris@jitsi.org>
2025-11-06 14:56:43 -06:00
Дамян Минков
1aca8ab985 feat(dialog): Adds name to all dialogs. (#16626)
* feat(dialog): Adds name to all dialogs.

The name is used for debugging purposes to be added to logs.

* squash: Drop empty string.
2025-11-06 09:49:30 -06:00
Jaya Allamsetty
f9daba728f chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v2101.0.0+8061f52a...v2109.0.0+cb9d000c
2025-11-05 15:45:04 -05:00
damencho
fbb6456317 fix(wait_for_host): Make sure the main room is set back to non-persistent. 2025-11-05 09:26:13 -06:00
damencho
52ead26bed feat(prosody): Adds a module to cleanup room with just service components in it. 2025-11-04 16:42:09 -06:00
damencho
8d1da83e3c fix(persistent_lobby): Avoids calling destroy twice. 2025-11-04 16:41:58 -06:00
Jaya Allamsetty
5453b615f5 fix(tests): Add missing helper function. 2025-11-04 11:04:54 -05:00
Jaya Allamsetty
81a7301a3e Revert "fix(large-video)pin prev speaker on stage when local user is dominant speaker. (#16511)"
This reverts commit 82d4628976.
2025-11-04 11:04:54 -05:00
Jaya Allamsetty
1138b7779b Revert "fix(filmstrip) Fixes an issue where remote tiles can disappear when SS is started"
This reverts commit 077602c427.
2025-11-04 11:04:54 -05:00
Calin-Teodor
2fd653d928 fix(chat): be explicit about screen navigation when polls are disabled 2025-11-04 15:35:24 +02:00
eastmancr
012c9fb329 feat(base): add WebRTC availability detection (#16608)
* feat(base/environment) add WebRTC availability detection

* feat(base/unsupported-browser) switch to JitsiMeetJS WebRTC detection

* fix(static/webrtcUnsupported) remove links
2025-11-03 14:25:10 -06:00
damencho
fdf95444e9 fix(lobby): Hide login button if authenticated(jwt is available). 2025-11-03 14:22:53 -06:00
Hristo Terezov
919c60b3d2 feat(chat): Add disableChat configuration option
Introduces a comprehensive disableChat config option that disables the entire chat feature including button visibility, notifications, sounds, private messages, and keyboard shortcuts. When disabled, the chat tab is hidden from the chat panel while allowing other tabs (polls, files, CC) to remain accessible.
2025-11-03 12:44:29 -06:00
Hristo Terezov
e02c4e8f7f feat(toolbox): Add polls and file sharing buttons to overflow menu
Adds dedicated buttons for polls and file sharing in the toolbar overflow menu, following the pattern of the CC button. Both buttons open the chat panel with their respective tab selected when clicked.
2025-11-03 12:44:29 -06:00
Дамян Минков
3fd9ce5f11 * fix(lobby): Updates metadata on destroy lobby room and let in participants on empty main.
* fix(lobby): Updates metadata on destroy lobby room.

* fix(visitors): Let people join lobby when main room is empty but with lobby.
2025-10-30 16:07:46 -05:00
bgrozev
93022b3281 feat: Filter transcription results. (#16606) 2025-10-30 15:38:00 -05:00
Jaya Allamsetty
5d63b31071 fix(video-layout) Possibly fixes auto-pinning of SS in a large call.
When a user joins a very large call with SS, sometime SS is not auto-pinned to stage. This may happen when lot of participant joins are processed at the same time and therefore the state for remoteScreenShares may not get updated in time. Added extra logging to help debug if this issue reproduces.
2025-10-30 15:21:29 -04:00
Jaya Allamsetty
4432f727a4 fix(tests) Check for continguous thunbnails in the filmstrip 2025-10-30 12:21:03 -04:00
Calin-Teodor
6f1bdb513a chore(android/sdk): invoke permissionListener immediatly 2025-10-30 10:52:13 +02:00
Jaya Allamsetty
ad144e6fd3 fix(tests) Check for the endpointID of the large-video (#16601) 2025-10-29 12:35:45 -04:00
Calin-Teodor
076d77a982 chore(android/sdk): handle foreground exceptions 2025-10-29 14:34:04 +02:00
damencho
5afdda7568 fix(tests): Give some time for recording events to be received. 2025-10-28 16:48:27 -05:00
Arvind Yadav.
6cb57c472c feat(avatar): Strip bracketed annotations from display names before generating initials
Fixes #16591.
2025-10-28 16:48:16 -05:00
178 changed files with 2436 additions and 6228 deletions

View File

@@ -37,7 +37,6 @@ public class JitsiMeetActivityDelegate {
* React Native module.
*/
private static PermissionListener permissionListener;
private static Callback permissionsCallback;
/**
* Tells whether or not the permissions request is currently in progress.
@@ -142,11 +141,6 @@ public class JitsiMeetActivityDelegate {
if (reactInstanceManager != null) {
reactInstanceManager.onHostResume(activity, new DefaultHardwareBackBtnHandlerImpl(activity));
}
if (permissionsCallback != null) {
permissionsCallback.invoke();
permissionsCallback = null;
}
}
/**
@@ -169,15 +163,10 @@ public class JitsiMeetActivityDelegate {
public static void onRequestPermissionsResult(
final int requestCode, final String[] permissions, final int[] grantResults) {
permissionsCallback = new Callback() {
@Override
public void invoke(Object... args) {
if (permissionListener != null
&& permissionListener.onRequestPermissionsResult(requestCode, permissions, grantResults)) {
permissionListener = null;
}
}
};
// Invoke the callback immediately
if (permissionListener != null && permissionListener.onRequestPermissionsResult(requestCode, permissions, grantResults)) {
permissionListener = null;
}
}
public static void requestPermissions(Activity activity, String[] permissions, int requestCode, PermissionListener listener) {

View File

@@ -99,6 +99,7 @@ public class JitsiMeetOngoingConferenceService extends Service implements Ongoin
public static void launch(Context context, HashMap<String, Object> extraData) {
List<String> permissionsList = new ArrayList<>();
Activity activity = (Activity) context;
PermissionListener listener = new PermissionListener() {
@Override
@@ -134,7 +135,7 @@ public class JitsiMeetOngoingConferenceService extends Service implements Ongoin
if (permissionsArray.length > 0) {
JitsiMeetActivityDelegate.requestPermissions(
(Activity) context,
activity,
permissionsArray,
PERMISSIONS_REQUEST_CODE,
listener
@@ -159,12 +160,20 @@ public class JitsiMeetOngoingConferenceService extends Service implements Ongoin
stopSelf();
JitsiMeetLogger.w(TAG + " Couldn't start service, notification is null");
} else {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK | ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE);
} else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) {
startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK);
} else {
startForeground(NOTIFICATION_ID, notification);
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK | ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE);
} else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) {
startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK);
} else {
startForeground(NOTIFICATION_ID, notification);
}
} catch (Exception e) {
// Handle ForegroundServiceStartNotAllowedException when app is in background and cannot start foreground service.
// See: https://developer.android.com/develop/background-work/services/fgs/restrictions-bg-start#wiu-restrictions
JitsiMeetLogger.w(TAG + " Failed to start foreground service", e);
stopSelf();
return;
}
}

View File

@@ -139,6 +139,9 @@ var config = {
// Disables polls feature.
// disablePolls: false,
// Disables chat feature entirely including notifications, sounds, and private messages.
// disableChat: false,
// Disables demote button from self-view
// disableSelfDemote: false,
@@ -904,6 +907,8 @@ var config = {
// alwaysVisible: false,
// // Indicates whether the toolbar should still autohide when chat is open
// autoHideWhileChatIsOpen: false,
// // Default background color for the main toolbar. Accepts any valid CSS color.
// // backgroundColor: '#ffffff',
// },
// Overrides the buttons displayed in the main toolbar. Depending on the screen size the number of displayed

View File

@@ -126,8 +126,16 @@
"messagebox": "Type a message",
"newMessages": "New messages",
"nickname": {
"featureChat": "chat",
"featureClosedCaptions": "closed captions",
"featureFileSharing": "file sharing",
"featurePolls": "polls",
"popover": "Choose a nickname",
"title": "Enter a nickname to use chat",
"titleWith1Features": "Enter a nickname to use {{feature1}}",
"titleWith2Features": "Enter a nickname to use {{feature1}} and {{feature2}}",
"titleWith3Features": "Enter a nickname to use {{feature1}}, {{feature2}} and {{feature3}}",
"titleWith4Features": "Enter a nickname to use {{feature1}}, {{feature2}}, {{feature3}} and {{feature4}}",
"titleWithCC": "Enter a nickname to use chat and closed captions",
"titleWithPolls": "Enter a nickname to use chat and polls",
"titleWithPollsAndCC": "Enter a nickname to use chat, polls and closed captions",
@@ -1429,6 +1437,7 @@
"exitFullScreen": "Exit full screen",
"exitTileView": "Exit tile view",
"feedback": "Leave feedback",
"fileSharing": "File sharing",
"giphy": "Toggle GIPHY menu",
"hangup": "Leave the meeting",
"help": "Help",
@@ -1464,6 +1473,7 @@
"openReactionsMenu": "Open reactions menu",
"participants": "Participants",
"pip": "Enter Picture-in-Picture mode",
"polls": "Polls",
"privateMessage": "Send private message",
"profile": "Edit your profile",
"raiseHand": "Raise your hand",

View File

@@ -340,6 +340,7 @@ function initCommands() {
APP.store.dispatch(setAssumedBandwidthBps(value));
},
'set-blurred-background': blurType => {
const tracks = APP.store.getState()['features/base/tracks'];
const videoTrack = getLocalVideoTrack(tracks)?.jitsiTrack;

View File

@@ -158,11 +158,10 @@ const VideoLayout = {
return;
}
const state = APP.store.getState();
const currentContainer = largeVideo.getCurrentContainer();
const currentContainerType = largeVideo.getCurrentContainerType();
const isOnLarge = this.isCurrentlyOnLarge(id);
const state = APP.store.getState();
const participant = getParticipantById(state, id);
const videoTrack = getVideoTrackByParticipant(state, participant);
const videoStream = videoTrack?.jitsiTrack;

1373
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -72,7 +72,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/v2101.0.0+8061f52a/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2109.0.0+cb9d000c/lib-jitsi-meet.tgz",
"lodash-es": "4.17.21",
"null-loader": "4.0.1",
"optional-require": "1.0.3",
@@ -136,7 +136,7 @@
"@babel/preset-env": "7.25.9",
"@babel/preset-react": "7.25.9",
"@jitsi/eslint-config": "6.0.4",
"@react-native-community/cli": "15.0.1",
"@react-native-community/cli": "17.0.1",
"@react-native-community/cli-platform-android": "15.0.1",
"@react-native-community/cli-platform-ios": "15.0.1",
"@react-native/babel-preset": "0.77.2",

View File

@@ -139,7 +139,7 @@ function _upgradeRoleStarted(thenableWithCancel: Object) {
* @returns {Function}
*/
export function hideLoginDialog() {
return hideDialog(LoginDialog);
return hideDialog('LoginDialog', LoginDialog);
}
/**
@@ -199,7 +199,7 @@ export function enableModeratorLogin() {
* @returns {Action}
*/
export function openWaitForOwnerDialog() {
return openDialog(WaitForOwnerDialog);
return openDialog('WaitForOwnerDialog', WaitForOwnerDialog);
}
@@ -240,7 +240,7 @@ export function waitForOwner() {
* @returns {Action}
*/
export function openLoginDialog() {
return openDialog(LoginDialog);
return openDialog('LoginDialog', LoginDialog);
}
/**

View File

@@ -65,7 +65,7 @@ export function openTokenAuthUrl(tokenAuthServiceUrl: string): any {
// Show warning for leaving conference only when in a conference.
if (!browser.isElectron() && getState()['features/base/conference'].conference) {
dispatch(openDialog(LoginQuestionDialog, {
dispatch(openDialog('LoginQuestionDialog', LoginQuestionDialog, {
handler: () => {
// Give time for the dialog to close.
setTimeout(() => redirect(), 500);

View File

@@ -209,7 +209,7 @@ MiddlewareRegistry.register(store => next => action => {
case STOP_WAIT_FOR_OWNER:
_clearExistingWaitForOwnerTimeout(store);
store.dispatch(hideDialog(WaitForOwnerDialog));
store.dispatch(hideDialog('WaitForOwnerDialog', WaitForOwnerDialog));
break;
case UPGRADE_ROLE_FINISHED: {

View File

@@ -55,6 +55,28 @@ function getFirstGraphemeUpper(word: string) {
return splitter.splitGraphemes(word)[0].toUpperCase();
}
/**
* Strips bracketed annotations from a display name. Handles multiple bracket types like (),
* [], and {}.
*
* @param {string} name - The display name to clean.
* @returns {string} The cleaned display name without bracketed annotations.
*/
function stripBracketedAnnotations(name: string): string {
// Match content within any of the bracket types at the end of the string
// This regex matches: (...) or [...] or {...} at the end
const bracketRegex = /\s*[([{][^)\]}]*[)\]}]$/;
let cleaned = name;
// Remove all trailing bracketed annotations (handle multiple occurrences)
while (bracketRegex.test(cleaned)) {
cleaned = cleaned.replace(bracketRegex, '');
}
return cleaned.trim();
}
/**
* Generates initials for a simple string.
*
@@ -64,7 +86,15 @@ function getFirstGraphemeUpper(word: string) {
export function getInitials(s?: string) {
// We don't want to use the domain part of an email address, if it is one
const initialsBasis = split(s, '@')[0];
const [ firstWord, ...remainingWords ] = initialsBasis.split(wordSplitRegex).filter(Boolean);
// Strip bracketed annotations (e.g., "(Department)", "[Team]", "{Org}")
// to prevent them from being considered as name parts
const cleanedName = stripBracketedAnnotations(initialsBasis);
// Fallback to original if cleaned name is empty
const nameForInitials = cleanedName || initialsBasis;
const [ firstWord, ...remainingWords ] = nameForInitials.split(wordSplitRegex).filter(Boolean);
return getFirstGraphemeUpper(firstWord) + getFirstGraphemeUpper(remainingWords.pop() || '');
}

View File

@@ -285,6 +285,7 @@ export interface IConfig {
disableAudioLevels?: boolean;
disableBeforeUnloadHandlers?: boolean;
disableCameraTintForeground?: boolean;
disableChat?: boolean;
disableChatSmileys?: boolean;
disableDeepLinking?: boolean;
disableFilmstripAutohiding?: boolean;
@@ -393,6 +394,7 @@ export interface IConfig {
disabled?: boolean;
initialWidth?: number;
minParticipantCountForTopPanel?: number;
stageFilmstripParticipants?: number;
};
flags?: {
ssrcRewritingEnabled: boolean;
@@ -616,6 +618,10 @@ export interface IConfig {
toolbarConfig?: {
alwaysVisible?: boolean;
autoHideWhileChatIsOpen?: boolean;
/**
* Background color for the main toolbar. Accepts any valid CSS color.
*/
backgroundColor?: string;
initialTimeout?: number;
timeout?: number;
};

View File

@@ -94,6 +94,7 @@ export default [
'disableAudioLevels',
'disableBeforeUnloadHandlers',
'disableCameraTintForeground',
'disableChat',
'disableChatSmileys',
'disableDeepLinking',
'disabledNotifications',

View File

@@ -14,6 +14,7 @@ import logger from './logger';
/**
* Signals Dialog to close its dialog.
*
* @param {string|undefined} name - The name of the component for logging purposes.
* @param {Object} [component] - The {@code Dialog} component to close/hide. If
* {@code undefined}, closes/hides {@code Dialog} regardless of which
* component it's rendering; otherwise, closes/hides {@code Dialog} only if
@@ -23,8 +24,8 @@ import logger from './logger';
* component: (React.Component | undefined)
* }}
*/
export function hideDialog(component?: ComponentType<any>) {
logger.info(`Hide dialog: ${getComponentDisplayName(component)}`);
export function hideDialog(name?: string, component?: ComponentType<any>) {
logger.info(`Hide dialog: ${name}`);
return {
type: HIDE_DIALOG,
@@ -48,6 +49,7 @@ export function hideSheet() {
/**
* Signals Dialog to open dialog.
*
* @param {string} name - The name of the component for logging purposes.
* @param {Object} component - The component to display as dialog.
* @param {Object} [componentProps] - The React {@code Component} props of the
* specified {@code component}.
@@ -57,8 +59,8 @@ export function hideSheet() {
* componentProps: (Object | undefined)
* }}
*/
export function openDialog(component: ComponentType<any>, componentProps?: Object) {
logger.info(`Open dialog: ${getComponentDisplayName(component)}`);
export function openDialog(name: string, component: ComponentType<any>, componentProps?: Object) {
logger.info(`Open dialog: ${name}`);
return {
type: OPEN_DIALOG,
@@ -92,35 +94,18 @@ export function openSheet(component: ComponentType<any>, componentProps?: Object
* is not already open. If it is open, then Dialog is signaled to close its
* dialog.
*
* @param {string} name - The name of the component for logging purposes.
* @param {Object} component - The component to display as dialog.
* @param {Object} [componentProps] - The React {@code Component} props of the
* specified {@code component}.
* @returns {Function}
*/
export function toggleDialog(component: ComponentType<any>, componentProps?: Object) {
export function toggleDialog(name: string, component: ComponentType<any>, componentProps?: Object) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
if (isDialogOpen(getState, component)) {
dispatch(hideDialog(component));
dispatch(hideDialog(name, component));
} else {
dispatch(openDialog(component, componentProps));
dispatch(openDialog(name, component, componentProps));
}
};
}
/**
* Extracts a printable name for a dialog component.
*
* @param {Object} component - The component to extract the name for.
*
* @returns {string} The display name.
*/
function getComponentDisplayName(component?: ComponentType<any>) {
if (!component) {
return '';
}
const name = component.displayName ?? component.name ?? 'Component';
return name.replace('withI18nextTranslation(Connect(', '') // dialogs with translations
.replace('))', ''); // dialogs with translations suffix
}

View File

@@ -26,7 +26,7 @@ MiddlewareRegistry.register((store: IStore) => (next: Function) => (action: AnyA
case SET_VIDEO_MUTED: {
if (LocalRecordingManager.isRecordingLocally() && LocalRecordingManager.selfRecording.on) {
if (action.muted && LocalRecordingManager.selfRecording.withVideo) {
dispatch(openDialog(StopRecordingDialog, { localRecordingVideoStop: true }));
dispatch(openDialog('StopRecordingDialog', StopRecordingDialog, { localRecordingVideoStop: true }));
return;
} else if (!action.muted && !LocalRecordingManager.selfRecording.withVideo) {

View File

@@ -62,6 +62,65 @@ const AVATAR_CHECKER_FUNCTIONS = [
];
/* eslint-enable arrow-body-style */
/**
* Returns the list of active speakers that should be moved to the top of the sorted list of participants so that the
* dominant speaker is visible always on the vertical filmstrip in stage layout.
*
* @param {Function | Object} stateful - The (whole) redux state, or redux's {@code getState} function to be used to
* retrieve the state.
* @returns {Array<string>}
*/
export function getActiveSpeakersToBeDisplayed(stateful: IStateful) {
const state = toState(stateful);
const {
dominantSpeaker,
fakeParticipants,
sortedRemoteVirtualScreenshareParticipants,
speakersList
} = state['features/base/participants'];
const { visibleRemoteParticipants } = state['features/filmstrip'];
let activeSpeakers = new Map(speakersList);
// Do not re-sort the active speakers if dominant speaker is currently visible.
if (dominantSpeaker && visibleRemoteParticipants.has(dominantSpeaker)) {
return activeSpeakers;
}
let availableSlotsForActiveSpeakers = visibleRemoteParticipants.size;
if (activeSpeakers.has(dominantSpeaker ?? '')) {
activeSpeakers.delete(dominantSpeaker ?? '');
}
// Add dominant speaker to the beginning of the list (not including self) since the active speaker list is always
// alphabetically sorted.
if (dominantSpeaker && dominantSpeaker !== getLocalParticipant(state)?.id) {
const updatedSpeakers = Array.from(activeSpeakers);
updatedSpeakers.splice(0, 0, [ dominantSpeaker, getParticipantById(state, dominantSpeaker)?.name ?? '' ]);
activeSpeakers = new Map(updatedSpeakers);
}
// Remove screenshares from the count.
if (sortedRemoteVirtualScreenshareParticipants) {
availableSlotsForActiveSpeakers -= sortedRemoteVirtualScreenshareParticipants.size * 2;
for (const screenshare of Array.from(sortedRemoteVirtualScreenshareParticipants.keys())) {
const ownerId = getVirtualScreenshareParticipantOwnerId(screenshare as string);
activeSpeakers.delete(ownerId);
}
}
// Remove fake participants from the count.
if (fakeParticipants) {
availableSlotsForActiveSpeakers -= fakeParticipants.size;
}
const truncatedSpeakersList = Array.from(activeSpeakers).slice(0, availableSlotsForActiveSpeakers);
truncatedSpeakersList.sort((a: any, b: any) => a[1].localeCompare(b[1]));
return new Map(truncatedSpeakersList);
}
/**
* Resolves the first loadable avatar URL for a participant.
*

View File

@@ -27,6 +27,7 @@ import {
isRemoteScreenshareParticipant,
isScreenShareParticipant
} from './functions';
import logger from './logger';
import { FakeParticipant, ILocalParticipant, IParticipant, ISourceInfo } from './types';
/**
@@ -76,12 +77,12 @@ const DEFAULT_STATE = {
numberOfParticipantsNotSupportingE2EE: 0,
overwrittenNameList: {},
pinnedParticipant: undefined,
previousSpeakers: new Set<string>(),
raisedHandsQueue: [],
remote: new Map(),
remoteVideoSources: new Set<string>(),
sortedRemoteVirtualScreenshareParticipants: new Map(),
sortedRemoteParticipants: new Map()
sortedRemoteParticipants: new Map(),
speakersList: new Map()
};
export interface IParticipantsState {
@@ -94,12 +95,12 @@ export interface IParticipantsState {
numberOfParticipantsNotSupportingE2EE: number;
overwrittenNameList: { [id: string]: string; };
pinnedParticipant?: string;
previousSpeakers: Set<string>;
raisedHandsQueue: Array<{ hasBeenNotified?: boolean; id: string; raisedHandTimestamp: number; }>;
remote: Map<string, IParticipant>;
remoteVideoSources: Set<string>;
sortedRemoteParticipants: Map<string, string>;
sortedRemoteVirtualScreenshareParticipants: Map<string, string>;
speakersList: Map<string, string>;
}
/**
@@ -156,10 +157,22 @@ ReducerRegistry.register<IParticipantsState>('features/base/participants',
const { participant } = action;
const { id, previousSpeakers = [] } = participant;
const { dominantSpeaker, local } = state;
const newSpeakers = [ id, ...previousSpeakers ];
const sortedSpeakersList: Array<Array<string>> = [];
// Build chronologically ordered Set of remote speakers (excluding local)
const previousSpeakersSet: Set<string>
= new Set(previousSpeakers.filter((speaker: string) => speaker !== local?.id));
for (const speaker of newSpeakers) {
if (speaker !== local?.id) {
const remoteParticipant = state.remote.get(speaker);
remoteParticipant
&& sortedSpeakersList.push(
[ speaker, _getDisplayName(state, remoteParticipant?.name) ]
);
}
}
// Keep the remote speaker list sorted alphabetically.
sortedSpeakersList.sort((a, b) => a[1].localeCompare(b[1]));
// Only one dominant speaker is allowed.
if (dominantSpeaker) {
@@ -169,8 +182,8 @@ ReducerRegistry.register<IParticipantsState>('features/base/participants',
if (_updateParticipantProperty(state, id, 'dominantSpeaker', true)) {
return {
...state,
dominantSpeaker: id,
previousSpeakers: previousSpeakersSet
dominantSpeaker: id, // @ts-ignore
speakersList: new Map(sortedSpeakersList)
};
}
@@ -352,6 +365,8 @@ ReducerRegistry.register<IParticipantsState>('features/base/participants',
sortedRemoteVirtualScreenshareParticipants.sort((a, b) => a[1].localeCompare(b[1]));
state.sortedRemoteVirtualScreenshareParticipants = new Map(sortedRemoteVirtualScreenshareParticipants);
logger.debug('Remote screenshare participant joined', id);
}
// Exclude the screenshare participant from the fake participant count to avoid duplicates.
@@ -423,7 +438,7 @@ ReducerRegistry.register<IParticipantsState>('features/base/participants',
}
// Remove the participant from the list of speakers.
state.previousSpeakers.delete(id);
state.speakersList.has(id) && state.speakersList.delete(id);
if (pinnedParticipant === id) {
state.pinnedParticipant = undefined;
@@ -436,6 +451,8 @@ ReducerRegistry.register<IParticipantsState>('features/base/participants',
if (sortedRemoteVirtualScreenshareParticipants.has(id)) {
sortedRemoteVirtualScreenshareParticipants.delete(id);
state.sortedRemoteVirtualScreenshareParticipants = new Map(sortedRemoteVirtualScreenshareParticipants);
logger.debug('Remote screenshare participant left', id);
}
if (oldParticipant && !oldParticipant.fakeParticipant && !isLocalScreenShare) {

View File

@@ -21,6 +21,7 @@ import {
getRemoteScreensharesBasedOnPresence,
getVirtualScreenshareParticipantOwnerId
} from './functions';
import logger from './logger';
import { FakeParticipant } from './types';
StateListenerRegistry.register(
@@ -69,14 +70,19 @@ function _createOrRemoveVirtualParticipants(
const addedScreenshareSourceNames = difference(newScreenshareSourceNames, oldScreenshareSourceNames);
if (removedScreenshareSourceNames.length) {
removedScreenshareSourceNames.forEach(id => dispatch(participantLeft(id, conference, {
fakeParticipant: FakeParticipant.RemoteScreenShare
})));
removedScreenshareSourceNames.forEach(id => {
logger.debug('Dispatching participantLeft for virtual screenshare', id);
dispatch(participantLeft(id, conference, {
fakeParticipant: FakeParticipant.RemoteScreenShare
}));
});
}
if (addedScreenshareSourceNames.length) {
addedScreenshareSourceNames.forEach(id => dispatch(
createVirtualScreenshareParticipant(id, false, conference)));
addedScreenshareSourceNames.forEach(id => {
logger.debug('Creating virtual screenshare participant', id);
dispatch(createVirtualScreenshareParticipant(id, false, conference));
});
}
}

View File

@@ -1,5 +1,7 @@
import { IReduxState, IStore } from '../../app/types';
import { isTrackStreamingStatusActive } from '../../connection-indicator/functions';
import { handleToggleVideoMuted } from '../../toolbox/actions.any';
import { muteLocal } from '../../video-menu/actions.any';
import { MEDIA_TYPE, VIDEO_TYPE } from '../media/constants';
import { getParticipantById, isScreenShareParticipant } from '../participants/functions';
import {
@@ -78,3 +80,43 @@ export function isRemoteVideoReceived({ getState }: IStore, id: string): boolean
return Boolean(videoTrack && !videoTrack.muted && isTrackStreamingStatusActive(videoTrack));
}
/**
* Mutes the local audio. Same as clicking the audio mute button.
*
* @param {IStore} store - The redux store.
* @returns {Promise} Resolves when the action is complete.
*/
export function audioMute({ dispatch }: IStore) {
return dispatch(muteLocal(true, MEDIA_TYPE.AUDIO));
}
/**
* Unmutes the local audio. Same as clicking the audio unmute button.
*
* @param {IStore} store - The redux store.
* @returns {Promise} Resolves when the action is complete.
*/
export function audioUnmute({ dispatch }: IStore) {
return dispatch(muteLocal(false, MEDIA_TYPE.AUDIO));
}
/**
* Mutes the local video. Same as clicking the video mute button.
*
* @param {IStore} store - The redux store.
* @returns {Promise} Resolves when the action is complete.
*/
export function videoMute({ dispatch }: IStore) {
return dispatch(handleToggleVideoMuted(true, true, true));
}
/**
* Unmutes the local video. Same as clicking the video unmute button.
*
* @param {IStore} store - The redux store.
* @returns {Promise} Resolves when the action is complete.
*/
export function videoUnmute({ dispatch }: IStore) {
return dispatch(handleToggleVideoMuted(false, true, true));
}

View File

@@ -8,11 +8,15 @@ import { getJitsiMeetGlobalNS } from '../util/helpers';
import { setConnectionState } from './actions';
import {
audioMute,
audioUnmute,
getLocalCameraEncoding,
getRemoteVideoType,
isLargeVideoReceived,
isRemoteVideoReceived,
isTestModeEnabled
isTestModeEnabled,
videoMute,
videoUnmute
} from './functions';
import logger from './logger';
@@ -85,10 +89,14 @@ function _bindTortureHelpers(store: IStore) {
// All torture helper methods go in here
getJitsiMeetGlobalNS().testing = {
audioMute: audioMute.bind(null, store),
audioUnmute: audioUnmute.bind(null, store),
getRemoteVideoType: getRemoteVideoType.bind(null, store),
isLargeVideoReceived: isLargeVideoReceived.bind(null, store),
getLocalCameraEncoding: getLocalCameraEncoding.bind(null, store),
isRemoteVideoReceived: isRemoteVideoReceived.bind(null, store)
isRemoteVideoReceived: isRemoteVideoReceived.bind(null, store),
videoMute: videoMute.bind(null, store),
videoUnmute: videoUnmute.bind(null, store),
};
}

View File

@@ -294,7 +294,7 @@ export function setCameraFacingMode(facingMode: string | undefined) {
* @returns {Object} - The open dialog action.
*/
export function openAllowToggleCameraDialog(onAllow: Function, initiatorId: string) {
return openDialog(AllowToggleCameraDialog, {
return openDialog('AllowToggleCameraDialog', AllowToggleCameraDialog, {
onAllow,
initiatorId
});

View File

@@ -58,7 +58,7 @@ const BreakoutRoomContextMenu = ({ room, actions = ALL_ACTIONS }: IProps) => {
}, [ dispatch, room ]);
const onRenameBreakoutRoom = useCallback(() => {
dispatch(openDialog(BreakoutRoomNamePrompt, {
dispatch(openDialog('BreakoutRoomNamePrompt', BreakoutRoomNamePrompt, {
breakoutRoomJid: room.jid,
initialRoomName: room.name
}));

View File

@@ -22,7 +22,7 @@ export * from './actions.any';
* }}
*/
export function openUpdateCalendarEventDialog(eventId: string) {
return openDialog(UpdateCalendarEventDialog, { eventId });
return openDialog('UpdateCalendarEventDialog', UpdateCalendarEventDialog, { eventId });
}
/**

View File

@@ -220,6 +220,34 @@ export function openCCPanel() {
};
}
/**
* Opens the chat panel with polls tab active.
*
* @returns {Object} The redux action.
*/
export function openPollsPanel() {
return async (dispatch: IStore['dispatch']) => {
dispatch(setFocusedTab(ChatTabs.POLLS));
dispatch({
type: OPEN_CHAT
});
};
}
/**
* Opens the chat panel with file sharing tab active.
*
* @returns {Object} The redux action.
*/
export function openFileSharingPanel() {
return async (dispatch: IStore['dispatch']) => {
dispatch(setFocusedTab(ChatTabs.FILE_SHARING));
dispatch({
type: OPEN_CHAT
});
};
}
/**
* Initiates the sending of messages between a moderator and a lobby attendee.

View File

@@ -1,30 +1,34 @@
import { IStore } from '../app/types';
import { IParticipant } from '../base/participants/types';
import { navigate } from '../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
import { screen } from '../mobile/navigation/routes';
import { OPEN_CHAT } from './actionTypes';
import { setFocusedTab } from './actions.any';
import { ChatTabs } from './constants';
export * from './actions.any';
/**
* Displays the chat panel.
* Displays the chat panel with the CHAT tab active.
*
* @param {Object} participant - The recipient for the private chat.
* @param {boolean} disablePolls - Checks if polls are disabled.
*
* @returns {{
* participant: participant,
* type: OPEN_CHAT
* }}
* @returns {Function}
*/
export function openChat(participant?: IParticipant | undefined | Object, disablePolls?: boolean) {
if (disablePolls) {
navigate(screen.conference.chat);
}
navigate(screen.conference.chatandpolls.main);
return (dispatch: IStore['dispatch']) => {
if (disablePolls) {
navigate(screen.conference.chat);
} else {
navigate(screen.conference.chatandpolls.main);
}
return {
participant,
type: OPEN_CHAT
dispatch(setFocusedTab(ChatTabs.CHAT));
dispatch({
participant,
type: OPEN_CHAT
});
};
}

View File

@@ -8,12 +8,13 @@ import {
SET_CHAT_WIDTH,
SET_USER_CHAT_WIDTH
} from './actionTypes';
import { closeChat } from './actions.any';
import { closeChat, setFocusedTab } from './actions.any';
import { ChatTabs } from './constants';
export * from './actions.any';
/**
* Displays the chat panel.
* Displays the chat panel with the CHAT tab active.
*
* @param {Object} participant - The recipient for the private chat.
* @param {Object} _disablePolls - Used on native.
@@ -24,6 +25,7 @@ export * from './actions.any';
*/
export function openChat(participant?: Object, _disablePolls?: boolean) {
return function(dispatch: IStore['dispatch']) {
dispatch(setFocusedTab(ChatTabs.CHAT));
dispatch({
participant,
type: OPEN_CHAT

View File

@@ -10,7 +10,7 @@ import { arePollsDisabled } from '../../../conference/functions.any';
import { navigate } from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
import { screen } from '../../../mobile/navigation/routes';
import { getUnreadPollCount } from '../../../polls/functions';
import { getUnreadCount, getUnreadFilesCount } from '../../functions';
import { getUnreadCount, getUnreadFilesCount, isChatDisabled } from '../../functions';
interface IProps extends AbstractButtonProps {
@@ -65,7 +65,7 @@ class ChatButton extends AbstractButton<IProps> {
* @returns {IProps}
*/
function _mapStateToProps(state: IReduxState, ownProps: any) {
const enabled = getFeatureFlag(state, CHAT_ENABLED, true);
const enabled = getFeatureFlag(state, CHAT_ENABLED, true) && !isChatDisabled(state);
const { visible = enabled } = ownProps;
return {

View File

@@ -24,7 +24,7 @@ import {
toggleChat
} from '../../actions.web';
import { CHAT_SIZE, ChatTabs, OPTION_GROUPCHAT, SMALL_WIDTH_THRESHOLD } from '../../constants';
import { getChatMaxSize } from '../../functions';
import { getChatMaxSize, getFocusedTab, isChatDisabled } from '../../functions';
import { IChatProps as AbstractProps } from '../../types';
import ChatHeader from './ChatHeader';
@@ -41,13 +41,18 @@ interface IProps extends AbstractProps {
/**
* The currently focused tab.
*/
_focusedTab: ChatTabs;
_focusedTab?: ChatTabs;
/**
* True if the CC tab is enabled and false otherwise.
*/
_isCCTabEnabled: boolean;
/**
* True if chat is disabled.
*/
_isChatDisabled: boolean;
/**
* True if file sharing tab is enabled.
*/
@@ -217,6 +222,7 @@ const Chat = ({
_isOpen,
_isPollsEnabled,
_isCCTabEnabled,
_isChatDisabled,
_isFileSharingTabEnabled,
_focusedTab,
_isResizing,
@@ -229,6 +235,11 @@ const Chat = ({
dispatch,
t
}: IProps) => {
// If no tabs are available, don't render the chat panel at all.
if (_isChatDisabled && !_isPollsEnabled && !_isCCTabEnabled && !_isFileSharingTabEnabled) {
return null;
}
const { classes, cx } = useStyles({ _isResizing, width: _width });
const [ isMouseDown, setIsMouseDown ] = useState(false);
const [ mousePosition, setMousePosition ] = useState<number | null>(null);
@@ -416,7 +427,7 @@ const Chat = ({
return (
<>
{renderTabs()}
<div
{!_isChatDisabled && (<div
aria-labelledby = { ChatTabs.CHAT }
className = { cx(
classes.chatPanel,
@@ -442,7 +453,7 @@ const Chat = ({
)}
<ChatInput
onSend = { onSendMessage } />
</div>
</div>) }
{ _isPollsEnabled && (
<>
<div
@@ -484,8 +495,18 @@ const Chat = ({
* @returns {ReactElement}
*/
function renderTabs() {
let tabs = [
{
// The only way focused tab will be undefined is when no tab is enabled. Therefore this function won't be
// executed because Chat component won't render anything. This should never happen but adding the check
// here to make TS happy (when passing the _focusedTab in the selected prop for Tabs).
if (!_focusedTab) {
return null;
}
let tabs = [];
// Only add chat tab if chat is not disabled.
if (!_isChatDisabled) {
tabs.push({
accessibilityLabel: t('chat.tabs.chat'),
countBadge:
_focusedTab !== ChatTabs.CHAT && _unreadMessagesCount > 0 ? _unreadMessagesCount : undefined,
@@ -493,8 +514,8 @@ const Chat = ({
controlsId: `${ChatTabs.CHAT}-panel`,
icon: IconMessage,
title: t('chat.tabs.chat')
}
];
});
}
if (_isPollsEnabled) {
tabs.push({
@@ -564,6 +585,8 @@ const Chat = ({
{_showNamePrompt
? <DisplayNameForm
isCCTabEnabled = { _isCCTabEnabled }
isChatDisabled = { _isChatDisabled }
isFileSharingEnabled = { _isFileSharingTabEnabled }
isPollsEnabled = { _isPollsEnabled } />
: renderChat()}
<div
@@ -602,7 +625,7 @@ const Chat = ({
* }}
*/
function _mapStateToProps(state: IReduxState, _ownProps: any) {
const { isOpen, focusedTab, messages, unreadMessagesCount, unreadFilesCount, width, isResizing } = state['features/chat'];
const { isOpen, messages, unreadMessagesCount, unreadFilesCount, width, isResizing } = state['features/chat'];
const { unreadPollsCount } = state['features/polls'];
const _localParticipant = getLocalParticipant(state);
@@ -611,8 +634,9 @@ function _mapStateToProps(state: IReduxState, _ownProps: any) {
_isOpen: isOpen,
_isPollsEnabled: !arePollsDisabled(state),
_isCCTabEnabled: isCCTabEnabled(state),
_isChatDisabled: isChatDisabled(state),
_isFileSharingTabEnabled: isFileSharingEnabled(state),
_focusedTab: focusedTab,
_focusedTab: getFocusedTab(state),
_messages: messages,
_unreadMessagesCount: unreadMessagesCount,
_unreadPollsCount: unreadPollsCount,

View File

@@ -9,6 +9,7 @@ import { IconMessage } from '../../../base/icons/svg';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import { closeOverflowMenuIfOpen } from '../../../toolbox/actions.web';
import { toggleChat } from '../../actions.web';
import { isChatDisabled } from '../../functions';
import ChatCounter from './ChatCounter';
@@ -91,7 +92,8 @@ class ChatButton extends AbstractButton<IProps> {
*/
const mapStateToProps = (state: IReduxState) => {
return {
_chatOpen: state['features/chat'].isOpen
_chatOpen: state['features/chat'].isOpen,
visible: !isChatDisabled(state)
};
};

View File

@@ -2,12 +2,12 @@ import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { IReduxState } from '../../../app/types';
import Icon from '../../../base/icons/components/Icon';
import { IconCloseLarge } from '../../../base/icons/svg';
import { isFileSharingEnabled } from '../../../file-sharing/functions.any';
import { toggleChat } from '../../actions.web';
import { ChatTabs } from '../../constants';
import { getFocusedTab, isChatDisabled } from '../../functions';
interface IProps {
@@ -40,7 +40,8 @@ interface IProps {
function ChatHeader({ className, isCCTabEnabled, isPollsEnabled }: IProps) {
const dispatch = useDispatch();
const { t } = useTranslation();
const { focusedTab } = useSelector((state: IReduxState) => state['features/chat']);
const _isChatDisabled = useSelector(isChatDisabled);
const focusedTab = useSelector(getFocusedTab);
const fileSharingTabEnabled = useSelector(isFileSharingEnabled);
const onCancel = useCallback(() => {
@@ -56,7 +57,7 @@ function ChatHeader({ className, isCCTabEnabled, isPollsEnabled }: IProps) {
let title = 'chat.title';
if (focusedTab === ChatTabs.CHAT) {
if (!_isChatDisabled && focusedTab === ChatTabs.CHAT) {
title = 'chat.tabs.chat';
} else if (isPollsEnabled && focusedTab === ChatTabs.POLLS) {
title = 'chat.tabs.polls';
@@ -64,6 +65,11 @@ function ChatHeader({ className, isCCTabEnabled, isPollsEnabled }: IProps) {
title = 'chat.tabs.closedCaptions';
} else if (fileSharingTabEnabled && focusedTab === ChatTabs.FILE_SHARING) {
title = 'chat.tabs.fileSharing';
} else {
// If the focused tab is not enabled, don't render the header.
// This should not happen in normal circumstances since Chat.tsx already checks
// if any tabs are available before rendering.
return null;
}
return (

View File

@@ -25,6 +25,16 @@ interface IProps extends WithTranslation {
*/
isCCTabEnabled: boolean;
/**
* Whether chat is disabled.
*/
isChatDisabled: boolean;
/**
* Whether file sharing is enabled.
*/
isFileSharingEnabled: boolean;
/**
* Whether the polls feature is enabled or not.
*/
@@ -74,18 +84,31 @@ class DisplayNameForm extends Component<IProps, IState> {
* @returns {ReactElement}
*/
override render() {
const { isCCTabEnabled, isPollsEnabled, t } = this.props;
const { isCCTabEnabled, isChatDisabled, isFileSharingEnabled, isPollsEnabled, t } = this.props;
let title = 'chat.nickname.title';
// Build array of enabled feature names (translated).
const features = [
!isChatDisabled ? t('chat.nickname.featureChat') : '',
isPollsEnabled ? t('chat.nickname.featurePolls') : '',
isFileSharingEnabled ? t('chat.nickname.featureFileSharing') : '',
isCCTabEnabled ? t('chat.nickname.featureClosedCaptions') : ''
].filter(Boolean);
if (isCCTabEnabled && isPollsEnabled) {
title = 'chat.nickname.titleWithPollsAndCC';
} else if (isCCTabEnabled) {
title = 'chat.nickname.titleWithCC';
} else if (isPollsEnabled) {
title = 'chat.nickname.titleWithPolls';
// Return null if no features available - component won't render.
if (features.length === 0) {
return null;
}
// Build translation arguments dynamically: { feature1: "chat", feature2: "polls", ... }
const translationArgs = features.reduce((acc, feature, index) => {
acc[`feature${index + 1}`] = feature;
return acc;
}, {} as Record<string, string>);
// Use dynamic translation key: "titleWith1Features", "titleWith2Features", etc.
const title = t(`chat.nickname.titleWith${features.length}Features`, translationArgs);
return (
<div id = 'nickname'>
<form onSubmit = { this._onSubmit }>

View File

@@ -12,11 +12,14 @@ import { isJwtFeatureEnabled } from '../base/jwt/functions';
import { getParticipantById, isPrivateChatEnabled } from '../base/participants/functions';
import { IParticipant } from '../base/participants/types';
import { escapeRegexp } from '../base/util/helpers';
import { arePollsDisabled } from '../conference/functions.any';
import { isFileSharingEnabled } from '../file-sharing/functions.any';
import { getParticipantsPaneWidth } from '../participants-pane/functions';
import { isCCTabEnabled } from '../subtitles/functions.any';
import { VIDEO_SPACE_MIN_SIZE } from '../video-layout/constants';
import { IVisitorChatParticipant } from '../visitors/types';
import { MESSAGE_TYPE_ERROR, MESSAGE_TYPE_LOCAL, TIMESTAMP_FORMAT } from './constants';
import { ChatTabs, MESSAGE_TYPE_ERROR, MESSAGE_TYPE_LOCAL, TIMESTAMP_FORMAT } from './constants';
import { IMessage } from './types';
/**
@@ -153,6 +156,53 @@ export function areSmileysDisabled(state: IReduxState) {
return disableChatSmileys;
}
/**
* Returns whether the chat feature is disabled.
*
* @param {IReduxState} state - The redux state.
* @returns {boolean} True if chat is disabled, false otherwise.
*/
export function isChatDisabled(state: IReduxState): boolean {
return Boolean(state['features/base/config']?.disableChat);
}
/**
* Gets the default focused tab based on what features are enabled.
* Returns the first available tab in priority order: CHAT -> POLLS -> FILE_SHARING -> CLOSED_CAPTIONS.
*
* @param {IReduxState} state - The redux state.
* @returns {ChatTabs | undefined} The default focused tab.
*/
export function getDefaultFocusedTab(state: IReduxState): ChatTabs | undefined {
if (!isChatDisabled(state)) {
return ChatTabs.CHAT;
}
if (!arePollsDisabled(state)) {
return ChatTabs.POLLS;
}
if (isFileSharingEnabled(state)) {
return ChatTabs.FILE_SHARING;
}
if (isCCTabEnabled(state)) {
return ChatTabs.CLOSED_CAPTIONS;
}
return undefined;
}
/**
* Returns the currently focused tab or the default focused tab if none is set.
*
* @param {IReduxState} state - The redux state.
* @returns {ChatTabs | undefined} The focused tab or undefined if no tabs are available.
*/
export function getFocusedTab(state: IReduxState): ChatTabs | undefined {
return state['features/chat'].focusedTab || getDefaultFocusedTab(state);
}
/**
* Returns the timestamp to display for the message.
*

View File

@@ -0,0 +1,23 @@
import { useSelector } from 'react-redux';
import ChatButton from './components/web/ChatButton';
import { isChatDisabled } from './functions';
const chat = {
key: 'chat',
Content: ChatButton,
group: 2
};
/**
* A hook that returns the chat button if chat is not disabled.
*
* @returns {Object | undefined} - The chat button object or undefined.
*/
export function useChatButton() {
const _isChatDisabled = useSelector(isChatDisabled);
if (!_isChatDisabled) {
return chat;
}
}

View File

@@ -25,6 +25,8 @@ import { IParticipant } from '../base/participants/types';
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import StateListenerRegistry from '../base/redux/StateListenerRegistry';
import { playSound, registerSound, unregisterSound } from '../base/sounds/actions';
import { arePollsDisabled } from '../conference/functions.any';
import { isFileSharingEnabled } from '../file-sharing/functions.any';
import { addGif } from '../gifs/actions';
import { extractGifURL, getGifDisplayMode, isGifEnabled, isGifMessage } from '../gifs/function.any';
import { showMessageNotification } from '../notifications/actions';
@@ -34,6 +36,7 @@ import { ADD_REACTION_MESSAGE } from '../reactions/actionTypes';
import { pushReactions } from '../reactions/actions.any';
import { ENDPOINT_REACTION_NAME } from '../reactions/constants';
import { getReactionMessageFromBuffer, isReactionsEnabled } from '../reactions/functions.any';
import { isCCTabEnabled } from '../subtitles/functions.any';
import { showToolbox } from '../toolbox/actions';
import { getDisplayName } from '../visitors/functions';
@@ -66,7 +69,9 @@ import {
} from './constants';
import {
getDisplayNameSuffix,
getFocusedTab,
getUnreadCount,
isChatDisabled,
isSendGroupChatDisabled,
isVisitorChatParticipant
} from './functions';
@@ -181,23 +186,28 @@ MiddlewareRegistry.register(store => next => action => {
case SET_FOCUSED_TAB:
case OPEN_CHAT: {
const focusedTab = action.tabId || getState()['features/chat'].focusedTab;
const state = store.getState();
const focusedTab = action.tabId || getFocusedTab(state);
if (focusedTab === ChatTabs.CHAT) {
// Don't allow opening chat if it's disabled AND user is trying to open the CHAT tab.
if (isChatDisabled(state)) {
return next(action);
}
unreadCount = 0;
if (typeof APP !== 'undefined') {
APP.API.notifyChatUpdated(unreadCount, true);
}
const { privateMessageRecipient } = store.getState()['features/chat'];
const { privateMessageRecipient } = state['features/chat'];
if (
isSendGroupChatDisabled(store.getState())
isSendGroupChatDisabled(state)
&& privateMessageRecipient
&& !action.participant
) {
const participant = getParticipantById(store.getState(), privateMessageRecipient.id);
const participant = getParticipantById(state, privateMessageRecipient.id);
if (participant) {
action.participant = participant;
@@ -207,7 +217,21 @@ MiddlewareRegistry.register(store => next => action => {
}
}
} else if (focusedTab === ChatTabs.POLLS) {
// Don't allow opening chat panel if polls are disabled AND user is trying to open the POLLS tab.
if (arePollsDisabled(state)) {
return next(action);
}
dispatch(resetUnreadPollsCount());
// Don't allow opening chat panel if file sharing is disabled AND user is trying to open the
// FILE_SHARING tab.
} else if (focusedTab === ChatTabs.FILE_SHARING && !isFileSharingEnabled(state)) {
return next(action);
// Don't allow opening chat panel if closed captions are disabled AND user is trying to open the
// CLOSED_CAPTIONS tab.
} else if (focusedTab === ChatTabs.CLOSED_CAPTIONS && !isCCTabEnabled(state)) {
return next(action);
}
break;
@@ -239,7 +263,7 @@ MiddlewareRegistry.register(store => next => action => {
const participantExists = getParticipantById(state, shouldSendPrivateMessageTo.id);
if (participantExists || shouldSendPrivateMessageTo.isFromVisitor) {
dispatch(openDialog(ChatPrivacyDialog, {
dispatch(openDialog('ChatPrivacyDialog', ChatPrivacyDialog, {
message: action.message,
participantID: shouldSendPrivateMessageTo.id,
isFromVisitor: shouldSendPrivateMessageTo.isFromVisitor,
@@ -576,6 +600,11 @@ function _handleReceivedMessage({ dispatch, getState }: IStore,
const { isOpen: isChatOpen } = state['features/chat'];
const { soundsIncomingMessage: soundEnabled, userSelectedNotifications } = state['features/base/settings'];
// Don't play sound or show notifications if chat is disabled.
if (isChatDisabled(state)) {
return;
}
if (soundEnabled && shouldPlaySound && !isChatOpen) {
dispatch(playSound(INCOMING_MSG_SOUND_ID));
}

View File

@@ -35,7 +35,7 @@ const DEFAULT_STATE = {
privateMessageRecipient: undefined,
lobbyMessageRecipient: undefined,
isLobbyChatActive: false,
focusedTab: ChatTabs.CHAT,
focusedTab: undefined,
isResizing: false,
width: {
current: CHAT_SIZE,
@@ -44,7 +44,7 @@ const DEFAULT_STATE = {
};
export interface IChatState {
focusedTab: ChatTabs;
focusedTab?: ChatTabs;
groupChatWithPermissions: boolean;
isLobbyChatActive: boolean;
isOpen: boolean;

View File

@@ -22,7 +22,7 @@ export function notifyKickedOut(participant: any, submit?: Function) {
return;
}
dispatch(openDialog(AlertDialog, {
dispatch(openDialog('AlertDialog', AlertDialog, {
contentKey: {
key: participant ? 'dialog.kickTitle' : 'dialog.kickSystemTitle',
params: {
@@ -52,7 +52,7 @@ export function notifyConferenceFailed(reasonKey: string, submit?: Function) {
// we have to push the opening of the dialog to the queue
// so that we make sure it will be visible after the events
// of conference destroyed are done
setTimeout(() => dispatch(openDialog(AlertDialog, {
setTimeout(() => dispatch(openDialog('AlertDialog', AlertDialog, {
contentKey: {
key: reasonKey
},
@@ -60,7 +60,7 @@ export function notifyConferenceFailed(reasonKey: string, submit?: Function) {
},
onSubmit: () => {
submit?.();
dispatch(hideDialog(AlertDialog));
dispatch(hideDialog('AlertDialog', AlertDialog));
}
})));
};

View File

@@ -17,7 +17,7 @@ import logger from './logger';
*/
export function openLeaveReasonDialog(title?: string) {
return (dispatch: IStore['dispatch']): Promise<void> => new Promise(resolve => {
dispatch(openDialog(LeaveReasonDialog, {
dispatch(openDialog('LeaveReasonDialog', LeaveReasonDialog, {
onClose: resolve,
title
}));

View File

@@ -26,7 +26,7 @@ function SpeakerStatsLabel() {
const { t } = useTranslation();
const onClick = () => {
dispatch(openDialog(SpeakerStats, { conference }));
dispatch(openDialog('SpeakerStats', SpeakerStats, { conference }));
};
if (count <= 2 || _isSpeakerStatsDisabled) {

View File

@@ -327,7 +327,7 @@ export function _mapDispatchToProps(dispatch: IStore['dispatch']) {
* @returns {void}
*/
_onOpenBandwidthDialog() {
dispatch(openDialog(BandwidthSettingsDialog));
dispatch(openDialog('BandwidthSettingsDialog', BandwidthSettingsDialog));
}
};
}

View File

@@ -18,7 +18,7 @@ type Options = {
export function showDesktopPicker(options: Options = {}, onSourceChoose: Function) {
const { desktopSharingSources } = options;
return openDialog(DesktopPicker, {
return openDialog('DesktopPicker', DesktopPicker, {
desktopSharingSources,
onSourceChoose
});

View File

@@ -14,7 +14,7 @@ export function openDisplayNamePrompt({ onPostSubmit, validateInput }: {
onPostSubmit?: Function;
validateInput?: Function;
}) {
return openDialog(DisplayNamePrompt, {
return openDialog('DisplayNamePrompt', DisplayNamePrompt, {
onPostSubmit,
validateInput
});

View File

@@ -16,7 +16,7 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
case SETTINGS_UPDATED: {
if (action.settings.displayName
&& isDialogOpen(getState, DisplayNamePrompt)) {
dispatch(hideDialog(DisplayNamePrompt));
dispatch(hideDialog('DisplayNamePrompt', DisplayNamePrompt));
}
}
}

View File

@@ -159,7 +159,7 @@ StateListenerRegistry.register(
});
conference.on(JitsiConferenceEvents.E2EE_VERIFICATION_READY, (pId: string, sas: object) => {
dispatch(openDialog(ParticipantVerificationDialog, { pId,
dispatch(openDialog('ParticipantVerificationDialog', ParticipantVerificationDialog, { pId,
sas }));
});

View File

@@ -31,7 +31,7 @@ class EmbedMeetingButton extends AbstractButton<AbstractButtonProps> {
const { dispatch } = this.props;
sendAnalytics(createToolbarEvent('embed.meeting'));
dispatch(openDialog(EmbedMeetingDialog));
dispatch(openDialog('EmbedMeetingDialog', EmbedMeetingDialog));
}
}

View File

@@ -109,7 +109,7 @@ export function maybeOpenFeedbackDialog(conference: IJitsiConference, title?: st
* @returns {Object}
*/
export function openFeedbackDialog(conference?: IJitsiConference, title?: string, onClose?: Function) {
return openDialog(FeedbackDialog, {
return openDialog('FeedbackDialog', FeedbackDialog, {
conference,
onClose,
title

View File

@@ -0,0 +1,45 @@
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import { IconShareDoc } from '../../../base/icons/svg';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import { openFileSharingPanel } from '../../../chat/actions.any';
import { isFileSharingEnabled } from '../../functions.any';
/**
* Component that renders a button to open the file sharing panel.
*
* @augments AbstractButton
*/
class FileSharingButton extends AbstractButton<AbstractButtonProps> {
override icon = IconShareDoc;
override label = 'toolbar.fileSharing';
override tooltip = 'toolbar.fileSharing';
/**
* Handles clicking the button to open the file sharing panel.
*
* @private
* @returns {void}
*/
override _handleClick() {
const { dispatch } = this.props;
dispatch(openFileSharingPanel());
}
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {IReduxState} state - The Redux state.
* @returns {Object} - Mapped props.
*/
function mapStateToProps(state: IReduxState) {
return {
visible: isFileSharingEnabled(state)
};
}
export default translate(connect(mapStateToProps)(FileSharingButton));

View File

@@ -0,0 +1,23 @@
import { useSelector } from 'react-redux';
import FileSharingButton from './components/web/FileSharingButton';
import { isFileSharingEnabled } from './functions.any';
const fileSharing = {
key: 'filesharing',
Content: FileSharingButton,
group: 2
};
/**
* A hook that returns the file sharing button if it is enabled and undefined otherwise.
*
* @returns {Object | undefined} - The file sharing button object or undefined.
*/
export function useFileSharingButton() {
const isEnabled = useSelector(isFileSharingEnabled);
if (isEnabled) {
return fileSharing;
}
}

View File

@@ -1,28 +1,12 @@
import { IReduxState, IStore } from '../app/types';
import { getParticipantById, getVirtualScreenshareParticipantOwnerId } from '../base/participants/functions';
import {
getActiveSpeakersToBeDisplayed,
getVirtualScreenshareParticipantOwnerId
} from '../base/participants/functions';
import { setRemoteParticipants } from './actions';
import { isFilmstripScrollVisible } from './functions';
/**
* Returns an array containing the first `count` items from a set.
*
* @param {Set<T>} set - The set from which to take items.
* @param {number} count - The number of items to take.
* @returns {T[]} An array containing the taken items.
* @private
*/
function _takeFirstN<T>(set: Set<T>, count: number): T[] {
const result: T[] = [];
for (const item of set) {
if (result.length >= count) break;
result.push(item);
}
return result;
}
/**
* Computes the reorderd list of the remote participants.
*
@@ -32,7 +16,7 @@ function _takeFirstN<T>(set: Set<T>, count: number): T[] {
* @returns {void}
* @private
*/
export function updateRemoteParticipants(store: IStore, force?: boolean, participantId?: string): void {
export function updateRemoteParticipants(store: IStore, force?: boolean, participantId?: string) {
const state = store.getState();
let reorderedParticipants = [];
const { sortedRemoteVirtualScreenshareParticipants } = state['features/base/participants'];
@@ -49,65 +33,44 @@ export function updateRemoteParticipants(store: IStore, force?: boolean, partici
}
const {
dominantSpeaker,
fakeParticipants,
previousSpeakers,
sortedRemoteParticipants
} = state['features/base/participants'];
const { visibleRemoteParticipants } = state['features/filmstrip'];
const remoteParticipants = new Map(sortedRemoteParticipants);
const screenShareParticipants = sortedRemoteVirtualScreenshareParticipants
? [ ...sortedRemoteVirtualScreenshareParticipants.keys() ] : [];
const sharedVideos = fakeParticipants ? Array.from(fakeParticipants.keys()) : [];
const speakers: Set<string> = new Set();
const dominant = dominantSpeaker ? getParticipantById(state, dominantSpeaker) : undefined;
const config = state['features/base/config'];
const defaultRemoteDisplayName = config?.defaultRemoteDisplayName ?? 'Fellow Jitster';
// Generate the remote active speakers list.
if (dominant && !dominant.local) {
speakers.add(dominant.id);
}
previousSpeakers.forEach(id => speakers.add(id));
const speakers = getActiveSpeakersToBeDisplayed(state);
for (const screenshare of screenShareParticipants) {
const ownerId = getVirtualScreenshareParticipantOwnerId(screenshare);
remoteParticipants.delete(ownerId);
remoteParticipants.delete(screenshare);
speakers.delete(ownerId);
}
// Calculate the number of slots available for active speakers and then sort them alphabetically to ensure
// consistent order.
const numberOfActiveSpeakerSlots
= visibleRemoteParticipants.size - screenShareParticipants.length - sharedVideos.length;
const activeSpeakersDisplayed = _takeFirstN(speakers, numberOfActiveSpeakerSlots)
.sort((a: string, b: string) => {
return (getParticipantById(state, a)?.name ?? defaultRemoteDisplayName)
.localeCompare(getParticipantById(state, b)?.name ?? defaultRemoteDisplayName);
});
for (const sharedVideo of sharedVideos) {
remoteParticipants.delete(sharedVideo);
}
for (const speaker of speakers.keys()) {
remoteParticipants.delete(speaker);
}
// Always update the order of the thubmnails.
const participantsWithScreenShare = screenShareParticipants.reduce<string[]>((acc, screenshare) => {
const ownerId = getVirtualScreenshareParticipantOwnerId(screenshare);
acc.push(ownerId);
acc.push(screenshare);
remoteParticipants.delete(ownerId);
remoteParticipants.delete(screenshare);
return acc;
}, []);
// Always update the order of the thumbnails.
reorderedParticipants = [
...participantsWithScreenShare,
...sharedVideos,
...activeSpeakersDisplayed,
...Array.from(speakers.keys()),
...Array.from(remoteParticipants.keys())
];

View File

@@ -1,4 +1,3 @@
import { getParticipantById } from '../base/participants/functions';
import StateListenerRegistry from '../base/redux/StateListenerRegistry';
import { isFilmstripScrollVisible, updateRemoteParticipants } from './functions';
@@ -26,15 +25,7 @@ StateListenerRegistry.register(
*/
StateListenerRegistry.register(
/* selector */ state => state['features/base/participants'].dominantSpeaker,
/* listener */ (dominantSpeaker, store) => {
const { visibleRemoteParticipants } = store.getState()['features/filmstrip'];
const dominant = getParticipantById(store.getState(), dominantSpeaker);
// Only update the remote participants if the dominant speaker is not currently visible.
if (!dominant?.local && !visibleRemoteParticipants.has(dominantSpeaker)) {
updateRemoteParticipants(store);
}
});
/* listener */ (dominantSpeaker, store) => updateRemoteParticipants(store));
/**
* Listens for changes in the filmstrip scroll visibility.

View File

@@ -559,7 +559,7 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<IProps, IState> {
* @returns {void}
*/
_showFailedInviteAlert() {
this.props.dispatch(openDialog(AlertDialog, {
this.props.dispatch(openDialog('AlertDialog', AlertDialog, {
contentKey: {
key: 'inviteDialog.alertText'
}

View File

@@ -95,7 +95,7 @@ class DialInSummary extends PureComponent<IProps> {
* @returns {void}
*/
_onError() {
this.props.dispatch(openDialog(DialInSummaryErrorDialog));
this.props.dispatch(openDialog('DialInSummaryErrorDialog', DialInSummaryErrorDialog));
}
/**

View File

@@ -41,7 +41,7 @@ MiddlewareRegistry.register(store => next => action => {
function _beginAddPeople({ dispatch }: IStore, next: Function, action: AnyAction) {
const result = next(action);
dispatch(openDialog(AddPeopleDialog));
dispatch(openDialog('AddPeopleDialog', AddPeopleDialog));
return result;
}
@@ -60,7 +60,7 @@ function _beginAddPeople({ dispatch }: IStore, next: Function, action: AnyAction
* @returns {*} The value returned by {@code next(action)}.
*/
function _hideAddPeopleDialog({ dispatch }: IStore, next: Function, action: AnyAction) {
dispatch(hideDialog(AddPeopleDialog));
dispatch(hideDialog('AddPeopleDialog', AddPeopleDialog));
return next(action);
}

View File

@@ -15,7 +15,7 @@ import { isFeatureDisabled } from './functions';
export function maybeShowPremiumFeatureDialog(feature: ParticipantFeaturesKey) {
return function(dispatch: IStore['dispatch'], getState: IStore['getState']) {
if (isFeatureDisabled(getState(), feature)) {
dispatch(openDialog(PremiumFeatureDialog));
dispatch(openDialog('PremiumFeatureDialog', PremiumFeatureDialog));
return true;
}

View File

@@ -11,6 +11,7 @@ import {
getVirtualScreenshareParticipantByOwnerId
} from '../base/participants/functions';
import { toState } from '../base/redux/functions';
import { isStageFilmstripAvailable } from '../filmstrip/functions';
import { getAutoPinSetting } from '../video-layout/functions';
import {
@@ -18,7 +19,6 @@ import {
SET_LARGE_VIDEO_DIMENSIONS,
UPDATE_KNOWN_LARGE_VIDEO_RESOLUTION
} from './actionTypes';
import { shouldHideLargeVideo } from './functions';
/**
* Action to select the participant to be displayed in LargeVideo based on the
@@ -34,8 +34,12 @@ export function selectParticipantInLargeVideo(participant?: string) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const state = getState();
// Skip large video updates when the large video container is hidden.
if (shouldHideLargeVideo(state)) {
if (isStageFilmstripAvailable(state, 2)) {
return;
}
// Keep Etherpad open.
if (state['features/etherpad'].editing) {
return;
}
@@ -161,26 +165,18 @@ function _electParticipantInLargeVideo(state: IReduxState) {
}
}
// Next, pick the dominant speaker or the last active speaker if the dominant speaker is local.
// Next, pick the dominant speaker (other than self).
participant = getDominantSpeakerParticipant(state);
let speakerId: string | undefined;
if (participant && !participant.local) {
// Return the screensharing participant id associated with this endpoint if multi-stream is enabled and
// auto_pin_latest_screen_share setting is disabled.
const screenshareParticipant = getVirtualScreenshareParticipantByOwnerId(state, participant.id);
if (participant?.local) {
const { previousSpeakers } = state['features/base/participants'];
if (previousSpeakers?.size) {
speakerId = previousSpeakers.keys().next().value;
}
} else if (participant) {
speakerId = participant.id;
return screenshareParticipant?.id ?? participant.id;
}
// Return the screensharing participant id associated with this endpoint.
if (speakerId) {
const screenshareParticipant = getVirtualScreenshareParticipantByOwnerId(state, speakerId);
return screenshareParticipant?.id ?? speakerId;
}
// In case this is the local participant.
participant = undefined;
// Next, pick the most recent participant with video.
const lastVisibleRemoteParticipant = _electLastVisibleRemoteParticipant(state);

View File

@@ -1,7 +1,5 @@
import { IReduxState } from '../app/types';
import { getParticipantById } from '../base/participants/functions';
import { isStageFilmstripAvailable } from '../filmstrip/functions';
import { shouldDisplayTileView } from '../video-layout/functions.any';
/**
* Selector for the participant currently displaying on the large video.
@@ -14,17 +12,3 @@ export function getLargeVideoParticipant(state: IReduxState) {
return getParticipantById(state, participantId ?? '');
}
/**
* Determines whether the large video container should be hidden.
* Large video is hidden in tile view, stage filmstrip mode (with multiple participants),
* or when editing etherpad.
*
* @param {IReduxState} state - The Redux state.
* @returns {boolean} True if large video should be hidden, false otherwise.
*/
export function shouldHideLargeVideo(state: IReduxState): boolean {
return shouldDisplayTileView(state)
|| isStageFilmstripAvailable(state, 2)
|| Boolean(state['features/etherpad']?.editing);
}

View File

@@ -1,26 +0,0 @@
import StateListenerRegistry from '../base/redux/StateListenerRegistry';
import { SELECT_LARGE_VIDEO_PARTICIPANT } from './actionTypes';
import { selectParticipantInLargeVideo } from './actions.any';
import { shouldHideLargeVideo } from './functions';
/**
* Updates the large video when transitioning from a hidden state to visible state.
* This ensures the large video is properly updated when exiting tile view, stage filmstrip,
* whiteboard, or etherpad editing modes.
*/
StateListenerRegistry.register(
/* selector */ state => shouldHideLargeVideo(state),
/* listener */ (isHidden, { dispatch }) => {
// When transitioning from hidden to visible state, select participant (because currently it is undefined).
// Otherwise set it to undefined because we don't show the large video.
if (!isHidden) {
dispatch(selectParticipantInLargeVideo());
} else {
dispatch({
type: SELECT_LARGE_VIDEO_PARTICIPANT,
participantId: undefined
});
}
}
);

View File

@@ -1 +0,0 @@
import './subscriber.any';

View File

@@ -4,7 +4,6 @@ import StateListenerRegistry from '../base/redux/StateListenerRegistry';
import { getVideoTrackByParticipant } from '../base/tracks/functions.web';
import { getLargeVideoParticipant } from './functions';
import './subscriber.any';
/**
* Updates the on stage participant video.

View File

@@ -467,7 +467,7 @@ export function _mapStateToProps(state: IReduxState) {
_knocking: knocking,
_lobbyChatMessages: messages,
_lobbyMessageRecipient: lobbyMessageRecipient?.name,
_login: showModeratorLogin,
_login: showModeratorLogin && !state['features/base/jwt'].jwt,
_hangUp: showHangUp,
_isLobbyChatActive: isLobbyChatActive,
_meetingName: getConferenceName(state),

View File

@@ -4,7 +4,6 @@ import { createMaterialTopTabNavigator } from '@react-navigation/material-top-ta
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { IReduxState } from '../../../../../app/types';
import {
getClientHeight,
getClientWidth
@@ -12,6 +11,7 @@ import {
import { setFocusedTab } from '../../../../../chat/actions.any';
import Chat from '../../../../../chat/components/native/Chat';
import { ChatTabs } from '../../../../../chat/constants';
import { getFocusedTab } from '../../../../../chat/functions';
import { resetUnreadPollsCount } from '../../../../../polls/actions';
import PollsPane from '../../../../../polls/components/native/PollsPane';
import { screen } from '../../../routes';
@@ -23,8 +23,8 @@ const ChatAndPolls = () => {
const clientHeight = useSelector(getClientHeight);
const clientWidth = useSelector(getClientWidth);
const dispatch = useDispatch();
const { focusedTab } = useSelector((state: IReduxState) => state['features/chat']);
const initialRouteName = focusedTab === ChatTabs.POLLS
const currentFocusedTab = useSelector(getFocusedTab);
const initialRouteName = currentFocusedTab === ChatTabs.POLLS
? screen.conference.chatandpolls.tab.polls
: screen.conference.chatandpolls.tab.chat;

View File

@@ -13,7 +13,7 @@ import PageReloadDialog from '../base/dialog/components/native/PageReloadDialog'
*/
export function openPageReloadDialog(
conferenceError?: Error, configError?: Error, connectionError?: ConnectionFailedError) {
return openDialog(PageReloadDialog, {
return openDialog('PageReloadDialog', PageReloadDialog, {
conferenceError,
configError,
connectionError

View File

@@ -34,7 +34,7 @@ export default function RenameButton({ breakoutRoomJid, name }: IProps) {
const dispatch = useDispatch();
const { classes, cx } = useStyles();
const onRename = useCallback(() => {
dispatch(openDialog(BreakoutRoomNamePrompt, {
dispatch(openDialog('BreakoutRoomNamePrompt', BreakoutRoomNamePrompt, {
breakoutRoomJid,
initialRoomName: name
}));

View File

@@ -68,7 +68,7 @@ export const RoomContextMenu = ({
}, [ dispatch, room ]);
const onRenameBreakoutRoom = useCallback(() => {
dispatch(openDialog(BreakoutRoomNamePrompt, {
dispatch(openDialog('BreakoutRoomNamePrompt', BreakoutRoomNamePrompt, {
breakoutRoomJid: room?.jid,
initialRoomName: room?.name
}));

View File

@@ -32,7 +32,7 @@ import styles from './styles';
export const ContextMenuMore = () => {
const dispatch = useDispatch();
const muteAllVideo = useCallback(() => {
dispatch(openDialog(MuteEveryonesVideoDialog));
dispatch(openDialog('MuteEveryonesVideoDialog', MuteEveryonesVideoDialog));
dispatch(hideSheet());
}, [ dispatch ]);
const conference = useSelector(getCurrentConference);

View File

@@ -42,7 +42,7 @@ const ParticipantsPaneFooter = (): JSX.Element => {
getFeatureFlag(state, BREAKOUT_ROOMS_BUTTON_ENABLED, true)
);
const openMoreMenu = useCallback(() => dispatch(openSheet(ContextMenuMore)), [ dispatch ]);
const muteAll = useCallback(() => dispatch(openDialog(MuteEveryoneDialog)),
const muteAll = useCallback(() => dispatch(openDialog('MuteEveryoneDialog', MuteEveryoneDialog)),
[ dispatch ]);
const showMoreActions = useSelector(isMoreActionsVisible);
const showMuteAll = useSelector(isMuteAllVisible);

View File

@@ -112,10 +112,10 @@ export const FooterContextMenu = ({ isOpen, onDrawerClose, onMouseLeave }: IProp
const { classes } = useStyles();
const muteAllVideo = useCallback(
() => dispatch(openDialog(MuteEveryonesVideoDialog)), [ dispatch ]);
() => dispatch(openDialog('MuteEveryonesVideoDialog', MuteEveryonesVideoDialog)), [ dispatch ]);
const muteAllDesktop = useCallback(
() => dispatch(openDialog(MuteEveryonesDesktopDialog)), [ dispatch ]);
() => dispatch(openDialog('MuteEveryonesDesktopDialog', MuteEveryonesDesktopDialog)), [ dispatch ]);
const openModeratorSettings = () => dispatch(openSettingsDialog(SETTINGS_TABS.MODERATOR));

View File

@@ -181,7 +181,7 @@ const ParticipantsPane = () => {
}, []);
const onMuteAll = useCallback(() => {
dispatch(openDialog(MuteEveryoneDialog));
dispatch(openDialog('MuteEveryoneDialog', MuteEveryoneDialog));
}, []);
const onToggleContext = useCallback(() => {

View File

@@ -0,0 +1,45 @@
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import { IconInfo } from '../../../base/icons/svg';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import { openPollsPanel } from '../../../chat/actions.any';
import { arePollsDisabled } from '../../../conference/functions.any';
/**
* Component that renders a button to open the polls panel.
*
* @augments AbstractButton
*/
class PollsButton extends AbstractButton<AbstractButtonProps> {
override icon = IconInfo;
override label = 'toolbar.polls';
override tooltip = 'toolbar.polls';
/**
* Handles clicking the button to open the polls panel.
*
* @private
* @returns {void}
*/
override _handleClick() {
const { dispatch } = this.props;
dispatch(openPollsPanel());
}
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {IReduxState} state - The Redux state.
* @returns {Object} - Mapped props.
*/
function mapStateToProps(state: IReduxState) {
return {
visible: !arePollsDisabled(state)
};
}
export default translate(connect(mapStateToProps)(PollsButton));

View File

@@ -0,0 +1,24 @@
import { useSelector } from 'react-redux';
import { arePollsDisabled } from '../conference/functions.any';
import PollsButton from './components/web/PollsButton';
const polls = {
key: 'polls',
Content: PollsButton,
group: 2
};
/**
* A hook that returns the polls button if it is enabled and undefined otherwise.
*
* @returns {Object | undefined} - The polls button object or undefined.
*/
export function usePollsButton() {
const isPollsDisabled = useSelector(arePollsDisabled);
if (!isPollsDisabled) {
return polls;
}
}

View File

@@ -110,7 +110,7 @@ class ReactionMenuDialog extends PureComponent<IProps> {
*/
_onCancel() {
if (this.props._isOpen) {
this.props.dispatch(hideDialog(ReactionMenu_));
this.props.dispatch(hideDialog('ReactionMenu_', ReactionMenu_));
return true;
}

View File

@@ -47,7 +47,7 @@ class ReactionsMenuButton extends AbstractButton<IProps> {
* @returns {void}
*/
override _handleClick() {
this.props.dispatch(openDialog(ReactionMenuDialog));
this.props.dispatch(openDialog('ReactionMenuDialog', ReactionMenuDialog));
}
/**

View File

@@ -76,7 +76,7 @@ export function showRecordingLimitNotification(streamType: string) {
*/
export function showStartRecordingNotification() {
return (dispatch: IStore['dispatch']) => {
const openDialogCallback = () => dispatch(openDialog(StartRecordingDialog));
const openDialogCallback = () => dispatch(openDialog('StartRecordingDialog', StartRecordingDialog));
dispatch(showStartRecordingNotificationWithCallback(openDialogCallback));
};

View File

@@ -34,7 +34,7 @@ class LiveStreamButton extends AbstractLiveStreamButton<Props> {
const { _isLiveStreamRunning, dispatch } = this.props;
if (_isLiveStreamRunning) {
dispatch(openDialog(StopLiveStreamDialog));
dispatch(openDialog('StopLiveStreamDialog', StopLiveStreamDialog));
} else {
navigate(screen.conference.liveStream);
}

View File

@@ -26,10 +26,10 @@ class LiveStreamButton extends AbstractLiveStreamButton<IProps> {
*/
override _onHandleClick() {
const { _isLiveStreamRunning, dispatch } = this.props;
const dialogComponent = _isLiveStreamRunning ? StopLiveStreamDialog : StartLiveStreamDialog;
const dialogName = _isLiveStreamRunning ? 'StopLiveStreamDialog' : 'StartLiveStreamDialog';
dispatch(openDialog(
_isLiveStreamRunning ? StopLiveStreamDialog : StartLiveStreamDialog
));
dispatch(openDialog(dialogName, dialogComponent));
}
}

View File

@@ -82,7 +82,7 @@ export default class AbstractHighlightButton<P extends IProps, S={}> extends Com
const dialogShown = dispatch(maybeShowPremiumFeatureDialog(MEET_FEATURES.RECORDING));
if (!dialogShown) {
dispatch(openDialog(StartRecordingDialog));
dispatch(openDialog('StartRecordingDialog', StartRecordingDialog));
}
} ],
appearance: NOTIFICATION_TYPE.NORMAL

View File

@@ -37,7 +37,7 @@ class RecordButton extends AbstractRecordButton<Props> {
const { _isRecordingRunning, dispatch } = this.props;
if (_isRecordingRunning) {
dispatch(openDialog(StopRecordingDialog));
dispatch(openDialog('StopRecordingDialog', StopRecordingDialog));
} else {
navigate(screen.conference.recording);
}

View File

@@ -138,7 +138,7 @@ export class HighlightButton extends AbstractHighlightButton<IProps, IState> {
const dialogShown = dispatch(maybeShowPremiumFeatureDialog(MEET_FEATURES.RECORDING));
if (!dialogShown) {
dispatch(openDialog(StartRecordingDialog));
dispatch(openDialog('StartRecordingDialog', StartRecordingDialog));
}
}

View File

@@ -26,10 +26,10 @@ class RecordingButton extends AbstractRecordButton<IProps> {
*/
override _onHandleClick() {
const { _isRecordingRunning, dispatch } = this.props;
const dialogComponent = _isRecordingRunning ? StopRecordingDialog : StartRecordingDialog;
const dialogName = _isRecordingRunning ? 'StopRecordingDialog' : 'StartRecordingDialog';
dispatch(openDialog(
_isRecordingRunning ? StopRecordingDialog : StartRecordingDialog
));
dispatch(openDialog(dialogName, dialogComponent));
}
}

View File

@@ -427,6 +427,6 @@ function _showExplicitConsentDialog(recorderSession: any, dispatch: IStore['disp
dispatch(setVideoUnmutePermissions(true, true));
dispatch(setAudioMuted(true));
dispatch(setVideoMuted(true));
dispatch(openDialog(RecordingConsentDialog));
dispatch(openDialog('RecordingConsentDialog', RecordingConsentDialog));
});
}

View File

@@ -64,7 +64,7 @@ let permissionsReplyListener: Function | undefined,
* @public
*/
export function openRemoteControlAuthorizationDialog(participantId: string) {
return openDialog(RemoteControlAuthorizationDialog, { participantId });
return openDialog('RemoteControlAuthorizationDialog', RemoteControlAuthorizationDialog, { participantId });
}
/**

View File

@@ -71,7 +71,7 @@ export function endRoomLockRequest(
= password
? dispatch(setPassword(conference, conference.lock, password))
: Promise.resolve();
const endRoomLockRequest_ = () => dispatch(hideDialog(SecurityDialog));
const endRoomLockRequest_ = () => dispatch(hideDialog('SecurityDialog', SecurityDialog));
setPassword_.then(endRoomLockRequest_, endRoomLockRequest_);
};
@@ -90,7 +90,7 @@ export function endRoomLockRequest(
* }}
*/
export function _openPasswordRequiredPrompt(conference: IJitsiConference) {
return openDialog(PasswordRequiredPrompt, { conference });
return openDialog('PasswordRequiredPrompt', PasswordRequiredPrompt, { conference });
}
/**

View File

@@ -74,7 +74,7 @@ MiddlewareRegistry.register(store => next => action => {
* @returns {*}
*/
function _conferenceJoined({ dispatch }: IStore, next: Function, action: AnyAction) {
dispatch(hideDialog(PasswordRequiredPrompt));
dispatch(hideDialog('PasswordRequiredPrompt', PasswordRequiredPrompt));
return next(action);
}
@@ -104,7 +104,7 @@ function _conferenceFailed({ dispatch }: IStore, next: Function, action: AnyActi
dispatch(_openPasswordRequiredPrompt(conference));
}
} else {
dispatch(hideDialog(PasswordRequiredPrompt));
dispatch(hideDialog('PasswordRequiredPrompt', PasswordRequiredPrompt));
}
return next(action);

View File

@@ -28,7 +28,7 @@ export function showSalesforceNotification() {
customActionNameKey: [ 'notify.linkToSalesforceKey' ],
customActionHandler: [ () => {
dispatch(hideNotification(SALESFORCE_LINK_NOTIFICATION_ID));
dispatch(openDialog(SalesforceLinkDialog));
dispatch(openDialog('SalesforceLinkDialog', SalesforceLinkDialog));
} ],
appearance: NOTIFICATION_TYPE.NORMAL
}, NOTIFICATION_TIMEOUT_TYPE.LONG));

View File

@@ -59,7 +59,7 @@ export function startAudioScreenShareFlow() {
// If we're already in a normal screen sharing session, warn the user.
if (isScreenVideoShared(state)) {
dispatch(openDialog(ShareMediaWarningDialog, { _isAudioScreenShareWarning: true }));
dispatch(openDialog('ShareMediaWarningDialog', ShareMediaWarningDialog, { _isAudioScreenShareWarning: true }));
return;
}
@@ -76,7 +76,7 @@ export function startAudioScreenShareFlow() {
return;
}
dispatch(openDialog(ShareAudioDialog));
dispatch(openDialog('ShareAudioDialog', ShareAudioDialog));
};
}
@@ -94,7 +94,7 @@ export function startScreenShareFlow(enabled: boolean) {
// If we're in an audio screen sharing session, warn the user.
if (audioOnlySharing) {
dispatch(openDialog(ShareMediaWarningDialog, { _isAudioScreenShareWarning: false }));
dispatch(openDialog('ShareMediaWarningDialog', ShareMediaWarningDialog, { _isAudioScreenShareWarning: false }));
return;
}

View File

@@ -78,7 +78,7 @@ export function openCameraCaptureDialog(callback: Function, componentProps: ICam
return;
}
dispatch(openDialog(CameraCaptureDialog, {
dispatch(openDialog('CameraCaptureDialog', CameraCaptureDialog, {
callback,
componentProps
}));

View File

@@ -10,6 +10,6 @@ import { SecurityDialog } from './components/security-dialog';
*/
export function toggleSecurityDialog() {
return function(dispatch: IStore['dispatch']) {
dispatch(toggleDialog(SecurityDialog));
dispatch(toggleDialog('SecurityDialog', SecurityDialog));
};
}

View File

@@ -21,7 +21,7 @@ export function openLogoutDialog() {
const config = state['features/base/config'];
const logoutUrl = config.tokenLogoutUrl;
dispatch(openDialog(LogoutDialog, {
dispatch(openDialog('LogoutDialog', LogoutDialog, {
onLogout() {
if (isTokenAuthEnabled(config)) {
if (logoutUrl) {

View File

@@ -53,7 +53,7 @@ export function openLogoutDialog() {
const { conference } = state['features/base/conference'];
const { jwt } = state['features/base/jwt'];
dispatch(openDialog(LogoutDialog, {
dispatch(openDialog('LogoutDialog', LogoutDialog, {
onLogout() {
if (isTokenAuthEnabled(config) && config.tokenAuthUrlAutoRedirect && jwt) {
@@ -90,7 +90,7 @@ export function openLogoutDialog() {
* @returns {Function}
*/
export function openSettingsDialog(defaultTab?: string, isDisplayedOnWelcomePage?: boolean) {
return openDialog(SettingsDialog, {
return openDialog('SettingsDialog', SettingsDialog, {
defaultTab,
isDisplayedOnWelcomePage
});

View File

@@ -81,7 +81,7 @@ export function setSharedVideoStatus({ videoUrl, status, time, ownerId, muted }:
* @returns {Function}
*/
export function showSharedVideoDialog(onPostSubmit: Function) {
return openDialog(SharedVideoDialog, { onPostSubmit });
return openDialog('SharedVideoDialog', SharedVideoDialog, { onPostSubmit });
}
/**
@@ -181,7 +181,7 @@ export function showConfirmPlayingDialog(actor: String, onSubmit: Function) {
// shows only one dialog at a time
dispatch(setConfirmShowVideo(false));
dispatch(openDialog(ShareVideoConfirmDialog, {
dispatch(openDialog('ShareVideoConfirmDialog', ShareVideoConfirmDialog, {
actorName: actor,
onSubmit: () => {
dispatch(setConfirmShowVideo(true));
@@ -198,6 +198,6 @@ export function showConfirmPlayingDialog(actor: String, onSubmit: Function) {
*/
export function hideConfirmPlayingDialog() {
return (dispatch: IStore['dispatch']) => {
dispatch(hideDialog(ShareVideoConfirmDialog));
dispatch(hideDialog('ShareVideoConfirmDialog', ShareVideoConfirmDialog));
};
}

View File

@@ -26,7 +26,7 @@ class SpeakerStatsButton extends AbstractSpeakerStatsButton {
const { dispatch } = this.props;
sendAnalytics(createToolbarEvent('speaker.stats'));
dispatch(openDialog(SpeakerStats));
dispatch(openDialog('SpeakerStats', SpeakerStats));
}
}

View File

@@ -12,6 +12,6 @@ export * from './actions.any';
*/
export function toggleLanguageSelectorDialog() {
return function(dispatch: IStore['dispatch']) {
dispatch(toggleDialog(LanguageSelectorDialog));
dispatch(toggleDialog('LanguageSelectorDialog', LanguageSelectorDialog));
};
}

View File

@@ -60,6 +60,7 @@ function Toolbox(props: IProps) {
const { clientWidth } = useSelector((state: IReduxState) => state['features/base/responsive-ui']);
const { customToolbarButtons } = useSelector((state: IReduxState) => state['features/base/config']);
const toolbarBackgroundColor = useSelector((state: IReduxState) => state['features/base/config'].toolbarConfig?.backgroundColor);
const {
mainToolbarButtonsThresholds,
toolbarButtons
@@ -79,6 +80,11 @@ function Toolbox(props: IProps) {
const { buttonStylesBorderless, hangupButtonStyles } = _styles;
const style = { ...styles.toolbox };
// Allow overriding the toolbox background color from config (configOverwrite/overwriteConfig).
if (toolbarBackgroundColor) {
style.backgroundColor = toolbarBackgroundColor as any;
}
// We have only hangup and raisehand button in _iAmVisitor mode
if (_iAmVisitor) {
style.justifyContent = 'center';

View File

@@ -29,7 +29,7 @@ class LinkToSalesforce extends AbstractButton<AbstractButtonProps> {
const { dispatch } = this.props;
sendAnalytics(createToolbarEvent('link.to.salesforce'));
dispatch(openDialog(SalesforceLinkDialog));
dispatch(openDialog('SalesforceLinkDialog', SalesforceLinkDialog));
}
}

View File

@@ -37,6 +37,11 @@ import Separator from './Separator';
*/
interface IProps {
/**
* Optional toolbar background color passed as a prop.
*/
toolbarBackgroundColor?: string;
/**
* Explicitly passed array with the buttons which this Toolbox should display.
*/
@@ -65,7 +70,8 @@ const useStyles = makeStyles()(() => {
* @returns {ReactElement}
*/
export default function Toolbox({
toolbarButtons
toolbarButtons,
toolbarBackgroundColor: toolbarBackgroundColorProp
}: IProps) {
const { classes, cx } = useStyles();
const { t } = useTranslation();
@@ -92,7 +98,10 @@ export default function Toolbox({
const localParticipant = useSelector(getLocalParticipant);
const transcribing = useSelector(isTranscribing);
const _isCCTabEnabled = useSelector(isCCTabEnabled);
// Read toolbar background color from config (if provided) or from props.
const toolbarBackgroundColorFromConfig = useSelector((state: IReduxState) =>
state['features/base/config'].toolbarConfig?.backgroundColor);
const toolbarBackgroundColor = toolbarBackgroundColorProp || toolbarBackgroundColorFromConfig;
// Do not convert to selector, it returns new array and will cause re-rendering of toolbox on every action.
const jwtDisabledButtons = getJwtDisabledButtons(transcribing, _isCCTabEnabled, localParticipant?.features);
@@ -242,7 +251,8 @@ export default function Toolbox({
return (
<div
className = { cx(rootClassNames, shiftUp && 'shift-up') }
id = 'new-toolbox'>
id = 'new-toolbox'
style = { toolbarBackgroundColor ? { backgroundColor: toolbarBackgroundColor } : undefined }>
<div className = { containerClassName }>
<div
className = 'toolbox-content-wrapper'

View File

@@ -151,6 +151,7 @@ export const TOOLBAR_BUTTONS: ToolbarButton[] = [
'embedmeeting',
'etherpad',
'feedback',
'filesharing',
'filmstrip',
'fullscreen',
'hangup',
@@ -163,6 +164,7 @@ export const TOOLBAR_BUTTONS: ToolbarButton[] = [
'mute-everyone',
'mute-video-everyone',
'participants-pane',
'polls',
'profile',
'raisehand',
'recording',

View File

@@ -13,10 +13,12 @@ import { raiseHand } from '../base/participants/actions';
import { getLocalParticipant, hasRaisedHand } from '../base/participants/functions';
import { isToggleCameraEnabled } from '../base/tracks/functions.web';
import { toggleChat } from '../chat/actions.web';
import ChatButton from '../chat/components/web/ChatButton';
import { isChatDisabled } from '../chat/functions';
import { useChatButton } from '../chat/hooks.web';
import { useEmbedButton } from '../embed-meeting/hooks';
import { useEtherpadButton } from '../etherpad/hooks';
import { useFeedbackButton } from '../feedback/hooks.web';
import { useFileSharingButton } from '../file-sharing/hooks.web';
import { setGifMenuVisibility } from '../gifs/actions';
import { isGifEnabled } from '../gifs/function.any';
import InviteButton from '../invite/components/add-people-dialog/web/InviteButton';
@@ -32,6 +34,7 @@ import {
isParticipantsPaneEnabled
} from '../participants-pane/functions';
import { useParticipantPaneButton } from '../participants-pane/hooks.web';
import { usePollsButton } from '../polls/hooks.web';
import { addReactionToBuffer } from '../reactions/actions.any';
import { toggleReactionsMenuVisibility } from '../reactions/actions.web';
import RaiseHandContainerButton from '../reactions/components/web/RaiseHandContainerButtons';
@@ -91,12 +94,6 @@ const profile = {
group: 1
};
const chat = {
key: 'chat',
Content: ChatButton,
group: 2
};
const desktop = {
key: 'desktop',
Content: ShareDesktopButton,
@@ -277,7 +274,10 @@ export function useToolboxButtons(
const reactions = useReactionsButton();
const participants = useParticipantPaneButton();
const tileview = useTileViewButton();
const chat = useChatButton();
const cc = useClosedCaptionButton();
const polls = usePollsButton();
const filesharing = useFileSharingButton();
const recording = useRecordingButton();
const liveStreaming = useLiveStreamingButton();
const linktosalesforce = useLinkToSalesforceButton();
@@ -309,6 +309,8 @@ export function useToolboxButtons(
fullscreen: _fullscreen,
security,
closedcaptions: cc,
polls,
filesharing,
recording,
livestreaming: liveStreaming,
linktosalesforce,
@@ -360,6 +362,7 @@ export const useKeyboardShortcuts = (toolbarButtons: Array<string>) => {
const _toolbarButtons = useSelector(
(state: IReduxState) => toolbarButtons || state['features/toolbox'].toolbarButtons);
const chatOpen = useSelector((state: IReduxState) => state['features/chat'].isOpen);
const _isChatDisabled = useSelector(isChatDisabled);
const desktopSharingButtonDisabled = useSelector(isDesktopShareButtonDisabled);
const desktopSharingEnabled = JitsiMeetJS.isDesktopSharingEnabled();
const fullScreen = useSelector((state: IReduxState) => state['features/toolbox'].fullScreen);
@@ -377,6 +380,11 @@ export const useKeyboardShortcuts = (toolbarButtons: Array<string>) => {
* @returns {void}
*/
function onToggleChat() {
// Don't toggle chat if it's disabled.
if (_isChatDisabled) {
return false;
}
sendAnalytics(createShortcutEvent(
'toggle.chat',
ACTION_SHORTCUT_TRIGGERED,
@@ -426,7 +434,7 @@ export const useKeyboardShortcuts = (toolbarButtons: Array<string>) => {
function onToggleVideoQuality() {
sendAnalytics(createShortcutEvent('video.quality'));
dispatch(toggleDialog(VideoQualityDialog));
dispatch(toggleDialog('VideoQualityDialog', VideoQualityDialog));
}
/**
@@ -515,7 +523,7 @@ export const useKeyboardShortcuts = (toolbarButtons: Array<string>) => {
'speaker.stats'
));
dispatch(toggleDialog(SpeakerStats, {
dispatch(toggleDialog('SpeakerStats', SpeakerStats, {
conference: APP.conference
}));
}
@@ -527,7 +535,7 @@ export const useKeyboardShortcuts = (toolbarButtons: Array<string>) => {
exec: onToggleVideoQuality,
helpDescription: 'toolbar.callQuality'
},
isButtonEnabled('chat', _toolbarButtons) && {
!_isChatDisabled && isButtonEnabled('chat', _toolbarButtons) && {
character: 'C',
exec: onToggleChat,
helpDescription: 'keyboardShortcuts.toggleChat'

View File

@@ -24,6 +24,7 @@ export type ToolbarButton = 'camera' |
'embedmeeting' |
'etherpad' |
'feedback' |
'filesharing' |
'filmstrip' |
'fullscreen' |
'hangup' |
@@ -38,6 +39,7 @@ export type ToolbarButton = 'camera' |
'noisesuppression' |
'overflowmenu' |
'participants-pane' |
'polls' |
'profile' |
'raisehand' |
'reactions' |

View File

@@ -1,5 +1,7 @@
import React, { Component } from 'react';
import JitsiMeetJS from '../../base/lib-jitsi-meet';
/**
* React component representing unsupported browser page.
*
@@ -8,12 +10,17 @@ import React, { Component } from 'react';
class DefaultUnsupportedDesktopBrowser extends Component {
/**
* Redirects to the static recommended browsers page that is also used for IE.
* Redirects to the static recommended browsers page or the WebRTC unsupported page.
* IE and other browsers without WebRTC support will show the WebRTC unsupported page.
*
* @returns {void}
*/
override componentDidMount() {
window.location.pathname = 'static/recommendedBrowsers.html';
if (!JitsiMeetJS.isWebRtcSupported()) {
window.location.pathname = 'static/webrtcUnsupported.html';
} else {
window.location.pathname = 'static/recommendedBrowsers.html';
}
}
/**

View File

@@ -11,6 +11,7 @@ import { getReceiverVideoQualityLevel } from '../video-quality/functions';
import { getMinHeightForQualityLvlMap } from '../video-quality/selector';
import { LAYOUTS } from './constants';
import logger from './logger';
/**
* A selector for retrieving the current automatic pinning setting.
@@ -113,39 +114,49 @@ export function shouldDisplayTileView(state: IReduxState) {
* Private helper to automatically pin the latest screen share stream or unpin
* if there are no more screen share streams.
*
* @param {Array<string>} screenShares - Array containing the list of all the screen sharing endpoints
* @param {Array<string>} previousScreenShares - Array containing the list of all the screen sharing endpoints
* before the update was triggered (including the ones that have been removed from redux because of the update).
* @param {Array<string>} currentScreenShares - Array containing the current list of screen sharing endpoints.
* @param {Store} store - The redux store.
* @returns {void}
*/
export function updateAutoPinnedParticipant(
screenShares: Array<string>, { dispatch, getState }: IStore) {
const state = getState();
const remoteScreenShares = state['features/video-layout'].remoteScreenShares;
previousScreenShares: Array<string>,
currentScreenShares: Array<string>,
{ dispatch, getState }: IStore) {
const pinned = getPinnedParticipant(getState);
// if the pinned participant is shared video or some other fake participant we want to skip auto-pinning
if (pinned?.fakeParticipant && pinned.fakeParticipant !== FakeParticipant.RemoteScreenShare) {
logger.debug('Skipping auto-pin: pinned participant is non-screenshare virtual participant', pinned.id);
return;
}
// Unpin the screen share when the screen sharing participant leaves. Switch to tile view if no other
// participant was pinned before screen share was auto-pinned, pin the previously pinned participant otherwise.
if (!remoteScreenShares?.length) {
if (!currentScreenShares?.length) {
let participantId = null;
if (pinned && !screenShares.find(share => share === pinned.id)) {
if (pinned && !previousScreenShares.find((share: string) => share === pinned.id)) {
participantId = pinned.id;
}
logger.debug('No more screenshares, unpinning or restoring previous pin', participantId);
dispatch(pinParticipant(participantId));
return;
}
const latestScreenShareParticipantId = remoteScreenShares[remoteScreenShares.length - 1];
const latestScreenShareParticipantId = currentScreenShares[currentScreenShares.length - 1];
if (latestScreenShareParticipantId) {
dispatch(pinParticipant(latestScreenShareParticipantId));
const alreadyPinned = pinned?.id === latestScreenShareParticipantId;
if (!alreadyPinned) {
logger.debug(`Auto pinning latest screen share participant: ${latestScreenShareParticipantId}`);
dispatch(pinParticipant(latestScreenShareParticipantId));
}
}
}

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