From 4b3e82458797d39df5d519d8d24808d003da2e91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Tue, 25 Jan 2022 11:56:39 +0100 Subject: Do not force reconnect when negotiation is needed for receiver peer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If the HPB is used it is enough to reconnect only with the receiver peer that needs a negotiation rather than forcing a full reconnection. Signed-off-by: Daniel Calviño Sánchez --- src/utils/webrtc/webrtc.js | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/utils/webrtc/webrtc.js b/src/utils/webrtc/webrtc.js index ec2c54d77..c18b587de 100644 --- a/src/utils/webrtc/webrtc.js +++ b/src/utils/webrtc/webrtc.js @@ -367,6 +367,10 @@ function usersChanged(signaling, newUsers, disconnectedSessionIds) { // TODO(jojo): Already create peer object to avoid duplicate offers. signaling.requestOffer(user, 'video') + // Clearing the previous delayedConnectionToPeer should not be + // needed here, but just in case. + clearInterval(delayedConnectionToPeer[user.sessionId]) + delayedConnectionToPeer[user.sessionId] = setInterval(function() { console.debug('No offer received for new peer, request offer again') @@ -781,6 +785,8 @@ export default function initWebRtc(signaling, _callParticipantCollection, _local signaling.requestOffer(peer.id, 'video') + clearInterval(delayedConnectionToPeer[peer.id]) + delayedConnectionToPeer[peer.id] = setInterval(function() { console.debug('No offer received, request offer again', peer) @@ -926,6 +932,24 @@ export default function initWebRtc(signaling, _callParticipantCollection, _local */ function setHandlerForNegotiationNeeded(peer) { peer.pc.addEventListener('negotiationneeded', function() { + // When the HPB is used and the negotiation is needed for a receiver + // peer (for example, to block the received video) there is no need + // to force a full reconnection, it is enough to reconnect only that + // peer. + if (signaling.hasFeature('mcu') && peer.id !== signaling.getSessionId()) { + signaling.requestOffer(peer.id, 'video') + + clearInterval(delayedConnectionToPeer[peer.id]) + + delayedConnectionToPeer[peer.id] = setInterval(function() { + console.debug('No offer received, request offer again', peer) + + signaling.requestOffer(peer.id, 'video') + }, 10000) + + return + } + // Negotiation needed will be first triggered before the connection // is established, but forcing a reconnection should be done only // once the connection was established. -- cgit v1.2.3 From c313e70a6ec6180e64f09627172bf2ce9a7d5b2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Wed, 26 Jan 2022 20:24:53 +0100 Subject: Fix expected simulcast levels not set again when resetting a receiver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before a negotiation needed event in a receiver Peer caused a forced reconnection, so the whole call was reset. Now only the affected Peer object is reset, so the simulcast levels previously set need to be set again on the new Peer object. Signed-off-by: Daniel Calviño Sánchez --- src/utils/webrtc/models/CallParticipantModel.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/utils/webrtc/models/CallParticipantModel.js b/src/utils/webrtc/models/CallParticipantModel.js index 354067454..fa9571c26 100644 --- a/src/utils/webrtc/models/CallParticipantModel.js +++ b/src/utils/webrtc/models/CallParticipantModel.js @@ -278,6 +278,11 @@ CallParticipantModel.prototype = { this.get('peer').on('extendedIceConnectionStateChange', this._handleExtendedIceConnectionStateChangeBound) this.get('peer').on('signalingStateChange', this._handleSignalingStateChangeBound) + + // Set expected state in Peer object. + if (this._simulcastVideoQuality !== undefined) { + this.setSimulcastVideoQuality(this._simulcastVideoQuality) + } }, _handleExtendedIceConnectionStateChange(extendedIceConnectionState) { @@ -363,6 +368,11 @@ CallParticipantModel.prototype = { // Reset state that depends on the screen Peer object. this._handlePeerStreamAdded(this.get('screenPeer')) + + // Set expected state in screen Peer object. + if (this._simulcastScreenQuality !== undefined) { + this.setSimulcastScreenQuality(this._simulcastScreenQuality) + } }, setUserId(userId) { @@ -374,6 +384,9 @@ CallParticipantModel.prototype = { }, setSimulcastVideoQuality(simulcastVideoQuality) { + // Store value to be able to apply it again if a new Peer object is set. + this._simulcastVideoQuality = simulcastVideoQuality + if (!this.get('peer') || !this.get('peer').enableSimulcast) { return } @@ -383,6 +396,10 @@ CallParticipantModel.prototype = { }, setSimulcastScreenQuality(simulcastScreenQuality) { + // Store value to be able to apply it again if a new screen Peer object + // is set. + this._simulcastScreenQuality = simulcastScreenQuality + if (!this.get('screenPeer') || !this.get('screenPeer').enableSimulcast) { return } -- cgit v1.2.3 From 48a74da64eac1a593116694f8c6ca299058f6304 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Thu, 3 Feb 2022 22:46:53 +0100 Subject: Update connections rather than create new ones on "negotiationneeded" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When an offer is requested to the HPB the old connection is stopped and a new one is created. If the HPB has support for updating the subscribers this is now used instead when negotiation is needed, as this prevents the existing connection (and thus the media) to be interrupted during the renegotiation. Signed-off-by: Daniel Calviño Sánchez --- src/utils/signaling.js | 5 +++-- src/utils/webrtc/webrtc.js | 23 ++++++++++++++++++++--- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/utils/signaling.js b/src/utils/signaling.js index 6b6fbab32..c0d1fbb52 100644 --- a/src/utils/signaling.js +++ b/src/utils/signaling.js @@ -1292,7 +1292,7 @@ Signaling.Standalone.prototype.processRoomParticipantsEvent = function(data) { } } -Signaling.Standalone.prototype.requestOffer = function(sessionid, roomType) { +Signaling.Standalone.prototype.requestOffer = function(sessionid, roomType, sid = undefined) { if (!this.hasFeature('mcu')) { console.warn("Can't request an offer without a MCU.") return @@ -1302,7 +1302,7 @@ Signaling.Standalone.prototype.requestOffer = function(sessionid, roomType) { // Got a user object. sessionid = sessionid.sessionId || sessionid.sessionid } - console.debug('Request offer from', sessionid) + console.debug('Request offer from', sessionid, sid) this.doSend({ type: 'message', message: { @@ -1313,6 +1313,7 @@ Signaling.Standalone.prototype.requestOffer = function(sessionid, roomType) { data: { type: 'requestoffer', roomType, + sid, }, }, }) diff --git a/src/utils/webrtc/webrtc.js b/src/utils/webrtc/webrtc.js index c18b587de..c5b97f7ab 100644 --- a/src/utils/webrtc/webrtc.js +++ b/src/utils/webrtc/webrtc.js @@ -937,14 +937,31 @@ export default function initWebRtc(signaling, _callParticipantCollection, _local // to force a full reconnection, it is enough to reconnect only that // peer. if (signaling.hasFeature('mcu') && peer.id !== signaling.getSessionId()) { - signaling.requestOffer(peer.id, 'video') + // If possible update connection rather than creating a new one. + let update = signaling.hasFeature('update-sdp') + + // Create a connection if the current one has failed, as it + // would require an ICE restart rather than update to recover. + if (update && (peer.pc.iceConnectionState === 'failed' || peer.pc.connectionState === 'failed')) { + update = false + } + + // If the connection needs to be updated but a new connection + // (or another update) is already pending ignore the new update. + // If a new connection needs to be created rather than updated + // then force it even if there is another one already pending. + if (update && delayedConnectionToPeer[peer.id]) { + return + } + + signaling.requestOffer(peer.id, 'video', update ? peer.sid : undefined) clearInterval(delayedConnectionToPeer[peer.id]) delayedConnectionToPeer[peer.id] = setInterval(function() { - console.debug('No offer received, request offer again', peer) + console.debug('No offer received, request offer again' + update ? '(update)' : '', peer) - signaling.requestOffer(peer.id, 'video') + signaling.requestOffer(peer.id, 'video', update ? peer.sid : undefined) }, 10000) return -- cgit v1.2.3 From 34d0084c82e3acef610b0449cc7a4c07fc2b6758 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Thu, 27 Jan 2022 19:50:03 +0100 Subject: Make possible to block the video of a receiver peer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remote video can be blocked and allowed again by adjusting the direction of the video transceiver of RTCPeerConnections, which triggers a "negotiationneeded" event. When a negotiation is needed a new exchange of offers and answers needs to be made between the peers. Renegotiations need to be supported by both peers, so currently this is available only with the HPB, and only on versions that support the "update-sdp" feature. When the HPB is being used "negotiationneeded" event causes a new offer to be requested, which will be an update if the HPB supports it. Updates rather than new connections are needed to prevent an audio interruption while the connection changes. Signed-off-by: Daniel Calviño Sánchez --- src/components/CallView/shared/Video.vue | 2 +- src/utils/webrtc/models/CallParticipantModel.js | 27 ++++++++++ src/utils/webrtc/simplewebrtc/peer.js | 68 +++++++++++++++++++++++++ 3 files changed, 96 insertions(+), 1 deletion(-) diff --git a/src/components/CallView/shared/Video.vue b/src/components/CallView/shared/Video.vue index 617417a01..32507da27 100644 --- a/src/components/CallView/shared/Video.vue +++ b/src/components/CallView/shared/Video.vue @@ -371,7 +371,7 @@ export default { }, hasVideo() { - return this.model.attributes.videoAvailable && this.sharedData.videoEnabled && (typeof this.model.attributes.stream === 'object') + return !this.model.attributes.videoBlocked && this.model.attributes.videoAvailable && this.sharedData.videoEnabled && (typeof this.model.attributes.stream === 'object') }, hasSelectedVideo() { diff --git a/src/utils/webrtc/models/CallParticipantModel.js b/src/utils/webrtc/models/CallParticipantModel.js index fa9571c26..6362e14e3 100644 --- a/src/utils/webrtc/models/CallParticipantModel.js +++ b/src/utils/webrtc/models/CallParticipantModel.js @@ -65,6 +65,9 @@ export default function CallParticipantModel(options) { audioElement: null, audioAvailable: undefined, speaking: undefined, + // "videoBlocked" is "true" only if the video is blocked and it would + // have been available in the remote peer if not blocked. + videoBlocked: undefined, videoAvailable: undefined, screen: null, // The audio element is part of the model to ensure that it can be @@ -89,6 +92,7 @@ export default function CallParticipantModel(options) { this._handleSignalingStateChangeBound = this._handleSignalingStateChange.bind(this) this._handleChannelMessageBound = this._handleChannelMessage.bind(this) this._handleRaisedHandBound = this._handleRaisedHand.bind(this) + this._handleRemoteVideoBlockedBound = this._handleRemoteVideoBlocked.bind(this) this._webRtc.on('peerStreamAdded', this._handlePeerStreamAddedBound) this._webRtc.on('peerStreamRemoved', this._handlePeerStreamRemovedBound) @@ -105,6 +109,7 @@ CallParticipantModel.prototype = { if (this.get('peer')) { this.get('peer').off('extendedIceConnectionStateChange', this._handleExtendedIceConnectionStateChangeBound) this.get('peer').off('signalingStateChange', this._handleSignalingStateChangeBound) + this.get('peer').off('remoteVideoBlocked', this._handleRemoteVideoBlockedBound) } this._webRtc.off('peerStreamAdded', this._handlePeerStreamAddedBound) @@ -249,6 +254,7 @@ CallParticipantModel.prototype = { if (this.get('peer')) { this.get('peer').off('extendedIceConnectionStateChange', this._handleExtendedIceConnectionStateChangeBound) this.get('peer').off('signalingStateChange', this._handleSignalingStateChangeBound) + this.get('peer').off('remoteVideoBlocked', this._handleRemoteVideoBlockedBound) } this.set('peer', peer) @@ -261,6 +267,7 @@ CallParticipantModel.prototype = { this.set('audioAvailable', false) this.set('speaking', false) this.set('videoAvailable', false) + this.set('videoBlocked', false) return } @@ -275,14 +282,19 @@ CallParticipantModel.prototype = { } this._handleSignalingStateChange(this.get('peer').pc.signalingState) this._handlePeerStreamAdded(this.get('peer')) + this._handleRemoteVideoBlocked(undefined) this.get('peer').on('extendedIceConnectionStateChange', this._handleExtendedIceConnectionStateChangeBound) this.get('peer').on('signalingStateChange', this._handleSignalingStateChangeBound) + this.get('peer').on('remoteVideoBlocked', this._handleRemoteVideoBlockedBound) // Set expected state in Peer object. if (this._simulcastVideoQuality !== undefined) { this.setSimulcastVideoQuality(this._simulcastVideoQuality) } + if (this._videoBlocked !== undefined) { + this.setVideoBlocked(this._videoBlocked) + } }, _handleExtendedIceConnectionStateChange(extendedIceConnectionState) { @@ -383,6 +395,21 @@ CallParticipantModel.prototype = { this.set('nextcloudSessionId', nextcloudSessionId) }, + setVideoBlocked(videoBlocked) { + // Store value to be able to apply it again if a new Peer object is set. + this._videoBlocked = videoBlocked + + if (!this.get('peer')) { + return + } + + this.get('peer').setRemoteVideoBlocked(videoBlocked) + }, + + _handleRemoteVideoBlocked(remoteVideoBlocked) { + this.set('videoBlocked', remoteVideoBlocked) + }, + setSimulcastVideoQuality(simulcastVideoQuality) { // Store value to be able to apply it again if a new Peer object is set. this._simulcastVideoQuality = simulcastVideoQuality diff --git a/src/utils/webrtc/simplewebrtc/peer.js b/src/utils/webrtc/simplewebrtc/peer.js index 6d64b20a4..6b0a987a8 100644 --- a/src/utils/webrtc/simplewebrtc/peer.js +++ b/src/utils/webrtc/simplewebrtc/peer.js @@ -31,6 +31,7 @@ function Peer(options) { this.oneway = options.oneway || false this.sharemyscreen = options.sharemyscreen || false this.stream = options.stream + this.receiverOnly = options.receiverOnly this.sendVideoIfAvailable = options.sendVideoIfAvailable === undefined ? true : options.sendVideoIfAvailable this.enableDataChannels = options.enableDataChannels === undefined ? this.parent.config.enableDataChannels : options.enableDataChannels this.enableSimulcast = options.enableSimulcast === undefined ? this.parent.config.enableSimulcast : options.enableSimulcast @@ -418,12 +419,49 @@ Peer.prototype.offer = function(options) { Peer.prototype.handleOffer = function(offer) { this.pc.setRemoteDescription(offer).then(function() { + this._blockRemoteVideoIfNeeded() + this.answer() }.bind(this)).catch(function(error) { console.warn('setRemoteDescription for offer failed: ', error) }) } +/** + * Blocks remote video based on "_remoteVideoShouldBeBlocked". + * + * 'remoteVideoBlocked' is emitted if the blocked state changes. + * + * Currently remote video can be blocked only when the HPB is used, so this + * method should be called immediately before creating the answer (the answer + * must be created in the same "tick" that this method is called). + * + * Note that if the transceiver direction changes after creating the answer but + * before setting it as the local description the "negotiationneeded" event will + * be automatically emitted again. + */ +Peer.prototype._blockRemoteVideoIfNeeded = function() { + const remoteVideoWasBlocked = this._remoteVideoBlocked + + this._remoteVideoBlocked = undefined + + this.pc.getTransceivers().forEach(transceiver => { + if (transceiver.mid === 'video' && !transceiver.stopped) { + if (this._remoteVideoShouldBeBlocked) { + transceiver.direction = 'inactive' + + this._remoteVideoBlocked = true + } else { + this._remoteVideoBlocked = false + } + } + }) + + if (remoteVideoWasBlocked !== this._remoteVideoBlocked) { + this.emit('remoteVideoBlocked', this._remoteVideoBlocked) + } +} + Peer.prototype.answer = function() { this.pc.createAnswer().then(function(answer) { this.pc.setLocalDescription(answer).then(function() { @@ -816,6 +854,36 @@ Peer.prototype.handleLocalTrackEnabledChanged = function(track, stream) { } } +Peer.prototype.setRemoteVideoBlocked = function(remoteVideoBlocked) { + // If the HPB is not used or if it is used and this is a sender peer the + // remote video can not be blocked. + // Besides that the remote video is not blocked either if the signaling + // server does not support updating the subscribers; in that case a new + // connection would need to be established and due to this the audio would + // be interrupted during the connection change. + if (!this.receiverOnly || !this.parent.config.connection.hasFeature('update-sdp')) { + return + } + + this._remoteVideoShouldBeBlocked = remoteVideoBlocked + + // The "negotiationneeded" event is emitted if needed based on the direction + // changes. + // Note that there will be a video transceiver even if the remote + // participant is sending a null video track (either because there is a + // camera but the video is disabled or because the camera was removed during + // the call), so a renegotiation could be needed also in that case. + this.pc.getTransceivers().forEach(transceiver => { + if (transceiver.mid === 'video' && !transceiver.stopped) { + if (remoteVideoBlocked) { + transceiver.direction = 'inactive' + } else { + transceiver.direction = 'recvonly' + } + } + }) +} + Peer.prototype.handleRemoteStreamAdded = function(event) { const self = this if (this.stream) { -- cgit v1.2.3 From 5cb20b992b131a9427c5e125d4fa3005bff708a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Thu, 3 Feb 2022 20:35:51 +0100 Subject: Add tests for UI feedback on connection status of remote participants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Calviño Sánchez --- src/__mocks__/attachmediastream.js | 17 + src/components/CallView/shared/Video.spec.js | 1036 ++++++++++++++++++++++++++ 2 files changed, 1053 insertions(+) create mode 100644 src/__mocks__/attachmediastream.js create mode 100644 src/components/CallView/shared/Video.spec.js diff --git a/src/__mocks__/attachmediastream.js b/src/__mocks__/attachmediastream.js new file mode 100644 index 000000000..74178ed27 --- /dev/null +++ b/src/__mocks__/attachmediastream.js @@ -0,0 +1,17 @@ +/** + * Basic "attachmediastream" implementation without using "webrtc-adapter", as + * "browserDetails" is null in unit tests. + * + * @param {MediaStream} stream the stream to attach + * @param {HTMLElement} element the element to attach the stream to + * @param {object} options ignored + */ +export default function(stream, element, options) { + if (!element) { + element = document.createElement(options.audio ? 'audio' : 'video') + } + + element.srcObject = stream + + return element +} diff --git a/src/components/CallView/shared/Video.spec.js b/src/components/CallView/shared/Video.spec.js new file mode 100644 index 000000000..0a96c5fe5 --- /dev/null +++ b/src/components/CallView/shared/Video.spec.js @@ -0,0 +1,1036 @@ +/** + * + * @copyright Copyright (c) 2022, Daniel Calviño Sánchez (danxuliu@gmail.com) + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +import Vuex from 'vuex' +import { createLocalVue, shallowMount } from '@vue/test-utils' +import { cloneDeep } from 'lodash' +import storeConfig from '../../../store/storeConfig' + +import EmitterMixin from '../../../utils/EmitterMixin' +import CallParticipantModel from '../../../utils/webrtc/models/CallParticipantModel' + +import Video from './Video' + +describe('Video.vue', () => { + let localVue + let store + let testStoreConfig + + let callParticipantModel + + function PeerMock() { + this._superEmitterMixin() + + this.id = 'theId' + this.nick = 'The nick' + this.pc = { + connectionState: 'new', + iceConnectionState: 'new', + signalingState: 'stable', + } + } + PeerMock.prototype._setIceConnectionState = function(iceConnectionState) { + this.pc.iceConnectionState = iceConnectionState + this._trigger('extendedIceConnectionStateChange', [iceConnectionState]) + } + PeerMock.prototype._setSignalingState = function(signalingState) { + this.pc.signalingState = signalingState + this._trigger('signalingStateChange', [signalingState]) + } + EmitterMixin.apply(PeerMock.prototype) + // Override _trigger from EmitterMixin, as it includes "this" as the first + // argument. + PeerMock.prototype._trigger = function(event, args) { + let handlers = this._handlers[event] + if (!handlers) { + return + } + + if (!args) { + args = [] + } + + handlers = handlers.slice(0) + for (let i = 0; i < handlers.length; i++) { + const handler = handlers[i] + handler.apply(handler, args) + } + } + + beforeEach(() => { + localVue = createLocalVue() + localVue.use(Vuex) + + testStoreConfig = cloneDeep(storeConfig) + store = new Vuex.Store(testStoreConfig) + + const webRtcMock = { + on: jest.fn(), + off: jest.fn(), + } + callParticipantModel = new CallParticipantModel({ + peerId: 'theId', + webRtc: webRtcMock, + }) + }) + + describe('connection state feedback', () => { + const connectionMessage = { + NOT_ESTABLISHED: 'Connection could not be established. Trying again …', + NOT_ESTABLISHED_NOT_RETRYING: 'Connection could not be established …', + LOST: 'Connection lost. Trying to reconnect …', + LOST_NOT_RETRYING: 'Connection was lost and could not be re-established …', + PROBLEMS: 'Connection problems …', + NONE: null, + } + + let wrapper + + // "setupWrapper()" needs to be called right before checking the wrapper + // to ensure that the component state is updated. If the wrapper is + // created at the beginning of each test "await Vue.nextTick()" would + // need to be called instead (and for that the tests would need to be + // async). + function setupWrapper() { + wrapper = shallowMount(Video, { + localVue, + store, + propsData: { + model: callParticipantModel, + token: 'theToken', + sharedData: { + promoted: false, + }, + }, + }) + } + + function assertConnectionMessageLabel(expectedText) { + const connectionMessageLabel = wrapper.find('.connection-message') + if (expectedText) { + expect(connectionMessageLabel.exists()).toBe(true) + expect(connectionMessageLabel.text()).toBe(expectedText) + } else { + expect(connectionMessageLabel.exists()).toBe(false) + } + } + + function assertLoadingIconIsShown(expected) { + const loadingIcon = wrapper.find('.icon-loading') + expect(loadingIcon.exists()).toBe(expected) + } + + function assertNotConnected(expected) { + const notConnected = wrapper.find('.not-connected') + expect(notConnected.exists()).toBe(expected) + } + + test('participant just created', () => { + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.NONE) + assertLoadingIconIsShown(true) + assertNotConnected(true) + }) + + test('no peer', () => { + callParticipantModel.setPeer(null) + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.NONE) + assertLoadingIconIsShown(false) + assertNotConnected(false) + }) + + describe('original connection', () => { + let peerMock + + beforeEach(() => { + peerMock = new PeerMock() + callParticipantModel.setPeer(peerMock) + }) + + test('peer just set', () => { + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.NONE) + assertLoadingIconIsShown(true) + assertNotConnected(true) + }) + + test('sending offer', () => { + peerMock._setSignalingState('have-local-offer') + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.NONE) + assertLoadingIconIsShown(true) + assertNotConnected(true) + }) + + test('receiving offer', () => { + peerMock._setSignalingState('have-remote-offer') + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.NONE) + assertLoadingIconIsShown(true) + assertNotConnected(true) + }) + + test('negotiation finished', () => { + peerMock._setSignalingState('have-remote-offer') + peerMock._setSignalingState('stable') + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.NONE) + assertLoadingIconIsShown(true) + assertNotConnected(true) + }) + + test('establishing connection', () => { + peerMock._setSignalingState('have-remote-offer') + peerMock._setSignalingState('stable') + peerMock._setIceConnectionState('checking') + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.NONE) + assertLoadingIconIsShown(true) + assertNotConnected(true) + }) + + test('connection established', () => { + peerMock._setSignalingState('have-remote-offer') + peerMock._setSignalingState('stable') + peerMock._setIceConnectionState('checking') + peerMock._setIceConnectionState('connected') + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.NONE) + assertLoadingIconIsShown(false) + assertNotConnected(false) + }) + + test('connection established (completed)', () => { + peerMock._setSignalingState('have-remote-offer') + peerMock._setSignalingState('stable') + peerMock._setIceConnectionState('checking') + peerMock._setIceConnectionState('connected') + peerMock._setIceConnectionState('completed') + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.NONE) + assertLoadingIconIsShown(false) + assertNotConnected(false) + }) + + test('disconnected', () => { + peerMock._setSignalingState('have-remote-offer') + peerMock._setSignalingState('stable') + peerMock._setIceConnectionState('checking') + peerMock._setIceConnectionState('connected') + peerMock._setIceConnectionState('disconnected') + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.PROBLEMS) + assertLoadingIconIsShown(true) + assertNotConnected(true) + }) + + test('disconnected without ever connecting', () => { + peerMock._setSignalingState('have-remote-offer') + peerMock._setSignalingState('stable') + peerMock._setIceConnectionState('checking') + peerMock._setIceConnectionState('disconnected') + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.PROBLEMS) + assertLoadingIconIsShown(true) + assertNotConnected(true) + }) + + test('disconnected long', () => { + peerMock._setSignalingState('have-remote-offer') + peerMock._setSignalingState('stable') + peerMock._setIceConnectionState('checking') + peerMock._setIceConnectionState('connected') + peerMock._setIceConnectionState('disconnected') + // Custom event emitted when there is no HPB and the connection + // has been disconnected for a few seconds + peerMock._trigger('extendedIceConnectionStateChange', ['disconnected-long']) + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.PROBLEMS) + assertLoadingIconIsShown(true) + assertNotConnected(true) + }) + + test('disconnected long without ever connecting', () => { + peerMock._setSignalingState('have-remote-offer') + peerMock._setSignalingState('stable') + peerMock._setIceConnectionState('checking') + peerMock._setIceConnectionState('disconnected') + // Custom event emitted when there is no HPB and the connection + // has been disconnected for a few seconds + peerMock._trigger('extendedIceConnectionStateChange', ['disconnected-long']) + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.PROBLEMS) + assertLoadingIconIsShown(true) + assertNotConnected(true) + }) + + test('failed', () => { + peerMock._setSignalingState('have-remote-offer') + peerMock._setSignalingState('stable') + peerMock._setIceConnectionState('checking') + peerMock._setIceConnectionState('connected') + peerMock._setIceConnectionState('disconnected') + peerMock._setIceConnectionState('failed') + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.LOST) + assertLoadingIconIsShown(true) + assertNotConnected(true) + }) + + test('failed without ever connecting', () => { + peerMock._setSignalingState('have-remote-offer') + peerMock._setSignalingState('stable') + peerMock._setIceConnectionState('checking') + peerMock._setIceConnectionState('failed') + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.NOT_ESTABLISHED) + assertLoadingIconIsShown(true) + assertNotConnected(true) + }) + }) + + describe('reconnection after no original connection', () => { + let newPeerMock + + beforeEach(() => { + callParticipantModel.setPeer(null) + + newPeerMock = new PeerMock() + callParticipantModel.setPeer(newPeerMock) + }) + + test('peer just set', () => { + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.NONE) + assertLoadingIconIsShown(true) + assertNotConnected(true) + }) + + test('sending offer', () => { + newPeerMock._setSignalingState('have-local-offer') + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.NONE) + assertLoadingIconIsShown(true) + assertNotConnected(true) + }) + + test('receiving offer', () => { + newPeerMock._setSignalingState('have-remote-offer') + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.NONE) + assertLoadingIconIsShown(true) + assertNotConnected(true) + }) + + test('negotiation finished', () => { + newPeerMock._setSignalingState('have-remote-offer') + newPeerMock._setSignalingState('stable') + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.NONE) + assertLoadingIconIsShown(true) + assertNotConnected(true) + }) + + test('establishing connection', () => { + newPeerMock._setSignalingState('have-remote-offer') + newPeerMock._setSignalingState('stable') + newPeerMock._setIceConnectionState('checking') + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.NONE) + assertLoadingIconIsShown(true) + assertNotConnected(true) + }) + + test('connection established', () => { + newPeerMock._setSignalingState('have-remote-offer') + newPeerMock._setSignalingState('stable') + newPeerMock._setIceConnectionState('checking') + newPeerMock._setIceConnectionState('connected') + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.NONE) + assertLoadingIconIsShown(false) + assertNotConnected(false) + }) + + test('connection established (completed)', () => { + newPeerMock._setSignalingState('have-remote-offer') + newPeerMock._setSignalingState('stable') + newPeerMock._setIceConnectionState('checking') + newPeerMock._setIceConnectionState('connected') + newPeerMock._setIceConnectionState('completed') + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.NONE) + assertLoadingIconIsShown(false) + assertNotConnected(false) + }) + + test('disconnected', () => { + newPeerMock._setSignalingState('have-remote-offer') + newPeerMock._setSignalingState('stable') + newPeerMock._setIceConnectionState('checking') + newPeerMock._setIceConnectionState('connected') + newPeerMock._setIceConnectionState('disconnected') + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.PROBLEMS) + assertLoadingIconIsShown(true) + assertNotConnected(true) + }) + + test('disconnected without ever connecting', () => { + newPeerMock._setSignalingState('have-remote-offer') + newPeerMock._setSignalingState('stable') + newPeerMock._setIceConnectionState('checking') + newPeerMock._setIceConnectionState('disconnected') + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.PROBLEMS) + assertLoadingIconIsShown(true) + assertNotConnected(true) + }) + + test('disconnected long', () => { + newPeerMock._setSignalingState('have-remote-offer') + newPeerMock._setSignalingState('stable') + newPeerMock._setIceConnectionState('checking') + newPeerMock._setIceConnectionState('connected') + newPeerMock._setIceConnectionState('disconnected') + // Custom event emitted when there is no HPB and the connection + // has been disconnected for a few seconds + newPeerMock._trigger('extendedIceConnectionStateChange', ['disconnected-long']) + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.PROBLEMS) + assertLoadingIconIsShown(true) + assertNotConnected(true) + }) + + test('disconnected long without ever connecting', () => { + newPeerMock._setSignalingState('have-remote-offer') + newPeerMock._setSignalingState('stable') + newPeerMock._setIceConnectionState('checking') + newPeerMock._setIceConnectionState('disconnected') + // Custom event emitted when there is no HPB and the connection + // has been disconnected for a few seconds + newPeerMock._trigger('extendedIceConnectionStateChange', ['disconnected-long']) + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.PROBLEMS) + assertLoadingIconIsShown(true) + assertNotConnected(true) + }) + + test('failed', () => { + newPeerMock._setSignalingState('have-remote-offer') + newPeerMock._setSignalingState('stable') + newPeerMock._setIceConnectionState('checking') + newPeerMock._setIceConnectionState('connected') + newPeerMock._setIceConnectionState('disconnected') + newPeerMock._setIceConnectionState('failed') + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.LOST) + assertLoadingIconIsShown(true) + assertNotConnected(true) + }) + + test('failed without ever connecting', () => { + newPeerMock._setSignalingState('have-remote-offer') + newPeerMock._setSignalingState('stable') + newPeerMock._setIceConnectionState('checking') + newPeerMock._setIceConnectionState('failed') + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.NOT_ESTABLISHED) + assertLoadingIconIsShown(true) + assertNotConnected(true) + }) + }) + + describe('reconnection', () => { + let peerMock + let newPeerMock + + beforeEach(() => { + peerMock = new PeerMock() + callParticipantModel.setPeer(peerMock) + + newPeerMock = new PeerMock() + }) + + describe('without having been connected', () => { + beforeEach(() => { + peerMock._setSignalingState('have-remote-offer') + peerMock._setSignalingState('stable') + peerMock._setIceConnectionState('checking') + peerMock._setIceConnectionState('failed') + + callParticipantModel.setPeer(newPeerMock) + }) + + test('peer just set', () => { + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.NOT_ESTABLISHED) + assertLoadingIconIsShown(true) + assertNotConnected(true) + }) + + test('sending offer', () => { + newPeerMock._setSignalingState('have-local-offer') + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.NOT_ESTABLISHED) + assertLoadingIconIsShown(true) + assertNotConnected(true) + }) + + test('receiving offer', () => { + newPeerMock._setSignalingState('have-remote-offer') + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.NOT_ESTABLISHED) + assertLoadingIconIsShown(true) + assertNotConnected(true) + }) + + test('negotiation finished', () => { + newPeerMock._setSignalingState('have-remote-offer') + newPeerMock._setSignalingState('stable') + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.NOT_ESTABLISHED) + assertLoadingIconIsShown(true) + assertNotConnected(true) + }) + + test('establishing connection', () => { + newPeerMock._setSignalingState('have-remote-offer') + newPeerMock._setSignalingState('stable') + newPeerMock._setIceConnectionState('checking') + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.NOT_ESTABLISHED) + assertLoadingIconIsShown(true) + assertNotConnected(true) + }) + + test('connection established', () => { + newPeerMock._setSignalingState('have-remote-offer') + newPeerMock._setSignalingState('stable') + newPeerMock._setIceConnectionState('checking') + newPeerMock._setIceConnectionState('connected') + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.NONE) + assertLoadingIconIsShown(false) + assertNotConnected(false) + }) + + test('disconnected', () => { + newPeerMock._setSignalingState('have-remote-offer') + newPeerMock._setSignalingState('stable') + newPeerMock._setIceConnectionState('checking') + newPeerMock._setIceConnectionState('connected') + newPeerMock._setIceConnectionState('disconnected') + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.PROBLEMS) + assertLoadingIconIsShown(true) + assertNotConnected(true) + }) + + test('disconnected without ever connecting', () => { + newPeerMock._setSignalingState('have-remote-offer') + newPeerMock._setSignalingState('stable') + newPeerMock._setIceConnectionState('checking') + newPeerMock._setIceConnectionState('disconnected') + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.NOT_ESTABLISHED) + assertLoadingIconIsShown(true) + assertNotConnected(true) + }) + + test('disconnected long', () => { + newPeerMock._setSignalingState('have-remote-offer') + newPeerMock._setSignalingState('stable') + newPeerMock._setIceConnectionState('checking') + newPeerMock._setIceConnectionState('connected') + newPeerMock._setIceConnectionState('disconnected') + // Custom event emitted when there is no HPB and the + // connection has been disconnected for a few seconds + newPeerMock._trigger('extendedIceConnectionStateChange', ['disconnected-long']) + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.PROBLEMS) + assertLoadingIconIsShown(true) + assertNotConnected(true) + }) + + test('disconnected long without ever connecting', () => { + newPeerMock._setSignalingState('have-remote-offer') + newPeerMock._setSignalingState('stable') + newPeerMock._setIceConnectionState('checking') + newPeerMock._setIceConnectionState('disconnected') + // Custom event emitted when there is no HPB and the + // connection has been disconnected for a few seconds + newPeerMock._trigger('extendedIceConnectionStateChange', ['disconnected-long']) + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.NOT_ESTABLISHED) + assertLoadingIconIsShown(true) + assertNotConnected(true) + }) + + test('failed', () => { + newPeerMock._setSignalingState('have-remote-offer') + newPeerMock._setSignalingState('stable') + newPeerMock._setIceConnectionState('checking') + newPeerMock._setIceConnectionState('connected') + newPeerMock._setIceConnectionState('disconnected') + newPeerMock._setIceConnectionState('failed') + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.LOST) + assertLoadingIconIsShown(true) + assertNotConnected(true) + }) + + test('failed without ever connecting', () => { + newPeerMock._setSignalingState('have-remote-offer') + newPeerMock._setSignalingState('stable') + newPeerMock._setIceConnectionState('checking') + newPeerMock._setIceConnectionState('failed') + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.NOT_ESTABLISHED) + assertLoadingIconIsShown(true) + assertNotConnected(true) + }) + + test('failed without ever connecting and not retrying', () => { + newPeerMock._setSignalingState('have-remote-offer') + newPeerMock._setSignalingState('stable') + newPeerMock._setIceConnectionState('checking') + newPeerMock._setIceConnectionState('failed') + // Custom event emitted when there is no HPB and the + // connection has failed several times in a row + newPeerMock._trigger('extendedIceConnectionStateChange', ['failed-no-restart']) + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.NOT_ESTABLISHED_NOT_RETRYING) + assertLoadingIconIsShown(false) + assertNotConnected(true) + }) + }) + + describe('after having been connected', () => { + beforeEach(() => { + peerMock._setSignalingState('have-remote-offer') + peerMock._setSignalingState('stable') + peerMock._setIceConnectionState('checking') + peerMock._setIceConnectionState('connected') + }) + + describe('without HPB', () => { + test('ICE restarted after disconnected long (no HPB)', () => { + peerMock._setIceConnectionState('disconnected') + // Custom event emitted when there is no HPB and the connection + // has been disconnected for a few seconds + peerMock._trigger('extendedIceConnectionStateChange', ['disconnected-long']) + peerMock._setSignalingState('have-local-offer') + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.LOST) + assertLoadingIconIsShown(true) + assertNotConnected(true) + }) + + test('ICE restarted after failed (no HPB)', () => { + peerMock._setIceConnectionState('disconnected') + peerMock._setIceConnectionState('failed') + peerMock._setSignalingState('have-local-offer') + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.LOST) + assertLoadingIconIsShown(true) + assertNotConnected(true) + }) + + test('negotiation finished', () => { + peerMock._setIceConnectionState('disconnected') + peerMock._setIceConnectionState('failed') + peerMock._setSignalingState('have-local-offer') + peerMock._setSignalingState('stable') + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.LOST) + assertLoadingIconIsShown(true) + assertNotConnected(true) + }) + + test('establishing connection', () => { + peerMock._setIceConnectionState('disconnected') + peerMock._setIceConnectionState('failed') + peerMock._setSignalingState('have-local-offer') + peerMock._setSignalingState('stable') + peerMock._setIceConnectionState('checking') + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.LOST) + assertLoadingIconIsShown(true) + assertNotConnected(true) + }) + + test('connection established', () => { + peerMock._setIceConnectionState('disconnected') + peerMock._setIceConnectionState('failed') + peerMock._setSignalingState('have-local-offer') + peerMock._setSignalingState('stable') + peerMock._setIceConnectionState('checking') + peerMock._setIceConnectionState('connected') + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.NONE) + assertLoadingIconIsShown(false) + assertNotConnected(false) + }) + + test('connection established (completed)', () => { + peerMock._setIceConnectionState('disconnected') + peerMock._setIceConnectionState('failed') + peerMock._setSignalingState('have-local-offer') + peerMock._setSignalingState('stable') + peerMock._setIceConnectionState('checking') + peerMock._setIceConnectionState('connected') + peerMock._setIceConnectionState('completed') + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.NONE) + assertLoadingIconIsShown(false) + assertNotConnected(false) + }) + + test('disconnected', () => { + peerMock._setIceConnectionState('disconnected') + peerMock._setIceConnectionState('failed') + peerMock._setSignalingState('have-local-offer') + peerMock._setSignalingState('stable') + peerMock._setIceConnectionState('checking') + peerMock._setIceConnectionState('connected') + peerMock._setIceConnectionState('disconnected') + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.PROBLEMS) + assertLoadingIconIsShown(true) + assertNotConnected(true) + }) + + test('disconnected without connecting the second time', () => { + peerMock._setIceConnectionState('disconnected') + peerMock._setIceConnectionState('failed') + peerMock._setSignalingState('have-local-offer') + peerMock._setSignalingState('stable') + peerMock._setIceConnectionState('checking') + peerMock._setIceConnectionState('disconnected') + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.LOST) + assertLoadingIconIsShown(true) + assertNotConnected(true) + }) + + test('disconnected long', () => { + peerMock._setIceConnectionState('disconnected') + peerMock._setIceConnectionState('failed') + peerMock._setSignalingState('have-local-offer') + peerMock._setSignalingState('stable') + peerMock._setIceConnectionState('checking') + peerMock._setIceConnectionState('connected') + peerMock._setIceConnectionState('disconnected') + // Custom event emitted when there is no HPB and the connection + // has been disconnected for a few seconds + peerMock._trigger('extendedIceConnectionStateChange', ['disconnected-long']) + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.PROBLEMS) + assertLoadingIconIsShown(true) + assertNotConnected(true) + }) + + test('disconnected long without connecting the second time', () => { + peerMock._setIceConnectionState('disconnected') + peerMock._setIceConnectionState('failed') + peerMock._setSignalingState('have-local-offer') + peerMock._setSignalingState('stable') + peerMock._setIceConnectionState('checking') + peerMock._setIceConnectionState('disconnected') + // Custom event emitted when there is no HPB and the connection + // has been disconnected for a few seconds + peerMock._trigger('extendedIceConnectionStateChange', ['disconnected-long']) + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.LOST) + assertLoadingIconIsShown(true) + assertNotConnected(true) + }) + + test('failed', () => { + peerMock._setIceConnectionState('disconnected') + peerMock._setIceConnectionState('failed') + peerMock._setSignalingState('have-local-offer') + peerMock._setSignalingState('stable') + peerMock._setIceConnectionState('checking') + peerMock._setIceConnectionState('connected') + peerMock._setIceConnectionState('disconnected') + peerMock._setIceConnectionState('failed') + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.LOST) + assertLoadingIconIsShown(true) + assertNotConnected(true) + }) + + test('failed without connecting the second time', () => { + peerMock._setIceConnectionState('disconnected') + peerMock._setIceConnectionState('failed') + peerMock._setSignalingState('have-local-offer') + peerMock._setSignalingState('stable') + peerMock._setIceConnectionState('checking') + peerMock._setIceConnectionState('failed') + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.LOST) + assertLoadingIconIsShown(true) + assertNotConnected(true) + }) + + test('failed without connecting the second time and not retrying', () => { + peerMock._setIceConnectionState('disconnected') + peerMock._setIceConnectionState('failed') + peerMock._setSignalingState('have-local-offer') + peerMock._setSignalingState('stable') + peerMock._setIceConnectionState('checking') + peerMock._setIceConnectionState('failed') + // Custom event emitted when there is no HPB and the + // connection has failed several times in a row + peerMock._trigger('extendedIceConnectionStateChange', ['failed-no-restart']) + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.LOST_NOT_RETRYING) + assertLoadingIconIsShown(false) + assertNotConnected(true) + }) + }) + + describe('with HPB', () => { + beforeEach(() => { + peerMock._setIceConnectionState('disconnected') + peerMock._setIceConnectionState('failed') + + callParticipantModel.setPeer(newPeerMock) + + newPeerMock._setSignalingState('have-remote-offer') + }) + + test('requested renegotiation after connection failed', () => { + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.LOST) + assertLoadingIconIsShown(true) + assertNotConnected(true) + }) + + test('negotiation finished', () => { + newPeerMock._setSignalingState('stable') + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.LOST) + assertLoadingIconIsShown(true) + assertNotConnected(true) + }) + + test('establishing connection', () => { + newPeerMock._setSignalingState('stable') + newPeerMock._setIceConnectionState('checking') + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.LOST) + assertLoadingIconIsShown(true) + assertNotConnected(true) + }) + + test('connection established', () => { + newPeerMock._setSignalingState('stable') + newPeerMock._setIceConnectionState('checking') + newPeerMock._setIceConnectionState('connected') + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.NONE) + assertLoadingIconIsShown(false) + assertNotConnected(false) + }) + + test('connection established (completed)', () => { + newPeerMock._setSignalingState('stable') + newPeerMock._setIceConnectionState('checking') + newPeerMock._setIceConnectionState('connected') + newPeerMock._setIceConnectionState('completed') + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.NONE) + assertLoadingIconIsShown(false) + assertNotConnected(false) + }) + + test('disconnected', () => { + newPeerMock._setSignalingState('stable') + newPeerMock._setIceConnectionState('checking') + newPeerMock._setIceConnectionState('connected') + newPeerMock._setIceConnectionState('disconnected') + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.PROBLEMS) + assertLoadingIconIsShown(true) + assertNotConnected(true) + }) + + test('disconnected without connecting the second time', () => { + newPeerMock._setSignalingState('stable') + newPeerMock._setIceConnectionState('checking') + newPeerMock._setIceConnectionState('disconnected') + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.LOST) + assertLoadingIconIsShown(true) + assertNotConnected(true) + }) + + test('failed', () => { + newPeerMock._setSignalingState('stable') + newPeerMock._setIceConnectionState('checking') + newPeerMock._setIceConnectionState('connected') + newPeerMock._setIceConnectionState('disconnected') + newPeerMock._setIceConnectionState('failed') + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.LOST) + assertLoadingIconIsShown(true) + assertNotConnected(true) + }) + + test('failed without connecting the second time', () => { + newPeerMock._setSignalingState('stable') + newPeerMock._setIceConnectionState('checking') + newPeerMock._setIceConnectionState('failed') + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.LOST) + assertLoadingIconIsShown(true) + assertNotConnected(true) + }) + }) + }) + }) + }) +}) -- cgit v1.2.3 From bbb178303036016417c4dc62646f3d98b55dd805 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Thu, 3 Feb 2022 21:39:15 +0100 Subject: Refactor "isNotConnected()" to "isConnected()" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Calviño Sánchez --- src/components/CallView/shared/Video.vue | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/CallView/shared/Video.vue b/src/components/CallView/shared/Video.vue index 32507da27..edb423e94 100644 --- a/src/components/CallView/shared/Video.vue +++ b/src/components/CallView/shared/Video.vue @@ -180,12 +180,12 @@ export default { return this.model.attributes.connectedAtLeastOnce }, - isNotConnected() { - return this.model.attributes.connectionState !== ConnectionState.CONNECTED && this.model.attributes.connectionState !== ConnectionState.COMPLETED + isConnected() { + return this.model.attributes.connectionState === ConnectionState.CONNECTED || this.model.attributes.connectionState === ConnectionState.COMPLETED }, isLoading() { - return this.isNotConnected && this.model.attributes.connectionState !== ConnectionState.FAILED_NO_RESTART + return !this.isConnected && this.model.attributes.connectionState !== ConnectionState.FAILED_NO_RESTART }, isDisconnected() { @@ -242,7 +242,7 @@ export default { containerClass() { return { 'videoContainer-dummy': this.placeholderForPromoted, - 'not-connected': !this.placeholderForPromoted && this.isNotConnected, + 'not-connected': !this.placeholderForPromoted && !this.isConnected, speaking: !this.placeholderForPromoted && this.model.attributes.speaking, promoted: !this.placeholderForPromoted && this.sharedData.promoted && !this.isGrid, 'video-container-grid': this.isGrid, -- cgit v1.2.3 From 6c002a6df1b23e34746790ead4ca29591f426ef9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Thu, 3 Feb 2022 21:42:35 +0100 Subject: Do not show the video as disconnected while updating the connection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If the connection transitions to "disconnected" (or was disconnected before starting the renegotiation) the UI will wrongly show the message about reconnecting due to connection problems. I have not found an easy way to solve this, as it would require to keep track of whether a renegotiation started due to handling the "negotiationneeded" event or something else (and, in the future, once renegotiations are implemented also without HPB, it should be necessary to know too if the offer was sent by the remote peer for other reasons). Due to this, for now a slightly misleading message will be shown if the connection is updated while "disconnected". Signed-off-by: Daniel Calviño Sánchez --- src/components/CallView/shared/Video.spec.js | 108 +++++++++++++++++++++++++++ src/components/CallView/shared/Video.vue | 6 +- 2 files changed, 113 insertions(+), 1 deletion(-) diff --git a/src/components/CallView/shared/Video.spec.js b/src/components/CallView/shared/Video.spec.js index 0a96c5fe5..44abff9af 100644 --- a/src/components/CallView/shared/Video.spec.js +++ b/src/components/CallView/shared/Video.spec.js @@ -336,6 +336,114 @@ describe('Video.vue', () => { }) }) + describe('renegotiation', () => { + let peerMock + + beforeEach(() => { + peerMock = new PeerMock() + callParticipantModel.setPeer(peerMock) + }) + + test('started after connection established', () => { + peerMock._setSignalingState('have-remote-offer') + peerMock._setSignalingState('stable') + peerMock._setIceConnectionState('checking') + peerMock._setIceConnectionState('connected') + peerMock._setSignalingState('have-remote-offer') + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.NONE) + assertLoadingIconIsShown(false) + assertNotConnected(false) + }) + + test('started after connection established and then finished', () => { + peerMock._setSignalingState('have-remote-offer') + peerMock._setSignalingState('stable') + peerMock._setIceConnectionState('checking') + peerMock._setIceConnectionState('connected') + peerMock._setSignalingState('have-remote-offer') + peerMock._setSignalingState('stable') + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.NONE) + assertLoadingIconIsShown(false) + assertNotConnected(false) + }) + + test('started before disconnected', () => { + peerMock._setSignalingState('have-remote-offer') + peerMock._setSignalingState('stable') + peerMock._setIceConnectionState('checking') + peerMock._setIceConnectionState('connected') + peerMock._setSignalingState('have-remote-offer') + peerMock._setIceConnectionState('disconnected') + + setupWrapper() + + // FIXME The message should be "PROBLEMS" rather than "LOST", as + // the negotiation is not caused by the disconnection itself. + // However it does not seem to be an easy way to do it right + // now. + assertConnectionMessageLabel(connectionMessage.LOST) + assertLoadingIconIsShown(true) + assertNotConnected(true) + }) + + test('started before disconnected and then finished', () => { + peerMock._setSignalingState('have-remote-offer') + peerMock._setSignalingState('stable') + peerMock._setIceConnectionState('checking') + peerMock._setIceConnectionState('connected') + peerMock._setSignalingState('have-remote-offer') + peerMock._setIceConnectionState('disconnected') + peerMock._setSignalingState('stable') + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.PROBLEMS) + assertLoadingIconIsShown(true) + assertNotConnected(true) + }) + + test('started after disconnected', () => { + peerMock._setSignalingState('have-remote-offer') + peerMock._setSignalingState('stable') + peerMock._setIceConnectionState('checking') + peerMock._setIceConnectionState('connected') + peerMock._setIceConnectionState('disconnected') + peerMock._setSignalingState('have-remote-offer') + + setupWrapper() + + // FIXME The message should be "PROBLEMS" rather than "LOST", as + // the negotiation is not caused by the disconnection itself. + // However it does not seem to be an easy way to do it right + // now. + assertConnectionMessageLabel(connectionMessage.LOST) + assertLoadingIconIsShown(true) + assertNotConnected(true) + }) + + test('started after disconnected and then finished', () => { + peerMock._setSignalingState('have-remote-offer') + peerMock._setSignalingState('stable') + peerMock._setIceConnectionState('checking') + peerMock._setIceConnectionState('connected') + peerMock._setIceConnectionState('disconnected') + peerMock._setSignalingState('have-remote-offer') + peerMock._setSignalingState('stable') + + setupWrapper() + + assertConnectionMessageLabel(connectionMessage.PROBLEMS) + assertLoadingIconIsShown(true) + assertNotConnected(true) + }) + }) + describe('reconnection after no original connection', () => { let newPeerMock diff --git a/src/components/CallView/shared/Video.vue b/src/components/CallView/shared/Video.vue index edb423e94..186f5168b 100644 --- a/src/components/CallView/shared/Video.vue +++ b/src/components/CallView/shared/Video.vue @@ -204,11 +204,15 @@ export default { * received yet). Similarly both "negotiating" and "connecting" need to * be checked, as the negotiation will start before the connection * attempt is started. + * + * If the negotiation is done while there is still a connection it is + * not regarded as reconnecting, as in that case it is a renegotiation + * to update the current connection. */ isReconnecting() { return this.model.attributes.connectionState === ConnectionState.FAILED || (!this.model.attributes.initialConnection - && (this.model.attributes.negotiating || this.model.attributes.connecting)) + && ((this.model.attributes.negotiating && !this.isConnected) || this.model.attributes.connecting)) }, isNoLongerTryingToReconnect() { -- cgit v1.2.3 From 4063922525645ea815d5d71d8cc01877c0a97187 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Tue, 19 Apr 2022 02:47:06 +0200 Subject: Add helper class to block a remote video when not needed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A remote video is not needed when it was explicitly disabled by the local user (independently of whether it is enabled by the remote participant) or when it is not visible. Signed-off-by: Daniel Calviño Sánchez --- src/utils/webrtc/RemoteVideoBlocker.js | 131 +++++++++++ src/utils/webrtc/RemoteVideoBlocker.spec.js | 336 ++++++++++++++++++++++++++++ 2 files changed, 467 insertions(+) create mode 100644 src/utils/webrtc/RemoteVideoBlocker.js create mode 100644 src/utils/webrtc/RemoteVideoBlocker.spec.js diff --git a/src/utils/webrtc/RemoteVideoBlocker.js b/src/utils/webrtc/RemoteVideoBlocker.js new file mode 100644 index 000000000..2a8a65cbd --- /dev/null +++ b/src/utils/webrtc/RemoteVideoBlocker.js @@ -0,0 +1,131 @@ +/** + * + * @copyright Copyright (c) 2022, Daniel Calviño Sánchez (danxuliu@gmail.com) + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +/** + * Helper to block the remote video when not needed. + * + * A remote video is not needed if the local user explicitly disabled it + * (independently of whether the remote user (and thus owner of the remote + * video) has it enabled or not) or if it is not visible. + * + * The remote video is not immediately hidden when no longer visible; a few + * seconds are waited to avoid blocking and unblocking on layout changes. + * + * "increaseVisibleCounter()" can be called several times by the same view, but + * "decreaseVisibleCounter()" must have been called a corresponding number of + * times once the view is destroyed. + * + * A single RemoteVideoBlocker is assumed to be associated with its + * CallParticipantModel, and it is also assumed to be the only element blocking + * and unblocking the video. Otherwise the result is undefined. + * + * Note that the RemoteVideoBlocker can be used on participants that do not have + * a video at all (for example, because they do not have a camera or they do not + * have video permissions). In that case the CallParticipantModel will block the + * video if needed if it becomes available. + * + * @param {object} callParticipantModel the model to block/unblock the video on. + */ +export default function RemoteVideoBlocker(callParticipantModel) { + this._model = callParticipantModel + + // Keep track of the blocked state here, as the Peer object may not block + // the video if some features are missing, and even if the video is blocked + // the attribute will not be updated right away but once the renegotiation + // is done. + this._blocked = false + + this._enabled = true + this._visibleCounter = 1 + + this._blockVideoTimeout = null + + // Block by default if not shown after creation. + this.decreaseVisibleCounter() +} + +RemoteVideoBlocker.prototype = { + + isVideoEnabled() { + return this._enabled + }, + + setVideoEnabled(enabled) { + this._enabled = enabled + + const hadBlockVideoTimeout = this._blockVideoTimeout + + clearTimeout(this._blockVideoTimeout) + this._blockVideoTimeout = null + + if (!this._visibleCounter && !hadBlockVideoTimeout) { + return + } + + this._setVideoBlocked(!enabled) + }, + + increaseVisibleCounter() { + this._visibleCounter++ + + clearTimeout(this._blockVideoTimeout) + this._blockVideoTimeout = null + + if (!this._enabled) { + return + } + + this._setVideoBlocked(false) + }, + + decreaseVisibleCounter() { + if (this._visibleCounter <= 0) { + console.error('Visible counter decreased when not visible') + + return + } + + this._visibleCounter-- + + if (this._visibleCounter > 0 || !this._enabled) { + return + } + + clearTimeout(this._blockVideoTimeout) + + this._blockVideoTimeout = setTimeout(() => { + this._setVideoBlocked(true) + + this._blockVideoTimeout = null + }, 5000) + }, + + _setVideoBlocked(blocked) { + if (this._blocked === blocked) { + return + } + + this._blocked = blocked + + this._model.setVideoBlocked(blocked) + }, + +} diff --git a/src/utils/webrtc/RemoteVideoBlocker.spec.js b/src/utils/webrtc/RemoteVideoBlocker.spec.js new file mode 100644 index 000000000..700b2918b --- /dev/null +++ b/src/utils/webrtc/RemoteVideoBlocker.spec.js @@ -0,0 +1,336 @@ +/** + * + * @copyright Copyright (c) 2022, Daniel Calviño Sánchez (danxuliu@gmail.com) + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +import RemoteVideoBlocker from './RemoteVideoBlocker' + +describe('RemoteVideoBlocker', () => { + let callParticipantModel + let remoteVideoBlocker + + beforeEach(() => { + jest.useFakeTimers() + + callParticipantModel = { + setVideoBlocked: jest.fn() + } + + remoteVideoBlocker = new RemoteVideoBlocker(callParticipantModel) + }) + + test('blocks the video by default if not shown in some seconds', () => { + jest.advanceTimersByTime(4000) + + expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(0) + + jest.advanceTimersByTime(1000) + + expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(1) + expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledWith(true) + + expect(remoteVideoBlocker.isVideoEnabled()).toBe(true) + }) + + describe('set video enabled', () => { + test('immediately blocks the video', () => { + remoteVideoBlocker.increaseVisibleCounter() + + remoteVideoBlocker.setVideoEnabled(false) + + expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(1) + expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledWith(true) + + expect(remoteVideoBlocker.isVideoEnabled()).toBe(false) + }) + test('immediately unblocks the video', () => { + remoteVideoBlocker.increaseVisibleCounter() + + remoteVideoBlocker.setVideoEnabled(false) + remoteVideoBlocker.setVideoEnabled(true) + + expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(2) + expect(callParticipantModel.setVideoBlocked).toHaveBeenNthCalledWith(2, false) + + expect(remoteVideoBlocker.isVideoEnabled()).toBe(true) + }) + }) + + describe('set video visible', () => { + test('does nothing if shown', () => { + remoteVideoBlocker.increaseVisibleCounter() + + jest.runAllTimers() + + expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(0) + expect(remoteVideoBlocker._visibleCounter).toBe(1) + + expect(remoteVideoBlocker.isVideoEnabled()).toBe(true) + }) + + test('does nothing if hidden without showing first', () => { + jest.runAllTimers() + + expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(1) + expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledWith(true) + + remoteVideoBlocker.decreaseVisibleCounter() + + jest.runAllTimers() + + expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(1) + expect(remoteVideoBlocker._visibleCounter).toBe(0) + + expect(remoteVideoBlocker.isVideoEnabled()).toBe(true) + }) + + test('blocks the video after some seconds when hidden', () => { + remoteVideoBlocker.increaseVisibleCounter() + remoteVideoBlocker.decreaseVisibleCounter() + + jest.advanceTimersByTime(4000) + + expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(0) + + jest.advanceTimersByTime(1000) + + expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(1) + expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledWith(true) + expect(remoteVideoBlocker._visibleCounter).toBe(0) + + expect(remoteVideoBlocker.isVideoEnabled()).toBe(true) + }) + + test('does nothing if shown again before blocking', () => { + remoteVideoBlocker.increaseVisibleCounter() + remoteVideoBlocker.decreaseVisibleCounter() + + jest.advanceTimersByTime(4000) + + remoteVideoBlocker.increaseVisibleCounter() + + jest.runAllTimers() + + expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(0) + expect(remoteVideoBlocker._visibleCounter).toBe(1) + + expect(remoteVideoBlocker.isVideoEnabled()).toBe(true) + }) + + test('immediately unblocks the video after showing', () => { + remoteVideoBlocker.increaseVisibleCounter() + remoteVideoBlocker.decreaseVisibleCounter() + + jest.runAllTimers() + + remoteVideoBlocker.increaseVisibleCounter() + + expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(2) + expect(callParticipantModel.setVideoBlocked).toHaveBeenNthCalledWith(2, false) + expect(remoteVideoBlocker._visibleCounter).toBe(1) + + expect(remoteVideoBlocker.isVideoEnabled()).toBe(true) + }) + + test('does nothing if not fully hidden', () => { + remoteVideoBlocker.increaseVisibleCounter() + remoteVideoBlocker.increaseVisibleCounter() + remoteVideoBlocker.increaseVisibleCounter() + remoteVideoBlocker.decreaseVisibleCounter() + remoteVideoBlocker.decreaseVisibleCounter() + remoteVideoBlocker.increaseVisibleCounter() + remoteVideoBlocker.increaseVisibleCounter() + remoteVideoBlocker.decreaseVisibleCounter() + + jest.runAllTimers() + + expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(0) + expect(remoteVideoBlocker._visibleCounter).toBe(2) + + expect(remoteVideoBlocker.isVideoEnabled()).toBe(true) + }) + }) + + describe('set video enabled and visible', () => { + test('immediately blocks the video if disabled when visible', () => { + remoteVideoBlocker.increaseVisibleCounter() + remoteVideoBlocker.increaseVisibleCounter() + remoteVideoBlocker.increaseVisibleCounter() + + remoteVideoBlocker.setVideoEnabled(false) + + expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(1) + expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledWith(true) + + jest.runAllTimers() + + expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(1) + + expect(remoteVideoBlocker.isVideoEnabled()).toBe(false) + }) + + test('immediately blocks the video if disabled before blocking after hidden', () => { + remoteVideoBlocker.increaseVisibleCounter() + remoteVideoBlocker.decreaseVisibleCounter() + + jest.advanceTimersByTime(4000) + + remoteVideoBlocker.setVideoEnabled(false) + + expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(1) + expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledWith(true) + + jest.runAllTimers() + + expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(1) + + expect(remoteVideoBlocker.isVideoEnabled()).toBe(false) + }) + + test('blocks the video after some seconds if hidden when enabled', () => { + remoteVideoBlocker.setVideoEnabled(true) + + remoteVideoBlocker.increaseVisibleCounter() + remoteVideoBlocker.decreaseVisibleCounter() + + jest.advanceTimersByTime(4000) + + expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(0) + + jest.advanceTimersByTime(1000) + + expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(1) + expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledWith(true) + + expect(remoteVideoBlocker.isVideoEnabled()).toBe(true) + }) + + test('does nothing if disabled when hidden', () => { + remoteVideoBlocker.increaseVisibleCounter() + remoteVideoBlocker.decreaseVisibleCounter() + + jest.runAllTimers() + + expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(1) + expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledWith(true) + + remoteVideoBlocker.setVideoEnabled(false) + + expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(1) + + expect(remoteVideoBlocker.isVideoEnabled()).toBe(false) + }) + + test('does nothing if enabled when hidden', () => { + remoteVideoBlocker.increaseVisibleCounter() + remoteVideoBlocker.decreaseVisibleCounter() + + jest.runAllTimers() + + expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(1) + expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledWith(true) + + remoteVideoBlocker.setVideoEnabled(false) + remoteVideoBlocker.setVideoEnabled(true) + + expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(1) + + expect(remoteVideoBlocker.isVideoEnabled()).toBe(true) + }) + + test('does nothing if hidden when disabled', () => { + remoteVideoBlocker.setVideoEnabled(false) + + expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(1) + expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledWith(true) + + remoteVideoBlocker.increaseVisibleCounter() + remoteVideoBlocker.decreaseVisibleCounter() + + jest.runAllTimers() + + expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(1) + + expect(remoteVideoBlocker.isVideoEnabled()).toBe(false) + }) + + test('does nothing if shown when disabled', () => { + remoteVideoBlocker.setVideoEnabled(false) + + expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(1) + expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledWith(true) + + remoteVideoBlocker.increaseVisibleCounter() + remoteVideoBlocker.decreaseVisibleCounter() + remoteVideoBlocker.increaseVisibleCounter() + + jest.runAllTimers() + + expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(1) + + expect(remoteVideoBlocker.isVideoEnabled()).toBe(false) + }) + + test('immediately unblocks the video if enabled after showing', () => { + remoteVideoBlocker.setVideoEnabled(false) + + expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(1) + expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledWith(true) + + remoteVideoBlocker.increaseVisibleCounter() + remoteVideoBlocker.decreaseVisibleCounter() + remoteVideoBlocker.increaseVisibleCounter() + remoteVideoBlocker.increaseVisibleCounter() + remoteVideoBlocker.increaseVisibleCounter() + + jest.runAllTimers() + + expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(1) + + remoteVideoBlocker.setVideoEnabled(true) + + expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(2) + expect(callParticipantModel.setVideoBlocked).toHaveBeenNthCalledWith(2, false) + + expect(remoteVideoBlocker.isVideoEnabled()).toBe(true) + }) + + test('immediately unblocks the video if shown after enabled', () => { + remoteVideoBlocker.increaseVisibleCounter() + remoteVideoBlocker.decreaseVisibleCounter() + + jest.runAllTimers() + + expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(1) + expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledWith(true) + + remoteVideoBlocker.setVideoEnabled(false) + remoteVideoBlocker.setVideoEnabled(true) + + expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(1) + + remoteVideoBlocker.increaseVisibleCounter() + + expect(callParticipantModel.setVideoBlocked).toHaveBeenCalledTimes(2) + expect(callParticipantModel.setVideoBlocked).toHaveBeenNthCalledWith(2, false) + + expect(remoteVideoBlocker.isVideoEnabled()).toBe(true) + }) + }) +}) -- cgit v1.2.3 From 114b83dcdde32005ba25233cfe38b66d1d19823d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Thu, 3 Feb 2022 22:06:37 +0100 Subject: Use RemoteVideoBlocker in call view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Now the remote videos will be blocked when not needed, that is, when explicitly disabled by the local user or when not visible. Signed-off-by: Daniel Calviño Sánchez --- src/components/CallView/CallView.vue | 7 ++++--- src/components/CallView/Grid/Grid.vue | 6 +++++- src/components/CallView/shared/Video.spec.js | 3 +++ src/components/CallView/shared/Video.vue | 8 +++++++- src/components/CallView/shared/VideoBottomBar.vue | 8 ++++---- 5 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/components/CallView/CallView.vue b/src/components/CallView/CallView.vue index 98b87f163..6d63bd418 100644 --- a/src/components/CallView/CallView.vue +++ b/src/components/CallView/CallView.vue @@ -141,6 +141,7 @@ import { loadState } from '@nextcloud/initial-state' import Grid from './Grid/Grid' import { SIMULCAST } from '../../constants' import { localMediaModel, localCallParticipantModel, callParticipantCollection } from '../../utils/webrtc/index' +import RemoteVideoBlocker from '../../utils/webrtc/RemoteVideoBlocker' import { fetchPeers } from '../../services/callsService' import { showMessage } from '@nextcloud/dialogs' import EmptyCallView from './shared/EmptyCallView' @@ -212,7 +213,7 @@ export default { callParticipantModelsWithVideo() { return this.callParticipantModels.filter(callParticipantModel => { return callParticipantModel.attributes.videoAvailable - && this.sharedDatas[callParticipantModel.attributes.peerId].videoEnabled + && this.sharedDatas[callParticipantModel.attributes.peerId].remoteVideoBlocker.isVideoEnabled() && (typeof callParticipantModel.attributes.stream === 'object') }) }, @@ -454,7 +455,7 @@ export default { addedModels.forEach(addedModel => { const sharedData = { promoted: false, - videoEnabled: true, + remoteVideoBlocker: new RemoteVideoBlocker(addedModel), screenVisible: false, } @@ -648,7 +649,7 @@ export default { // Toggles videos on and off handleToggleVideo({ peerId, value }) { - this.sharedDatas[peerId].videoEnabled = value + this.sharedDatas[peerId].remoteVideoBlocker.setVideoEnabled(value) }, adjustSimulcastQuality() { diff --git a/src/components/CallView/Grid/Grid.vue b/src/components/CallView/Grid/Grid.vue index c7fe91e73..63496c808 100644 --- a/src/components/CallView/Grid/Grid.vue +++ b/src/components/CallView/Grid/Grid.vue @@ -584,7 +584,11 @@ export default { placeholderSharedData() { return { - videoEnabled: true, + videoEnabled: { + isVideoEnabled() { + return true + }, + }, screenVisible: false, } }, diff --git a/src/components/CallView/shared/Video.spec.js b/src/components/CallView/shared/Video.spec.js index 44abff9af..224a96521 100644 --- a/src/components/CallView/shared/Video.spec.js +++ b/src/components/CallView/shared/Video.spec.js @@ -117,6 +117,9 @@ describe('Video.vue', () => { model: callParticipantModel, token: 'theToken', sharedData: { + remoteVideoBlocker: { + increaseVisibleCounter: jest.fn(), + }, promoted: false, }, }, diff --git a/src/components/CallView/shared/Video.vue b/src/components/CallView/shared/Video.vue index 186f5168b..e1d792bec 100644 --- a/src/components/CallView/shared/Video.vue +++ b/src/components/CallView/shared/Video.vue @@ -375,7 +375,7 @@ export default { }, hasVideo() { - return !this.model.attributes.videoBlocked && this.model.attributes.videoAvailable && this.sharedData.videoEnabled && (typeof this.model.attributes.stream === 'object') + return !this.model.attributes.videoBlocked && this.model.attributes.videoAvailable && this.sharedData.remoteVideoBlocker.isVideoEnabled() && (typeof this.model.attributes.stream === 'object') }, hasSelectedVideo() { @@ -473,10 +473,16 @@ export default { }, mounted() { + this.sharedData.remoteVideoBlocker.increaseVisibleCounter() + // Set initial state this._setStream(this.model.attributes.stream) }, + destroyed() { + this.sharedData.remoteVideoBlocker.decreaseVisibleCounter() + }, + methods: { _setStream(stream) { diff --git a/src/components/CallView/shared/VideoBottomBar.vue b/src/components/CallView/shared/VideoBottomBar.vue index ca4b23477..5b078a48a 100644 --- a/src/components/CallView/shared/VideoBottomBar.vue +++ b/src/components/CallView/shared/VideoBottomBar.vue @@ -200,11 +200,11 @@ export default { }, showVideoButton() { - return this.sharedData.videoEnabled + return this.sharedData.remoteVideoBlocker.isVideoEnabled() }, videoButtonTooltip() { - if (this.sharedData.videoEnabled) { + if (this.sharedData.remoteVideoBlocker.isVideoEnabled()) { return t('spreed', 'Disable video') } @@ -220,7 +220,7 @@ export default { }, showNameIndicator() { - return !this.model.attributes.videoAvailable || !this.sharedData.videoEnabled || this.showVideoOverlay || this.isSelected || this.isPromoted || this.isSpeaking + return !this.model.attributes.videoAvailable || !this.sharedData.remoteVideoBlocker.isVideoEnabled() || this.showVideoOverlay || this.isSelected || this.isPromoted || this.isSpeaking }, boldenNameIndicator() { @@ -257,7 +257,7 @@ export default { toggleVideo() { emit('talk:video:toggled', { peerId: this.model.attributes.peerId, - value: !this.sharedData.videoEnabled, + value: !this.sharedData.remoteVideoBlocker.isVideoEnabled(), }) }, -- cgit v1.2.3 From 4e4ff9bad44a92959188608240e4f020616a5bee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Wed, 20 Apr 2022 10:14:51 +0200 Subject: Handle toggling video on and off directly in the bottom bar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "talk:video:toggled" event is triggered by VideoBottomBar and only handled in CallView to enable or disable the remote video. As the handling just calls a method in the RemoteVideoBlocker this can be directly done in the VideoBottomBar instead. Signed-off-by: Daniel Calviño Sánchez --- src/components/CallView/CallView.vue | 7 ------- src/components/CallView/shared/VideoBottomBar.vue | 5 +---- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/src/components/CallView/CallView.vue b/src/components/CallView/CallView.vue index 6d63bd418..b34a17707 100644 --- a/src/components/CallView/CallView.vue +++ b/src/components/CallView/CallView.vue @@ -405,7 +405,6 @@ export default { callParticipantCollection.on('remove', this._lowerHandWhenParticipantLeaves) - subscribe('talk:video:toggled', this.handleToggleVideo) subscribe('switch-screen-to-id', this._switchScreenToId) }, beforeDestroy() { @@ -413,7 +412,6 @@ export default { callParticipantCollection.off('remove', this._lowerHandWhenParticipantLeaves) - unsubscribe('talk:video:toggled', this.handleToggleVideo) unsubscribe('switch-screen-to-id', this._switchScreenToId) }, methods: { @@ -647,11 +645,6 @@ export default { } }, 1500), - // Toggles videos on and off - handleToggleVideo({ peerId, value }) { - this.sharedDatas[peerId].remoteVideoBlocker.setVideoEnabled(value) - }, - adjustSimulcastQuality() { this.callParticipantModels.forEach(callParticipantModel => { this.adjustSimulcastQualityForParticipant(callParticipantModel) diff --git a/src/components/CallView/shared/VideoBottomBar.vue b/src/components/CallView/shared/VideoBottomBar.vue index 5b078a48a..74baadec2 100644 --- a/src/components/CallView/shared/VideoBottomBar.vue +++ b/src/components/CallView/shared/VideoBottomBar.vue @@ -255,10 +255,7 @@ export default { }, toggleVideo() { - emit('talk:video:toggled', { - peerId: this.model.attributes.peerId, - value: !this.sharedData.remoteVideoBlocker.isVideoEnabled(), - }) + this.sharedData.remoteVideoBlocker.setVideoEnabled(!this.sharedData.remoteVideoBlocker.isVideoEnabled()) }, switchToScreen() { -- cgit v1.2.3 From 31373cef9d354ed3b815cc2d1b7ec2a111425217 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Wed, 20 Apr 2022 10:00:23 -0300 Subject: Move system message listeners to static methods Signed-off-by: Vitor Mattos --- lib/Chat/SystemMessage/Listener.php | 476 +++++++++++++++++++----------------- lib/Share/RoomShareProvider.php | 4 +- tests/psalm-baseline.xml | 8 +- 3 files changed, 253 insertions(+), 235 deletions(-) diff --git a/lib/Chat/SystemMessage/Listener.php b/lib/Chat/SystemMessage/Listener.php index 6190c1e48..ad055c574 100644 --- a/lib/Chat/SystemMessage/Listener.php +++ b/lib/Chat/SystemMessage/Listener.php @@ -77,281 +77,303 @@ class Listener implements IEventListener { } public static function register(IEventDispatcher $dispatcher): void { - $dispatcher->addListener(Room::EVENT_BEFORE_SESSION_JOIN_CALL, static function (ModifyParticipantEvent $event) { - $room = $event->getRoom(); - /** @var self $listener */ - $listener = \OC::$server->get(self::class); - /** @var ParticipantService $participantService */ - $participantService = \OC::$server->get(ParticipantService::class); + $dispatcher->addListener(Room::EVENT_BEFORE_SESSION_JOIN_CALL, self::class . '::sendSystemMessageAboutBeginOfCall'); + $dispatcher->addListener(Room::EVENT_AFTER_SESSION_LEAVE_CALL, self::class . '::sendSystemMessageAboutCallLeft'); + $dispatcher->addListener(Room::EVENT_AFTER_ROOM_CREATE, self::class . '::sendSystemMessageAboutConversationCreated'); + $dispatcher->addListener(Room::EVENT_AFTER_NAME_SET, self::class . '::sendSystemMessageAboutConversationRenamed'); + $dispatcher->addListener(Room::EVENT_AFTER_DESCRIPTION_SET, self::class . '::sendSystemMessageAboutRoomDescriptionChanges'); + $dispatcher->addListener(Room::EVENT_AFTER_PASSWORD_SET, self::class . '::sendSystemMessageAboutRoomPassword'); + $dispatcher->addListener(Room::EVENT_AFTER_TYPE_SET, self::class . '::sendSystemGuestPermissionsMessage'); + $dispatcher->addListener(Room::EVENT_AFTER_READONLY_SET, self::class . '::sendSystemReadOnlyMessage'); + $dispatcher->addListener(Room::EVENT_AFTER_LISTABLE_SET, self::class . '::sendSystemListableMessage'); + $dispatcher->addListener(Room::EVENT_AFTER_LOBBY_STATE_SET, self::class . '::sendSystemLobbyMessage'); + $dispatcher->addListener(Room::EVENT_AFTER_USERS_ADD, self::class . '::addSystemMessageUserAdded'); + $dispatcher->addListener(Room::EVENT_AFTER_USER_REMOVE, self::class . '::sendSystemMessageUserRemoved'); + $dispatcher->addListener(Room::EVENT_AFTER_PARTICIPANT_TYPE_SET, self::class . '::sendSystemMessageAboutPromoteOrDemoteModerator'); + $dispatcher->addListener('OCP\Share::postShare', self::class . '::fixMimeTypeOfVoiceMessage'); + $dispatcher->addListener(RoomShareProvider::EVENT_SHARE_FILE_AGAIN, self::class . '::fixMimeTypeOfVoiceMessage'); + } - if ($participantService->hasActiveSessionsInCall($room)) { - $listener->sendSystemMessage($room, 'call_joined', [], $event->getParticipant()); - } else { - $listener->sendSystemMessage($room, 'call_started', [], $event->getParticipant()); - } - }); - $dispatcher->addListener(Room::EVENT_AFTER_SESSION_LEAVE_CALL, static function (ModifyParticipantEvent $event) { - if ($event instanceof ModifyEveryoneEvent) { - // No individual system message if the call is ended for everyone - return; - } + public static function sendSystemMessageAboutBeginOfCall(ModifyParticipantEvent $event): void { + $room = $event->getRoom(); + /** @var self $listener */ + $listener = \OC::$server->get(self::class); + /** @var ParticipantService $participantService */ + $participantService = \OC::$server->get(ParticipantService::class); - if ($event->getNewValue() === $event->getOldValue()) { - return; - } + if ($participantService->hasActiveSessionsInCall($room)) { + $listener->sendSystemMessage($room, 'call_joined', [], $event->getParticipant()); + } else { + $listener->sendSystemMessage($room, 'call_started', [], $event->getParticipant()); + } + } - $room = $event->getRoom(); + public static function sendSystemMessageAboutCallLeft(ModifyParticipantEvent $event): void { + if ($event instanceof ModifyEveryoneEvent) { + // No individual system message if the call is ended for everyone + return; + } - $session = $event->getParticipant()->getSession(); - if (!$session instanceof Session) { - // This happens in case the user was kicked/lobbied - return; - } + if ($event->getNewValue() === $event->getOldValue()) { + return; + } - /** @var self $listener */ - $listener = \OC::$server->get(self::class); + $room = $event->getRoom(); - $listener->sendSystemMessage($room, 'call_left', [], $event->getParticipant()); - }); + $session = $event->getParticipant()->getSession(); + if (!$session instanceof Session) { + // This happens in case the user was kicked/lobbied + return; + } - $dispatcher->addListener(Room::EVENT_AFTER_ROOM_CREATE, static function (RoomEvent $event) { - $room = $event->getRoom(); - /** @var self $listener */ - $listener = \OC::$server->get(self::class); + /** @var self $listener */ + $listener = \OC::$server->get(self::class); - $listener->sendSystemMessage($room, 'conversation_created'); - }); - $dispatcher->addListener(Room::EVENT_AFTER_NAME_SET, static function (ModifyRoomEvent $event) { - if ($event->getOldValue() === '' || - $event->getNewValue() === '') { - return; - } + $listener->sendSystemMessage($room, 'call_left', [], $event->getParticipant()); + } - $room = $event->getRoom(); - /** @var self $listener */ - $listener = \OC::$server->get(self::class); + public static function sendSystemMessageAboutConversationCreated(RoomEvent $event): void { + $room = $event->getRoom(); + /** @var self $listener */ + $listener = \OC::$server->get(self::class); + + $listener->sendSystemMessage($room, 'conversation_created'); + } - $listener->sendSystemMessage($room, 'conversation_renamed', [ - 'newName' => $event->getNewValue(), - 'oldName' => $event->getOldValue(), + public static function sendSystemMessageAboutConversationRenamed(ModifyRoomEvent $event): void { + if ($event->getOldValue() === '' || + $event->getNewValue() === '') { + return; + } + + $room = $event->getRoom(); + /** @var self $listener */ + $listener = \OC::$server->get(self::class); + + $listener->sendSystemMessage($room, 'conversation_renamed', [ + 'newName' => $event->getNewValue(), + 'oldName' => $event->getOldValue(), + ]); + } + + public static function sendSystemMessageAboutRoomDescriptionChanges(ModifyRoomEvent $event): void { + $room = $event->getRoom(); + /** @var self $listener */ + $listener = \OC::$server->get(self::class); + + if ($event->getNewValue() !== '') { + $listener->sendSystemMessage($room, 'description_set', [ + 'newDescription' => $event->getNewValue(), ]); - }); - $dispatcher->addListener(Room::EVENT_AFTER_DESCRIPTION_SET, static function (ModifyRoomEvent $event) { - $room = $event->getRoom(); + } else { + $listener->sendSystemMessage($room, 'description_removed'); + } + } + + public static function sendSystemMessageAboutRoomPassword(ModifyRoomEvent $event): void { + $room = $event->getRoom(); + /** @var self $listener */ + $listener = \OC::$server->get(self::class); + + if ($event->getNewValue() !== '') { + $listener->sendSystemMessage($room, 'password_set'); + } else { + $listener->sendSystemMessage($room, 'password_removed'); + } + } + + public static function sendSystemGuestPermissionsMessage(ModifyRoomEvent $event): void { + $room = $event->getRoom(); + + if ($event->getOldValue() === Room::TYPE_ONE_TO_ONE) { + return; + } + + if ($event->getNewValue() === Room::TYPE_PUBLIC) { /** @var self $listener */ $listener = \OC::$server->get(self::class); - - if ($event->getNewValue() !== '') { - $listener->sendSystemMessage($room, 'description_set', [ - 'newDescription' => $event->getNewValue(), - ]); - } else { - $listener->sendSystemMessage($room, 'description_removed'); - } - }); - $dispatcher->addListener(Room::EVENT_AFTER_PASSWORD_SET, static function (ModifyRoomEvent $event) { - $room = $event->getRoom(); + $listener->sendSystemMessage($room, 'guests_allowed'); + } elseif ($event->getNewValue() === Room::TYPE_GROUP) { /** @var self $listener */ $listener = \OC::$server->get(self::class); + $listener->sendSystemMessage($room, 'guests_disallowed'); + } + } - if ($event->getNewValue() !== '') { - $listener->sendSystemMessage($room, 'password_set'); - } else { - $listener->sendSystemMessage($room, 'password_removed'); - } - }); - $dispatcher->addListener(Room::EVENT_AFTER_TYPE_SET, static function (ModifyRoomEvent $event) { - $room = $event->getRoom(); + public static function sendSystemReadOnlyMessage(ModifyRoomEvent $event): void { + $room = $event->getRoom(); - if ($event->getOldValue() === Room::TYPE_ONE_TO_ONE) { - return; - } + if ($room->getType() === Room::TYPE_CHANGELOG) { + return; + } - if ($event->getNewValue() === Room::TYPE_PUBLIC) { - /** @var self $listener */ - $listener = \OC::$server->get(self::class); - $listener->sendSystemMessage($room, 'guests_allowed'); - } elseif ($event->getNewValue() === Room::TYPE_GROUP) { - /** @var self $listener */ - $listener = \OC::$server->get(self::class); - $listener->sendSystemMessage($room, 'guests_disallowed'); - } - }); - $dispatcher->addListener(Room::EVENT_AFTER_READONLY_SET, static function (ModifyRoomEvent $event) { - $room = $event->getRoom(); + /** @var self $listener */ + $listener = \OC::$server->get(self::class); - if ($room->getType() === Room::TYPE_CHANGELOG) { - return; - } + if ($event->getNewValue() === Room::READ_ONLY) { + $listener->sendSystemMessage($room, 'read_only'); + } elseif ($event->getNewValue() === Room::READ_WRITE) { + $listener->sendSystemMessage($room, 'read_only_off'); + } + } - /** @var self $listener */ - $listener = \OC::$server->get(self::class); + public static function sendSystemListableMessage(ModifyRoomEvent $event): void { + $room = $event->getRoom(); - if ($event->getNewValue() === Room::READ_ONLY) { - $listener->sendSystemMessage($room, 'read_only'); - } elseif ($event->getNewValue() === Room::READ_WRITE) { - $listener->sendSystemMessage($room, 'read_only_off'); - } - }); - $dispatcher->addListener(Room::EVENT_AFTER_LISTABLE_SET, static function (ModifyRoomEvent $event) { - $room = $event->getRoom(); + /** @var self $listener */ + $listener = \OC::$server->get(self::class); - /** @var self $listener */ - $listener = \OC::$server->get(self::class); + if ($event->getNewValue() === Room::LISTABLE_NONE) { + $listener->sendSystemMessage($room, 'listable_none'); + } elseif ($event->getNewValue() === Room::LISTABLE_USERS) { + $listener->sendSystemMessage($room, 'listable_users'); + } elseif ($event->getNewValue() === Room::LISTABLE_ALL) { + $listener->sendSystemMessage($room, 'listable_all'); + } + } - if ($event->getNewValue() === Room::LISTABLE_NONE) { - $listener->sendSystemMessage($room, 'listable_none'); - } elseif ($event->getNewValue() === Room::LISTABLE_USERS) { - $listener->sendSystemMessage($room, 'listable_users'); - } elseif ($event->getNewValue() === Room::LISTABLE_ALL) { - $listener->sendSystemMessage($room, 'listable_all'); - } - }); - $dispatcher->addListener(Room::EVENT_AFTER_LOBBY_STATE_SET, static function (ModifyLobbyEvent $event) { - if ($event->getNewValue() === $event->getOldValue()) { - return; - } + public static function sendSystemLobbyMessage(ModifyLobbyEvent $event): void { + if ($event->getNewValue() === $event->getOldValue()) { + return; + } - $room = $event->getRoom(); + $room = $event->getRoom(); - /** @var self $listener */ - $listener = \OC::$server->get(self::class); + /** @var self $listener */ + $listener = \OC::$server->get(self::class); - if ($event->isTimerReached()) { - $listener->sendSystemMessage($room, 'lobby_timer_reached'); - } elseif ($event->getNewValue() === Webinary::LOBBY_NONE) { - $listener->sendSystemMessage($room, 'lobby_none'); - } elseif ($event->getNewValue() === Webinary::LOBBY_NON_MODERATORS) { - $listener->sendSystemMessage($room, 'lobby_non_moderators'); - } - }); + if ($event->isTimerReached()) { + $listener->sendSystemMessage($room, 'lobby_timer_reached'); + } elseif ($event->getNewValue() === Webinary::LOBBY_NONE) { + $listener->sendSystemMessage($room, 'lobby_none'); + } elseif ($event->getNewValue() === Webinary::LOBBY_NON_MODERATORS) { + $listener->sendSystemMessage($room, 'lobby_non_moderators'); + } + } - $dispatcher->addListener(Room::EVENT_AFTER_USERS_ADD, static function (AddParticipantsEvent $event) { - $room = $event->getRoom(); - if ($room->getType() === Room::TYPE_ONE_TO_ONE) { - return; - } + public static function addSystemMessageUserAdded(AddParticipantsEvent $event): void { + $room = $event->getRoom(); + if ($room->getType() === Room::TYPE_ONE_TO_ONE) { + return; + } - /** @var self $listener */ - $listener = \OC::$server->get(self::class); + /** @var self $listener */ + $listener = \OC::$server->get(self::class); + + $participants = $event->getParticipants(); - $participants = $event->getParticipants(); - - foreach ($participants as $participant) { - if ($participant['actorType'] !== 'users') { - continue; - } - - $participantType = null; - if (isset($participant['participantType'])) { - $participantType = $participant['participantType']; - } - - $userJoinedFileRoom = $room->getObjectType() === 'file' && $participantType !== Participant::USER_SELF_JOINED; - - // add a message "X joined the conversation", whenever user $userId: - if ( - // - has joined a file room but not through a public link - $userJoinedFileRoom - // - has been added by another user (and not when creating a conversation) - || $listener->getUserId() !== $participant['actorId'] - // - has joined a listable room on their own - || $participantType === Participant::USER) { - $comment = $listener->sendSystemMessage( - $room, - 'user_added', - ['user' => $participant['actorId']], - null, - $event->shouldSkipLastMessageUpdate() - ); - - $event->setLastMessage($comment); - } + foreach ($participants as $participant) { + if ($participant['actorType'] !== 'users') { + continue; } - }); - $dispatcher->addListener(Room::EVENT_AFTER_USER_REMOVE, static function (RemoveUserEvent $event) { - $room = $event->getRoom(); - if ($room->getType() === Room::TYPE_ONE_TO_ONE) { - return; + $participantType = null; + if (isset($participant['participantType'])) { + $participantType = $participant['participantType']; } - if ($event->getReason() === Room::PARTICIPANT_LEFT - && $event->getParticipant()->getAttendee()->getParticipantType() === Participant::USER_SELF_JOINED) { - // Self-joined user closes the tab/window or leaves via the menu - return; + $userJoinedFileRoom = $room->getObjectType() === 'file' && $participantType !== Participant::USER_SELF_JOINED; + + // add a message "X joined the conversation", whenever user $userId: + if ( + // - has joined a file room but not through a public link + $userJoinedFileRoom + // - has been added by another user (and not when creating a conversation) + || $listener->getUserId() !== $participant['actorId'] + // - has joined a listable room on their own + || $participantType === Participant::USER) { + $comment = $listener->sendSystemMessage( + $room, + 'user_added', + ['user' => $participant['actorId']], + null, + $event->shouldSkipLastMessageUpdate() + ); + + $event->setLastMessage($comment); } + } + } + + public static function sendSystemMessageUserRemoved(RemoveUserEvent $event): void { + $room = $event->getRoom(); + if ($room->getType() === Room::TYPE_ONE_TO_ONE) { + return; + } + + if ($event->getReason() === Room::PARTICIPANT_LEFT + && $event->getParticipant()->getAttendee()->getParticipantType() === Participant::USER_SELF_JOINED) { + // Self-joined user closes the tab/window or leaves via the menu + return; + } + + /** @var self $listener */ + $listener = \OC::$server->get(self::class); + $listener->sendSystemMessage($room, 'user_removed', ['user' => $event->getUser()->getUID()]); + } + + public static function sendSystemMessageAboutPromoteOrDemoteModerator(ModifyParticipantEvent $event): void { + $room = $event->getRoom(); + $attendee = $event->getParticipant()->getAttendee(); + + if ($attendee->getActorType() !== Attendee::ACTOR_USERS && $attendee->getActorType() !== Attendee::ACTOR_GUESTS) { + return; + } + + if ($event->getNewValue() === Participant::MODERATOR) { /** @var self $listener */ $listener = \OC::$server->get(self::class); - $listener->sendSystemMessage($room, 'user_removed', ['user' => $event->getUser()->getUID()]); - }); - $dispatcher->addListener(Room::EVENT_AFTER_PARTICIPANT_TYPE_SET, static function (ModifyParticipantEvent $event) { - $room = $event->getRoom(); - $attendee = $event->getParticipant()->getAttendee(); - - if ($attendee->getActorType() !== Attendee::ACTOR_USERS && $attendee->getActorType() !== Attendee::ACTOR_GUESTS) { - return; - } - - if ($event->getNewValue() === Participant::MODERATOR) { + $listener->sendSystemMessage($room, 'moderator_promoted', ['user' => $attendee->getActorId()]); + } elseif ($event->getNewValue() === Participant::USER) { + if ($event->getOldValue() === Participant::USER_SELF_JOINED) { /** @var self $listener */ $listener = \OC::$server->get(self::class); - $listener->sendSystemMessage($room, 'moderator_promoted', ['user' => $attendee->getActorId()]); - } elseif ($event->getNewValue() === Participant::USER) { - if ($event->getOldValue() === Participant::USER_SELF_JOINED) { - /** @var self $listener */ - $listener = \OC::$server->get(self::class); - $listener->sendSystemMessage($room, 'user_added', ['user' => $attendee->getActorId()]); - } else { - /** @var self $listener */ - $listener = \OC::$server->get(self::class); - $listener->sendSystemMessage($room, 'moderator_demoted', ['user' => $attendee->getActorId()]); - } - } elseif ($event->getNewValue() === Participant::GUEST_MODERATOR) { - /** @var self $listener */ - $listener = \OC::$server->get(self::class); - $listener->sendSystemMessage($room, 'guest_moderator_promoted', ['session' => $attendee->getActorId()]); - } elseif ($event->getNewValue() === Participant::GUEST) { + $listener->sendSystemMessage($room, 'user_added', ['user' => $attendee->getActorId()]); + } else { /** @var self $listener */ $listener = \OC::$server->get(self::class); - $listener->sendSystemMessage($room, 'guest_moderator_demoted', ['session' => $attendee->getActorId()]); + $listener->sendSystemMessage($room, 'moderator_demoted', ['user' => $attendee->getActorId()]); } - }); - $listener = function (GenericEvent $event): void { - /** @var IShare $share */ - $share = $event->getSubject(); - - if ($share->getShareType() !== IShare::TYPE_ROOM) { - return; - } - + } elseif ($event->getNewValue() === Participant::GUEST_MODERATOR) { /** @var self $listener */ $listener = \OC::$server->get(self::class); + $listener->sendSystemMessage($room, 'guest_moderator_promoted', ['session' => $attendee->getActorId()]); + } elseif ($event->getNewValue() === Participant::GUEST) { + /** @var self $listener */ + $listener = \OC::$server->get(self::class); + $listener->sendSystemMessage($room, 'guest_moderator_demoted', ['session' => $attendee->getActorId()]); + } + } - /** @var Manager $manager */ - $manager = \OC::$server->get(Manager::class); + public static function fixMimeTypeOfVoiceMessage(GenericEvent $event): void { + /** @var IShare $share */ + $share = $event->getSubject(); - $room = $manager->getRoomByToken($share->getSharedWith()); - $metaData = \OC::$server->getRequest()->getParam('talkMetaData') ?? ''; - $metaData = json_decode($metaData, true); - $metaData = is_array($metaData) ? $metaData : []; + if ($share->getShareType() !== IShare::TYPE_ROOM) { + return; + } + + /** @var self $listener */ + $listener = \OC::$server->get(self::class); + + /** @var Manager $manager */ + $manager = \OC::$server->get(Manager::class); - if (isset($metaData['messageType']) && $metaData['messageType'] === 'voice-message') { - if ($share->getNode()->getMimeType() !== 'audio/mpeg' - && $share->getNode()->getMimeType() !== 'audio/wav') { - unset($metaData['messageType']); - } + $room = $manager->getRoomByToken($share->getSharedWith()); + $metaData = \OC::$server->getRequest()->getParam('talkMetaData') ?? ''; + $metaData = json_decode($metaData, true); + $metaData = is_array($metaData) ? $metaData : []; + + if (isset($metaData['messageType']) && $metaData['messageType'] === 'voice-message') { + if ($share->getNode()->getMimeType() !== 'audio/mpeg' + && $share->getNode()->getMimeType() !== 'audio/wav') { + unset($metaData['messageType']); } - $metaData['mimeType'] = $share->getNode()->getMimeType(); - - $listener->sendSystemMessage($room, 'file_shared', ['share' => $share->getId(), 'metaData' => $metaData]); - }; - /** - * @psalm-suppress UndefinedClass - */ - $dispatcher->addListener('OCP\Share::postShare', $listener); - $dispatcher->addListener(RoomShareProvider::class . '::' . 'share_file_again', $listener); + } + $metaData['mimeType'] = $share->getNode()->getMimeType(); + + $listener->sendSystemMessage($room, 'file_shared', ['share' => $share->getId(), 'metaData' => $metaData]); } public function handle(Event $event): void { diff --git a/lib/Share/RoomShareProvider.php b/lib/Share/RoomShareProvider.php index 28dbfef15..eee33bc82 100644 --- a/lib/Share/RoomShareProvider.php +++ b/lib/Share/RoomShareProvider.php @@ -71,6 +71,8 @@ class RoomShareProvider implements IShareProvider { public const TALK_FOLDER = '/Talk'; public const TALK_FOLDER_PLACEHOLDER = '/{TALK_PLACEHOLDER}'; + public const EVENT_SHARE_FILE_AGAIN = self::class . '::shareFileAgain'; + private IDBConnection $dbConnection; private ISecureRandom $secureRandom; private IShareManager $shareManager; @@ -153,7 +155,7 @@ class RoomShareProvider implements IShareProvider { foreach ($existingShares as $existingShare) { if ($existingShare->getSharedWith() === $share->getSharedWith()) { // FIXME Should be moved away from GenericEvent as soon as OCP\Share20\IManager did move too - $this->dispatcher->dispatch(self::class . '::' . 'share_file_again', new GenericEvent($existingShare)); + $this->dispatcher->dispatch(self::EVENT_SHARE_FILE_AGAIN, new GenericEvent($existingShare)); throw new GenericShareException('Already shared', $this->l->t('Path is already shared with this room'), 403); } } diff --git a/tests/psalm-baseline.xml b/tests/psalm-baseline.xml index 1d44ea2cb..eb40d3941 100644 --- a/tests/psalm-baseline.xml +++ b/tests/psalm-baseline.xml @@ -37,12 +37,6 @@ getById - - - $listener - $listener - - Base @@ -259,7 +253,7 @@ new GenericEvent($existingShare) - self::class . '::' . 'share_file_again' + self::EVENT_SHARE_FILE_AGAIN Cache -- cgit v1.2.3 From 00712f3a85c46dc509d59a86eaaa67a02d5f1611 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Wed, 20 Apr 2022 11:32:12 -0300 Subject: Fix inverted argument Signed-off-by: Vitor Mattos --- lib/Share/RoomShareProvider.php | 2 +- tests/psalm-baseline.xml | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/Share/RoomShareProvider.php b/lib/Share/RoomShareProvider.php index eee33bc82..5d2d2316e 100644 --- a/lib/Share/RoomShareProvider.php +++ b/lib/Share/RoomShareProvider.php @@ -155,7 +155,7 @@ class RoomShareProvider implements IShareProvider { foreach ($existingShares as $existingShare) { if ($existingShare->getSharedWith() === $share->getSharedWith()) { // FIXME Should be moved away from GenericEvent as soon as OCP\Share20\IManager did move too - $this->dispatcher->dispatch(self::EVENT_SHARE_FILE_AGAIN, new GenericEvent($existingShare)); + $this->dispatcher->dispatch(new GenericEvent($existingShare), self::EVENT_SHARE_FILE_AGAIN); throw new GenericShareException('Already shared', $this->l->t('Path is already shared with this room'), 403); } } diff --git a/tests/psalm-baseline.xml b/tests/psalm-baseline.xml index eb40d3941..e6434b106 100644 --- a/tests/psalm-baseline.xml +++ b/tests/psalm-baseline.xml @@ -251,10 +251,6 @@ - - new GenericEvent($existingShare) - self::EVENT_SHARE_FILE_AGAIN - Cache -- cgit v1.2.3 From a2b35e1a67d50f73e0f8451f2a9891554fb671a3 Mon Sep 17 00:00:00 2001 From: Nikola Date: Wed, 20 Apr 2022 21:07:50 +0200 Subject: 7138: updated reaction styling Signed-off-by: Nikola --- .../MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.vue | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.vue b/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.vue index fb0c58d48..2a4f1f320 100644 --- a/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.vue +++ b/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.vue @@ -475,7 +475,8 @@ export default { .message-buttons-bar { display: flex; right: 14px; - bottom: -4px; + top: 0px; + transform: translateY(-35%); position: absolute; background-color: var(--color-main-background); border-radius: calc($clickable-area / 2); -- cgit v1.2.3 From f182d2079fcd47ba0752e22e16dfc219e8e79bf5 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Thu, 21 Apr 2022 09:19:55 -0300 Subject: Change property type Signed-off-by: Vitor Mattos --- lib/Chat/SystemMessage/Listener.php | 6 ++++- lib/Events/AlreadySharedEvent.php | 45 +++++++++++++++++++++++++++++++++++++ lib/Share/RoomShareProvider.php | 10 ++++----- 3 files changed, 54 insertions(+), 7 deletions(-) create mode 100644 lib/Events/AlreadySharedEvent.php diff --git a/lib/Chat/SystemMessage/Listener.php b/lib/Chat/SystemMessage/Listener.php index ad055c574..e8a50724c 100644 --- a/lib/Chat/SystemMessage/Listener.php +++ b/lib/Chat/SystemMessage/Listener.php @@ -346,7 +346,11 @@ class Listener implements IEventListener { } } - public static function fixMimeTypeOfVoiceMessage(GenericEvent $event): void { + /** + * @param GenericEvent|Event $event + * @return void + */ + public static function fixMimeTypeOfVoiceMessage($event): void { /** @var IShare $share */ $share = $event->getSubject(); diff --git a/lib/Events/AlreadySharedEvent.php b/lib/Events/AlreadySharedEvent.php new file mode 100644 index 000000000..665e65a03 --- /dev/null +++ b/lib/Events/AlreadySharedEvent.php @@ -0,0 +1,45 @@ + + * + * @author Vitor Mattos + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + + +namespace OCA\Talk\Events; + +use OCP\EventDispatcher\Event; + +class AlreadySharedEvent extends Event { + private $subject; + public function __construct($subject = null) { + $this->subject = $subject; + } + + /** + * Getter for subject property. + * + * @return mixed + */ + public function getSubject() { + return $this->subject; + } +} diff --git a/lib/Share/RoomShareProvider.php b/lib/Share/RoomShareProvider.php index 5d2d2316e..9e3c4ad90 100644 --- a/lib/Share/RoomShareProvider.php +++ b/lib/Share/RoomShareProvider.php @@ -29,11 +29,11 @@ declare(strict_types=1); namespace OCA\Talk\Share; use OC\Files\Cache\Cache; +use OCA\Talk\Events\AlreadySharedEvent; use OCA\Talk\Events\RoomEvent; use OCA\Talk\Exceptions\ParticipantNotFoundException; use OCA\Talk\Exceptions\RoomNotFoundException; use OCA\Talk\Manager; -use OCA\Talk\Participant; use OCA\Talk\Room; use OCA\Talk\Service\ParticipantService; use OCP\AppFramework\Utility\ITimeFactory; @@ -50,8 +50,6 @@ use OCP\Share\Exceptions\ShareNotFound; use OCP\Share\IManager as IShareManager; use OCP\Share\IShare; use OCP\Share\IShareProvider; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; -use Symfony\Component\EventDispatcher\GenericEvent; /** * Share provider for room shares. @@ -76,7 +74,7 @@ class RoomShareProvider implements IShareProvider { private IDBConnection $dbConnection; private ISecureRandom $secureRandom; private IShareManager $shareManager; - private EventDispatcherInterface $dispatcher; + private IEventDispatcher $dispatcher; private Manager $manager; private ParticipantService $participantService; protected ITimeFactory $timeFactory; @@ -87,7 +85,7 @@ class RoomShareProvider implements IShareProvider { IDBConnection $connection, ISecureRandom $secureRandom, IShareManager $shareManager, - EventDispatcherInterface $dispatcher, + IEventDispatcher $dispatcher, Manager $manager, ParticipantService $participantService, ITimeFactory $timeFactory, @@ -155,7 +153,7 @@ class RoomShareProvider implements IShareProvider { foreach ($existingShares as $existingShare) { if ($existingShare->getSharedWith() === $share->getSharedWith()) { // FIXME Should be moved away from GenericEvent as soon as OCP\Share20\IManager did move too - $this->dispatcher->dispatch(new GenericEvent($existingShare), self::EVENT_SHARE_FILE_AGAIN); + $this->dispatcher->dispatch(self::EVENT_SHARE_FILE_AGAIN, new AlreadySharedEvent($existingShare)); throw new GenericShareException('Already shared', $this->l->t('Path is already shared with this room'), 403); } } -- cgit v1.2.3 From 36cb3d0e3d1db7c0df57b21131a42c057ae876fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Fri, 22 Apr 2022 22:26:46 +0200 Subject: Fix reactions endpoints not accessible by guests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Calviño Sánchez --- lib/Controller/ReactionController.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/Controller/ReactionController.php b/lib/Controller/ReactionController.php index d55be4dbc..779af9730 100644 --- a/lib/Controller/ReactionController.php +++ b/lib/Controller/ReactionController.php @@ -45,7 +45,7 @@ class ReactionController extends AEnvironmentAwareController { } /** - * @NoAdminRequired + * @PublicPage * @RequireParticipant * @RequireReadWriteConversation * @RequireModeratorOrNoLobby @@ -75,7 +75,7 @@ class ReactionController extends AEnvironmentAwareController { } /** - * @NoAdminRequired + * @PublicPage * @RequireParticipant * @RequireReadWriteConversation * @RequireModeratorOrNoLobby @@ -103,7 +103,7 @@ class ReactionController extends AEnvironmentAwareController { } /** - * @NoAdminRequired + * @PublicPage * @RequireParticipant * @RequireReadWriteConversation * @RequireModeratorOrNoLobby -- cgit v1.2.3 From c9b94f569da65765a7c4da8418a0ae882a364fec Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 23 Apr 2022 01:02:42 +0000 Subject: Bump @nextcloud/moment from 1.2.0 to 1.2.1 Bumps [@nextcloud/moment](https://github.com/nextcloud/nextcloud-moment) from 1.2.0 to 1.2.1. - [Release notes](https://github.com/nextcloud/nextcloud-moment/releases) - [Changelog](https://github.com/nextcloud/nextcloud-moment/blob/master/CHANGELOG.md) - [Commits](https://github.com/nextcloud/nextcloud-moment/compare/v1.2.0...v1.2.1) --- updated-dependencies: - dependency-name: "@nextcloud/moment" dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package-lock.json | 50 +++++++++++++++++++++++++------------------------- package.json | 2 +- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/package-lock.json b/package-lock.json index 003ab7be4..de074aa46 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "@nextcloud/event-bus": "^2.1.1", "@nextcloud/initial-state": "^1.2.1", "@nextcloud/l10n": "^1.4.1", - "@nextcloud/moment": "^1.2.0", + "@nextcloud/moment": "^1.2.1", "@nextcloud/paths": "^2.1.0", "@nextcloud/router": "^2.0.0", "@nextcloud/vue": "^5.3.0", @@ -2744,14 +2744,14 @@ } }, "node_modules/@nextcloud/moment": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@nextcloud/moment/-/moment-1.2.0.tgz", - "integrity": "sha512-HOnZqoYQg0eOQW369s5v7jZWmRNYCsadHnVjN+DSXQQ1n4fHKmr0EkdOFHJu1Br5Rd6Fxi4wRw7E7pD1CVZmgA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@nextcloud/moment/-/moment-1.2.1.tgz", + "integrity": "sha512-v/yfrZ4Jo8YM1v0DLXKjRLwKOhzE4Y6DcgyZAM1vJ5jOMvkHpICuTDJRw8oOtrr/1H6FqI6EMZcYogeGD+rwSA==", "dependencies": { - "@nextcloud/l10n": "1.4.1", - "core-js": "3.18.2", + "@nextcloud/l10n": "^1.4.1", + "core-js": "^3.21.1", "jed": "^1.1.1", - "moment": "2.29.1", + "moment": "^2.29.2", "node-gettext": "^3.0.0" } }, @@ -8306,9 +8306,9 @@ } }, "node_modules/core-js": { - "version": "3.18.2", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.18.2.tgz", - "integrity": "sha512-zNhPOUoSgoizoSQFdX1MeZO16ORRb9FFQLts8gSYbZU5FcgXhp24iMWMxnOQo5uIaIG7/6FA/IqJPwev1o9ZXQ==", + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.22.2.tgz", + "integrity": "sha512-Z5I2vzDnEIqO2YhELVMFcL1An2CIsFe9Q7byZhs8c/QxummxZlAHw33TUHbIte987LkisOgL0LwQ1P9D6VISnA==", "hasInstallScript": true, "funding": { "type": "opencollective", @@ -16273,9 +16273,9 @@ "dev": true }, "node_modules/moment": { - "version": "2.29.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", - "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==", + "version": "2.29.3", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz", + "integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==", "engines": { "node": "*" } @@ -26525,14 +26525,14 @@ } }, "@nextcloud/moment": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@nextcloud/moment/-/moment-1.2.0.tgz", - "integrity": "sha512-HOnZqoYQg0eOQW369s5v7jZWmRNYCsadHnVjN+DSXQQ1n4fHKmr0EkdOFHJu1Br5Rd6Fxi4wRw7E7pD1CVZmgA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@nextcloud/moment/-/moment-1.2.1.tgz", + "integrity": "sha512-v/yfrZ4Jo8YM1v0DLXKjRLwKOhzE4Y6DcgyZAM1vJ5jOMvkHpICuTDJRw8oOtrr/1H6FqI6EMZcYogeGD+rwSA==", "requires": { - "@nextcloud/l10n": "1.4.1", - "core-js": "3.18.2", + "@nextcloud/l10n": "^1.4.1", + "core-js": "^3.21.1", "jed": "^1.1.1", - "moment": "2.29.1", + "moment": "^2.29.2", "node-gettext": "^3.0.0" } }, @@ -30948,9 +30948,9 @@ } }, "core-js": { - "version": "3.18.2", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.18.2.tgz", - "integrity": "sha512-zNhPOUoSgoizoSQFdX1MeZO16ORRb9FFQLts8gSYbZU5FcgXhp24iMWMxnOQo5uIaIG7/6FA/IqJPwev1o9ZXQ==" + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.22.2.tgz", + "integrity": "sha512-Z5I2vzDnEIqO2YhELVMFcL1An2CIsFe9Q7byZhs8c/QxummxZlAHw33TUHbIte987LkisOgL0LwQ1P9D6VISnA==" }, "core-js-compat": { "version": "3.13.0", @@ -37168,9 +37168,9 @@ "dev": true }, "moment": { - "version": "2.29.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", - "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==" + "version": "2.29.3", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz", + "integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==" }, "mrmime": { "version": "1.0.0", diff --git a/package.json b/package.json index bc85e426d..7b9622652 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "@nextcloud/event-bus": "^2.1.1", "@nextcloud/initial-state": "^1.2.1", "@nextcloud/l10n": "^1.4.1", - "@nextcloud/moment": "^1.2.0", + "@nextcloud/moment": "^1.2.1", "@nextcloud/paths": "^2.1.0", "@nextcloud/router": "^2.0.0", "@nextcloud/vue": "^5.3.0", -- cgit v1.2.3 From 14cc070594d479b231dcacf48b91fbb33b0bb3cd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 23 Apr 2022 01:03:10 +0000 Subject: Bump webdav from 4.8.0 to 4.9.0 Bumps [webdav](https://github.com/perry-mitchell/webdav-client) from 4.8.0 to 4.9.0. - [Release notes](https://github.com/perry-mitchell/webdav-client/releases) - [Changelog](https://github.com/perry-mitchell/webdav-client/blob/master/CHANGELOG.md) - [Commits](https://github.com/perry-mitchell/webdav-client/compare/v4.8.0...v4.9.0) --- updated-dependencies: - dependency-name: webdav dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package-lock.json | 79 ++++++++++++++++++++++++++++++++++++++++--------------- package.json | 2 +- 2 files changed, 59 insertions(+), 22 deletions(-) diff --git a/package-lock.json b/package-lock.json index 003ab7be4..e4bfc1c15 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,7 +54,7 @@ "vue-shortkey": "^3.1.7", "vue2-leaflet": "^2.7.1", "vuex": "^3.6.2", - "webdav": "^4.8.0", + "webdav": "^4.9.0", "webrtc-adapter": "^8.1.1", "webrtcsupport": "^2.2.0", "wildemitter": "^1.2.1", @@ -6224,11 +6224,11 @@ "dev": true }, "node_modules/axios": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz", - "integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==", + "version": "0.26.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", + "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", "dependencies": { - "follow-redirects": "^1.14.4" + "follow-redirects": "^1.14.8" } }, "node_modules/babel-code-frame": { @@ -22353,22 +22353,41 @@ } }, "node_modules/webdav": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/webdav/-/webdav-4.8.0.tgz", - "integrity": "sha512-CVJvxu0attEfoQUKraDiNh3uMjNPNl+BY0pbcKbyc/X+8IXDnqAT4tT4Ge12w+j49fYuVpFVkpEGwBZabv7Uhw==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/webdav/-/webdav-4.9.0.tgz", + "integrity": "sha512-pMuRtZcjBk3i6q1iY5wBHdablKftoBfhrQEWWEejSh2LXgd0J6VE5V0c1tUlMrFHaVDx8iCoB9kupNzy8SMC4A==", "dependencies": { - "axios": "^0.24.0", + "axios": "^0.26.1", "base-64": "^1.0.0", "fast-xml-parser": "^3.19.0", "he": "^1.2.0", "hot-patcher": "^0.5.0", "layerr": "^0.1.2", "md5": "^2.3.0", - "minimatch": "^3.0.4", + "minimatch": "^5.0.1", "nested-property": "^4.0.0", "path-posix": "^1.0.0", "url-join": "^4.0.1", - "url-parse": "^1.5.3" + "url-parse": "^1.5.10" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/webdav/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/webdav/node_modules/minimatch": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", + "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "dependencies": { + "brace-expansion": "^2.0.1" }, "engines": { "node": ">=10" @@ -29234,11 +29253,11 @@ "dev": true }, "axios": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz", - "integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==", + "version": "0.26.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", + "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", "requires": { - "follow-redirects": "^1.14.4" + "follow-redirects": "^1.14.8" } }, "babel-code-frame": { @@ -42075,22 +42094,40 @@ } }, "webdav": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/webdav/-/webdav-4.8.0.tgz", - "integrity": "sha512-CVJvxu0attEfoQUKraDiNh3uMjNPNl+BY0pbcKbyc/X+8IXDnqAT4tT4Ge12w+j49fYuVpFVkpEGwBZabv7Uhw==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/webdav/-/webdav-4.9.0.tgz", + "integrity": "sha512-pMuRtZcjBk3i6q1iY5wBHdablKftoBfhrQEWWEejSh2LXgd0J6VE5V0c1tUlMrFHaVDx8iCoB9kupNzy8SMC4A==", "requires": { - "axios": "^0.24.0", + "axios": "^0.26.1", "base-64": "^1.0.0", "fast-xml-parser": "^3.19.0", "he": "^1.2.0", "hot-patcher": "^0.5.0", "layerr": "^0.1.2", "md5": "^2.3.0", - "minimatch": "^3.0.4", + "minimatch": "^5.0.1", "nested-property": "^4.0.0", "path-posix": "^1.0.0", "url-join": "^4.0.1", - "url-parse": "^1.5.3" + "url-parse": "^1.5.10" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", + "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "requires": { + "brace-expansion": "^2.0.1" + } + } } }, "webidl-conversions": { diff --git a/package.json b/package.json index bc85e426d..982d34b6b 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "vue-shortkey": "^3.1.7", "vue2-leaflet": "^2.7.1", "vuex": "^3.6.2", - "webdav": "^4.8.0", + "webdav": "^4.9.0", "webrtc-adapter": "^8.1.1", "webrtcsupport": "^2.2.0", "wildemitter": "^1.2.1", -- cgit v1.2.3 From a404591bb8ec9517eb45a1681267805708687ea5 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Mon, 25 Apr 2022 11:38:07 -0300 Subject: Move signaling listeners anonymous functions to static Signed-off-by: Vitor Mattos --- lib/Signaling/Listener.php | 511 +++++++++++++++++++++++---------------------- 1 file changed, 257 insertions(+), 254 deletions(-) diff --git a/lib/Signaling/Listener.php b/lib/Signaling/Listener.php index 26af22371..899932108 100644 --- a/lib/Signaling/Listener.php +++ b/lib/Signaling/Listener.php @@ -54,312 +54,315 @@ class Listener { } protected static function registerInternalSignaling(IEventDispatcher $dispatcher): void { - $listener = static function (RoomEvent $event): void { - if (!self::isUsingInternalSignaling()) { - return; - } + $dispatcher->addListener(Room::EVENT_AFTER_ROOM_CONNECT, [self::class, 'refreshParticipantListUsingRoomEvent']); + $dispatcher->addListener(Room::EVENT_AFTER_GUEST_CONNECT, [self::class, 'refreshParticipantListUsingRoomEvent']); + $dispatcher->addListener(Room::EVENT_AFTER_SESSION_JOIN_CALL, [self::class, 'refreshParticipantListUsingRoomEvent']); + $dispatcher->addListener(Room::EVENT_AFTER_SESSION_UPDATE_CALL_FLAGS, [self::class, 'refreshParticipantListUsingRoomEvent']); + $dispatcher->addListener(Room::EVENT_AFTER_SESSION_LEAVE_CALL, [self::class, 'refreshParticipantListUsingRoomEvent']); + $dispatcher->addListener(Room::EVENT_AFTER_PERMISSIONS_SET, [self::class, 'refreshParticipantListUsingRoomEvent']); + $dispatcher->addListener(GuestManager::EVENT_AFTER_NAME_UPDATE, [self::class, 'refreshParticipantListUsingRoomEvent']); + $dispatcher->addListener(Room::EVENT_BEFORE_ROOM_DELETE, [self::class, 'refreshParticipantListUsingRoomEvent']); + + $dispatcher->addListener(Room::EVENT_BEFORE_USER_REMOVE, [self::class, 'refreshParticipantListUsingParticipantEvent']); + $dispatcher->addListener(Room::EVENT_BEFORE_PARTICIPANT_REMOVE, [self::class, 'refreshParticipantListUsingParticipantEvent']); + $dispatcher->addListener(Room::EVENT_BEFORE_ROOM_DISCONNECT, [self::class, 'refreshParticipantListUsingParticipantEvent']); + $dispatcher->addListener(Room::EVENT_AFTER_PARTICIPANT_PERMISSIONS_SET, [self::class, 'refreshParticipantListUsingParticipantEvent']); + } - /** @var Messages $messages */ - $messages = \OC::$server->get(Messages::class); - $messages->addMessageForAllParticipants($event->getRoom(), 'refresh-participant-list'); - }; - $dispatcher->addListener(Room::EVENT_AFTER_ROOM_CONNECT, $listener); - $dispatcher->addListener(Room::EVENT_AFTER_GUEST_CONNECT, $listener); - $dispatcher->addListener(Room::EVENT_AFTER_SESSION_JOIN_CALL, $listener); - $dispatcher->addListener(Room::EVENT_AFTER_SESSION_UPDATE_CALL_FLAGS, $listener); - $dispatcher->addListener(Room::EVENT_AFTER_SESSION_LEAVE_CALL, $listener); - $dispatcher->addListener(Room::EVENT_AFTER_PERMISSIONS_SET, $listener); - $dispatcher->addListener(GuestManager::EVENT_AFTER_NAME_UPDATE, $listener); - - $listener = static function (ParticipantEvent $event): void { - if (!self::isUsingInternalSignaling()) { - return; - } + protected static function registerExternalSignaling(IEventDispatcher $dispatcher): void { + $dispatcher->addListener(Room::EVENT_AFTER_USERS_ADD, [self::class, 'notifyAfterUsersAdd']); + $dispatcher->addListener(Room::EVENT_AFTER_NAME_SET, [self::class, 'notifyAfterRoomSettingsChanged']); + $dispatcher->addListener(Room::EVENT_AFTER_DESCRIPTION_SET, [self::class, 'notifyAfterRoomSettingsChanged']); + $dispatcher->addListener(Room::EVENT_AFTER_PASSWORD_SET, [self::class, 'notifyAfterRoomSettingsChanged']); + $dispatcher->addListener(Room::EVENT_AFTER_TYPE_SET, [self::class, 'notifyAfterRoomSettingsChanged']); + $dispatcher->addListener(Room::EVENT_AFTER_READONLY_SET, [self::class, 'notifyAfterRoomSettingsChanged']); + $dispatcher->addListener(Room::EVENT_AFTER_LISTABLE_SET, [self::class, 'notifyAfterRoomSettingsChanged']); + $dispatcher->addListener(Room::EVENT_AFTER_LOBBY_STATE_SET, [self::class, 'notifyAfterRoomSettingsChanged']); + $dispatcher->addListener(Room::EVENT_AFTER_SIP_ENABLED_SET, [self::class, 'notifyAfterRoomSettingsChanged']); + // TODO remove handler with "roomModified" in favour of handler with + // "participantsModified" once the clients no longer expect a + // "roomModified" message for participant type changes. + $dispatcher->addListener(Room::EVENT_AFTER_PARTICIPANT_TYPE_SET, [self::class, 'notifyAfterRoomSettingsChanged']); + $dispatcher->addListener(Room::EVENT_AFTER_PARTICIPANT_TYPE_SET, [self::class, 'notifyAfterParticipantTypeAndPermissionsSet']); + $dispatcher->addListener(Room::EVENT_AFTER_PARTICIPANT_PERMISSIONS_SET, [self::class, 'notifyAfterParticipantTypeAndPermissionsSet']); + $dispatcher->addListener(Room::EVENT_AFTER_PERMISSIONS_SET, [self::class, 'notifyAfterPermissionSet']); + $dispatcher->addListener(Room::EVENT_BEFORE_ROOM_DELETE, [self::class, 'notifyBeforeRoomDeleted']); + $dispatcher->addListener(Room::EVENT_AFTER_USER_REMOVE, [self::class, 'notifyAfterUserRemoved']); + $dispatcher->addListener(Room::EVENT_AFTER_PARTICIPANT_REMOVE, [self::class, 'notifyAfterParticipantRemoved']); + $dispatcher->addListener(Room::EVENT_AFTER_ROOM_DISCONNECT, [self::class, 'notifyAfterRoomDisconected']); + $dispatcher->addListener(Room::EVENT_AFTER_SESSION_JOIN_CALL, [self::class, 'notifyAfterJoinUpdateAndLeave']); + $dispatcher->addListener(Room::EVENT_AFTER_SESSION_UPDATE_CALL_FLAGS, [self::class, 'notifyAfterJoinUpdateAndLeave']); + $dispatcher->addListener(Room::EVENT_AFTER_SESSION_LEAVE_CALL, [self::class, 'notifyAfterJoinUpdateAndLeave']); + $dispatcher->addListener(Room::EVENT_AFTER_END_CALL_FOR_EVERYONE, [self::class, 'sendEndCallForEveryone']); + $dispatcher->addListener(Room::EVENT_AFTER_GUESTS_CLEAN, [self::class, 'notifyParticipantsAfterGuestClean']); + $dispatcher->addListener(GuestManager::EVENT_AFTER_NAME_UPDATE, [self::class, 'notifyParticipantsAfterNameUpdated']); + $dispatcher->addListener(ChatManager::EVENT_AFTER_MESSAGE_SEND, [self::class, 'notifyUsersViaExternalSignalingToRefreshTheChat']); + $dispatcher->addListener(ChatManager::EVENT_AFTER_SYSTEM_MESSAGE_SEND, [self::class, 'notifyUsersViaExternalSignalingToRefreshTheChat']); + $dispatcher->addListener(ChatManager::EVENT_AFTER_MULTIPLE_SYSTEM_MESSAGE_SEND, [self::class, 'notifyUsersViaExternalSignalingToRefreshTheChat']); + } - $room = $event->getRoom(); - - /** @var Messages $messages */ - $messages = \OC::$server->get(Messages::class); - $messages->addMessageForAllParticipants($room, 'refresh-participant-list'); - }; - $dispatcher->addListener(Room::EVENT_BEFORE_USER_REMOVE, $listener); - $dispatcher->addListener(Room::EVENT_BEFORE_PARTICIPANT_REMOVE, $listener); - $dispatcher->addListener(Room::EVENT_BEFORE_ROOM_DISCONNECT, $listener); - $dispatcher->addListener(Room::EVENT_AFTER_PARTICIPANT_PERMISSIONS_SET, $listener); - - $listener = static function (RoomEvent $event): void { - $room = $event->getRoom(); - if (!self::isUsingInternalSignaling()) { - return; - } + public static function refreshParticipantListUsingRoomEvent(RoomEvent $event): void { + if (!self::isUsingInternalSignaling()) { + return; + } - /** @var Messages $messages */ - $messages = \OC::$server->get(Messages::class); - $messages->addMessageForAllParticipants($room, 'refresh-participant-list'); - }; - $dispatcher->addListener(Room::EVENT_BEFORE_ROOM_DELETE, $listener); + /** @var Messages $messages */ + $messages = \OC::$server->get(Messages::class); + $messages->addMessageForAllParticipants($event->getRoom(), 'refresh-participant-list'); } - protected static function registerExternalSignaling(IEventDispatcher $dispatcher): void { - $dispatcher->addListener(Room::EVENT_AFTER_USERS_ADD, static function (AddParticipantsEvent $event) { - if (self::isUsingInternalSignaling()) { - return; - } + public static function refreshParticipantListUsingParticipantEvent(ParticipantEvent $event): void { + if (!self::isUsingInternalSignaling()) { + return; + } - /** @var BackendNotifier $notifier */ - $notifier = \OC::$server->get(BackendNotifier::class); + /** @var Messages $messages */ + $messages = \OC::$server->get(Messages::class); + $messages->addMessageForAllParticipants($event->getRoom(), 'refresh-participant-list'); + } - $notifier->roomInvited($event->getRoom(), $event->getParticipants()); - }); - $listener = static function (RoomEvent $event): void { - if (self::isUsingInternalSignaling()) { - return; - } + public static function notifyAfterUsersAdd(AddParticipantsEvent $event): void { + if (self::isUsingInternalSignaling()) { + return; + } - /** @var BackendNotifier $notifier */ - $notifier = \OC::$server->get(BackendNotifier::class); - - $notifier->roomModified($event->getRoom()); - }; - $dispatcher->addListener(Room::EVENT_AFTER_NAME_SET, $listener); - $dispatcher->addListener(Room::EVENT_AFTER_DESCRIPTION_SET, $listener); - $dispatcher->addListener(Room::EVENT_AFTER_PASSWORD_SET, $listener); - $dispatcher->addListener(Room::EVENT_AFTER_TYPE_SET, $listener); - $dispatcher->addListener(Room::EVENT_AFTER_READONLY_SET, $listener); - $dispatcher->addListener(Room::EVENT_AFTER_LISTABLE_SET, $listener); - $dispatcher->addListener(Room::EVENT_AFTER_LOBBY_STATE_SET, $listener); - $dispatcher->addListener(Room::EVENT_AFTER_SIP_ENABLED_SET, $listener); - // TODO remove handler with "roomModified" in favour of handler with - // "participantsModified" once the clients no longer expect a - // "roomModified" message for participant type changes. - $dispatcher->addListener(Room::EVENT_AFTER_PARTICIPANT_TYPE_SET, $listener); + /** @var BackendNotifier $notifier */ + $notifier = \OC::$server->get(BackendNotifier::class); - $listener = static function (ModifyParticipantEvent $event): void { - if (self::isUsingInternalSignaling()) { - return; - } + $notifier->roomInvited($event->getRoom(), $event->getParticipants()); + } - /** @var BackendNotifier $notifier */ - $notifier = \OC::$server->get(BackendNotifier::class); + public static function notifyAfterRoomSettingsChanged(RoomEvent $event): void { + if (self::isUsingInternalSignaling()) { + return; + } - $sessionIds = []; - // If the participant is not active in the room the "participants" - // request will be sent anyway, although with an empty "changed" - // property. + /** @var BackendNotifier $notifier */ + $notifier = \OC::$server->get(BackendNotifier::class); - /** @var SessionService $sessionService */ - $sessionService = \OC::$server->get(SessionService::class); - $sessions = $sessionService->getAllSessionsForAttendee($event->getParticipant()->getAttendee()); - foreach ($sessions as $session) { - $sessionIds[] = $session->getSessionId(); - } + $notifier->roomModified($event->getRoom()); + } - $notifier->participantsModified($event->getRoom(), $sessionIds); - }; - $dispatcher->addListener(Room::EVENT_AFTER_PARTICIPANT_TYPE_SET, $listener); - $dispatcher->addListener(Room::EVENT_AFTER_PARTICIPANT_PERMISSIONS_SET, $listener); + public static function notifyAfterParticipantTypeAndPermissionsSet(ModifyParticipantEvent $event): void { + if (self::isUsingInternalSignaling()) { + return; + } - $dispatcher->addListener(Room::EVENT_AFTER_PERMISSIONS_SET, static function (RoomEvent $event) { - if (self::isUsingInternalSignaling()) { - return; - } + /** @var BackendNotifier $notifier */ + $notifier = \OC::$server->get(BackendNotifier::class); - /** @var BackendNotifier $notifier */ - $notifier = \OC::$server->get(BackendNotifier::class); - - $sessionIds = []; - - // Setting the room permissions resets the permissions of all - // participants, even those with custom attendee permissions. - - // FIXME This approach does not scale, as the update message for all - // the sessions in a conversation can exceed the allowed size of the - // request in conversations with a large number of participants. - // However, note that a single message with the general permissions - // to be set on all participants can not be sent either, as the - // general permissions could be overriden by custom attendee - // permissions in specific participants. - - /** @var ParticipantService $participantService */ - $participantService = \OC::$server->get(ParticipantService::class); - $participants = $participantService->getSessionsAndParticipantsForRoom($event->getRoom()); - foreach ($participants as $participant) { - $session = $participant->getSession(); - if ($session) { - $sessionIds[] = $session->getSessionId(); - } - } + $sessionIds = []; + // If the participant is not active in the room the "participants" + // request will be sent anyway, although with an empty "changed" + // property. - $notifier->participantsModified($event->getRoom(), $sessionIds); - }); + /** @var SessionService $sessionService */ + $sessionService = \OC::$server->get(SessionService::class); + $sessions = $sessionService->getAllSessionsForAttendee($event->getParticipant()->getAttendee()); + foreach ($sessions as $session) { + $sessionIds[] = $session->getSessionId(); + } - $dispatcher->addListener(Room::EVENT_BEFORE_ROOM_DELETE, static function (RoomEvent $event) { - if (self::isUsingInternalSignaling()) { - return; - } + $notifier->participantsModified($event->getRoom(), $sessionIds); + } - /** @var BackendNotifier $notifier */ - $notifier = \OC::$server->get(BackendNotifier::class); - /** @var ParticipantService $participantService */ - $participantService = \OC::$server->get(ParticipantService::class); - - $room = $event->getRoom(); - $notifier->roomDeleted($room, $participantService->getParticipantUserIds($room)); - }); - $dispatcher->addListener(Room::EVENT_AFTER_USER_REMOVE, static function (RemoveUserEvent $event) { - if (self::isUsingInternalSignaling()) { - return; - } + public static function notifyAfterPermissionSet(RoomEvent $event): void { + if (self::isUsingInternalSignaling()) { + return; + } - /** @var BackendNotifier $notifier */ - $notifier = \OC::$server->get(BackendNotifier::class); + /** @var BackendNotifier $notifier */ + $notifier = \OC::$server->get(BackendNotifier::class); - $notifier->roomsDisinvited($event->getRoom(), [$event->getUser()->getUID()]); - }); - $dispatcher->addListener(Room::EVENT_AFTER_PARTICIPANT_REMOVE, static function (RemoveParticipantEvent $event) { - if (self::isUsingInternalSignaling()) { - return; + $sessionIds = []; + + // Setting the room permissions resets the permissions of all + // participants, even those with custom attendee permissions. + + // FIXME This approach does not scale, as the update message for all + // the sessions in a conversation can exceed the allowed size of the + // request in conversations with a large number of participants. + // However, note that a single message with the general permissions + // to be set on all participants can not be sent either, as the + // general permissions could be overriden by custom attendee + // permissions in specific participants. + + /** @var ParticipantService $participantService */ + $participantService = \OC::$server->get(ParticipantService::class); + $participants = $participantService->getSessionsAndParticipantsForRoom($event->getRoom()); + foreach ($participants as $participant) { + $session = $participant->getSession(); + if ($session) { + $sessionIds[] = $session->getSessionId(); } + } - /** @var BackendNotifier $notifier */ - $notifier = \OC::$server->get(BackendNotifier::class); + $notifier->participantsModified($event->getRoom(), $sessionIds); + } - $sessionIds = []; + public static function notifyBeforeRoomDeleted(RoomEvent $event): void { + if (self::isUsingInternalSignaling()) { + return; + } - /** @var SessionService $sessionService */ - $sessionService = \OC::$server->get(SessionService::class); - $sessions = $sessionService->getAllSessionsForAttendee($event->getParticipant()->getAttendee()); - foreach ($sessions as $session) { - $sessionIds[] = $session->getSessionId(); - } + /** @var BackendNotifier $notifier */ + $notifier = \OC::$server->get(BackendNotifier::class); + /** @var ParticipantService $participantService */ + $participantService = \OC::$server->get(ParticipantService::class); - if ($event->getParticipant()->getSession()) { - $sessionIds[] = $event->getParticipant()->getSession()->getSessionId(); - $notifier->roomSessionsRemoved($event->getRoom(), $sessionIds); - } + $room = $event->getRoom(); + $notifier->roomDeleted($room, $participantService->getParticipantUserIds($room)); + } - if (!empty($sessionIds)) { - $notifier->roomSessionsRemoved($event->getRoom(), $sessionIds); - } - }); - $dispatcher->addListener(Room::EVENT_AFTER_ROOM_DISCONNECT, static function (ParticipantEvent $event) { - if (self::isUsingInternalSignaling()) { - return; - } + public static function notifyAfterUserRemoved(RemoveUserEvent $event): void { + if (self::isUsingInternalSignaling()) { + return; + } - /** @var BackendNotifier $notifier */ - $notifier = \OC::$server->get(BackendNotifier::class); - - $sessionIds = []; - if ($event->getParticipant()->getSession()) { - // Only for guests and self-joined users disconnecting is "leaving" and therefor should trigger a disinvite - $attendeeParticipantType = $event->getParticipant()->getAttendee()->getParticipantType(); - if ($attendeeParticipantType === Participant::GUEST - || $attendeeParticipantType === Participant::GUEST_MODERATOR) { - $sessionIds[] = $event->getParticipant()->getSession()->getSessionId(); - $notifier->roomSessionsRemoved($event->getRoom(), $sessionIds); - } - if ($attendeeParticipantType === Participant::USER_SELF_JOINED) { - $notifier->roomsDisinvited($event->getRoom(), [$event->getParticipant()->getAttendee()->getActorId()]); - } - } - }); + /** @var BackendNotifier $notifier */ + $notifier = \OC::$server->get(BackendNotifier::class); - $listener = static function (ModifyParticipantEvent $event): void { - if (self::isUsingInternalSignaling()) { - return; - } + $notifier->roomsDisinvited($event->getRoom(), [$event->getUser()->getUID()]); + } - if ($event instanceof ModifyEveryoneEvent) { - // If everyone is disconnected, we will not do O(n) requests. - // Instead, the listener of Room::EVENT_AFTER_END_CALL_FOR_EVERYONE - // will send all sessions to the HPB with 1 request. - return; - } + public static function notifyAfterParticipantRemoved(RemoveParticipantEvent $event): void { + if (self::isUsingInternalSignaling()) { + return; + } + + /** @var BackendNotifier $notifier */ + $notifier = \OC::$server->get(BackendNotifier::class); - /** @var BackendNotifier $notifier */ - $notifier = \OC::$server->get(BackendNotifier::class); + $sessionIds = []; - $sessionIds = []; + /** @var SessionService $sessionService */ + $sessionService = \OC::$server->get(SessionService::class); + $sessions = $sessionService->getAllSessionsForAttendee($event->getParticipant()->getAttendee()); + foreach ($sessions as $session) { + $sessionIds[] = $session->getSessionId(); + } - /** @var SessionService $sessionService */ - $sessionService = \OC::$server->get(SessionService::class); - $sessions = $sessionService->getAllSessionsForAttendee($event->getParticipant()->getAttendee()); - foreach ($sessions as $session) { - $sessionIds[] = $session->getSessionId(); - } + if ($event->getParticipant()->getSession()) { + $sessionIds[] = $event->getParticipant()->getSession()->getSessionId(); + $notifier->roomSessionsRemoved($event->getRoom(), $sessionIds); + } - if (!empty($sessionIds)) { - $notifier->roomInCallChanged( - $event->getRoom(), - $event->getNewValue(), - $sessionIds - ); + if (!empty($sessionIds)) { + $notifier->roomSessionsRemoved($event->getRoom(), $sessionIds); + } + } + + public static function notifyAfterRoomDisconected(ParticipantEvent $event): void { + if (self::isUsingInternalSignaling()) { + return; + } + + /** @var BackendNotifier $notifier */ + $notifier = \OC::$server->get(BackendNotifier::class); + + $sessionIds = []; + if ($event->getParticipant()->getSession()) { + // Only for guests and self-joined users disconnecting is "leaving" and therefor should trigger a disinvite + $attendeeParticipantType = $event->getParticipant()->getAttendee()->getParticipantType(); + if ($attendeeParticipantType === Participant::GUEST + || $attendeeParticipantType === Participant::GUEST_MODERATOR) { + $sessionIds[] = $event->getParticipant()->getSession()->getSessionId(); + $notifier->roomSessionsRemoved($event->getRoom(), $sessionIds); } - }; - $dispatcher->addListener(Room::EVENT_AFTER_SESSION_JOIN_CALL, $listener); - $dispatcher->addListener(Room::EVENT_AFTER_SESSION_UPDATE_CALL_FLAGS, $listener); - $dispatcher->addListener(Room::EVENT_AFTER_SESSION_LEAVE_CALL, $listener); - - $dispatcher->addListener(Room::EVENT_AFTER_END_CALL_FOR_EVERYONE, static function (EndCallForEveryoneEvent $event): void { - if (self::isUsingInternalSignaling()) { - return; + if ($attendeeParticipantType === Participant::USER_SELF_JOINED) { + $notifier->roomsDisinvited($event->getRoom(), [$event->getParticipant()->getAttendee()->getActorId()]); } + } + } - $sessionIds = $event->getSessionIds(); + public static function notifyAfterJoinUpdateAndLeave(ModifyParticipantEvent $event): void { + if (self::isUsingInternalSignaling()) { + return; + } - if (empty($sessionIds)) { - return; - } + if ($event instanceof ModifyEveryoneEvent) { + // If everyone is disconnected, we will not do O(n) requests. + // Instead, the listener of Room::EVENT_AFTER_END_CALL_FOR_EVERYONE + // will send all sessions to the HPB with 1 request. + return; + } + + /** @var BackendNotifier $notifier */ + $notifier = \OC::$server->get(BackendNotifier::class); + + $sessionIds = []; - /** @var BackendNotifier $notifier */ - $notifier = \OC::$server->get(BackendNotifier::class); + /** @var SessionService $sessionService */ + $sessionService = \OC::$server->get(SessionService::class); + $sessions = $sessionService->getAllSessionsForAttendee($event->getParticipant()->getAttendee()); + foreach ($sessions as $session) { + $sessionIds[] = $session->getSessionId(); + } + if (!empty($sessionIds)) { $notifier->roomInCallChanged( $event->getRoom(), $event->getNewValue(), $sessionIds ); - }); + } + } - $dispatcher->addListener(Room::EVENT_AFTER_GUESTS_CLEAN, static function (RoomEvent $event) { - if (self::isUsingInternalSignaling()) { - return; - } + public static function sendEndCallForEveryone(EndCallForEveryoneEvent $event): void { + if (self::isUsingInternalSignaling()) { + return; + } - /** @var BackendNotifier $notifier */ - $notifier = \OC::$server->get(BackendNotifier::class); + $sessionIds = $event->getSessionIds(); - // TODO: The list of removed session ids should be passed through the event - // so the signaling server can optimize forwarding the message. - $sessionIds = []; - $notifier->participantsModified($event->getRoom(), $sessionIds); - }); - $dispatcher->addListener(GuestManager::EVENT_AFTER_NAME_UPDATE, static function (ModifyParticipantEvent $event) { - if (self::isUsingInternalSignaling()) { - return; - } + if (empty($sessionIds)) { + return; + } - /** @var BackendNotifier $notifier */ - $notifier = \OC::$server->get(BackendNotifier::class); + /** @var BackendNotifier $notifier */ + $notifier = \OC::$server->get(BackendNotifier::class); - $sessionIds = []; + $notifier->roomInCallChanged( + $event->getRoom(), + $event->getNewValue(), + $sessionIds + ); + } - /** @var SessionService $sessionService */ - $sessionService = \OC::$server->get(SessionService::class); - $sessions = $sessionService->getAllSessionsForAttendee($event->getParticipant()->getAttendee()); - foreach ($sessions as $session) { - $sessionIds[] = $session->getSessionId(); - } + public static function notifyParticipantsAfterGuestClean(RoomEvent $event): void { + if (self::isUsingInternalSignaling()) { + return; + } - if (!empty($sessionIds)) { - $notifier->participantsModified($event->getRoom(), $sessionIds); - } - }); + /** @var BackendNotifier $notifier */ + $notifier = \OC::$server->get(BackendNotifier::class); - $dispatcher->addListener(ChatManager::EVENT_AFTER_MESSAGE_SEND, [self::class, 'notifyUsersViaExternalSignalingToRefreshTheChat']); - $dispatcher->addListener(ChatManager::EVENT_AFTER_SYSTEM_MESSAGE_SEND, [self::class, 'notifyUsersViaExternalSignalingToRefreshTheChat']); - $dispatcher->addListener(ChatManager::EVENT_AFTER_MULTIPLE_SYSTEM_MESSAGE_SEND, [self::class, 'notifyUsersViaExternalSignalingToRefreshTheChat']); + // TODO: The list of removed session ids should be passed through the event + // so the signaling server can optimize forwarding the message. + $sessionIds = []; + $notifier->participantsModified($event->getRoom(), $sessionIds); + } + + public static function notifyParticipantsAfterNameUpdated(ModifyParticipantEvent $event): void { + if (self::isUsingInternalSignaling()) { + return; + } + + /** @var BackendNotifier $notifier */ + $notifier = \OC::$server->get(BackendNotifier::class); + + $sessionIds = []; + + /** @var SessionService $sessionService */ + $sessionService = \OC::$server->get(SessionService::class); + $sessions = $sessionService->getAllSessionsForAttendee($event->getParticipant()->getAttendee()); + foreach ($sessions as $session) { + $sessionIds[] = $session->getSessionId(); + } + + if (!empty($sessionIds)) { + $notifier->participantsModified($event->getRoom(), $sessionIds); + } } public static function notifyUsersViaExternalSignalingToRefreshTheChat(ChatEvent $event): void { -- cgit v1.2.3 From e13ad504caf5cca24775ae9b015823282d0cd9b3 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Mon, 25 Apr 2022 17:09:32 +0200 Subject: Fix representation of guest reactions Signed-off-by: Joas Schilling --- .../MessagesList/MessagesGroup/Message/Message.vue | 17 ++++++++++++++++- src/store/guestNameStore.js | 8 ++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/components/MessagesList/MessagesGroup/Message/Message.vue b/src/components/MessagesList/MessagesGroup/Message/Message.vue index 612b8de11..a58212cac 100644 --- a/src/components/MessagesList/MessagesGroup/Message/Message.vue +++ b/src/components/MessagesList/MessagesGroup/Message/Message.vue @@ -190,6 +190,7 @@ import EmojiPicker from '@nextcloud/vue/dist/Components/EmojiPicker' import EmoticonOutline from 'vue-material-design-icons/EmoticonOutline.vue' import Popover from '@nextcloud/vue/dist/Components/Popover' import { showError, showSuccess, showWarning, TOAST_DEFAULT_TIMEOUT } from '@nextcloud/dialogs' +import { ATTENDEE } from '../../../../constants' export default { name: 'Message', @@ -738,12 +739,26 @@ export default { && list[item].actorId === this.$store.getters.getActorId()) { summary.unshift(t('spreed', 'You')) } else { - summary.push(list[item].actorDisplayName) + summary.push(this.getDisplayNameForReaction(list[item])) } } return summary.join(', ') }, + + getDisplayNameForReaction(reaction) { + const displayName = reaction.actorDisplayName.trim() + + if (reaction.actorType === ATTENDEE.ACTOR_TYPE.GUESTS) { + return this.$store.getters.getGuestNameWithGuestSuffix(this.token, reaction.actorId) + } + + if (displayName === '') { + return t('spreed', 'Deleted user') + } + + return displayName + }, }, } diff --git a/src/store/guestNameStore.js b/src/store/guestNameStore.js index 94f39acc5..48da79d9d 100644 --- a/src/store/guestNameStore.js +++ b/src/store/guestNameStore.js @@ -39,6 +39,14 @@ const getters = { } return t('spreed', 'Guest') }, + + getGuestNameWithGuestSuffix: (state, getters) => (token, actorId) => { + const displayName = getters.getGuestName(token, actorId) + if (displayName === t('spreed', 'Guest')) { + return displayName + } + return t('spreed', '{guest} (guest)', { guest: displayName }) + }, } const mutations = { -- cgit v1.2.3 From b73eb9fb0497718cc46dbe10674da58aa1bbf8f3 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Mon, 25 Apr 2022 17:25:19 +0200 Subject: Add integration tests for guest reactions Signed-off-by: Joas Schilling --- tests/integration/features/bootstrap/FeatureContext.php | 1 + tests/integration/features/reaction/react.feature | 16 +++++++++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/tests/integration/features/bootstrap/FeatureContext.php b/tests/integration/features/bootstrap/FeatureContext.php index fd722589a..23a22880b 100644 --- a/tests/integration/features/bootstrap/FeatureContext.php +++ b/tests/integration/features/bootstrap/FeatureContext.php @@ -2300,6 +2300,7 @@ class FeatureContext implements Context, SnippetAcceptingContext { $result = array_map(static function ($reaction, $list) use ($expected): array { $list = array_map(function ($reaction) { unset($reaction['timestamp']); + $reaction['actorId'] = ($reaction['actorType'] === 'guests') ? self::$sessionIdToUser[$reaction['actorId']] : (string) $reaction['actorId']; return $reaction; }, $list); Assert::assertArrayHasKey($reaction, $expected, 'Not expected reaction: ' . $reaction); diff --git a/tests/integration/features/reaction/react.feature b/tests/integration/features/reaction/react.feature index 7c9613e8b..d53b044e0 100644 --- a/tests/integration/features/reaction/react.feature +++ b/tests/integration/features/reaction/react.feature @@ -25,11 +25,14 @@ Feature: reaction/react | users | participant1 | participant1-displayname | 👍 | | users | participant2 | participant2-displayname | 👍 | And user "participant1" react with "🚀" on message "Message 1" to room "room" with 201 + Then user "guest" joins room "room" with 200 (v4) + And user "guest" react with "👤" on message "Message 1" to room "room" with 201 Then user "participant1" sees the following messages in room "room" with 200 - | room | actorType | actorId | actorDisplayName | message | messageParameters | reactions | reactionsSelf | - | room | users | participant1 | participant1-displayname | Message 1 | [] | {"👍":2,"🚀":1} | ["👍","🚀"] | + | room | actorType | actorId | actorDisplayName | message | messageParameters | reactions | reactionsSelf | + | room | users | participant1 | participant1-displayname | Message 1 | [] | {"👍":2,"👤":1,"🚀":1} | ["👍","🚀"] | Then user "participant1" sees the following system messages in room "room" with 200 | room | actorType | actorId | actorDisplayName | systemMessage | + | room | guests | guest | | reaction | | room | users | participant1 | participant1-displayname | reaction | | room | users | participant1 | participant1-displayname | reaction | | room | users | participant2 | participant2-displayname | reaction | @@ -66,17 +69,24 @@ Feature: reaction/react And user "participant2" react with "👍" on message "Message 1" to room "room" with 201 | actorType | actorId | actorDisplayName | reaction | | users | participant2 | participant2-displayname | 👍 | + Then user "guest" joins room "room" with 200 (v4) + And user "guest" react with "👤" on message "Message 1" to room "room" with 201 Then user "participant1" sees the following messages in room "room" with 200 | room | actorType | actorId | actorDisplayName | message | messageParameters | reactions | - | room | users | participant1 | participant1-displayname | Message 1 | [] | {"👍":1} | + | room | users | participant1 | participant1-displayname | Message 1 | [] | {"👤":1,"👍":1} | And user "participant2" delete react with "👍" on message "Message 1" to room "room" with 200 | actorType | actorId | actorDisplayName | reaction | + | guests | guest | | 👤 | + And user "guest" delete react with "👤" on message "Message 1" to room "room" with 200 + | actorType | actorId | actorDisplayName | reaction | Then user "participant1" sees the following messages in room "room" with 200 | room | actorType | actorId | actorDisplayName | message | messageParameters | reactions | | room | users | participant1 | participant1-displayname | Message 1 | [] | [] | Then user "participant1" sees the following system messages in room "room" with 200 | room | actorType | actorId | actorDisplayName | systemMessage | + | room | guests | guest | | reaction_revoked | | room | users | participant2 | participant2-displayname | reaction_revoked | + | room | guests | guest | | reaction_deleted | | room | users | participant2 | participant2-displayname | reaction_deleted | | room | users | participant1 | participant1-displayname | user_added | | room | users | participant1 | participant1-displayname | conversation_created | -- cgit v1.2.3 From 875b75c2dab7df79be2679c46980e85a733af7e3 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Mon, 25 Apr 2022 12:34:12 -0300 Subject: Move status listeners anonymous functions to static Signed-off-by: Vitor Mattos --- lib/Status/Listener.php | 36 +++++++++++++++--------------------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/lib/Status/Listener.php b/lib/Status/Listener.php index 4bf4e7ffe..80b8722aa 100644 --- a/lib/Status/Listener.php +++ b/lib/Status/Listener.php @@ -43,32 +43,24 @@ class Listener { } public static function register(IEventDispatcher $dispatcher): void { - $dispatcher->addListener(Room::EVENT_BEFORE_SESSION_JOIN_CALL, static function (ModifyParticipantEvent $event) { - /** @var self $listener */ - $listener = \OC::$server->get(self::class); - $listener->setUserStatus($event); - }); + $dispatcher->addListener(Room::EVENT_BEFORE_SESSION_JOIN_CALL, [self::class, 'setUserStatus']); - $dispatcher->addListener(Room::EVENT_AFTER_SESSION_LEAVE_CALL, static function (ModifyParticipantEvent $event) { - /** @var self $listener */ - $listener = \OC::$server->get(self::class); - $listener->revertUserStatus($event); - }); + $dispatcher->addListener(Room::EVENT_AFTER_SESSION_LEAVE_CALL, [self::class, 'revertUserStatus']); - $dispatcher->addListener(Room::EVENT_AFTER_END_CALL_FOR_EVERYONE, static function (EndCallForEveryoneEvent $event) { - /** @var self $listener */ - $listener = \OC::$server->get(self::class); - $listener->revertUserStatusOnEndCallForEveryone($event); - }); + $dispatcher->addListener(Room::EVENT_AFTER_END_CALL_FOR_EVERYONE, [self::class, 'revertUserStatusOnEndCallForEveryone']); } - public function setUserStatus(ModifyParticipantEvent $event): void { + public static function setUserStatus(ModifyParticipantEvent $event): void { + /** @var self $listener */ + $listener = \OC::$server->get(self::class); if ($event->getParticipant()->getAttendee()->getActorType() === Attendee::ACTOR_USERS) { - $this->statusManager->setUserStatus($event->getParticipant()->getAttendee()->getActorId(), 'call', IUserStatus::AWAY, true); + $listener->statusManager->setUserStatus($event->getParticipant()->getAttendee()->getActorId(), 'call', IUserStatus::AWAY, true); } } - public function revertUserStatus(ModifyParticipantEvent $event): void { + public static function revertUserStatus(ModifyParticipantEvent $event): void { + /** @var self $listener */ + $listener = \OC::$server->get(self::class); if ($event instanceof ModifyEveryoneEvent) { // Do not revert the status with 3 queries per user. // We will update it in one go at the end. @@ -76,14 +68,16 @@ class Listener { } if ($event->getParticipant()->getAttendee()->getActorType() === Attendee::ACTOR_USERS) { - $this->statusManager->revertUserStatus($event->getParticipant()->getAttendee()->getActorId(), 'call', IUserStatus::AWAY); + $listener->statusManager->revertUserStatus($event->getParticipant()->getAttendee()->getActorId(), 'call', IUserStatus::AWAY); } } - public function revertUserStatusOnEndCallForEveryone(EndCallForEveryoneEvent $event): void { + public static function revertUserStatusOnEndCallForEveryone(EndCallForEveryoneEvent $event): void { + /** @var self $listener */ + $listener = \OC::$server->get(self::class); $userIds = $event->getUserIds(); if (!empty($userIds)) { - $this->statusManager->revertMultipleUserStatus($userIds, 'call', IUserStatus::AWAY); + $listener->statusManager->revertMultipleUserStatus($userIds, 'call', IUserStatus::AWAY); } } } -- cgit v1.2.3 From 41cbc76ae335e892704ce9f7b1c10ca05ca55a08 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Mon, 25 Apr 2022 13:47:57 -0300 Subject: Move sharing listeners to static Signed-off-by: Vitor Mattos --- lib/Listener/RestrictStartingCalls.php | 12 +++++------- tests/php/Listener/RestrictStartingCallsTest.php | 2 ++ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/Listener/RestrictStartingCalls.php b/lib/Listener/RestrictStartingCalls.php index ed041ef0c..59e95639c 100644 --- a/lib/Listener/RestrictStartingCalls.php +++ b/lib/Listener/RestrictStartingCalls.php @@ -42,18 +42,16 @@ class RestrictStartingCalls { } public static function register(IEventDispatcher $dispatcher): void { - $dispatcher->addListener(Room::EVENT_BEFORE_SESSION_JOIN_CALL, static function (ModifyParticipantEvent $event) { - /** @var self $listener */ - $listener = \OC::$server->get(self::class); - $listener->checkStartCallPermissions($event); - }, 1000); + $dispatcher->addListener(Room::EVENT_BEFORE_SESSION_JOIN_CALL, [self::class, 'checkStartCallPermissions'], 1000); } /** * @param ModifyParticipantEvent $event * @throws ForbiddenException */ - public function checkStartCallPermissions(ModifyParticipantEvent $event): void { + public static function checkStartCallPermissions(ModifyParticipantEvent $event): void { + /** @var self $listener */ + $listener = \OC::$server->get(self::class); $room = $event->getRoom(); $participant = $event->getParticipant(); @@ -63,7 +61,7 @@ class RestrictStartingCalls { return; } - if (!$participant->canStartCall($this->config) && !$this->participantService->hasActiveSessionsInCall($room)) { + if (!$participant->canStartCall($listener->config) && !$listener->participantService->hasActiveSessionsInCall($room)) { throw new ForbiddenException('Can not start a call'); } } diff --git a/tests/php/Listener/RestrictStartingCallsTest.php b/tests/php/Listener/RestrictStartingCallsTest.php index b94da5a55..2ffbe04ab 100644 --- a/tests/php/Listener/RestrictStartingCallsTest.php +++ b/tests/php/Listener/RestrictStartingCallsTest.php @@ -97,7 +97,9 @@ class RestrictStartingCallsTest extends TestCase { $this->expectException(ForbiddenException::class); } + $this->overwriteService(RestrictStartingCalls::class, $this->listener); $this->listener->checkStartCallPermissions($event); + $this->restoreService(RestrictStartingCalls::class); if (!$throws) { self::assertTrue(true); -- cgit v1.2.3 From a00908d2b2e48048c4b5ea0bbdd6ca32d45a3e36 Mon Sep 17 00:00:00 2001 From: Nextcloud bot Date: Tue, 26 Apr 2022 03:23:22 +0000 Subject: [tx-robot] updated from transifex Signed-off-by: Nextcloud bot --- l10n/bg.js | 25 +++++++++++++++++++++++++ l10n/bg.json | 25 +++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/l10n/bg.js b/l10n/bg.js index b902c8ef6..5812ab8e5 100644 --- a/l10n/bg.js +++ b/l10n/bg.js @@ -47,6 +47,7 @@ OC.L10N.register( "The command does not exist" : "Командата не съществува", "An error occurred while running the command. Please ask an administrator to check the logs." : "Възникна грешка при изпълнение на командата. Моля, помолете администратор да провери регистрационните /журнали/ файлове.", "Talk updates ✅" : "Актуализации на Talk ✅", + "Reaction deleted by author" : "Реакцията е изтрита от автор", "{actor} created the conversation" : "{actor} създаде разговора", "You created the conversation" : "Вие създадохте разговор", "An administrator created the conversation" : "Администратор създаде разговора", @@ -148,6 +149,8 @@ OC.L10N.register( "You stopped Matterbridge" : "Спряхте Matterbridge", "{actor} deleted a message" : "{actor} изтри съобщение", "You deleted a message" : "Изтрихте съобщение", + "{actor} deleted a reaction" : "{actor} изтри реакция", + "You deleted a reaction" : "Изтрихте реакция", "{actor} cleared the history of the conversation" : "{actor} изчисти историята на разговора", "You cleared the history of the conversation" : "Изчистихте историята на разговора", "Message deleted by author" : "Съобщението е изтрито от автора", @@ -207,6 +210,11 @@ OC.L10N.register( "A deleted user replied to your message in conversation {call}" : "Изтрит потребител отговори на съобщението ви в разговор {call}", "{guest} (guest) replied to your message in conversation {call}" : "{guest} (гост) отговори на съобщението ви в разговор {call}", "A guest replied to your message in conversation {call}" : "Гост отговори на съобщението ви в разговор {call}", + "{user} reacted with {reaction} to your private message" : "{user} реагира с {reaction} на вашето лично съобщение", + "{user} reacted with {reaction} to your message in conversation {call}" : "{user} реагира с {reaction} на вашето съобщение в разговор {call}", + "A deleted user reacted with {reaction} to your message in conversation {call}" : "Изтрит потребител реагира с {reaction} на вашето съобщение в разговор {call}", + "{guest} (guest) reacted with {reaction} to your message in conversation {call}" : "{guest} (гост) реагира с {reaction} на вашето съобщение в разговор {call}", + "A guest reacted with {reaction} to your message in conversation {call}" : "Гост реагира с {reaction} на вашето съобщение в разговор {call}", "{user} mentioned you in a private conversation" : "{user} ви спомена в личен разговор", "{user} mentioned you in conversation {call}" : "{user} ви спомена в разговор {call}", "A deleted user mentioned you in conversation {call}" : "Изтрит потребител ви спомена в разговор {call}", @@ -616,6 +624,8 @@ OC.L10N.register( "OK: Running version: {version}" : "ОК: Работна версия: {version}", "Error: Cannot connect to server" : "Грешка: Невъзможно свързване със сървъра", "Error: Server did not respond with proper JSON" : "Грешка: Сървърът не отговори с правилен JSON", + "Could not get version" : "Не можа да се получи версията", + "Error: Running version: {version}; Server needs to be updated to be compatible with this version of Talk" : "Грешка: Използвана версия: {version}; Сървърът трябва да бъде актуализиран, за да бъде съвместим с тази версия на приложението Talk", "Error: Server responded with: {error}" : "Грешка: Сървърът отговори с: {error}", "Error: Unknown error occurred" : "Грешка: Възникна неизвестна грешка", "High-performance backend" : "Високопроизводителен сървър", @@ -648,6 +658,7 @@ OC.L10N.register( "Checking …" : "Проверка ...", "Failed: WebAssembly is disabled or not supported in this browser. Please enable WebAssembly or use a browser with support for it to do the check." : "Неуспешно: WebAssembly е деактивиран или не се поддържа в този браузър. Моля, активирайте WebAssembly или използвайте браузър с поддръжка за него, за да направите проверката.", "Failed: \".wasm\" and \".tflite\" files were not properly returned by the web server. Please check \"System requirements\" section in Talk documentation." : "Неуспех: файловете „wasm“ и „.tflite“ не са върнати правилно от уеб сървъра. Моля, проверете раздела „Системни изисквания“ в документацията на Talk.", + "OK: \".wasm\" and \".tflite\" files were properly returned by the web server." : "OK: файловете „wasm“ и „.tflite“ са върнати правилно от уеб сървъра.", "{nickName} raised their hand." : "{nickName} вдигна ръка.", "A participant raised their hand." : "Един участник вдигна ръка.", "Previous page of videos" : "Предишна страница с видеоклипове", @@ -718,6 +729,7 @@ OC.L10N.register( "An error occurred while starting screensharing." : "Възникна грешка при стартиране на споделянето на екрана.", "Back" : "Назад", "Access to camera was denied" : "Беше отказан достъп до камера", + "Error while accessing camera: It is likely in use by another program" : "Грешка при достъп до камера: възможно е тя се използва от друга програма", "Error while accessing camera" : "Грешка при достъп до камера", "You have been muted by a moderator" : "Бяхте заглушен от модератор", "You" : "Ти", @@ -1025,16 +1037,26 @@ OC.L10N.register( "Chat" : "Съобщения", "Details" : "Подробности", "Settings" : "Настройки", + "Shared items" : "Споделени елементи", "Participants ({count})" : "Участници ({count})", "Media" : "Медия", "Files" : "Файлове", + "Deck cards" : "Deck карти", + "Voice messages" : "Гласови съобщения", "Locations" : "Местоположения", "Audio" : "Аудио", "Other" : "Други", + "Show all media" : "Показване на всички медии", "Show all files" : "Показване на всички файлове", + "Show all deck cards" : "Показване на всички deck карти", + "Show all voice messages" : "Показване на всички гласови съобщения", + "Show all locations" : "Показване на всички местоположения", + "Show all audio" : "Показване на всички аудиозаписи", + "Show all other" : "Показване на всичко останало", "Projects" : "Проекти", "Meeting ID: {meetingId}" : "Идентификатор на срещата: {meetingId}", "Your PIN: {attendeePin}" : "Вашият ПИН: {attendeePin}", + "Display name: {name}" : "Име за визуализация: {name}", "Attachments folder" : "Папка с прикачени файлове", "Privacy" : "Поверителност", "Share my read-status and show the read-status of others" : "Споделяне на моето състояние на четене и показване на състоянието на четене и на другите", @@ -1048,6 +1070,7 @@ OC.L10N.register( "Fullscreen the chat or call" : "Чат на цял екран или обаждане", "Search" : "Търсене", "Shortcuts while in a call" : "Преки пътища по време на обаждане", + "Camera on and off" : "Включване и изключване на камера", "Microphone on and off" : "Включване и изключване на микрофон", "Space bar" : "Интервал", "Push to talk or push to mute" : "Натиснете за разговор или натиснете за заглушаване", @@ -1109,6 +1132,8 @@ OC.L10N.register( "Error while uploading file \"{fileName}\"" : "Грешка при качването на файл „{fileName}“", "An error happened when trying to share your file" : "Възникна грешка при опит за споделяне на вашия файл", "Could not post message: {errorMessage}" : "Не можа да се публикува съобщение: {errorMessage}", + "Failed to add reaction" : "Неуспешно добавяне на реакция", + "Failed to remove reaction" : "Неуспешно премахване на реакция", "Failed to join the conversation. Try to reload the page." : "Присъединяването към разговора не беше успешно. Опитайте да презаредите страницата.", "You are trying to join a conversation while having an active session in another window or device. This is currently not supported by Nextcloud Talk. What do you want to do?" : "Опитвате се да се присъедините към разговор, докато имате активна сесия в друг прозорец или устройство. Това в момента не се поддържа от Nextcloud Talk. Какво искате да направите?", "Join here" : "Присъединяване тук", diff --git a/l10n/bg.json b/l10n/bg.json index 7e3d9a213..005b5873e 100644 --- a/l10n/bg.json +++ b/l10n/bg.json @@ -45,6 +45,7 @@ "The command does not exist" : "Командата не съществува", "An error occurred while running the command. Please ask an administrator to check the logs." : "Възникна грешка при изпълнение на командата. Моля, помолете администратор да провери регистрационните /журнали/ файлове.", "Talk updates ✅" : "Актуализации на Talk ✅", + "Reaction deleted by author" : "Реакцията е изтрита от автор", "{actor} created the conversation" : "{actor} създаде разговора", "You created the conversation" : "Вие създадохте разговор", "An administrator created the conversation" : "Администратор създаде разговора", @@ -146,6 +147,8 @@ "You stopped Matterbridge" : "Спряхте Matterbridge", "{actor} deleted a message" : "{actor} изтри съобщение", "You deleted a message" : "Изтрихте съобщение", + "{actor} deleted a reaction" : "{actor} изтри реакция", + "You deleted a reaction" : "Изтрихте реакция", "{actor} cleared the history of the conversation" : "{actor} изчисти историята на разговора", "You cleared the history of the conversation" : "Изчистихте историята на разговора", "Message deleted by author" : "Съобщението е изтрито от автора", @@ -205,6 +208,11 @@ "A deleted user replied to your message in conversation {call}" : "Изтрит потребител отговори на съобщението ви в разговор {call}", "{guest} (guest) replied to your message in conversation {call}" : "{guest} (гост) отговори на съобщението ви в разговор {call}", "A guest replied to your message in conversation {call}" : "Гост отговори на съобщението ви в разговор {call}", + "{user} reacted with {reaction} to your private message" : "{user} реагира с {reaction} на вашето лично съобщение", + "{user} reacted with {reaction} to your message in conversation {call}" : "{user} реагира с {reaction} на вашето съобщение в разговор {call}", + "A deleted user reacted with {reaction} to your message in conversation {call}" : "Изтрит потребител реагира с {reaction} на вашето съобщение в разговор {call}", + "{guest} (guest) reacted with {reaction} to your message in conversation {call}" : "{guest} (гост) реагира с {reaction} на вашето съобщение в разговор {call}", + "A guest reacted with {reaction} to your message in conversation {call}" : "Гост реагира с {reaction} на вашето съобщение в разговор {call}", "{user} mentioned you in a private conversation" : "{user} ви спомена в личен разговор", "{user} mentioned you in conversation {call}" : "{user} ви спомена в разговор {call}", "A deleted user mentioned you in conversation {call}" : "Изтрит потребител ви спомена в разговор {call}", @@ -614,6 +622,8 @@ "OK: Running version: {version}" : "ОК: Работна версия: {version}", "Error: Cannot connect to server" : "Грешка: Невъзможно свързване със сървъра", "Error: Server did not respond with proper JSON" : "Грешка: Сървърът не отговори с правилен JSON", + "Could not get version" : "Не можа да се получи версията", + "Error: Running version: {version}; Server needs to be updated to be compatible with this version of Talk" : "Грешка: Използвана версия: {version}; Сървърът трябва да бъде актуализиран, за да бъде съвместим с тази версия на приложението Talk", "Error: Server responded with: {error}" : "Грешка: Сървърът отговори с: {error}", "Error: Unknown error occurred" : "Грешка: Възникна неизвестна грешка", "High-performance backend" : "Високопроизводителен сървър", @@ -646,6 +656,7 @@ "Checking …" : "Проверка ...", "Failed: WebAssembly is disabled or not supported in this browser. Please enable WebAssembly or use a browser with support for it to do the check." : "Неуспешно: WebAssembly е деактивиран или не се поддържа в този браузър. Моля, активирайте WebAssembly или използвайте браузър с поддръжка за него, за да направите проверката.", "Failed: \".wasm\" and \".tflite\" files were not properly returned by the web server. Please check \"System requirements\" section in Talk documentation." : "Неуспех: файловете „wasm“ и „.tflite“ не са върнати правилно от уеб сървъра. Моля, проверете раздела „Системни изисквания“ в документацията на Talk.", + "OK: \".wasm\" and \".tflite\" files were properly returned by the web server." : "OK: файловете „wasm“ и „.tflite“ са върнати правилно от уеб сървъра.", "{nickName} raised their hand." : "{nickName} вдигна ръка.", "A participant raised their hand." : "Един участник вдигна ръка.", "Previous page of videos" : "Предишна страница с видеоклипове", @@ -716,6 +727,7 @@ "An error occurred while starting screensharing." : "Възникна грешка при стартиране на споделянето на екрана.", "Back" : "Назад", "Access to camera was denied" : "Беше отказан достъп до камера", + "Error while accessing camera: It is likely in use by another program" : "Грешка при достъп до камера: възможно е тя се използва от друга програма", "Error while accessing camera" : "Грешка при достъп до камера", "You have been muted by a moderator" : "Бяхте заглушен от модератор", "You" : "Ти", @@ -1023,16 +1035,26 @@ "Chat" : "Съобщения", "Details" : "Подробности", "Settings" : "Настройки", + "Shared items" : "Споделени елементи", "Participants ({count})" : "Участници ({count})", "Media" : "Медия", "Files" : "Файлове", + "Deck cards" : "Deck карти", + "Voice messages" : "Гласови съобщения", "Locations" : "Местоположения", "Audio" : "Аудио", "Other" : "Други", + "Show all media" : "Показване на всички медии", "Show all files" : "Показване на всички файлове", + "Show all deck cards" : "Показване на всички deck карти", + "Show all voice messages" : "Показване на всички гласови съобщения", + "Show all locations" : "Показване на всички местоположения", + "Show all audio" : "Показване на всички аудиозаписи", + "Show all other" : "Показване на всичко останало", "Projects" : "Проекти", "Meeting ID: {meetingId}" : "Идентификатор на срещата: {meetingId}", "Your PIN: {attendeePin}" : "Вашият ПИН: {attendeePin}", + "Display name: {name}" : "Име за визуализация: {name}", "Attachments folder" : "Папка с прикачени файлове", "Privacy" : "Поверителност", "Share my read-status and show the read-status of others" : "Споделяне на моето състояние на четене и показване на състоянието на четене и на другите", @@ -1046,6 +1068,7 @@ "Fullscreen the chat or call" : "Чат на цял екран или обаждане", "Search" : "Търсене", "Shortcuts while in a call" : "Преки пътища по време на обаждане", + "Camera on and off" : "Включване и изключване на камера", "Microphone on and off" : "Включване и изключване на микрофон", "Space bar" : "Интервал", "Push to talk or push to mute" : "Натиснете за разговор или натиснете за заглушаване", @@ -1107,6 +1130,8 @@ "Error while uploading file \"{fileName}\"" : "Грешка при качването на файл „{fileName}“", "An error happened when trying to share your file" : "Възникна грешка при опит за споделяне на вашия файл", "Could not post message: {errorMessage}" : "Не можа да се публикува съобщение: {errorMessage}", + "Failed to add reaction" : "Неуспешно добавяне на реакция", + "Failed to remove reaction" : "Неуспешно премахване на реакция", "Failed to join the conversation. Try to reload the page." : "Присъединяването към разговора не беше успешно. Опитайте да презаредите страницата.", "You are trying to join a conversation while having an active session in another window or device. This is currently not supported by Nextcloud Talk. What do you want to do?" : "Опитвате се да се присъедините към разговор, докато имате активна сесия в друг прозорец или устройство. Това в момента не се поддържа от Nextcloud Talk. Какво искате да направите?", "Join here" : "Присъединяване тук", -- cgit v1.2.3 From 2777ac86103e06158d86f608217745de46482042 Mon Sep 17 00:00:00 2001 From: Marco Date: Tue, 26 Apr 2022 09:52:04 +0200 Subject: Update src/components/RightSidebar/SharedItems/SharedItemsBrowser/SharedItemsBrowser.vue Co-authored-by: Joas Schilling <213943+nickvergessen@users.noreply.github.com> --- .../RightSidebar/SharedItems/SharedItemsBrowser/SharedItemsBrowser.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/RightSidebar/SharedItems/SharedItemsBrowser/SharedItemsBrowser.vue b/src/components/RightSidebar/SharedItems/SharedItemsBrowser/SharedItemsBrowser.vue index 9d4075b2b..e072cf746 100644 --- a/src/components/RightSidebar/SharedItems/SharedItemsBrowser/SharedItemsBrowser.vue +++ b/src/components/RightSidebar/SharedItems/SharedItemsBrowser/SharedItemsBrowser.vue @@ -163,7 +163,7 @@ export default { ::v-deep .button-vue { border-radius: var(--border-radius-large); &.active { - background-color: var(--color-primary-light-hover); + background-color: var(--color-primary-element-hover); } } -- cgit v1.2.3 From a7d53da05bca7a64c75ae5d365b5a98d8f2288fe Mon Sep 17 00:00:00 2001 From: marco Date: Tue, 26 Apr 2022 10:33:27 +0200 Subject: Fix modal in fullscreen mode Signed-off-by: marco --- .../SharedItems/SharedItemsBrowser/SharedItemsBrowser.vue | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/RightSidebar/SharedItems/SharedItemsBrowser/SharedItemsBrowser.vue b/src/components/RightSidebar/SharedItems/SharedItemsBrowser/SharedItemsBrowser.vue index e072cf746..f32e4914b 100644 --- a/src/components/RightSidebar/SharedItems/SharedItemsBrowser/SharedItemsBrowser.vue +++ b/src/components/RightSidebar/SharedItems/SharedItemsBrowser/SharedItemsBrowser.vue @@ -20,7 +20,7 @@ -->