Skip to content

Commit

Permalink
Merge pull request #14152 from nextcloud/fix/14029/muted-speaker
Browse files Browse the repository at this point in the history
  • Loading branch information
Antreesy authored Jan 29, 2025
2 parents 66491fd + aadd013 commit 00e504a
Show file tree
Hide file tree
Showing 3 changed files with 129 additions and 72 deletions.
94 changes: 81 additions & 13 deletions src/components/CallView/shared/LocalAudioControlButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,39 +4,57 @@
-->

<template>
<NcButton :title="audioButtonTitle"
:type="type"
:aria-label="audioButtonAriaLabel"
:class="{ 'no-audio-available': !model.attributes.audioAvailable }"
:disabled="!isAudioAllowed"
@click.stop="toggleAudio">
<template #icon>
<VolumeIndicator :audio-preview-available="model.attributes.audioAvailable"
:audio-enabled="showMicrophoneOn"
:current-volume="model.attributes.currentVolume"
:volume-threshold="model.attributes.volumeThreshold"
overlay-muted-color="#888888" />
<NcPopover ref="popover"
:boundary="boundaryElement"
:show-triggers="[]"
:hide-triggers="['click']"
:auto-hide="false"
:focus-trap="false"
:shown="popupShown">
<template #trigger>
<NcButton :title="audioButtonTitle"
:type="type"
:aria-label="audioButtonAriaLabel"
:class="{ 'no-audio-available': !model.attributes.audioAvailable }"
:disabled="!isAudioAllowed"
@click.stop="toggleAudio">
<template #icon>
<VolumeIndicator :audio-preview-available="model.attributes.audioAvailable"
:audio-enabled="showMicrophoneOn"
:current-volume="model.attributes.currentVolume"
:volume-threshold="model.attributes.volumeThreshold"
overlay-muted-color="#888888" />
</template>
</NcButton>
</template>
</NcButton>
<div class="popover-hint">
<span>{{ speakingWhileMutedWarner?.message }}</span>
</div>
</NcPopover>
</template>

<script>
import { onBeforeUnmount, ref, watch } from 'vue'

import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus'
import { t } from '@nextcloud/l10n'

import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
import NcPopover from '@nextcloud/vue/dist/Components/NcPopover.js'
import { useHotKey } from '@nextcloud/vue/dist/Composables/useHotKey.js'

import VolumeIndicator from '../../UIShared/VolumeIndicator.vue'

import { PARTICIPANT } from '../../../constants.ts'
import BrowserStorage from '../../../services/BrowserStorage.js'
import SpeakingWhileMutedWarner from '../../../utils/webrtc/SpeakingWhileMutedWarner.js'

export default {
name: 'LocalAudioControlButton',

components: {
NcButton,
NcPopover,
VolumeIndicator,
},

Expand All @@ -56,6 +74,11 @@ export default {
default: OCP.Accessibility.disableKeyboardShortcuts(),
},

disableMutedWarning: {
type: Boolean,
default: false,
},

type: {
type: String,
default: 'tertiary-no-background',
Expand All @@ -67,6 +90,45 @@ export default {
},
},

setup(props) {
const boundaryElement = document.querySelector('.main-view')

const popover = ref(null)
const popupShown = ref(false)
const speakingWhileMutedWarner = !props.disableMutedWarning
? ref(new SpeakingWhileMutedWarner(props.model))
: ref(null)

if (!props.disableMutedWarning) {
watch(() => speakingWhileMutedWarner.value.showPopup, (newValue) => {
popupShown.value = newValue && isVisible(popover.value?.$el)
})

onBeforeUnmount(() => {
speakingWhileMutedWarner.value.destroy()
})
}

/**
* Check if component is visible and not obstructed by others
* @param element HTML element
*/
function isVisible(element) {
if (!element) {
return false // Element doesn't exist, therefore - not visible
}
const rect = element.getBoundingClientRect()
return document.elementsFromPoint(rect.left, rect.top)?.[0] === element
}

return {
boundaryElement,
popover,
popupShown,
speakingWhileMutedWarner,
}
},

computed: {
isAudioAllowed() {
return this.conversation.permissions & PARTICIPANT.PERMISSIONS.PUBLISH_AUDIO
Expand Down Expand Up @@ -152,4 +214,10 @@ export default {
.no-audio-available {
opacity: .7;
}

.popover-hint {
padding: calc(3 * var(--default-grid-baseline));
max-width: 300px;
text-align: left;
}
</style>
9 changes: 0 additions & 9 deletions src/components/TopBar/TopBarMediaControls.vue
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,6 @@ import { useIsInCall } from '../../composables/useIsInCall.js'
import { PARTICIPANT } from '../../constants.ts'
import { CONNECTION_QUALITY } from '../../utils/webrtc/analyzers/PeerConnectionAnalyzer.js'
import { callAnalyzer } from '../../utils/webrtc/index.js'
import SpeakingWhileMutedWarner from '../../utils/webrtc/SpeakingWhileMutedWarner.js'

export default {

Expand Down Expand Up @@ -397,14 +396,6 @@ export default {
},
},

mounted() {
this.speakingWhileMutedWarner = new SpeakingWhileMutedWarner(this.model)
},

beforeDestroy() {
this.speakingWhileMutedWarner.destroy()
},

methods: {
t,

Expand Down
98 changes: 48 additions & 50 deletions src/utils/webrtc/SpeakingWhileMutedWarner.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,41 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { showWarning, TOAST_PERMANENT_TIMEOUT } from '@nextcloud/dialogs'
import { TOAST_DEFAULT_TIMEOUT } from '@nextcloud/dialogs'
import { t } from '@nextcloud/l10n'

/**
* Helper to warn the user if they are talking while muted.
*
* The WebRTC helper emits events when it detects that the user is speaking
* while muted; this helper shows a warning to the user based on those
* events.
*
* The warning is not immediately shown, though; the WebRTC helper flags
* even short sounds as "speaking" (provided they are strong enough), so to
* prevent unnecesary warnings the user has to speak for a few seconds for
* the warning to be shown. On the other hand, the warning is hidden as soon
* as the WebRTC helper detects that the speaking has stopped; in this case
* there is no delay, as the helper itself has a delay before emitting the
* event.
*
* The way of warning the user changes depending on whether Talk is visible
* or not; if it is visible the warning is shown in the Talk UI, but if it
* is not it is shown using a browser notification, which will be visible
* to the user even if the browser window is not in the foreground (provided
* the user granted the permissions to receive notifications from the site).
*
* @param {object} LocalMediaModel the model that emits "speakingWhileMuted"
* events.
* "setSpeakingWhileMutedNotification" method.
*/
* Helper to warn the user if they are talking while muted.
*
* The WebRTC helper emits events when it detects that the user is speaking
* while muted; this helper shows a warning to the user based on those
* events.
*
* The warning is not immediately shown, though; the WebRTC helper flags
* even short sounds as "speaking" (provided they are strong enough), so to
* prevent unnecessary warnings the user has to speak for a few seconds for
* the warning to be shown. On the other hand, the warning is hidden as soon
* as the WebRTC helper detects that the speaking has stopped; in this case
* there is no delay, as the helper itself has a delay before emitting the
* event.
*
* The way of warning the user changes depending on whether Talk is visible
* or not; if it is visible the warning is shown in the Talk UI, but if it
* is not it is shown using a browser notification, which will be visible
* to the user even if the browser window is not in the foreground (provided
* the user granted the permissions to receive notifications from the site).
*
* @param {object} LocalMediaModel the model that emits "speakingWhileMuted"
* events.
*/
export default function SpeakingWhileMutedWarner(LocalMediaModel) {
this._model = LocalMediaModel
this._toast = null
this._startedSpeakingTimeout = undefined
this._startedShowWarningTimeout = undefined

/** Public properties to use in Vue components */
this.message = t('spreed', 'You seem to be talking while muted, please unmute yourself for others to hear you')
this.showPopup = false

this._handleSpeakingWhileMutedChangeBound = this._handleSpeakingWhileMutedChange.bind(this)

Expand All @@ -42,6 +46,7 @@ export default function SpeakingWhileMutedWarner(LocalMediaModel) {
SpeakingWhileMutedWarner.prototype = {

destroy() {
this._hideWarning()
this._model.off('change:speakingWhileMuted', this._handleSpeakingWhileMutedChangeBound)
},

Expand Down Expand Up @@ -71,40 +76,28 @@ SpeakingWhileMutedWarner.prototype = {
},

_showWarning() {
const message = t('spreed', 'You seem to be talking while muted, please unmute yourself for others to hear you')

if (!document.hidden) {
this._showNotification(message)
this.showPopup = true
} else {
this._pendingBrowserNotification = true

this._showBrowserNotification(message).catch(function() {
this._showBrowserNotification().catch(function() {
if (this._pendingBrowserNotification) {
this._pendingBrowserNotification = false

this._showNotification(message)
this.showPopup = true
}
}.bind(this))
}
},

_showNotification(message) {
if (this._toast) {
return
}
this._startedShowWarningTimeout = setTimeout(function() {
delete this._startedShowWarningTimeout

this._toast = showWarning(message, {
timeout: TOAST_PERMANENT_TIMEOUT,
onClick: () => {
this._toast.hideToast()
},
onRemove: () => {
this._toast = null
}
})
this._hideWarning()
}.bind(this), TOAST_DEFAULT_TIMEOUT)
},

_showBrowserNotification(message) {
_showBrowserNotification() {
return new Promise(function(resolve, reject) {
if (this._browserNotification) {
resolve()
Expand All @@ -127,7 +120,7 @@ SpeakingWhileMutedWarner.prototype = {

if (Notification.permission === 'granted') {
this._pendingBrowserNotification = false
this._browserNotification = new Notification(message)
this._browserNotification = new Notification(this.message)
resolve()

return
Expand All @@ -137,7 +130,7 @@ SpeakingWhileMutedWarner.prototype = {
if (permission === 'granted') {
if (this._pendingBrowserNotification) {
this._pendingBrowserNotification = false
this._browserNotification = new Notification(message)
this._browserNotification = new Notification(this.message)
}
resolve()
} else {
Expand All @@ -150,15 +143,20 @@ SpeakingWhileMutedWarner.prototype = {
_hideWarning() {
this._pendingBrowserNotification = false

if (this._toast) {
this._toast.hideToast()
if (this.showPopup) {
this.showPopup = false
}

if (this._browserNotification) {
this._browserNotification.close()

this._browserNotification = null
}

if (this._startedShowWarningTimeout) {
clearTimeout(this._startedShowWarningTimeout)
delete this._startedShowWarningTimeout
}
},

}

0 comments on commit 00e504a

Please sign in to comment.