Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added "Raise Hand" feature #4569

Merged
merged 20 commits into from Dec 16, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 4 additions & 3 deletions docs/capabilities.md
Expand Up @@ -56,8 +56,9 @@ title: Capabilities
* `sip-support` - Whether conversations API v3 exists and SIP can be configured and enabled by moderators. The conversations API will come with some new values `sipEnabled` which signals whether this conversation has SIP configured as well as `canEnableSIP` to see if a user can enable it. When it is enabled `attendeePin` will contain the unique dial-in code for this user.

## 11.0
* `config => previews => max-gif-size` - Maximum size in bytes below which a GIF can be embedded directly in the page at render time. Bigger files will be rendered statically using the preview endpoint instead. Can be set with `occ config:app:set spreed max-gif-size --value=X` where X is the new value in bytes. Defaults to 3 MB.
* `chat-read-status` - On conversation API v3 and the chat API the last common read message is exposed which can be used to update the "read status" flag of own chat messages. The info should be shown only when the user also shares their read status. The user's value can be found in `config => chat => read-privacy`.
* `config => chat => read-privacy` - See `chat-read-status`
* `phonebook-search` - Is present when the server has the endpoint to search for phone numbers to find matches in the accounts list
* `listable-rooms` - Conversations can searched for even when not joined. A "listable" attribute set on rooms defines the scope of who can find it.
* `phonebook-search` - Is present when the server has the endpoint to search for phone numbers to find matches in the accounts list
* `raise-hand` - Participants can raise or lower hand, the state change is sent through signaling messages.
* `config => chat => read-privacy` - See `chat-read-status`
* `config => previews => max-gif-size` - Maximum size in bytes below which a GIF can be embedded directly in the page at render time. Bigger files will be rendered statically using the preview endpoint instead. Can be set with `occ config:app:set spreed max-gif-size --value=X` where X is the new value in bytes. Defaults to 3 MB.
PVince81 marked this conversation as resolved.
Show resolved Hide resolved
1 change: 1 addition & 0 deletions lib/Capabilities.php
Expand Up @@ -84,6 +84,7 @@ public function getCapabilities(): array {
'sip-support',
'chat-read-status',
'phonebook-search',
'raise-hand',
],
'config' => [
'attachments' => [
Expand Down
31 changes: 31 additions & 0 deletions src/components/CallView/CallView.vue
Expand Up @@ -144,6 +144,7 @@
import Grid from './Grid/Grid'
import { localMediaModel, localCallParticipantModel, callParticipantCollection } from '../../utils/webrtc/index'
import { fetchPeers } from '../../services/callsService'
import { showMessage } from '@nextcloud/dialogs'
import LocalMediaControls from './shared/LocalMediaControls'
import EmptyCallView from './shared/EmptyCallView'
import Video from './shared/Video'
Expand Down Expand Up @@ -182,6 +183,7 @@ export default {
localMediaModel: localMediaModel,
localCallParticipantModel: localCallParticipantModel,
sharedDatas: {},
raisedHandUnwatchers: {},
speakingUnwatchers: {},
screenUnwatchers: {},
speakers: [],
Expand Down Expand Up @@ -401,6 +403,11 @@ export default {
// Not reactive, but not a problem
delete this.screenUnwatchers[removedModelId]

this.raisedHandUnwatchers[removedModelId]()
// Not reactive, but not a problem
delete this.raisedHandUnwatchers[removedModelId]
this.$store.dispatch('setParticipantHandRaised', { peerId: removedModelId, raised: false })

const index = this.speakers.findIndex(speaker => speaker.id === removedModelId)
this.speakers.splice(index, 1)

Expand Down Expand Up @@ -434,6 +441,13 @@ export default {
}, function(screen) {
this._setScreenAvailable(addedModel.attributes.peerId, screen)
})

// Not reactive, but not a problem
this.raisedHandUnwatchers[addedModel.attributes.peerId] = this.$watch(function() {
return addedModel.attributes.raisedHand
}, function(raisedHand) {
this._handleParticipantRaisedHand(addedModel, raisedHand)
})
})
},

Expand Down Expand Up @@ -467,6 +481,23 @@ export default {
}
},

_handleParticipantRaisedHand(callParticipantModel, raisedHand) {
const nickName = callParticipantModel.attributes.name || callParticipantModel.attributes.userId
// sometimes the nick name is not available yet...
if (nickName) {
if (raisedHand) {
showMessage(t('spreed', 'Participant {nickName} raised their hand.', { nickName: nickName }))
PVince81 marked this conversation as resolved.
Show resolved Hide resolved
}
} else {
if (raisedHand) {
showMessage(t('spreed', 'A participant raised their hand.'))
PVince81 marked this conversation as resolved.
Show resolved Hide resolved
}
}

// update in callViewStore
this.$store.dispatch('setParticipantHandRaised', { peerId: callParticipantModel.attributes.peerId, raised: raisedHand })
},

_setScreenAvailable(id, screen) {
if (screen) {
this.screens.unshift(id)
Expand Down
77 changes: 74 additions & 3 deletions src/components/CallView/shared/LocalMediaControls.vue
Expand Up @@ -17,7 +17,6 @@
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-->

<template>
<div v-shortkey.push="['space']"
@shortkey="handleShortkey">
Expand Down Expand Up @@ -81,6 +80,42 @@
</li>
</ul>
</div>
<button
PVince81 marked this conversation as resolved.
Show resolved Hide resolved
v-shortkey.once="['r']"
v-tooltip="t('spreed', 'Lower hand')"
class="lower-hand"
:class="model.attributes.raisedHand ? '' : 'hidden-visually'"
:tabindex="model.attributes.raisedHand ? 0 : -1"
:aria-label="t('spreed', 'Lower hand')"
@shortkey="toggleHandRaised"
@click.stop="toggleHandRaised">
<Hand
:size="24"
title=""
fill-color="#ffffff"
decorative />
</button>
<Actions
v-tooltip="t('spreed', 'More actions')"
:aria-label="t('spreed', 'More actions')">
<ActionButton
:close-after-click="true"
@click="toggleHandRaised">
<Hand
slot="icon"
:size="16"
decorative
title="" />
{{ raiseHandButtonLabel }}
</ActionButton>
<ActionSeparator />
<ActionButton
icon="icon-settings"
:close-after-click="true"
@click="showSettings">
{{ t('spreed', 'Settings') }}
</ActionButton>
</Actions>
</div>
<div class="network-connection-state">
<Popover
Expand Down Expand Up @@ -122,11 +157,14 @@

<script>
import escapeHtml from 'escape-html'
import { emit } from '@nextcloud/event-bus'
import { showMessage } from '@nextcloud/dialogs'
import Hand from 'vue-material-design-icons/Hand'
import Popover from '@nextcloud/vue/dist/Components/Popover'
import Tooltip from '@nextcloud/vue/dist/Directives/Tooltip'
import SpeakingWhileMutedWarner from '../../../utils/webrtc/SpeakingWhileMutedWarner'
import NetworkStrength2Alert from 'vue-material-design-icons/NetworkStrength2Alert'
import { Actions, ActionSeparator, ActionButton } from '@nextcloud/vue'
import { callAnalyzer } from '../../../utils/webrtc/index'
import { CONNECTION_QUALITY } from '../../../utils/webrtc/analyzers/PeerConnectionAnalyzer'

Expand All @@ -141,6 +179,10 @@ export default {
components: {
NetworkStrength2Alert,
Popover,
Actions,
ActionSeparator,
ActionButton,
Hand,
},

props: {
Expand Down Expand Up @@ -171,10 +213,17 @@ export default {
qualityWarningInGracePeriodTimeout: null,
audioEnabledBeforeSpacebarKeydown: undefined,
spacebarKeyDown: false,
raisingHandNotification: null,
}
},

computed: {
raiseHandButtonLabel() {
if (!this.model.attributes.raisedHand) {
return t('spreed', 'Raise hand')
}
return t('spreed', 'Lower hand')
},

audioButtonClass() {
return {
Expand Down Expand Up @@ -448,6 +497,10 @@ export default {
this.$refs.volumeIndicator.style.height = height + 'px'
},

showSettings() {
emit('show-settings')
},

/**
* This method executes on spacebar keydown and keyup
*/
Expand Down Expand Up @@ -519,6 +572,16 @@ export default {
}
},

toggleHandRaised() {
const raisedHand = !this.model.attributes.raisedHand
if (this.raisingHandNotification) {
this.raisingHandNotification.hideToast()
this.raisingHandNotification = null
}
this.model.toggleHandRaised(raisedHand)
this.$store.dispatch('setParticipantHandRaised', { peerId: this.localCallParticipantModel.attributes.peerId, raised: raisedHand })
},

shareScreen() {
if (!this.model.attributes.localScreen) {
this.startShareScreen('screen')
Expand Down Expand Up @@ -628,6 +691,12 @@ export default {
border-bottom-color: transparent;
}

.buttons-bar {
button, .action-item {
vertical-align: middle;
}
}

.buttons-bar button, .buttons-bar button:active {
background-color: transparent;
border: none;
Expand All @@ -644,13 +713,15 @@ export default {

.buttons-bar button.audio-disabled,
.buttons-bar button.video-disabled,
.buttons-bar button.screensharing-disabled {
.buttons-bar button.screensharing-disabled,
.buttons-bar button.lower-hand {
opacity: .7;
}

.buttons-bar button.audio-disabled:not(.no-audio-available),
.buttons-bar button.video-disabled:not(.no-video-available),
.buttons-bar button.screensharing-disabled {
.buttons-bar button.screensharing-disabled,
.buttons-bar button.lower-hand {
&:hover,
&:focus {
opacity: 1;
Expand Down
25 changes: 25 additions & 0 deletions src/components/CallView/shared/VideoBottomBar.vue
Expand Up @@ -24,6 +24,17 @@
<div v-if="!isSidebar"
class="bottom-bar"
:class="{'bottom-bar--video-on' : hasShadow, 'bottom-bar--big': isBig }">
<transition name="fade">
<div
v-if="!connectionStateFailedNoRestart && model.attributes.raisedHand"
class="bottom-bar__statusIndicator">
<Hand
PVince81 marked this conversation as resolved.
Show resolved Hide resolved
class="handIndicator"
decorative
title=""
fill-color="#ffffff" />
</div>
</transition>
<transition name="fade">
<div v-show="showNameIndicator"
class="bottom-bar__nameIndicator"
Expand Down Expand Up @@ -71,10 +82,15 @@
import { ConnectionState } from '../../../utils/webrtc/models/CallParticipantModel'
import Tooltip from '@nextcloud/vue/dist/Directives/Tooltip'
import { PARTICIPANT } from '../../../constants'
import Hand from 'vue-material-design-icons/Hand'

export default {
name: 'VideoBottomBar',

components: {
Hand,
},

directives: {
tooltip: Tooltip,
},
Expand Down Expand Up @@ -247,6 +263,10 @@ export default {
font-weight: bold;
}
}
&__statusIndicator {
margin-left: 6px;
margin-right: 6px;
}
&__mediaIndicator {
position: relative;
background-size: 22px;
Expand All @@ -265,6 +285,11 @@ export default {
}
}

.handIndicator {
margin-top: 8px;
}

.handIndicator,
.muteIndicator,
.hideRemoteVideo,
.screensharingIndicator,
Expand Down
Expand Up @@ -86,6 +86,11 @@
:size="24"
title=""
decorative />
<Hand
v-if="callIcon === 'hand'"
:size="24"
title=""
decorative />
</div>
<Actions
v-if="canModerate && !isSearched"
Expand Down Expand Up @@ -129,6 +134,7 @@ import Actions from '@nextcloud/vue/dist/Components/Actions'
import Microphone from 'vue-material-design-icons/Microphone'
import Phone from 'vue-material-design-icons/Phone'
import Video from 'vue-material-design-icons/Video'
import Hand from 'vue-material-design-icons/Hand'
import { CONVERSATION, PARTICIPANT } from '../../../../../constants'
import UserStatus from '../../../../../mixins/userStatus'
import isEqual from 'lodash/isEqual'
Expand All @@ -145,6 +151,7 @@ export default {
Microphone,
Phone,
Video,
Hand,
},

directives: {
Expand Down Expand Up @@ -270,10 +277,20 @@ export default {
label() {
return this.participant.label
},
raisedHand() {
if (this.isSearched || this.participant.inCall === PARTICIPANT.CALL_FLAG_DISCONNECTED) {
return false
}

return this.$store.getters.isParticipantRaisedHand(this.participant.sessionId)
},
callIcon() {
if (this.isSearched || this.participant.inCall === PARTICIPANT.CALL_FLAG.DISCONNECTED) {
return ''
}
if (this.raisedHand) {
return 'hand'
}
const withVideo = this.participant.inCall & PARTICIPANT.CALL_FLAG.WITH_VIDEO
if (withVideo) {
return 'video'
Expand All @@ -291,6 +308,8 @@ export default {
return t('spreed', 'Joined with video')
} else if (this.callIcon === 'phone') {
return t('spreed', 'Joined via phone')
} else if (this.callIcon === 'hand') {
return t('spreed', 'Raised their hand')
}
return null
},
Expand Down
6 changes: 6 additions & 0 deletions src/components/SettingsDialog/SettingsDialog.vue
Expand Up @@ -100,6 +100,12 @@
{{ t('spreed', 'Push to talk or push to mute') }}
</dd>
</div>
<div>
<dt><kbd>R</kbd></dt>
<dd class="shortcut-description">
{{ t('spreed', 'Raise or lower hand') }}
</dd>
</div>
</dl>
</AppSettingsSection>
</AppSettingsDialog>
Expand Down