Compare commits

...

10 Commits

Author SHA1 Message Date
Saúl Ibarra Corretgé
5e3463735b fix(polls) use medium timeout for poll notifications 2022-01-05 16:42:07 +01:00
tmoldovan8x8
7dd9785e48 fix(rn, recording) adds _toggleScreenshotCapture function to AbstractStopRecordingDialog 2022-01-05 16:52:20 +02:00
Hamza KHAIT
315c884f0d fix(lang) update French translation 2022-01-05 11:33:36 +01:00
José Luís Andrade
8590315244 fix(lang) update Portuguese Translation 2022-01-05 11:20:39 +01:00
Horatiu Muresan
6b260c27a5 fix(aot) Let jitsi-meet-electron-sdk do the close (#10679) 2022-01-04 13:21:30 +02:00
Horatiu Muresan
197dbfbbcb feat(toolbar-button-clicked) Enhance toolbar buttons with notify click
- add possibility to allow execution of the button's routine besides triggering
`toolbarButtonClicked` API event
- keep backwards compatibility
- get rid of `ToolbarButton`
2022-01-04 13:21:00 +02:00
Calin Chitu
847dc2a7bb fix(participants-pane) fix search value clear when closing pane 2022-01-04 10:52:42 +02:00
Christoph Settgast
7f26e7f927 fix(lang) update German translation 2021-12-28 07:15:37 +01:00
Mejans
6247d45b9e fix(lang) update Occitan translation 2021-12-24 17:30:52 +01:00
Дамян Минков
04f9ad32e1 fix(breakout-rooms): Adds few nil checks in lua code. 2021-12-23 14:40:39 -06:00
57 changed files with 533 additions and 559 deletions

View File

@@ -615,41 +615,61 @@ var config = {
// alwaysVisible: false
// },
// Toolbar buttons which have their click event exposed through the API on
// `toolbarButtonClicked` event instead of executing the normal click routine.
// Toolbar buttons which have their click/tap event exposed through the API on
// `toolbarButtonClicked`. Passing a string for the button key will
// prevent execution of the click/tap routine; passing an object with `key` and
// `preventExecution` flag on false will not prevent execution of the click/tap
// routine. Below array with mixed mode for passing the buttons.
// buttonsWithNotifyClick: [
// 'camera',
// 'chat',
// 'closedcaptions',
// 'desktop',
// 'download',
// 'embedmeeting',
// 'etherpad',
// 'feedback',
// 'filmstrip',
// 'fullscreen',
// 'hangup',
// 'help',
// 'invite',
// 'livestreaming',
// 'microphone',
// 'mute-everyone',
// 'mute-video-everyone',
// 'participants-pane',
// 'profile',
// 'raisehand',
// 'recording',
// 'security',
// 'select-background',
// 'settings',
// 'shareaudio',
// 'sharedvideo',
// 'shortcuts',
// 'stats',
// 'tileview',
// 'toggle-camera',
// 'videoquality',
// '__end'
// 'camera',
// {
// key: 'chat',
// preventExecution: false
// },
// {
// key: 'closedcaptions',
// preventExecution: true
// },
// 'desktop',
// 'download',
// 'embedmeeting',
// 'etherpad',
// 'feedback',
// 'filmstrip',
// 'fullscreen',
// 'hangup',
// 'help',
// {
// key: 'invite',
// preventExecution: false
// },
// 'livestreaming',
// 'microphone',
// 'mute-everyone',
// 'mute-video-everyone',
// 'participants-pane',
// 'profile',
// {
// key: 'raisehand',
// preventExecution: true
// },
// 'recording',
// 'security',
// 'select-background',
// 'settings',
// 'shareaudio',
// 'sharedvideo',
// 'shortcuts',
// 'stats',
// 'tileview',
// 'toggle-camera',
// 'videoquality',
// // The add passcode button from the security dialog.
// {
// key: 'add-passcode',
// preventExecution: false
// }
// '__end'
// ],
// List of pre meeting screens buttons to hide. The values must be one or more of the 5 allowed buttons:

View File

@@ -581,10 +581,12 @@
"allowedUnmute": "Sie können die Stummschaltung aufheben, Ihre Kamera einschalten oder Ihren Bildschirm teilen.",
"audioUnmuteBlockedTitle": "Stummschaltung kann nicht aufgehoben werden!",
"audioUnmuteBlockedDescription": "Díe Stummschaltung kann aus Überlastungsschutzgründen temporär nicht aufgehoben werden.",
"chatMessages": "Chatnachrichten",
"connectedOneMember": "{{name}} nimmt am Meeting teil",
"connectedThreePlusMembers": "{{name}} und {{count}} andere Personen nehmen am Meeting teil",
"connectedTwoMembers": "{{first}} und {{second}} nehmen am Meeting teil",
"disconnected": "getrennt",
"displayNotifications": "Benachrichtigungen anzeigen für",
"focus": "Konferenzleitung",
"focusFail": "{{component}} ist im Moment nicht verfügbar wiederholen in {{ms}} Sekunden",
"hostAskedUnmute": "Die Moderation bittet Sie, das Mikrofon zu aktivieren",
@@ -782,6 +784,7 @@
"title": "Profil"
},
"raisedHand": "Ich möchte sprechen",
"raisedHandsLabel": "Anzahl gehobener Hände",
"recording": {
"limitNotificationDescriptionWeb": "Wegen hoher Nachfrage ist Ihre Aufnahme auf {{limit}} Min. begrenzt. Für unlimitierte Aufnahmen nutzen Sie bitte <a href={{url}} rel='noopener noreferrer' target='_blank'>{{app}}</a>.",
"limitNotificationDescriptionNative": "Wegen hoher Nachfrage ist Ihre Aufnahme auf {{limit}} Min. begrenzt. Für unlimitierte Aufnahmen nutzen Sie bitte <3>{{app}}</3>.",
@@ -857,6 +860,7 @@
"selectAudioOutput": "Audioausgabe",
"selectCamera": "Kamera",
"selectMic": "Mikrofon",
"selfView": "Eigene Ansicht",
"sounds": "Hinweistöne",
"speakers": "Lautsprecher",
"startAudioMuted": "Alle Personen treten stummgeschaltet bei",

View File

@@ -654,7 +654,7 @@
"actions": {
"allow": "Autoriser les participants à:",
"allowVideo": "permettre la vidéo",
"audioModeration": "ouvrir leur micro",
"audioModeration": "Rouvrir leur micro",
"blockEveryoneMicCamera": "Bloquer tous les micros et caméras",
"invite": "Inviter quelqu'un",
"askUnmute": "Demander de réactiver le micro",

View File

@@ -158,7 +158,8 @@
"joinInApp": "Rejónher la conferéncia en utilizant laplicacion",
"launchWebButton": "Lançar del navigador",
"title": "Aviada de vòstra conferéncia dins {{app}}…",
"tryAgainButton": "Tornar ensajar del burèu"
"tryAgainButton": "Tornar ensajar del burèu",
"unsupportedBrowser": "Sembla quutilizatz un navigador que prenèm pas en carga."
},
"defaultLink": "ex. {{url}}",
"defaultNickname": "ex. Joan Delpuèch",
@@ -421,7 +422,7 @@
"noPassword": "Pas cap",
"noRoom": "Cap de sala pas donada per la jónher.",
"numbers": "Sonar de numèros",
"password": "$t(lockRoomPasswordUppercase):",
"password": "$t(lockRoomPasswordUppercase): ",
"title": "Partejar",
"tooltip": "Partejar lo ligam e las informacions daquesta conferéncia",
"copyNumber": "Copiar lo numèro",
@@ -627,9 +628,17 @@
"moderationInEffectCSDescription": "Volgatz levar la man se volètz partejar vòstre ecran.",
"moderationInEffectVideoDescription": "Volgatz levar la man se volètz aviar vòstra camèra.",
"audioUnmuteBlockedTitle": "Restabliment del son del microfòn blocat!",
"videoUnmuteBlockedTitle": "Restabliment de la camèra blocat!",
"videoUnmuteBlockedTitle": "Restabliment de la camèra e del partiment de burèu blocat !",
"audioUnmuteBlockedDescription": "Las operacion de restabliment del son microfòn son estadas blocadas pel moment a causa de limitas sistèma.",
"videoUnmuteBlockedDescription": "Las operacion de restabliment de la camèra son estadas blocadas pel moment a causa de limitas sistèma."
"videoUnmuteBlockedDescription": "Las operacion de restabliment de la camèra e del partiment del burèu son estadas blocadas pel moment a causa de limitas sistèma.",
"chatMessages": "Messatges del chat",
"displayNotifications": "Afichar las notificacions per",
"leftOneMember": "{{name}} a quitat la conferéncia",
"leftThreePlusMembers": "{{name}} e un molon dautres an quitat la conferéncia",
"leftTwoMembers": "{{first}} e {{second}} an quitat la conferéncia",
"raisedHands": "{{participantName}} e {{raisedHands}} autres",
"selfViewTitle": "Podètz totjorn quitar damagar vòstra pròpria vista a partir dels paramètres",
"reactionSoundsForAll": "Desactivar los sons per totes"
},
"passwordDigitsOnly": "Fins a {{number}} chifras",
"passwordSetRemotely": "causit per qualqu'un mai",
@@ -758,7 +767,7 @@
"about": "Podètz ajustar un $t(lockRoomPassword) per rejónher una conferéncia. Los participants deuràn fornir lo $t(lockRoomPassword) abans dobténer lautorizacion de dintrar dins la conferéncia.",
"aboutReadOnly": "Los participants que son moderators pòdon ajustar un $t(lockRoomPassword) a la conferéncia. Los participants deuràn fornir lo $t(lockRoomPassword) abans daver lautorizacion de rejónher la conferéncia.",
"insecureRoomNameWarning": "Lo nom de la sala es pas segur. De monde indesirables poirián rejónher vòstra conferéncia.",
"securityOptions": "Opcions de seguretat"
"header": "Opcions de seguretat"
},
"settings": {
"calendar": {
@@ -795,7 +804,9 @@
"talkWhileMuted": "Parlar en mut",
"desktopShareHighFpsWarning": "Una frequéncia dimatge mai nauta pel partiment burèu pòt afectar la benda passanta. Devètz reaviar lo partiment decran per aplicar los paramètres novèls.",
"desktopShareWarning": "Devètz reaviar lo partiment decran per prendre en compte las modificacions.",
"incomingMessage": "Messatge dintrant"
"incomingMessage": "Messatge dintrant",
"selfView": "Vista de se",
"startReactionsMuted": "Començan totes amb las reaccions sonòras amudidas"
},
"settingsView": {
"advanced": "Avançat",
@@ -898,7 +909,6 @@
"clap": "Picar de las mans",
"laugh": "Rire",
"like": "Levar lo det gròs",
"muteEveryonesVideo": "Copar la vidèo del monde",
"muteEveryoneElsesVideo": "Copar la vidèo de los demai",
"participants": "Participants",
"remoteVideoMute": "Copar la camèra del participant",
@@ -910,7 +920,9 @@
"collapse": "Plegar",
"muteEveryoneElse": "Copar lo microfòn dels autres",
"reactionsMenu": "Dobrir / Tampar lo menú de reaccions",
"breakoutRoom": "Rejónher/quitar la sala de reünion"
"breakoutRoom": "Rejónher/quitar la sala de reünion",
"muteEveryoneElsesVideoStream": "Arrestar la vidèo de totes los autres",
"muteEveryonesVideoStream": "Arrestar la vidèo de tot lo monde"
},
"addPeople": "Ajustar de monde a vòstra sonada",
"audioOnlyOff": "Desactivar lo mòde connexion febla",
@@ -1064,7 +1076,8 @@
"videomute": "Lo participant a arrestat la camèra",
"domuteVideo": "Desactivar la camèra",
"domuteVideoOfOthers": "Desactivar la camèra dels demai",
"videoMuted": "Camèra desactivada"
"videoMuted": "Camèra desactivada",
"hideSelfView": "Amagar pròpria vista"
},
"welcomepage": {
"accessibilityLabel": {
@@ -1135,7 +1148,8 @@
"image6": "Forèst ",
"desktopShareError": "Creacion impossibla dun partiment de burèu",
"desktopShare": "Partiment de burèu",
"webAssemblyWarning": "WebAssembly pas pres en carga"
"webAssemblyWarning": "WebAssembly pas pres en carga",
"webAssemblyWarningDescription": "WebAssembly es desactivat o pas pres en carga per aqueste navigador"
},
"participantsPane": {
"headings": {
@@ -1160,7 +1174,8 @@
"videoModeration": "Aviar lor vidèo",
"allowVideo": "Autorizar la vidèo",
"moreModerationActions": "Mai dopcions de moderacion",
"moreParticipantOptions": "Mai dopcions de participant"
"moreParticipantOptions": "Mai dopcions de participant",
"moreModerationControls": "Opcions de moderacion suplementàrias"
},
"search": "Cercar participants"
},
@@ -1207,7 +1222,12 @@
"autoAssign": "Atribucion auto a las salas de reünion"
},
"defaultName": "Sala de reünion #{{index}}",
"mainRoom": "Sala principala"
"mainRoom": "Sala principala",
"notifications": {
"joinedMainRoom": "Retorn a la sala principala",
"joinedTitle": "Salas suplementàrias",
"joined": "Dintrada a la sala suplementària « {{name}} »"
}
},
"privacyView": {
"header": "Confidencialitat"
@@ -1217,5 +1237,6 @@
},
"blankPage": {
"meetingEnded": "Conferéncia acabada."
}
},
"raisedHandsLabel": "Nombre de mans levadas"
}

View File

@@ -13,7 +13,7 @@
"failedToAdd": "Falha ao adicionar participantes",
"footerText": "A marcação está desactivada.",
"googleEmail": "Email do Google",
"inviteMoreHeader": "Você é o único na reunião",
"inviteMoreHeader": "É o único na reunião",
"inviteMoreMailSubject": "Participar na reunião {{appName}}",
"inviteMorePrompt": "Convidar mais pessoas",
"linkCopied": "Link copiado para a área de transferência",
@@ -94,7 +94,7 @@
"privateNotice": "Mensagem privada para {{recipient}}",
"message": "Mensagem",
"messageAccessibleTitle": "{{user}} disse:",
"messageAccessibleTitleMe": "Você disse:",
"messageAccessibleTitleMe": "Eu disse:",
"smileysPanel": "Painel de Emojis",
"tabs": {
"chat": "Chat",
@@ -515,7 +515,7 @@
"busyTitle": "Todas as transmissões estão atualmente ocupadas",
"changeSignIn": "Alternar contas.",
"choose": "Escolha uma transmissão em direto",
"chooseCTA": "Escolha uma opção de transmissão. Você está conectado atualmente como {{email}}.",
"chooseCTA": "Escolha uma opção de transmissão. Está conectado atualmente como {{email}}.",
"enterStreamKey": "Insira sua chave de transmissão em direto do YouTube aqui.",
"error": "Falha na transmissão em direto. Tente de novo.",
"errorAPI": "Ocorreu um erro ao acessar suas transmissões do YouTube. Por favor tente logar novamente.",
@@ -534,7 +534,7 @@
"pending": "Iniciando Transmissão em Direto...",
"serviceName": "Serviço de Transmissão em Direto",
"sessionAlreadyActive": "Esta sessão já está a ser gravada ou transmitida em direto.",
"signedInAs": "Você está conectado como:",
"signedInAs": "Está conectado como:",
"signIn": "Faça login no Google",
"signInCTA": "Faça login ou insira sua chave de transmissão em Direto do YouTube.",
"signOut": "Sair",
@@ -562,7 +562,7 @@
"engaged": "Gravação local iniciada.",
"finished": "Sessão de gravação {{token}} terminada. Por favor, envie o arquivo gravado para o moderador.",
"finishedModerator": "Sessão de gravação {{token}} terminada. A gravação da faixa local foi salva. Por favor, peça aos outros participantes para enviar suas gravações.",
"notModerator": "Você não é o moderador. Você não pode iniciar ou parar a gravação local."
"notModerator": "Não é o moderador. Não pode iniciar ou parar a gravação local."
},
"moderator": "Moderador",
"no": "Não",
@@ -581,10 +581,12 @@
"allowedUnmute": "Pode ligar o seu microfone, ligar a sua câmara ou partilhar o seu ecrã.",
"audioUnmuteBlockedTitle": "Ligar microfone bloqueado!",
"audioUnmuteBlockedDescription": "A operação de ligar o microfone foi temporariamente bloqueada devido aos limites do sistema.",
"chatMessages": "Mensagens de chat",
"connectedOneMember": "{{name}} entrou na reunião",
"connectedThreePlusMembers": "{{name}} e muitos outros entraram na reunião",
"connectedTwoMembers": "{{first}} e {{second}} entraram na reunião",
"disconnected": "desconectado",
"displayNotifications": "Mostrar notificações para",
"focus": "Foco da conferência",
"focusFail": "{{component}} não disponĩvel - tente em {{ms}} seg.",
"hostAskedUnmute": "O moderador gostaria que você falasse",
@@ -597,8 +599,8 @@
"leftTwoMembers": "{{first}} e {{second}} deixaram a reunião",
"me": "Eu",
"moderator": "É agora um moderador",
"muted": "Você iniciou uma conversa com o microfone desativado.",
"mutedTitle": "Você está silenciado!",
"muted": "Iniciou uma conversa com o microfone desativado.",
"mutedTitle": "Está silenciado!",
"mutedRemotelyTitle": "Foi silenciado pelo {{participantDisplayName}}",
"mutedRemotelyDescription": "Pode sempre voltar a ativar o microfone quando estiver pronto para falar. Silencie de volta quando estiver pronto para manter o barulho afastado da reunião.",
"videoMutedRemotelyTitle": "A sua câmara foi desligada pelo {{participantDisplayName}}.",
@@ -611,7 +613,7 @@
"screenShareNoAudioTitle": "Não foi possível partilhar o áudio do sistema!",
"selfViewTitle": "Pode sempre reexibir a autovisualização a partir das definições",
"somebody": "Alguém",
"startSilentTitle": "Você entrou sem saída de áudio!",
"startSilentTitle": "Entrou sem saída de áudio!",
"startSilentDescription": "Volte à reunião para habilitar o áudio",
"suboptimalBrowserWarning": "Tememos que sua experiência de reunião não seja tão boa aqui. Estamos procurando maneiras de melhorar isso, mas até então, tente usar um dos <a href='{{recommendedBrowserPageLink}}' target='_blank'>navegadores completamente suportados</a>.",
"suboptimalExperienceTitle": "Alerta do navegador",
@@ -652,7 +654,7 @@
"actions": {
"allow": "Permitir aos participantes:",
"allowVideo": "Permitir vídeo",
"audioModeration": "Ativarem o microfone deles",
"audioModeration": "Ativar o microfone deles",
"blockEveryoneMicCamera": "Bloquear o microfone e a câmara de todos",
"invite": "Convidar alguém",
"askUnmute": "Pedir para ligar o microfone",
@@ -664,7 +666,7 @@
"stopEveryonesVideo": "Desligar a câmara de todos",
"stopVideo": "Desligar a câmara",
"unblockEveryoneMicCamera": "Desbloquear o microfone e a câmara de todos",
"videoModeration": "Ligarem a câmara deles",
"videoModeration": "Ligar a câmara deles",
"moreModerationControls": "Mais controlos de moderação"
},
"search": "Pesquisar participantes"
@@ -782,6 +784,7 @@
"title": "Perfil"
},
"raisedHand": "Gostaria de falar",
"raisedHandsLabel": "Número de mãos levantadas",
"recording": {
"limitNotificationDescriptionWeb": "Devido à grande procura, a sua gravação será limitada a {{limit}} min. For unlimited recordings try <a href={{url}} rel='noopener noreferrer' target='_blank'>{{app}}</a>.",
"limitNotificationDescriptionNative": "Due to high demand your recording will be limited to {{limit}} min. Para gravações ilimitadas tente <3>{{app}}</3>.",
@@ -857,6 +860,7 @@
"selectAudioOutput": "Saída de áudio",
"selectCamera": "Câmara",
"selectMic": "Microfone",
"selfView": "Autovisualização",
"sounds": "Sons",
"speakers": "Participantes",
"startAudioMuted": "Todos começam com microfone desligado",
@@ -1142,7 +1146,7 @@
"join": "Toque para entrar",
"roomname": "Digite o nome da sala"
},
"appDescription": "Vá em frente, converse por vídeo com toda a equipa. Na verdade, convide todos os que conhece. {{app}} é uma solução de videoconferência totalmente criptografada e 100% de código aberto que você pode usar todos os dias, a cada dia, gratuitamente — sem necessidade de conta.",
"appDescription": "Vá em frente, converse por vídeo com toda a equipa. Na verdade, convide todos os que conhece. {{app}} é uma solução de videoconferência totalmente criptografada e 100% de código aberto que pode usar todos os dias, a cada dia, gratuitamente — sem necessidade de conta.",
"audioVideoSwitch": {
"audio": "Voz",
"video": "Vídeo"
@@ -1166,7 +1170,7 @@
"privacy": "Política de Privacidade",
"recentList": "Recente",
"recentListDelete": "Remover",
"recentListEmpty": "Sua lista recente está vazia. As reuniões que você realizar serão exibidas aqui.",
"recentListEmpty": "A sua lista recente está atualmente vazia. Converse com a sua equipa e encontrará aqui todas as suas reuniões recentes.",
"reducedUIText": "Bem-vindo ao {{app}}!",
"roomNameAllowedChars": "Nome da reunião não deve conter qualquer um destes caracteres: ?. &, :, ', \", %, #.",
"roomname": "Digite o nome da sala",

View File

@@ -1521,12 +1521,14 @@ class API {
* Notify external application ( if API is enabled) that a toolbar button was clicked.
*
* @param {string} key - The key of the toolbar button.
* @param {boolean} preventExecution - Whether execution of the button click was prevented or not.
* @returns {void}
*/
notifyToolbarButtonClicked(key: string) {
notifyToolbarButtonClicked(key: string, preventExecution: boolean) {
this._sendEvent({
name: 'toolbar-button-clicked',
key
key,
preventExecution
});
}

View File

@@ -23,6 +23,5 @@ export default class HangupButton extends AbstractHangupButton<Props, *> {
*/
_doHangup() {
api.executeCommand('hangup');
window.close();
}
}

View File

@@ -23,14 +23,6 @@ export default class AbstractAudioMuteButton<P: Props, S: *>
* @returns {void}
*/
_handleClick() {
const { handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
this._setAudioMuted(!this._isAudioMuted());
}

View File

@@ -2,6 +2,7 @@
import React, { Component } from 'react';
import { NOTIFY_CLICK_MODE } from '../../../toolbox/constants';
import { combineStyles } from '../../styles';
import type { Styles } from './AbstractToolboxItem';
@@ -14,6 +15,11 @@ export type Props = {
*/
afterClick: ?Function,
/**
* The button's key.
*/
buttonKey?: string,
/**
* Extra styles which will be applied in conjunction with `styles` or
* `toggledStyles` when the button is disabled;.
@@ -25,6 +31,12 @@ export type Props = {
*/
handleClick?: Function,
/**
* Notify mode for `toolbarButtonClicked` event -
* whether to only notify or to also prevent button click routine.
*/
notifyMode?: string,
/**
* Whether to show the label or not.
*/
@@ -51,6 +63,8 @@ export type Props = {
visible: boolean
};
declare var APP: Object;
/**
* Default style for disabled buttons.
*/
@@ -134,6 +148,17 @@ export default class AbstractButton<P: Props, S: *> extends Component<P, S> {
this._onClick = this._onClick.bind(this);
}
/**
* Helper function to be implemented by subclasses, which should be used
* to handle a key being down.
*
* @protected
* @returns {void}
*/
_onKeyDown() {
// To be implemented by subclass.
}
/**
* Helper function to be implemented by subclasses, which should be used
* to handle the button being clicked / pressed.
@@ -248,17 +273,29 @@ export default class AbstractButton<P: Props, S: *> extends Component<P, S> {
_onClick: (*) => void;
/**
* Handles clicking / pressing the button, and toggles the audio mute state
* accordingly.
* Handles clicking / pressing the button.
*
* @param {Object} e - Event.
* @private
* @returns {void}
*/
_onClick(e) {
const { afterClick } = this.props;
const { afterClick, handleClick, notifyMode, buttonKey } = this.props;
if (typeof APP !== 'undefined' && notifyMode) {
APP.API.notifyToolbarButtonClicked(
buttonKey, notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY
);
}
if (notifyMode !== NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY) {
if (handleClick) {
handleClick();
}
this._handleClick();
}
this._handleClick();
afterClick && afterClick(e);
// blur after click to release focus from button to allow PTT.
@@ -288,6 +325,7 @@ export default class AbstractButton<P: Props, S: *> extends Component<P, S> {
<ToolboxItem
disabled = { this._isDisabled() }
onClick = { this._onClick }
onKeyDown = { this._onKeyDown }
{ ...props } />
);
}

View File

@@ -22,14 +22,6 @@ export default class AbstractVideoMuteButton<P : Props, S : *>
* @returns {void}
*/
_handleClick() {
const { handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
this._setVideoMuted(!this._isVideoMuted());
}

View File

@@ -6,7 +6,15 @@ import { Icon } from '../../icons';
import { Tooltip } from '../../tooltip';
import AbstractToolboxItem from './AbstractToolboxItem';
import type { Props } from './AbstractToolboxItem';
import type { Props as AbstractToolboxItemProps } from './AbstractToolboxItem';
type Props = AbstractToolboxItemProps & {
/**
* On key down handler.
*/
onKeyDown: Function
};
/**
* Web implementation of {@code AbstractToolboxItem}.
@@ -53,6 +61,7 @@ export default class ToolboxItem extends AbstractToolboxItem<Props> {
disabled,
elementAfter,
onClick,
onKeyDown,
showLabel,
tooltipPosition,
toggled
@@ -64,6 +73,7 @@ export default class ToolboxItem extends AbstractToolboxItem<Props> {
'aria-label': this.accessibilityLabel,
className: className + (disabled ? ' disabled' : ''),
onClick: disabled ? undefined : onClick,
onKeyDown: disabled ? undefined : onKeyDown,
onKeyPress: this._onKeyPress,
tabIndex: 0,
role: showLabel ? 'menuitem' : 'button'

View File

@@ -2,11 +2,17 @@
import React from 'react';
import { NOTIFY_CLICK_MODE } from '../../../../toolbox/constants';
import { Icon } from '../../../icons';
import { Tooltip } from '../../../tooltip';
type Props = {
/**
* The button's key.
*/
buttonKey?: string,
/**
* The decorated component (ToolboxButton).
*/
@@ -22,6 +28,12 @@ type Props = {
*/
iconDisabled: boolean,
/**
* Notify mode for `toolbarButtonClicked` event -
* whether to only notify or to also prevent button click routine.
*/
notifyMode?: string,
/**
* Click handler for the small icon.
*/
@@ -68,6 +80,8 @@ type Props = {
iconId: string
};
declare var APP: Object;
/**
* Displays the `ToolboxButtonWithIcon` component.
*
@@ -80,6 +94,8 @@ export default function ToolboxButtonWithIcon(props: Props) {
icon,
iconDisabled,
iconTooltip,
buttonKey,
notifyMode,
onIconClick,
onIconKeyDown,
styles,
@@ -97,7 +113,17 @@ export default function ToolboxButtonWithIcon(props: Props) {
= 'settings-button-small-icon settings-button-small-icon--disabled';
} else {
iconProps.className = 'settings-button-small-icon';
iconProps.onClick = onIconClick;
iconProps.onClick = () => {
if (typeof APP !== 'undefined' && notifyMode) {
APP.API.notifyToolbarButtonClicked(
buttonKey, notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY
);
}
if (notifyMode !== NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY) {
onIconClick();
}
};
iconProps.onKeyDown = onIconKeyDown;
iconProps.role = 'button';
iconProps.tabIndex = 0;

View File

@@ -49,22 +49,6 @@ class ChatButton extends AbstractButton<Props, *> {
// Unused.
}
/**
* Handles clicking / pressing the button, and opens the appropriate dialog.
*
* @protected
* @returns {void}
*/
_handleClick() {
const { handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
}
/**
* Indicates whether this button is in toggled state or not.
*

View File

@@ -36,13 +36,7 @@ class EmbedMeetingButton extends AbstractButton<Props, *> {
* @returns {void}
*/
_handleClick() {
const { dispatch, handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
const { dispatch } = this.props;
sendAnalytics(createToolbarEvent('embed.meeting'));
dispatch(openDialog(EmbedMeetingDialog));

View File

@@ -9,7 +9,6 @@ import { connect } from '../../base/redux';
import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components';
import { toggleDocument } from '../../etherpad/actions';
type Props = AbstractButtonProps & {
/**
@@ -59,13 +58,7 @@ class SharedDocumentButton extends AbstractButton<Props, *> {
* @returns {void}
*/
_handleClick() {
const { _editing, dispatch, handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
const { _editing, dispatch } = this.props;
sendAnalytics(createToolbarEvent(
'toggle.etherpad',

View File

@@ -40,13 +40,7 @@ class FeedbackButton extends AbstractButton<Props, *> {
* @returns {void}
*/
_handleClick() {
const { _conference, dispatch, handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
const { _conference, dispatch } = this.props;
sendAnalytics(createToolbarEvent('feedback'));
dispatch(openFeedbackDialog(_conference));

View File

@@ -34,13 +34,7 @@ class InviteButton extends AbstractButton<Props, *> {
* @returns {void}
*/
_handleClick() {
const { dispatch, handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
const { dispatch } = this.props;
sendAnalytics(createToolbarEvent('invite'));
dispatch(beginAddPeople());

View File

@@ -34,13 +34,7 @@ class KeyboardShortcutsButton extends AbstractButton<Props, *> {
* @returns {void}
*/
_handleClick() {
const { dispatch, handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
const { dispatch } = this.props;
sendAnalytics(createToolbarEvent('shortcuts'));
dispatch(openKeyboardShortcutsDialog());

View File

@@ -36,13 +36,7 @@ class LocalRecording extends AbstractButton<Props, *> {
* @returns {void}
*/
_handleClick() {
const { dispatch, handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
const { dispatch } = this.props;
sendAnalytics(createToolbarEvent('local.recording'));
dispatch(openDialog(LocalRecordingInfoDialog));

View File

@@ -107,7 +107,8 @@ function MeetingParticipants({
{showInviteButton && <InviteButton />}
<ClearableInput
onChange = { setSearchString }
placeholder = { t('participantsPane.search') } />
placeholder = { t('participantsPane.search') }
value = { searchString } />
<div>
<MeetingParticipantItems
askUnmuteText = { askUnmuteText }

View File

@@ -25,22 +25,6 @@ class ParticipantsPaneButton extends AbstractButton<Props, *> {
label = 'toolbar.participants';
tooltip = 'toolbar.participants';
/**
* Handles clicking / pressing the button, and opens the appropriate dialog.
*
* @protected
* @returns {void}
*/
_handleClick() {
const { handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
}
/**
* Indicates whether this button is in toggled state or not.
*

View File

@@ -83,7 +83,7 @@ StateListenerRegistry.register(
appearance: NOTIFICATION_TYPE.NORMAL,
titleKey: 'polls.notification.title',
descriptionKey: 'polls.notification.description'
}, NOTIFICATION_TIMEOUT_TYPE.SHORT));
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
break;
}

View File

@@ -0,0 +1,73 @@
// @flow
import { translate } from '../../../base/i18n';
import { IconRaisedHand } from '../../../base/icons';
import { getLocalParticipant, hasRaisedHand } from '../../../base/participants';
import { connect } from '../../../base/redux';
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
/**
* The type of the React {@code Component} props of {@link RaiseHandButton}.
*/
type Props = AbstractButtonProps & {
/**
* Whether or not the hand is raised.
*/
raisedHand: boolean,
};
/**
* Implementation of a button for raising hand.
*/
class RaiseHandButton extends AbstractButton<Props, *> {
accessibilityLabel = 'toolbar.accessibilityLabel.raiseHand';
icon = IconRaisedHand;
label = 'toolbar.raiseHand';
toggledLabel = 'toolbar.raiseHand';
/**
* Retrieves tooltip dynamically.
*/
get tooltip() {
return 'toolbar.raiseHand';
}
/**
* Required by linter due to AbstractButton overwritten prop being writable.
*
* @param {string} _value - The value.
*/
set tooltip(_value) {
// Unused.
}
/**
* Indicates whether this button is in toggled state or not.
*
* @override
* @protected
* @returns {boolean}
*/
_isToggled() {
return this.props.raisedHand;
}
}
/**
* Function that maps parts of Redux state tree into component props.
*
* @param {Object} state - Redux state.
* @returns {Object}
*/
const mapStateToProps = state => {
const localParticipant = getLocalParticipant(state);
return {
raisedHand: hasRaisedHand(localParticipant)
};
};
export default translate(connect(mapStateToProps)(RaiseHandButton));

View File

@@ -4,16 +4,15 @@ import React, { useCallback } from 'react';
import { isMobileBrowser } from '../../../base/environment/utils';
import { translate } from '../../../base/i18n';
import { IconArrowUp, IconRaisedHand } from '../../../base/icons';
import { getLocalParticipant, hasRaisedHand } from '../../../base/participants';
import { IconArrowUp } from '../../../base/icons';
import { connect } from '../../../base/redux';
import { ToolboxButtonWithIcon } from '../../../base/toolbox/components';
import ToolbarButton from '../../../toolbox/components/web/ToolbarButton';
import { toggleReactionsMenuVisibility } from '../../actions.web';
import { type ReactionEmojiProps } from '../../constants';
import { getReactionsQueue, isReactionsEnabled } from '../../functions.any';
import { getReactionsMenuVisibility } from '../../functions.web';
import RaiseHandButton from './RaiseHandButton';
import ReactionEmoji from './ReactionEmoji';
import ReactionsMenuPopup from './ReactionsMenuPopup';
@@ -24,6 +23,11 @@ type Props = {
*/
_reactionsEnabled: Boolean,
/**
* The button's key.
*/
buttonKey?: string,
/**
* Redux dispatch function.
*/
@@ -45,9 +49,10 @@ type Props = {
isMobile: boolean,
/**
* Whether or not the local participant's hand is raised.
* Notify mode for `toolbarButtonClicked` event -
* whether to only notify or to also prevent button click routine.
*/
raisedHand: boolean,
notifyMode?: string,
/**
* The array of reactions to be displayed.
@@ -70,11 +75,12 @@ declare var APP: Object;
*/
function ReactionsMenuButton({
_reactionsEnabled,
buttonKey,
dispatch,
handleClick,
isOpen,
isMobile,
raisedHand,
notifyMode,
reactionsQueue,
t
}: Props) {
@@ -82,30 +88,31 @@ function ReactionsMenuButton({
dispatch(toggleReactionsMenuVisibility());
}, [ dispatch ]);
const raiseHandButton = (<ToolbarButton
accessibilityLabel = { t('toolbar.accessibilityLabel.raiseHand') }
icon = { IconRaisedHand }
key = 'raise-hand'
onClick = { handleClick }
toggled = { raisedHand }
tooltip = { t('toolbar.raiseHand') } />);
return (
<div className = 'reactions-menu-popup-container'>
<ReactionsMenuPopup>
{!_reactionsEnabled || isMobile ? raiseHandButton
{!_reactionsEnabled || isMobile ? (
<RaiseHandButton
buttonKey = { buttonKey }
handleClick = { handleClick }
notifyMode = { notifyMode } />)
: (
<ToolboxButtonWithIcon
ariaControls = 'reactions-menu-dialog'
ariaExpanded = { isOpen }
ariaHasPopup = { true }
ariaLabel = { t('toolbar.accessibilityLabel.reactionsMenu') }
buttonKey = { buttonKey }
icon = { IconArrowUp }
iconDisabled = { false }
iconId = 'reactions-menu-button'
iconTooltip = { t(`toolbar.${isOpen ? 'closeReactionsMenu' : 'openReactionsMenu'}`) }
notifyMode = { notifyMode }
onIconClick = { toggleReactionsMenu }>
{raiseHandButton}
<RaiseHandButton
buttonKey = { buttonKey }
handleClick = { handleClick }
notifyMode = { notifyMode } />
</ToolboxButtonWithIcon>
)}
</ReactionsMenuPopup>
@@ -125,14 +132,11 @@ function ReactionsMenuButton({
* @returns {Object}
*/
function mapStateToProps(state) {
const localParticipant = getLocalParticipant(state);
return {
_reactionsEnabled: isReactionsEnabled(state),
isOpen: getReactionsMenuVisibility(state),
isMobile: isMobileBrowser(),
reactionsQueue: getReactionsQueue(state),
raisedHand: hasRaisedHand(localParticipant)
reactionsQueue: getReactionsQueue(state)
};
}

View File

@@ -77,13 +77,7 @@ export default class AbstractLiveStreamButton<P: Props> extends AbstractButton<P
* @returns {void}
*/
async _handleClick() {
const { _isLiveStreamRunning, dispatch, handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
const { _isLiveStreamRunning, dispatch } = this.props;
const dialogShown = await dispatch(maybeShowPremiumFeatureDialog(FEATURES.RECORDING));

View File

@@ -78,13 +78,7 @@ export default class AbstractRecordButton<P: Props> extends AbstractButton<P, *>
* @returns {void}
*/
async _handleClick() {
const { _isRecordingRunning, dispatch, handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
const { _isRecordingRunning, dispatch } = this.props;
sendAnalytics(createToolbarEvent(
'recording.button',

View File

@@ -78,10 +78,16 @@ export default class AbstractStopRecordingDialog<P: Props>
return true;
}
/**
* To be overwritten by web component.
*/
_toggleScreenshotCapture: () => void;
/**
* Toggles screenshot capture feature.
*
* @returns {void}
*/
_toggleScreenshotCapture() {
// To be implemented by subclass.
}
}
/**

View File

@@ -47,13 +47,7 @@ class ShareAudioButton extends AbstractButton<Props, *> {
* @returns {void}
*/
_handleClick() {
const { dispatch, handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
const { dispatch } = this.props;
dispatch(startAudioScreenShareFlow());
dispatch(setOverflowMenuVisible(false));

View File

@@ -56,13 +56,7 @@ export default class AbstractSecurityDialogButton<P: Props, S:*>
* @returns {void}
*/
_handleClick() {
const { _locked, handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
const { _locked } = this.props;
sendAnalytics(createToolbarEvent('toggle.security', { enable: !_locked }));
this._handleClickSecurityButton();

View File

@@ -6,11 +6,19 @@ import React, { useRef } from 'react';
import { translate } from '../../../../base/i18n';
import { copyText } from '../../../../base/util';
import { NOTIFY_CLICK_MODE } from '../../../../toolbox/constants';
import PasswordForm from './PasswordForm';
const KEY = 'add-passcode';
type Props = {
/**
* Toolbar buttons which have their click exposed through the API.
*/
buttonsWithNotifyClick: Array<string | Object>,
/**
* Whether or not the current user can modify the current password.
*/
@@ -59,12 +67,15 @@ type Props = {
t: Function
};
declare var APP: Object;
/**
* Component that handles the password manipulation from the invite dialog.
*
* @returns {React$Element<any>}
*/
function PasswordSection({
buttonsWithNotifyClick,
canEditPassword,
conference,
locked,
@@ -97,7 +108,31 @@ function PasswordSection({
* @returns {void}
*/
function onTogglePasswordEditState() {
setPasswordEditEnabled(!passwordEditEnabled);
if (typeof APP === 'undefined' || !buttonsWithNotifyClick?.length) {
setPasswordEditEnabled(!passwordEditEnabled);
return;
}
let notifyMode;
const notify = buttonsWithNotifyClick.find(
(btn: string | Object) =>
(typeof btn === 'string' && btn === KEY)
|| (typeof btn === 'object' && btn.key === KEY)
);
if (notify) {
notifyMode = typeof notify === 'string' || notify.preventExecution
? NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY
: NOTIFY_CLICK_MODE.ONLY_NOTIFY;
APP.API.notifyToolbarButtonClicked(
KEY, notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY
);
}
if (notifyMode === NOTIFY_CLICK_MODE.ONLY_NOTIFY) {
setPasswordEditEnabled(!passwordEditEnabled);
}
}
/**

View File

@@ -13,6 +13,11 @@ import PasswordSection from './PasswordSection';
type Props = {
/**
* Toolbar buttons which have their click exposed through the API.
*/
_buttonsWithNotifyClick: Array<string | Object>,
/**
* Whether or not the current user can modify the current password.
*/
@@ -57,6 +62,7 @@ type Props = {
* @returns {React$Element<any>}
*/
function SecurityDialog({
_buttonsWithNotifyClick,
_canEditPassword,
_conference,
_locked,
@@ -82,6 +88,7 @@ function SecurityDialog({
<div className = 'security-dialog'>
<LobbySection />
<PasswordSection
buttonsWithNotifyClick = { _buttonsWithNotifyClick }
canEditPassword = { _canEditPassword }
conference = { _conference }
locked = { _locked }
@@ -117,11 +124,12 @@ function mapStateToProps(state) {
locked,
password
} = state['features/base/conference'];
const { roomPasswordNumberOfDigits } = state['features/base/config'];
const { roomPasswordNumberOfDigits, buttonsWithNotifyClick } = state['features/base/config'];
const showE2ee = Boolean(e2eeSupported) && isLocalParticipantModerator(state);
return {
_buttonsWithNotifyClick: buttonsWithNotifyClick,
_canEditPassword: isLocalParticipantModerator(state),
_conference: conference,
_dialIn: state['features/invite'],

View File

@@ -40,17 +40,7 @@ class SettingsButton extends AbstractButton<Props, *> {
* @returns {void}
*/
_handleClick() {
const {
defaultTab = SETTINGS_TABS.DEVICES,
dispatch,
handleClick
} = this.props;
if (handleClick) {
handleClick();
return;
}
const { defaultTab = SETTINGS_TABS.DEVICES, dispatch } = this.props;
sendAnalytics(createToolbarEvent('settings'));
dispatch(openSettingsDialog(defaultTab));

View File

@@ -66,14 +66,6 @@ class SharedVideoButton extends AbstractButton<Props, *> {
* @returns {void}
*/
_handleClick() {
const { handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
this._doToggleSharedVideo();
}

View File

@@ -20,13 +20,7 @@ class SpeakerStatsButton extends AbstractSpeakerStatsButton {
* @returns {void}
*/
_handleClick() {
const { dispatch, handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
const { dispatch } = this.props;
sendAnalytics(createToolbarEvent('speaker.stats'));
dispatch(openDialog(SpeakerStats));

View File

@@ -38,13 +38,7 @@ export class AbstractClosedCaptionButton
* @returns {void}
*/
async _handleClick() {
const { _requestingSubtitles, dispatch, handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
const { _requestingSubtitles, dispatch } = this.props;
sendAnalytics(createToolbarEvent('transcribing.ccButton',
{

View File

@@ -10,7 +10,7 @@ import { getFeatureFlag, AUDIO_MUTE_BUTTON_ENABLED } from '../../base/flags';
import { translate } from '../../base/i18n';
import { MEDIA_TYPE } from '../../base/media';
import { connect } from '../../base/redux';
import { AbstractAudioMuteButton } from '../../base/toolbox/components';
import { AbstractAudioMuteButton, AbstractButton } from '../../base/toolbox/components';
import type { AbstractButtonProps } from '../../base/toolbox/components';
import { isLocalTrackMuted } from '../../base/tracks';
import { muteLocal } from '../../video-menu/actions';
@@ -120,7 +120,7 @@ class AudioMuteButton extends AbstractAudioMuteButton<Props, *> {
ACTION_SHORTCUT_TRIGGERED,
{ enable: !this._isAudioMuted() }));
super._handleClick();
AbstractButton.prototype._onClick.call(this);
}
/**

View File

@@ -32,13 +32,7 @@ class DownloadButton extends AbstractButton<Props, *> {
* @returns {void}
*/
_handleClick() {
const { _downloadAppsUrl, handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
const { _downloadAppsUrl } = this.props;
sendAnalytics(createToolbarEvent('download.pressed'));
openURLInBrowser(_downloadAppsUrl);

View File

@@ -33,13 +33,7 @@ class HelpButton extends AbstractButton<Props, *> {
* @returns {void}
*/
_handleClick() {
const { _userDocumentationURL, handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
const { _userDocumentationURL } = this.props;
sendAnalytics(createToolbarEvent('help.pressed'));
openURLInBrowser(_userDocumentationURL);

View File

@@ -39,13 +39,7 @@ class MuteEveryoneButton extends AbstractButton<Props, *> {
* @returns {void}
*/
_handleClick() {
const { dispatch, localParticipantId, handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
const { dispatch, localParticipantId } = this.props;
sendAnalytics(createToolbarEvent('mute.everyone.pressed'));
dispatch(openDialog(MuteEveryoneDialog, {

View File

@@ -39,13 +39,7 @@ class MuteEveryonesVideoButton extends AbstractButton<Props, *> {
* @returns {void}
*/
_handleClick() {
const { dispatch, localParticipantId, handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
const { dispatch, localParticipantId } = this.props;
sendAnalytics(createToolbarEvent('mute.everyone.pressed'));
dispatch(openDialog(MuteEveryonesVideoDialog, {

View File

@@ -16,7 +16,7 @@ import {
setVideoMuted
} from '../../base/media';
import { connect } from '../../base/redux';
import { AbstractVideoMuteButton } from '../../base/toolbox/components';
import { AbstractButton, AbstractVideoMuteButton } from '../../base/toolbox/components';
import type { AbstractButtonProps } from '../../base/toolbox/components';
import { getLocalVideoType, isLocalCameraTrackMuted } from '../../base/tracks';
import { isVideoMuteButtonDisabled } from '../functions';
@@ -146,7 +146,7 @@ class VideoMuteButton extends AbstractVideoMuteButton<Props, *> {
ACTION_SHORTCUT_TRIGGERED,
{ enable: !this._isVideoMuted() }));
super._handleClick();
AbstractButton.prototype._onClick.call(this);
}
/**

View File

@@ -15,6 +15,11 @@ import AudioMuteButton from '../AudioMuteButton';
type Props = {
/**
* The button's key.
*/
buttonKey?: string,
/**
* External handler for click action.
*/
@@ -35,6 +40,12 @@ type Props = {
*/
isDisabled: boolean,
/**
* Notify mode for `toolbarButtonClicked` event -
* whether to only notify or to also prevent button click routine.
*/
notifyMode?: string,
/**
* Used for translation.
*/
@@ -94,13 +105,7 @@ class AudioSettingsButton extends Component<Props> {
* @returns {void}
*/
_onClick() {
const { handleClick, onAudioOptionsClick } = this.props;
if (handleClick) {
handleClick();
return;
}
const { onAudioOptionsClick } = this.props;
onAudioOptionsClick();
}
@@ -111,7 +116,7 @@ class AudioSettingsButton extends Component<Props> {
* @inheritdoc
*/
render() {
const { handleClick, hasPermissions, isDisabled, visible, isOpen, t } = this.props;
const { hasPermissions, isDisabled, visible, isOpen, buttonKey, notifyMode, t } = this.props;
const settingsDisabled = !hasPermissions
|| isDisabled
|| !JitsiMeetJS.mediaDevices.isMultipleAudioInputSupported();
@@ -123,16 +128,22 @@ class AudioSettingsButton extends Component<Props> {
ariaExpanded = { isOpen }
ariaHasPopup = { true }
ariaLabel = { t('toolbar.audioSettings') }
buttonKey = { buttonKey }
icon = { IconArrowUp }
iconDisabled = { settingsDisabled }
iconId = 'audio-settings-button'
iconTooltip = { t('toolbar.audioSettings') }
notifyMode = { notifyMode }
onIconClick = { this._onClick }
onIconKeyDown = { this._onEscClick }>
<AudioMuteButton handleClick = { handleClick } />
<AudioMuteButton
buttonKey = { buttonKey }
notifyMode = { notifyMode } />
</ToolboxButtonWithIcon>
</AudioSettingsPopup>
) : <AudioMuteButton handleClick = { handleClick } />;
) : <AudioMuteButton
buttonKey = { buttonKey }
notifyMode = { notifyMode } />;
}
}

View File

@@ -61,22 +61,6 @@ class FullscreenButton extends AbstractButton<Props, *> {
// Unused.
}
/**
* Handles clicking / pressing the button, and opens the appropriate dialog.
*
* @protected
* @returns {void}
*/
_handleClick() {
const { handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
}
/**
* Indicates whether this button is in toggled state or not.
*

View File

@@ -5,7 +5,6 @@ import React, { Component } from 'react';
import { createToolbarEvent, sendAnalytics } from '../../../analytics';
import { translate } from '../../../base/i18n';
import { IconHorizontalPoints } from '../../../base/icons';
import { connect } from '../../../base/redux';
import { ReactionEmoji, ReactionsMenu } from '../../../reactions/components';
import { type ReactionEmojiProps } from '../../../reactions/constants';
@@ -13,7 +12,7 @@ import { getReactionsQueue } from '../../../reactions/functions.any';
import Drawer from './Drawer';
import JitsiPortal from './JitsiPortal';
import ToolbarButton from './ToolbarButton';
import OverflowToggleButton from './OverflowToggleButton';
/**
* The type of the React {@code Component} props of {@link OverflowMenuButton}.
@@ -78,8 +77,8 @@ class OverflowMenuButton extends Component<Props> {
// Bind event handlers so they are only bound once per instance.
this._onCloseDialog = this._onCloseDialog.bind(this);
this._onToggleDialogVisibility
= this._onToggleDialogVisibility.bind(this);
this._toggleDialogVisibility
= this._toggleDialogVisibility.bind(this);
this._onEscClick = this._onEscClick.bind(this);
}
@@ -113,7 +112,10 @@ class OverflowMenuButton extends Component<Props> {
{
overflowDrawer ? (
<>
{this._renderToolbarButton()}
<OverflowToggleButton
handleClick = { this._toggleDialogVisibility }
isOpen = { isOpen }
onKeyDown = { this._onEscClick } />
<JitsiPortal>
<Drawer
isOpen = { isOpen }
@@ -136,7 +138,10 @@ class OverflowMenuButton extends Component<Props> {
isOpen = { isOpen }
onClose = { this._onCloseDialog }
placement = 'top-end'>
{this._renderToolbarButton()}
<OverflowToggleButton
handleClick = { this._toggleDialogVisibility }
isOpen = { isOpen }
onKeyDown = { this._onEscClick } />
</InlineDialog>
)
}
@@ -144,30 +149,6 @@ class OverflowMenuButton extends Component<Props> {
);
}
_renderToolbarButton: () => React$Node;
/**
* Renders the actual toolbar overflow menu button.
*
* @returns {ReactElement}
*/
_renderToolbarButton() {
const { ariaControls, isOpen, t } = this.props;
return (
<ToolbarButton
accessibilityLabel =
{ t('toolbar.accessibilityLabel.moreActions') }
aria-controls = { ariaControls }
aria-haspopup = 'true'
icon = { IconHorizontalPoints }
onClick = { this._onToggleDialogVisibility }
onKeyDown = { this._onEscClick }
toggled = { isOpen }
tooltip = { t('toolbar.moreActions') } />
);
}
_onCloseDialog: () => void;
/**
@@ -181,7 +162,7 @@ class OverflowMenuButton extends Component<Props> {
this.props.onVisibilityChange(false);
}
_onToggleDialogVisibility: () => void;
_toggleDialogVisibility: () => void;
/**
* Callback invoked to signal that an event has occurred that should change
@@ -190,7 +171,7 @@ class OverflowMenuButton extends Component<Props> {
* @private
* @returns {void}
*/
_onToggleDialogVisibility() {
_toggleDialogVisibility() {
sendAnalytics(createToolbarEvent('overflow'));
this.props.onVisibilityChange(!this.props.isOpen);

View File

@@ -0,0 +1,73 @@
// @flow
import { translate } from '../../../base/i18n';
import { IconHorizontalPoints } from '../../../base/icons';
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
/**
* The type of the React {@code Component} props of {@link OverflowToggleButton}.
*/
type Props = AbstractButtonProps & {
/**
* Whether the more options menu is open.
*/
isOpen: boolean,
/**
* External handler for key down action.
*/
onKeyDown: Function,
};
/**
* Implementation of a button for toggleing the overflow menu.
*/
class OverflowToggleButton extends AbstractButton<Props, *> {
accessibilityLabel = 'toolbar.accessibilityLabel.moreActions';
icon = IconHorizontalPoints;
label = 'toolbar.moreActions';
toggledLabel = 'toolbar.moreActions';
/**
* Retrieves tooltip dynamically.
*/
get tooltip() {
return 'toolbar.moreActions';
}
/**
* Required by linter due to AbstractButton overwritten prop being writable.
*
* @param {string} _value - The value.
*/
set tooltip(_value) {
// Unused.
}
/**
* Indicates whether this button is in toggled state or not.
*
* @override
* @protected
* @returns {boolean}
*/
_isToggled() {
return this.props.isOpen;
}
/**
* Indicates whether a key was pressed.
*
* @override
* @protected
* @returns {boolean}
*/
_onKeyDown() {
this.props.onKeyDown();
}
}
export default translate(OverflowToggleButton);

View File

@@ -95,13 +95,7 @@ class ProfileButton extends AbstractButton<Props, *> {
* @returns {void}
*/
_handleClick() {
const { dispatch, _unclickable, handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
const { dispatch, _unclickable } = this.props;
if (!_unclickable) {
sendAnalytics(createToolbarEvent('profile'));

View File

@@ -67,22 +67,6 @@ class ShareDesktopButton extends AbstractButton<Props, *> {
// Unused.
}
/**
* Handles clicking / pressing the button, and opens the appropriate dialog.
*
* @protected
* @returns {void}
*/
_handleClick() {
const { handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
}
/**
* Indicates whether this button is in toggled state or not.
*

View File

@@ -41,13 +41,7 @@ class ToggleCameraButton extends AbstractButton<Props, any> {
* @returns {void}
*/
_handleClick() {
const { dispatch, handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
const { dispatch } = this.props;
dispatch(toggleCamera());
}

View File

@@ -1,135 +0,0 @@
/* @flow */
import React from 'react';
import { Icon } from '../../../base/icons';
import { Tooltip } from '../../../base/tooltip';
import AbstractToolbarButton from '../AbstractToolbarButton';
import type { Props as AbstractToolbarButtonProps }
from '../AbstractToolbarButton';
/**
* The type of the React {@code Component} props of {@link ToolbarButton}.
*/
export type Props = AbstractToolbarButtonProps & {
/**
* The text to display in the tooltip.
*/
tooltip: string,
/**
* From which direction the tooltip should appear, relative to the
* button.
*/
tooltipPosition: string,
/**
* KeyDown handler.
*/
onKeyDown?: Function
};
/**
* Represents a button in the toolbar.
*
* @augments AbstractToolbarButton
*/
class ToolbarButton extends AbstractToolbarButton<Props> {
/**
* Default values for {@code ToolbarButton} component's properties.
*
* @static
*/
static defaultProps = {
tooltipPosition: 'top'
};
/**
* Initializes a new {@code ToolbarButton} instance.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
this._onKeyPress = this._onKeyPress.bind(this);
this._onClick = this._onClick.bind(this);
}
_onKeyPress: (Object) => void;
/**
* Handles 'Enter' and Space key on the button to trigger onClick for accessibility.
*
* @param {Object} event - The key event.
* @private
* @returns {void}
*/
_onKeyPress(event) {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
this.props.onClick();
}
}
_onClick: (Object) => void;
/**
* Handles button click.
*
* @param {Object} e - The key event.
* @private
* @returns {void}
*/
_onClick(e) {
this.props.onClick(e);
// blur after click to release focus from button to allow PTT.
e && e.currentTarget && e.currentTarget.blur();
}
/**
* Renders the button of this {@code ToolbarButton}.
*
* @param {Object} children - The children, if any, to be rendered inside
* the button. Presumably, contains the icon of this {@code ToolbarButton}.
* @protected
* @returns {ReactElement} The button of this {@code ToolbarButton}.
*/
_renderButton(children) {
return (
<div
aria-label = { this.props.accessibilityLabel }
aria-pressed = { this.props.toggled }
className = 'toolbox-button'
onClick = { this._onClick }
onKeyDown = { this.props.onKeyDown }
onKeyPress = { this._onKeyPress }
role = 'button'
tabIndex = { 0 }>
{ this.props.tooltip
? <Tooltip
content = { this.props.tooltip }
position = { this.props.tooltipPosition }>
{ children }
</Tooltip>
: children }
</div>
);
}
/**
* Renders the icon of this {@code ToolbarButton}.
*
* @inheritdoc
*/
_renderIcon() {
return (
<div className = { `toolbox-icon ${this.props.toggled ? 'toggled' : ''}` }>
<Icon src = { this.props.icon } />
</div>
);
}
}
export default ToolbarButton;

View File

@@ -76,7 +76,7 @@ import {
setToolbarHovered,
showToolbox
} from '../../actions';
import { THRESHOLDS, NOT_APPLICABLE, DRAWER_MAX_HEIGHT } from '../../constants';
import { THRESHOLDS, NOT_APPLICABLE, DRAWER_MAX_HEIGHT, NOTIFY_CLICK_MODE } from '../../constants';
import { isToolboxVisible } from '../../functions';
import DownloadButton from '../DownloadButton';
import HangupButton from '../HangupButton';
@@ -106,7 +106,7 @@ type Props = {
/**
* Toolbar buttons which have their click exposed through the API.
*/
_buttonsWithNotifyClick: Array<string>,
_buttonsWithNotifyClick: Array<string | Object>,
/**
* Whether or not the chat feature is currently displayed.
@@ -819,22 +819,31 @@ class Toolbox extends Component<Props> {
}
/**
* Overwrites click handlers for buttons in case click is exposed through the iframe API.
* Sets the notify click mode for the buttons.
*
* @param {Object} buttons - The list of toolbar buttons.
* @returns {void}
*/
_overwriteButtonsClickHandlers(buttons) {
_setButtonsNotifyClickMode(buttons) {
if (typeof APP === 'undefined' || !this.props._buttonsWithNotifyClick?.length) {
return;
}
Object.values(buttons).forEach((button: any) => {
if (
typeof button === 'object'
&& this.props._buttonsWithNotifyClick.includes(button.key)
) {
button.handleClick = () => APP.API.notifyToolbarButtonClicked(button.key);
if (typeof button === 'object') {
const notify = this.props._buttonsWithNotifyClick.find(
(btn: string | Object) =>
(typeof btn === 'string' && btn === button.key)
|| (typeof btn === 'object' && btn.key === button.key)
);
if (notify) {
const notifyMode = typeof notify === 'string' || notify.preventExecution
? NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY
: NOTIFY_CLICK_MODE.ONLY_NOTIFY;
button.notifyMode = notifyMode;
}
}
});
}
@@ -854,7 +863,7 @@ class Toolbox extends Component<Props> {
const buttons = this._getAllButtons();
this._overwriteButtonsClickHandlers(buttons);
this._setButtonsNotifyClickMode(buttons);
const isHangupVisible = isToolbarButtonEnabled('hangup', _toolbarButtons);
const { order } = THRESHOLDS.find(({ width }) => _clientWidth > width)
|| THRESHOLDS[THRESHOLDS.length - 1];
@@ -1265,6 +1274,7 @@ class Toolbox extends Component<Props> {
{mainMenuButtons.map(({ Content, key, ...rest }) => Content !== Separator && (
<Content
{ ...rest }
buttonKey = { key }
key = { key } />))}
{Boolean(overflowMenuButtons.length) && (
@@ -1292,6 +1302,7 @@ class Toolbox extends Component<Props> {
{showSeparator && <Separator key = { `hr${group}` } />}
<Content
{ ...rest }
buttonKey = { key }
key = { key }
showLabel = { true } />
</Fragment>

View File

@@ -16,6 +16,11 @@ import VideoMuteButton from '../VideoMuteButton';
type Props = {
/**
* The button's key.
*/
buttonKey?: string,
/**
* External handler for click action.
*/
@@ -41,6 +46,12 @@ type Props = {
*/
isDisabled: boolean,
/**
* Notify mode for `toolbarButtonClicked` event -
* whether to only notify or to also prevent button click routine.
*/
notifyMode?: string,
/**
* Flag controlling the visibility of the button.
* VideoSettings popup is currently disabled on mobile browsers
@@ -112,13 +123,7 @@ class VideoSettingsButton extends Component<Props> {
* @returns {void}
*/
_onClick() {
const { handleClick, onVideoOptionsClick } = this.props;
if (handleClick) {
handleClick();
return;
}
const { onVideoOptionsClick } = this.props;
onVideoOptionsClick();
}
@@ -129,7 +134,7 @@ class VideoSettingsButton extends Component<Props> {
* @inheritdoc
*/
render() {
const { handleClick, t, visible, isOpen } = this.props;
const { t, visible, isOpen, buttonKey, notifyMode } = this.props;
return visible ? (
<VideoSettingsPopup>
@@ -138,16 +143,22 @@ class VideoSettingsButton extends Component<Props> {
ariaExpanded = { isOpen }
ariaHasPopup = { true }
ariaLabel = { this.props.t('toolbar.videoSettings') }
buttonKey = { buttonKey }
icon = { IconArrowUp }
iconDisabled = { this._isIconDisabled() }
iconId = 'video-settings-button'
iconTooltip = { t('toolbar.videoSettings') }
notifyMode = { notifyMode }
onIconClick = { this._onClick }
onIconKeyDown = { this._onEscClick }>
<VideoMuteButton handleClick = { handleClick } />
<VideoMuteButton
buttonKey = { buttonKey }
notifyMode = { notifyMode } />
</ToolboxButtonWithIcon>
</VideoSettingsPopup>
) : <VideoMuteButton handleClick = { handleClick } />;
) : <VideoMuteButton
buttonKey = { buttonKey }
notifyMode = { notifyMode } />;
}
}

View File

@@ -1,6 +1,5 @@
export { default as AudioSettingsButton } from './AudioSettingsButton';
export { default as VideoSettingsButton } from './VideoSettingsButton';
export { default as ToolbarButton } from './ToolbarButton';
export { default as Toolbox } from './Toolbox';
export { default as Drawer } from './Drawer';
export { default as JitsiPortal } from './JitsiPortal';

View File

@@ -33,3 +33,8 @@ export const NOT_APPLICABLE = 'N/A';
export const TOOLBAR_TIMEOUT = 4000;
export const DRAWER_MAX_HEIGHT = '80vh - 64px';
export const NOTIFY_CLICK_MODE = {
ONLY_NOTIFY: 'ONLY_NOTIFY',
PREVENT_AND_NOTIFY: 'PREVENT_AND_NOTIFY'
};

View File

@@ -52,13 +52,7 @@ class TileViewButton<P: Props> extends AbstractButton<P, *> {
* @returns {void}
*/
_handleClick() {
const { _tileViewEnabled, dispatch, handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
const { _tileViewEnabled, dispatch } = this.props;
const value = !_tileViewEnabled;

View File

@@ -39,24 +39,6 @@ class VideoQualityButton extends AbstractButton<Props, *> {
label = 'videoStatus.performanceSettings';
tooltip = 'videoStatus.performanceSettings';
icon = IconGauge;
/**
* Handles clicking / pressing the button.
*
* @override
* @protected
* @returns {void}
*/
_handleClick() {
const { handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
}
}
export default translate(VideoQualityButton);

View File

@@ -43,13 +43,7 @@ class VideoBackgroundButton extends AbstractButton<Props, *> {
* @returns {void}
*/
_handleClick() {
const { dispatch, handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
const { dispatch } = this.props;
dispatch(openDialog(VirtualBackgroundDialog));
}

View File

@@ -213,7 +213,7 @@ end
function destroy_breakout_room(room_jid, message)
local main_room, main_room_jid = get_main_room(room_jid);
if room_jid == main_room_jid then
if room_jid == main_room_jid or not main_room then
return;
end
@@ -339,7 +339,7 @@ function on_occupant_joined(event)
local main_room = get_main_room(room.jid);
if main_room._data.breakout_rooms_active then
if main_room and main_room._data.breakout_rooms_active then
if jid_node(event.occupant.jid) ~= 'focus' then
broadcast_breakout_rooms(room.jid);
end
@@ -388,6 +388,10 @@ function on_occupant_left(event)
local main_room = get_main_room(room_jid);
if not main_room then
return;
end
if main_room._data.breakout_rooms_active and jid_node(event.occupant.jid) ~= 'focus' then
broadcast_breakout_rooms(room_jid);
end
@@ -477,7 +481,7 @@ function process_breakout_rooms_muc_loaded(breakout_rooms_muc, host_module)
event.formdata['muc#roominfo_breakout_main_room'] = main_room_jid;
-- If the main room has a lobby, make it so this breakout room also uses it.
if (main_room._data.lobbyroom and main_room:get_members_only()) then
if (main_room and main_room._data.lobbyroom and main_room:get_members_only()) then
table.insert(event.form, {
name = 'muc#roominfo_lobbyroom';
label = 'Lobby room jid';