diff options
Diffstat (limited to 'src/components')
14 files changed, 614 insertions, 32 deletions
diff --git a/src/components/CallView/CallView.vue b/src/components/CallView/CallView.vue index c289a2129..5f470f3db 100644 --- a/src/components/CallView/CallView.vue +++ b/src/components/CallView/CallView.vue @@ -30,6 +30,7 @@ <LocalMediaControls class="local-media-controls" :class="{ 'local-media-controls--sidebar': isSidebar }" + :token="token" :model="localMediaModel" :show-actions="!isSidebar" :local-call-participant-model="localCallParticipantModel" @@ -90,6 +91,7 @@ :is-stripe="false" :show-controls="false" :is-big="true" + :token="token" :local-media-model="localMediaModel" :video-container-aspect-ratio="videoContainerAspectRatio" :local-call-participant-model="localCallParticipantModel" @@ -143,6 +145,7 @@ :show-controls="false" :fit-video="true" :is-stripe="true" + :token="token" :local-media-model="localMediaModel" :video-container-aspect-ratio="videoContainerAspectRatio" :local-call-participant-model="localCallParticipantModel" diff --git a/src/components/CallView/Grid/Grid.vue b/src/components/CallView/Grid/Grid.vue index d4191ff3e..e7b0f8d72 100644 --- a/src/components/CallView/Grid/Grid.vue +++ b/src/components/CallView/Grid/Grid.vue @@ -112,6 +112,7 @@ class="video" :is-grid="true" :fit-video="isStripe" + :token="token" :local-media-model="localMediaModel" :video-container-aspect-ratio="videoContainerAspectRatio" :local-call-participant-model="localCallParticipantModel" @@ -135,6 +136,7 @@ :fit-video="true" :is-stripe="true" :show-controls="false" + :token="token" :local-media-model="localMediaModel" :video-container-aspect-ratio="videoContainerAspectRatio" :local-call-participant-model="localCallParticipantModel" diff --git a/src/components/CallView/shared/LocalMediaControls.vue b/src/components/CallView/shared/LocalMediaControls.vue index a4c6a4382..a4fbb09b2 100644 --- a/src/components/CallView/shared/LocalMediaControls.vue +++ b/src/components/CallView/shared/LocalMediaControls.vue @@ -219,6 +219,7 @@ import Video from 'vue-material-design-icons/Video' import VideoOff from 'vue-material-design-icons/VideoOff' import Popover from '@nextcloud/vue/dist/Components/Popover' import Tooltip from '@nextcloud/vue/dist/Directives/Tooltip' +import { PARTICIPANT } from '../../../constants' import SpeakingWhileMutedWarner from '../../../utils/webrtc/SpeakingWhileMutedWarner' import NetworkStrength2Alert from 'vue-material-design-icons/NetworkStrength2Alert' import { Actions, ActionSeparator, ActionButton } from '@nextcloud/vue' @@ -249,6 +250,10 @@ export default { }, props: { + token: { + type: String, + required: true, + }, model: { type: Object, required: true, @@ -289,10 +294,26 @@ export default { return t('spreed', 'Lower hand (R)') }, + conversation() { + return this.$store.getters.conversation(this.token) || this.$store.getters.dummyConversation + }, + + isAudioAllowed() { + return this.conversation.publishingPermissions === PARTICIPANT.PUBLISHING_PERMISSIONS.ALL + }, + + isVideoAllowed() { + return this.conversation.publishingPermissions === PARTICIPANT.PUBLISHING_PERMISSIONS.ALL + }, + + isScreensharingAllowed() { + return this.conversation.publishingPermissions === PARTICIPANT.PUBLISHING_PERMISSIONS.ALL + }, + audioButtonClass() { return { - 'audio-disabled': this.model.attributes.audioAvailable && !this.model.attributes.audioEnabled, - 'no-audio-available': !this.model.attributes.audioAvailable, + 'audio-disabled': this.isAudioAllowed && this.model.attributes.audioAvailable && !this.model.attributes.audioEnabled, + 'no-audio-available': !this.isAudioAllowed || !this.model.attributes.audioAvailable, } }, @@ -301,6 +322,10 @@ export default { }, audioButtonTooltip() { + if (!this.isAudioAllowed) { + return t('spreed', 'You are not allowed to enable audio') + } + if (!this.model.attributes.audioAvailable) { return { content: t('spreed', 'No audio'), @@ -348,8 +373,8 @@ export default { videoButtonClass() { return { - 'video-disabled': this.model.attributes.videoAvailable && !this.model.attributes.videoEnabled, - 'no-video-available': !this.model.attributes.videoAvailable, + 'video-disabled': this.isVideoAllowed && this.model.attributes.videoAvailable && !this.model.attributes.videoEnabled, + 'no-video-available': !this.isVideoAllowed || !this.model.attributes.videoAvailable, } }, @@ -358,6 +383,10 @@ export default { }, videoButtonTooltip() { + if (!this.isVideoAllowed) { + return t('spreed', 'You are not allowed to enable video') + } + if (!this.model.attributes.videoAvailable) { return t('spreed', 'No camera') } @@ -391,15 +420,24 @@ export default { screenSharingButtonClass() { return { - 'screensharing-disabled': !this.model.attributes.localScreen, + 'screensharing-disabled': this.isScreensharingAllowed && !this.model.attributes.localScreen, + 'no-screensharing-available': !this.isScreensharingAllowed, } }, screenSharingButtonTooltip() { + if (!this.isScreensharingAllowed) { + return t('spreed', 'You are not allowed to enable screensharing') + } + if (this.screenSharingMenuOpen) { return null } + if (!this.isScreensharingAllowed) { + return t('spreed', 'No screensharing') + } + return this.model.attributes.localScreen ? t('spreed', 'Screensharing options') : t('spreed', 'Enable screensharing') }, @@ -614,6 +652,10 @@ export default { }, toggleScreenSharingMenu() { + if (!this.isScreensharingAllowed) { + return + } + if (!this.model.getWebRtc().capabilities.supportScreenSharing) { if (window.location.protocol === 'https:') { showMessage(t('spreed', 'Screen sharing is not supported by your browser.')) @@ -746,7 +788,7 @@ export default { .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:not(.no-screensharing-available), .buttons-bar button.lower-hand { &:hover, &:focus { @@ -755,7 +797,8 @@ export default { } .buttons-bar button.no-audio-available, -.buttons-bar button.no-video-available { +.buttons-bar button.no-video-available, +.buttons-bar button.no-screensharing-available { &, & * { opacity: .7; cursor: not-allowed; @@ -763,7 +806,8 @@ export default { } .buttons-bar button.no-audio-available:active, -.buttons-bar button.no-video-available:active { +.buttons-bar button.no-video-available:active, +.buttons-bar button.no-screensharing-available:active { background-color: transparent; } diff --git a/src/components/CallView/shared/LocalVideo.vue b/src/components/CallView/shared/LocalVideo.vue index 534d192bb..8886f8959 100644 --- a/src/components/CallView/shared/LocalVideo.vue +++ b/src/components/CallView/shared/LocalVideo.vue @@ -53,6 +53,7 @@ <LocalMediaControls v-if="showControls" class="local-media-controls" + :token="token" :model="localMediaModel" :local-call-participant-model="localCallParticipantModel" :screen-sharing-button-hidden="isSidebar" @@ -97,6 +98,10 @@ export default { mixins: [video], props: { + token: { + type: String, + required: true, + }, localMediaModel: { type: Object, required: true, diff --git a/src/components/Description/Description.vue b/src/components/Description/Description.vue index 300b91980..1d709c6b2 100644 --- a/src/components/Description/Description.vue +++ b/src/components/Description/Description.vue @@ -38,7 +38,7 @@ <template v-if="editing"> <button class="nc-button nc-button__main description__action" - :aria-label="t('spreed','Cancel editing description')" + :aria-label="t('spreed', 'Cancel editing description')" @click="handleCancelEditing"> <Close decorative @@ -47,7 +47,7 @@ </button> <button class="nc-button nc-button__main primary description__action" - :aria-label="t('spreed','Submit conversation description')" + :aria-label="t('spreed', 'Submit conversation description')" :disabled="!canSubmit" @click="handleSubmitDescription"> <Check @@ -65,7 +65,7 @@ </template> <button v-if="!editing && editable" class="nc-button nc-button__main" - :aria-label="t('spreed','Edit conversation description')" + :aria-label="t('spreed', 'Edit conversation description')" @click="handleEditDescription"> <Pencil decorative diff --git a/src/components/LeftSidebar/NewGroupConversation/NewGroupConversation.vue b/src/components/LeftSidebar/NewGroupConversation/NewGroupConversation.vue index 647b1772f..df32e225c 100644 --- a/src/components/LeftSidebar/NewGroupConversation/NewGroupConversation.vue +++ b/src/components/LeftSidebar/NewGroupConversation/NewGroupConversation.vue @@ -22,10 +22,10 @@ <template> <div class="wrapper"> <button slot="trigger" - v-tooltip.bottom="t('spreed','Create a new group conversation')" + v-tooltip.bottom="t('spreed', 'Create a new group conversation')" class="toggle" icon="" - :aria-label="t('spreed','Create a new group conversation')" + :aria-label="t('spreed', 'Create a new group conversation')" @click="showModal"> <Plus decorative diff --git a/src/components/MessagesList/MessagesGroup/Message/Message.vue b/src/components/MessagesList/MessagesGroup/Message/Message.vue index 6671b01ad..93065ed19 100644 --- a/src/components/MessagesList/MessagesGroup/Message/Message.vue +++ b/src/components/MessagesList/MessagesGroup/Message/Message.vue @@ -151,6 +151,12 @@ the main body of the message as well as a quote. @click.stop="handleMarkAsUnread"> {{ t('spreed', 'Mark as unread') }} </ActionButton> + <ActionLink + v-if="linkToFile" + icon="icon-text" + :href="linkToFile"> + {{ t('spreed', 'Go to file') }} + </ActionLink> <ActionSeparator v-if="messageActions.length > 0" /> <template v-for="action in messageActions"> @@ -187,6 +193,7 @@ the main body of the message as well as a quote. <script> import ActionButton from '@nextcloud/vue/dist/Components/ActionButton' +import ActionLink from '@nextcloud/vue/dist/Components/ActionLink' import Actions from '@nextcloud/vue/dist/Components/Actions' import ActionSeparator from '@nextcloud/vue/dist/Components/ActionSeparator' import Tooltip from '@nextcloud/vue/dist/Directives/Tooltip' @@ -226,6 +233,7 @@ export default { components: { Actions, ActionButton, + ActionLink, CallButton, Quote, RichText, @@ -562,18 +570,30 @@ export default { && this.actorType === this.$store.getters.getActorType() }, + isFileShare() { + return this.message === '{file}' && this.messageParameters?.file + }, + + linkToFile() { + if (this.isFileShare) { + return this.messageParameters?.file?.link + } + return '' + }, + isDeleteable() { if (this.isConversationReadOnly) { return false } - const isFileShare = this.message === '{file}' - && this.messageParameters?.file + const isObjectShare = this.message === '{object}' + && this.messageParameters?.object return (moment(this.timestamp * 1000).add(6, 'h')) > moment() && this.messageType === 'comment' && !this.isDeleting - && !isFileShare + && !this.isFileShare + && !isObjectShare && (this.isMyMsg || (this.conversation.type !== CONVERSATION.TYPE.ONE_TO_ONE && (this.participant.participantType === PARTICIPANT.TYPE.OWNER diff --git a/src/components/MessagesList/MessagesGroup/Message/MessagePart/AudioPlayer.vue b/src/components/MessagesList/MessagesGroup/Message/MessagePart/AudioPlayer.vue new file mode 100644 index 000000000..64540cf1d --- /dev/null +++ b/src/components/MessagesList/MessagesGroup/Message/MessagePart/AudioPlayer.vue @@ -0,0 +1,82 @@ +<!-- + - @copyright Copyright (c) 2021 Marco Ambrosini <marcoambrosini@pm.me> + - + - @author Marco Ambrosini <marcoambrosini@pm.me> + - + - @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 <http://www.gnu.org/licenses/>. +--> + +<template> + <div class="wrapper"> + <audio + controls + :src="fileURL"> + {{ t('spreed', 'Your browser does not support playing audio files') }} + </audio> + </div> +</template> + +<script> +import { generateRemoteUrl } from '@nextcloud/router' +import { encodePath } from '@nextcloud/paths' + +export default { + name: 'AudioPlayer', + + props: { + /** + * File name + */ + name: { + type: String, + required: true, + }, + link: { + type: String, + required: true, + }, + /** + * File path relative to the user's home storage, + * or link share root, includes the file name. + */ + path: { + type: String, + required: true, + }, + }, + + computed: { + internalAbsolutePath() { + if (this.path.startsWith('/')) { + return this.path + } + return '/' + this.path + }, + + fileURL() { + const userId = this.$store.getters.getUserId() + if (userId === null) { + // guest mode, use public link download URL + return this.link + '/download/' + encodePath(this.name) + } else { + // use direct DAV URL + return generateRemoteUrl(`dav/files/${userId}`) + encodePath(this.internalAbsolutePath) + } + }, + }, + +} +</script> diff --git a/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue b/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue index b013d62d0..162f1e3b7 100644 --- a/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue +++ b/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue @@ -26,7 +26,7 @@ :tabindex="wrapperTabIndex" class="file-preview" :class="{ 'file-preview--viewer-available': isViewerAvailable, 'file-preview--upload-editor': isUploadEditor }" - @click="handleClick" + @click.exact="handleClick" @keydown.enter="handleClick"> <div v-if="!isLoading" @@ -78,6 +78,7 @@ import Close from 'vue-material-design-icons/Close' import PlayCircleOutline from 'vue-material-design-icons/PlayCircleOutline' import { getCapabilities } from '@nextcloud/capabilities' import { encodePath } from '@nextcloud/paths' +import AudioPlayer from './AudioPlayer' const PREVIEW_TYPE = { TEMPORARY: 0, @@ -236,6 +237,13 @@ export default { is: 'div', tag: 'div', } + } else if (this.mimetype.startsWith('audio')) { + return { + is: AudioPlayer, + name: this.name, + path: this.path, + link: this.link, + } } return { is: 'a', diff --git a/src/components/NewMessageForm/AdvancedInput/AdvancedInput.vue b/src/components/NewMessageForm/AdvancedInput/AdvancedInput.vue index a0d11281d..222a51846 100644 --- a/src/components/NewMessageForm/AdvancedInput/AdvancedInput.vue +++ b/src/components/NewMessageForm/AdvancedInput/AdvancedInput.vue @@ -235,6 +235,7 @@ export default { } }, }, + mounted() { this.focusInput() /** @@ -245,10 +246,12 @@ export default { this.atWhoPanelExtraClasses = 'talk candidate-mentions' }, + beforeDestroy() { EventBus.$off('routeChange', this.focusInput) EventBus.$off('focusChatInput', this.focusInput) }, + methods: { onBlur() { // requires a short delay to avoid blocking click event handlers @@ -412,10 +415,10 @@ export default { overflow: visible; width: 100%; border:none; - margin: 0 6px !important; + margin: 0 4px !important; word-break: break-word; white-space: pre-wrap; - padding: 8px 16px; + padding: 8px 16px 8px 48px; } // Support for the placeholder text in the div contenteditable diff --git a/src/components/NewMessageForm/AudioRecorder/AudioRecorder.vue b/src/components/NewMessageForm/AudioRecorder/AudioRecorder.vue new file mode 100644 index 000000000..74b31da29 --- /dev/null +++ b/src/components/NewMessageForm/AudioRecorder/AudioRecorder.vue @@ -0,0 +1,351 @@ +<!-- + - @copyright Copyright (c) 2021 Marco Ambrosini <marcoambrosini@pm.me> + - + - @author Marco Ambrosini <marcoambrosini@pm.me> + - + - @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 <http://www.gnu.org/licenses/>. +--> + +<template> + <div + class="audio-recorder"> + <button + v-if="!isRecording" + v-tooltip.auto="{ + content: startRecordingTooltip, + delay: tooltipDelay, + }" + class="audio-recorder__trigger nc-button nc-button__main" + @click="start"> + <Microphone + :size="16" + title="" + decorative /> + </button> + <div v-else class="wrapper"> + <button + v-tooltip.auto="{ + content: abortRecordingTooltip, + delay: tooltipDelay, + }" + class="audio-recorder__stop nc-button nc-button__main" + @click="abortRecording"> + <Close + :size="16" + title="" + decorative /> + </button> + <div class="audio-recorder__info"> + <div class="recording-indicator fadeOutIn" /> + <span + class="time"> + {{ parsedRecordTime }}</span> + </div> + <button + v-tooltip.auto="{ + content: stopRecordingTooltip, + delay: tooltipDelay, + }" + class="audio-recorder__trigger nc-button nc-button__main" + :class="{'audio-recorder__trigger--recording': isRecording}" + @click="stop"> + <Check + :size="16" + title="" + decorative /> + </button> + </div> + </div> +</template> + +<script> +import Microphone from 'vue-material-design-icons/Microphone' +import Close from 'vue-material-design-icons/Close' +import Check from 'vue-material-design-icons/Check' +import Tooltip from '@nextcloud/vue/dist/Directives/Tooltip' +import { mediaDevicesManager } from '../../../utils/webrtc/index' +import { showError } from '@nextcloud/dialogs' + +export default { + name: 'AudioRecorder', + + components: { + Microphone, + Close, + Check, + }, + + directives: { + tooltip: Tooltip, + }, + + data() { + return { + // The audio stream object + audioStream: null, + // The media recorder which generate the recorded chunks + mediaRecorder: null, + // The chunks array + chunks: [], + // The final audio file blob + blob: null, + // The blob url + URL: '', + // Switched to true if the recording is aborted |