Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/spreed.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/App.vue47
-rw-r--r--src/FilesSidebarCallViewApp.vue2
-rw-r--r--src/FilesSidebarTabApp.vue4
-rw-r--r--src/PublicShareAuthRequestPasswordButton.vue42
-rw-r--r--src/PublicShareAuthSidebar.vue2
-rw-r--r--src/PublicShareSidebar.vue25
-rw-r--r--src/PublicShareSidebarTrigger.vue73
-rw-r--r--src/assets/buttons.scss58
-rw-r--r--src/components/AdminSettings/AllowedGroups.vue15
-rw-r--r--src/components/AdminSettings/Command.vue2
-rw-r--r--src/components/AdminSettings/Commands.vue7
-rw-r--r--src/components/AdminSettings/GeneralSettings.vue43
-rw-r--r--src/components/AdminSettings/HostedSignalingServer.vue38
-rw-r--r--src/components/AdminSettings/MatterbridgeIntegration.vue24
-rw-r--r--src/components/AdminSettings/SIPBridge.vue22
-rw-r--r--src/components/AdminSettings/SignalingServer.vue25
-rw-r--r--src/components/AdminSettings/SignalingServers.vue51
-rw-r--r--src/components/AdminSettings/StunServer.vue30
-rw-r--r--src/components/AdminSettings/StunServers.vue47
-rw-r--r--src/components/AdminSettings/TurnServer.vue41
-rw-r--r--src/components/AdminSettings/TurnServers.vue43
-rw-r--r--src/components/AdminSettings/WebServerSetupChecks.vue63
-rw-r--r--src/components/AvatarWrapper/AvatarWrapper.vue17
-rw-r--r--src/components/AvatarWrapper/AvatarWrapperSmall.vue4
-rw-r--r--src/components/CallView/CallView.vue4
-rw-r--r--src/components/CallView/Grid/Grid.vue16
-rw-r--r--src/components/CallView/shared/EmptyCallView.vue13
-rw-r--r--src/components/CallView/shared/LocalMediaControls.vue284
-rw-r--r--src/components/CallView/shared/Video.vue2
-rw-r--r--src/components/CallView/shared/VideoBackground.vue4
-rw-r--r--src/components/CallView/shared/VideoBottomBar.vue30
-rw-r--r--src/components/ConversationIcon.vue24
-rw-r--r--src/components/ConversationSettings/ConversationPermissionsSettings.vue20
-rw-r--r--src/components/ConversationSettings/ConversationSettingsDialog.vue15
-rw-r--r--src/components/ConversationSettings/DangerZone.vue11
-rw-r--r--src/components/ConversationSettings/ExpirationSettings.vue135
-rw-r--r--src/components/ConversationSettings/LinkShareSettings.vue44
-rw-r--r--src/components/ConversationSettings/Matterbridge/BridgePart.vue4
-rw-r--r--src/components/ConversationSettings/Matterbridge/MatterbridgeSettings.vue41
-rw-r--r--src/components/ConversationSettings/NotificationsSettings.vue22
-rw-r--r--src/components/Description/Description.vue17
-rw-r--r--src/components/DeviceChecker/DeviceChecker.vue32
-rw-r--r--src/components/LeftSidebar/ConversationsList/Conversation.vue29
-rw-r--r--src/components/LeftSidebar/ConversationsList/ConversationsList.vue4
-rw-r--r--src/components/LeftSidebar/LeftSidebar.vue121
-rw-r--r--src/components/LeftSidebar/NewGroupConversation/Confirmation/Confirmation.vue6
-rw-r--r--src/components/LeftSidebar/NewGroupConversation/NewGroupConversation.vue12
-rw-r--r--src/components/LeftSidebar/NewGroupConversation/PasswordProtect/PasswordProtect.vue4
-rw-r--r--src/components/LeftSidebar/NewGroupConversation/SetContacts/ContactSelectionBubble/ContactSelectionBubble.vue33
-rw-r--r--src/components/LeftSidebar/NewGroupConversation/SetContacts/SetContacts.vue39
-rw-r--r--src/components/LeftSidebar/NewGroupConversation/SetConversationName/SetConversationName.vue4
-rw-r--r--src/components/LeftSidebar/NewGroupConversation/SetConversationType/SetConversationType.vue4
-rw-r--r--src/components/LeftSidebar/SearchBox/SearchBox.vue32
-rw-r--r--src/components/LobbyScreen.vue4
-rw-r--r--src/components/MessagesList/MessagesGroup/Message/Message.spec.js21
-rw-r--r--src/components/MessagesList/MessagesGroup/Message/Message.vue67
-rw-r--r--src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/Forwarder.vue (renamed from src/components/MessagesList/MessagesGroup/Message/MessagePart/Forwarder.vue)7
-rw-r--r--src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.spec.js1
-rw-r--r--src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.vue54
-rw-r--r--src/components/MessagesList/MessagesGroup/Message/MessagePart/AudioPlayer.vue4
-rw-r--r--src/components/MessagesList/MessagesGroup/Message/MessagePart/Contact.vue4
-rw-r--r--src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue19
-rw-r--r--src/components/MessagesList/MessagesGroup/Message/MessagePart/Location.vue21
-rw-r--r--src/components/MessagesList/MessagesGroup/MessagesGroup.vue4
-rw-r--r--src/components/MessagesList/MessagesList.vue10
-rw-r--r--src/components/NewMessageForm/AdvancedInput/AdvancedInput.vue4
-rw-r--r--src/components/NewMessageForm/AudioRecorder/AudioRecorder.vue23
-rw-r--r--src/components/NewMessageForm/NewMessageForm.vue35
-rw-r--r--src/components/PermissionsEditor/PermissionsEditor.vue11
-rw-r--r--src/components/Quote.vue29
-rw-r--r--src/components/RightSidebar/Participants/ParticipantsList/Participant/Participant.vue58
-rw-r--r--src/components/RightSidebar/Participants/ParticipantsList/Participant/ParticipantPermissionsEditor/ParticipantPermissionsEditor.vue4
-rw-r--r--src/components/RightSidebar/Participants/ParticipantsSearchResults/ParticipantsSearchResults.vue13
-rw-r--r--src/components/RightSidebar/Participants/ParticipantsTab.vue4
-rw-r--r--src/components/RightSidebar/RightSidebar.vue36
-rw-r--r--src/components/RightSidebar/SharedItems/SharedItems.vue7
-rw-r--r--src/components/RightSidebar/SharedItems/SharedItemsBrowser/SharedItemsBrowser.vue8
-rw-r--r--src/components/RightSidebar/SharedItems/SharedItemsTab.vue8
-rw-r--r--src/components/SetGuestUsername.vue15
-rw-r--r--src/components/SettingsDialog/SettingsDialog.vue26
-rw-r--r--src/components/TopBar/CallButton.vue56
-rw-r--r--src/components/TopBar/TopBar.vue52
-rw-r--r--src/components/UploadEditor.vue12
-rw-r--r--src/components/VolumeIndicator/VolumeIndicator.vue4
-rw-r--r--src/components/missingMaterialDesignIcons/CancelPresentation.vue24
-rw-r--r--src/components/missingMaterialDesignIcons/CategoryMonitoring.vue39
-rw-r--r--src/components/missingMaterialDesignIcons/GridView.vue52
-rw-r--r--src/components/missingMaterialDesignIcons/Lobby.vue37
-rw-r--r--src/components/missingMaterialDesignIcons/MenuPeople.vue37
-rw-r--r--src/components/missingMaterialDesignIcons/PresentToAll.vue24
-rw-r--r--src/components/missingMaterialDesignIcons/PromotedView.vue52
-rw-r--r--src/init.js2
-rw-r--r--src/main.js2
-rw-r--r--src/mainFilesSidebar.js2
-rw-r--r--src/mainFilesSidebarLoader.js4
-rw-r--r--src/mainPublicShareAuthSidebar.js2
-rw-r--r--src/mainPublicShareSidebar.js16
-rw-r--r--src/mixins/browserCheck.js4
-rw-r--r--src/mixins/devices.js4
-rw-r--r--src/mixins/getParticipants.js4
-rw-r--r--src/mixins/sessionIssueHandler.js4
-rw-r--r--src/mixins/sharedItems.js4
-rw-r--r--src/mixins/talkHashCheck.js4
-rw-r--r--src/mixins/userStatus.js4
-rw-r--r--src/mixins/video.js4
-rw-r--r--src/router/router.js4
-rw-r--r--src/services/EventBus.js4
-rw-r--r--src/services/conversationsService.js17
-rw-r--r--src/services/filesIntegrationServices.js4
-rw-r--r--src/services/filesSharingServices.js4
-rw-r--r--src/services/messagesService.js4
-rw-r--r--src/services/participantsService.js4
-rw-r--r--src/services/sharedItemsService.js4
-rw-r--r--src/store/audioRecorderStore.js4
-rw-r--r--src/store/callViewStore.js4
-rw-r--r--src/store/conversationsStore.js14
-rw-r--r--src/store/fileUploadStore.js4
-rw-r--r--src/store/index.js4
-rw-r--r--src/store/integrationsStore.js4
-rw-r--r--src/store/messagesStore.js6
-rw-r--r--src/store/newGroupConversationStore.js4
-rw-r--r--src/store/quoteReplyStore.js4
-rw-r--r--src/store/reactionsStore.js4
-rw-r--r--src/store/sharedItemsStore.js4
-rw-r--r--src/store/sidebarStore.js4
-rw-r--r--src/store/storeConfig.js4
-rw-r--r--src/store/tokenStore.js4
-rw-r--r--src/store/windowVisibilityStore.js4
-rw-r--r--src/utils/cancelableRequest.js4
-rw-r--r--src/utils/fileUpload.js4
-rw-r--r--src/utils/media/pipeline/BlackVideoEnforcer.js169
-rw-r--r--src/utils/media/pipeline/BlackVideoEnforcer.spec.js1505
-rw-r--r--src/utils/webrtc/simplewebrtc/localmedia.js71
-rw-r--r--src/utils/webrtc/simplewebrtc/peer.js24
-rw-r--r--src/views/AdminSettings.vue32
-rw-r--r--src/views/MainView.vue1
-rw-r--r--src/views/RoomSelector.vue4
137 files changed, 3425 insertions, 1183 deletions
diff --git a/src/App.vue b/src/App.vue
index 94d0f8dac..f6a1618d1 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -1,7 +1,7 @@
<!--
- - @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@pm.me>
+ - @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@icloud.com>
-
- - @author Marco Ambrosini <marcoambrosini@pm.me>
+ - @author Marco Ambrosini <marcoambrosini@icloud.com>
-
- @license GNU AGPL version 3 or any later version
-
@@ -289,7 +289,6 @@ export default {
})
const beforeRouteChangeListener = (to, from, next) => {
-
if (this.isNextcloudTalkHashDirty) {
// Nextcloud Talk configuration changed, reload the page when changing configuration
window.location = generateUrl('call/' + to.params.token)
@@ -300,16 +299,10 @@ export default {
* This runs whenever the new route is a conversation.
*/
if (to.name === 'conversation') {
- // Page title
- const nextConversationName = this.getConversationName(to.params.token)
- this.setPageTitle(nextConversationName)
// Update current token in the token store
this.$store.dispatch('updateToken', to.params.token)
}
- if (to.name === 'notfound') {
- this.setPageTitle('')
- }
/**
* Fires a global event that tells the whole app that the route has changed. The event
* carries the from and to objects as payload
@@ -346,7 +339,19 @@ export default {
} else {
beforeRouteChangeListener(to, from, next)
}
+ })
+ Router.afterEach((to) => {
+ /**
+ * Change the page title only after the route was changed
+ */
+ if (to.name === 'conversation') {
+ // Page title
+ const nextConversationName = this.getConversationName(to.params.token)
+ this.setPageTitle(nextConversationName)
+ } else if (to.name === 'notfound') {
+ this.setPageTitle('')
+ }
})
if (getCurrentUser()) {
@@ -493,10 +498,20 @@ export default {
</script>
<style lang="scss">
-/** override toastify position due to top bar */
-body.has-topbar .toastify-top {
- margin-top: 105px;
+body {
+ overflow: hidden;
+
+ /** override toastify position due to top bar */
+ &.has-topbar .toastify-top {
+ margin-top: 105px;
+ }
}
+
+/* FIXME: remove after https://github.com/nextcloud/nextcloud-vue/issues/2097 is solved */
+.mx-datepicker-main.mx-datepicker-popup {
+ z-index: 10001 !important;
+}
+
</style>
<style lang="scss" scoped>
@@ -519,9 +534,13 @@ body.has-topbar .toastify-top {
}
}
- ::v-deep .app-navigation-toggle:before {
+ ::v-deep .app-navigation-toggle {
/* Force white handle when inside a call */
- color: #FFFFFF;
+ color: #D8D8D8;
+
+ &:active {
+ color: #FFFFFF;
+ }
}
}
diff --git a/src/FilesSidebarCallViewApp.vue b/src/FilesSidebarCallViewApp.vue
index 4ee815bea..869416ff3 100644
--- a/src/FilesSidebarCallViewApp.vue
+++ b/src/FilesSidebarCallViewApp.vue
@@ -1,7 +1,7 @@
<!--
- @copyright Copyright (c) 2019, Daniel Calviño Sánchez <danxuliu@gmail.com>
-
- - @author Marco Ambrosini <marcoambrosini@pm.me>
+ - @author Marco Ambrosini <marcoambrosini@icloud.com>
-
- @license GNU AGPL version 3 or any later version
-
diff --git a/src/FilesSidebarTabApp.vue b/src/FilesSidebarTabApp.vue
index 9cc28facc..e9a4bd9c7 100644
--- a/src/FilesSidebarTabApp.vue
+++ b/src/FilesSidebarTabApp.vue
@@ -1,7 +1,7 @@
<!--
- - @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@pm.me>
+ - @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@icloud.com>
-
- - @author Marco Ambrosini <marcoambrosini@pm.me>
+ - @author Marco Ambrosini <marcoambrosini@icloud.com>
-
- @license GNU AGPL version 3 or any later version
-
diff --git a/src/PublicShareAuthRequestPasswordButton.vue b/src/PublicShareAuthRequestPasswordButton.vue
index edd7e0109..86d3456a1 100644
--- a/src/PublicShareAuthRequestPasswordButton.vue
+++ b/src/PublicShareAuthRequestPasswordButton.vue
@@ -23,13 +23,14 @@
<!-- "submit-wrapper" is used to mimic the login button and thus get
automatic colouring of the confirm icon by the Theming app. -->
<div id="submit-wrapper" class="request-password-wrapper">
- <input id="request-password-button"
- class="primary button-vue"
- type="button"
- :value="t('spreed', 'Request password')"
+ <Button id="request-password-button"
+ type="primary"
+ :wide="true"
:disabled="isRequestInProgress"
- @click="requestPassword">
- <div class="icon" :class="iconClass" />
+ @click="requestPassword"
+ @keydown.enter="requestPassword">
+ {{ t('spreed', 'Request password') }}
+ </Button>
</div>
<p v-if="hasRequestFailed" class="warning error-message">
{{ t('spreed', 'Error requesting the password.') }}
@@ -38,6 +39,7 @@
</template>
<script>
+import Button from '@nextcloud/vue/dist/Components/Button'
import { getPublicShareAuthConversationToken } from './services/publicShareAuthService.js'
import browserCheck from './mixins/browserCheck.js'
import '@nextcloud/dialogs/styles/toast.scss'
@@ -46,6 +48,10 @@ export default {
name: 'PublicShareAuthRequestPasswordButton',
+ components: {
+ Button,
+ },
+
mixins: [
browserCheck,
],
@@ -102,27 +108,3 @@ export default {
},
}
</script>
-
-<style lang="scss" scoped>
-/* Request password button has the appearance of the log in button */
-.request-password-wrapper {
- position: relative;
- width: 280px;
- margin: 16px auto;
-}
-
-.request-password-wrapper .icon {
- position: absolute;
- right: 23px;
- pointer-events: none;
-}
-
-input#request-password-button {
- width: 269px;
- padding: 10px 10px;
-}
-
-input#request-password-button:disabled ~ .icon {
- opacity: 0.5;
-}
-</style>
diff --git a/src/PublicShareAuthSidebar.vue b/src/PublicShareAuthSidebar.vue
index af0652a3f..0d869e03c 100644
--- a/src/PublicShareAuthSidebar.vue
+++ b/src/PublicShareAuthSidebar.vue
@@ -1,7 +1,7 @@
<!--
- @copyright Copyright (c) 2020, Daniel Calviño Sánchez <danxuliu@gmail.com>
-
- - @author Marco Ambrosini <marcoambrosini@pm.me>
+ - @author Marco Ambrosini <marcoambrosini@icloud.com>
-
- @license GNU AGPL version 3 or any later version
-
diff --git a/src/PublicShareSidebar.vue b/src/PublicShareSidebar.vue
index df94d8204..919867ebf 100644
--- a/src/PublicShareSidebar.vue
+++ b/src/PublicShareSidebar.vue
@@ -1,7 +1,7 @@
<!--
- @copyright Copyright (c) 2020, Daniel Calviño Sánchez <danxuliu@gmail.com>
-
- - @author Marco Ambrosini <marcoambrosini@pm.me>
+ - @author Marco Ambrosini <marcoambrosini@icloud.com>
-
- @license GNU AGPL version 3 or any later version
-
@@ -26,10 +26,15 @@
<div v-if="!conversation" class="emptycontent room-not-joined">
<div class="icon icon-talk" />
<h2>{{ t('spreed', 'Discuss this file') }}</h2>
- <button class="primary" :disabled="joiningConversation" @click="joinConversation">
+ <ButtonVue type="primary"
+ class="button-centered"
+ :disabled="joiningConversation"
+ @click="joinConversation">
+ <template #icon>
+ <span v-if="joiningConversation" class="icon icon-loading-small" />
+ </template>
{{ t('spreed', 'Join conversation') }}
- <span v-if="joiningConversation" class="icon icon-loading-small" />
- </button>
+ </ButtonVue>
</div>
<template v-else>
<CallView v-if="isInCall"
@@ -45,6 +50,7 @@
</template>
<script>
+import ButtonVue from '@nextcloud/vue/dist/Components/Button'
import PreventUnload from 'vue-prevent-unload'
import { loadState } from '@nextcloud/initial-state'
import CallView from './components/CallView/CallView.vue'
@@ -69,6 +75,7 @@ export default {
name: 'PublicShareSidebar',
components: {
+ ButtonVue,
CallButton,
CallView,
ChatView,
@@ -285,6 +292,7 @@ export default {
right: -5px;
}
+#talk-sidebar .button-centered,
#talk-sidebar .call-button {
/* Center button horizontally. */
margin-left: auto;
@@ -294,6 +302,15 @@ export default {
margin-bottom: 10px;
}
+#talk-sidebar .button-centered {
+ /*
+ * When there is an icon the servers empty-content rule
+ * .emptycontent [class*="icon-"] is matching button-vue--icon-and-text
+ * setting the height to 64px, so we need to reset this.
+ */
+ height: 44px;
+}
+
#talk-sidebar #call-container {
position: relative;
diff --git a/src/PublicShareSidebarTrigger.vue b/src/PublicShareSidebarTrigger.vue
new file mode 100644
index 000000000..99a049ede
--- /dev/null
+++ b/src/PublicShareSidebarTrigger.vue
@@ -0,0 +1,73 @@
+<!--
+ - @copyright Copyright (c) 2022 Daniel Calviño Sánchez <danxuliu@gmail.com>
+ -
+ - @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="button-holder">
+ <Button type="tertiary-on-primary"
+ :aria-label="ariaLabel"
+ @click="$emit('click')">
+ <template #icon>
+ <MenuPeople :size="20" />
+ </template>
+ </Button>
+ </div>
+</template>
+
+<script>
+import Button from '@nextcloud/vue/dist/Components/Button'
+import MenuPeople from './components/missingMaterialDesignIcons/MenuPeople.vue'
+
+export default {
+
+ name: 'PublicShareSidebarTrigger',
+
+ components: {
+ Button,
+ MenuPeople,
+ },
+
+ props: {
+ sidebarState: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ computed: {
+ ariaLabel() {
+ if (this.sidebarState.isOpen) {
+ return t('spreed', 'Close Talk sidebar')
+ }
+
+ return t('spreed', 'Open Talk sidebar')
+ },
+ },
+
+}
+</script>
+
+<style scoped>
+.button-holder {
+ margin: 2px 5px 2px 2px;
+ display: flex;
+ justify-content: center;
+ height: 44px !important;
+}
+</style>
diff --git a/src/assets/buttons.scss b/src/assets/buttons.scss
deleted file mode 100644
index 26313ce36..000000000
--- a/src/assets/buttons.scss
+++ /dev/null
@@ -1,58 +0,0 @@
-/**
- * @copyright Copyright (c) 2020 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/>.
- *
- */
-
-@import 'variables';
-
-.nc-button {
- width: $clickable-area;
- height: $clickable-area;
- flex-shrink: 0;
- border: 0;
- padding: 0;
- z-index: 1;
- display: flex;
- align-items: center;
- justify-content: center;
- margin: 0;
- &:not(.primary) {
- background-color: transparent;
- }
- &__main {
- &:hover,
- &:focus {
- background-color: var(--color-background-hover);
- }
- &:disabled {
- &:hover {
- background-color: var(--color-primary-element);
- }
- }
- }
- // Used on top of gray background such as hovered messages
- &__main--dark {
- &:hover,
- &:focus {
- background-color: var(--color-background-darker);
- }
- }
-
-}
diff --git a/src/components/AdminSettings/AllowedGroups.vue b/src/components/AdminSettings/AllowedGroups.vue
index 4a5aeb568..204cd4495 100644
--- a/src/components/AdminSettings/AllowedGroups.vue
+++ b/src/components/AdminSettings/AllowedGroups.vue
@@ -33,8 +33,9 @@
{{ t('spreed', 'Users that cannot use Talk anymore will still be listed as participants in their previous conversations and also their chat messages will be kept.') }}
</p>
- <p class="allowed-groups-settings-content">
+ <div class="allowed-groups-settings-content">
<Multiselect v-model="allowedGroups"
+ name="allow_groups_use_talk"
class="allowed-groups-select"
:options="groups"
:placeholder="t('spreed', 'Limit using Talk')"
@@ -54,11 +55,12 @@
@click="saveAllowedGroups">
{{ saveLabelAllowedGroups }}
</Button>
- </p>
+ </div>
<h3>{{ t('spreed', 'Limit creating a public and group conversation') }}</h3>
- <p class="allowed-groups-settings-content">
+ <div class="allowed-groups-settings-content">
<Multiselect v-model="canStartConversations"
+ name="allow_groups_start_conversation"
class="allowed-groups-select"
:options="groups"
:placeholder="t('spreed', 'Limit creating conversations')"
@@ -78,19 +80,20 @@
@click="saveStartConversationsGroups">
{{ saveLabelStartConversations }}
</Button>
- </p>
+ </div>
<h3>{{ t('spreed', 'Limit starting a call') }}</h3>
- <p>
+ <div class="allowed-groups-settings-content">
<Multiselect id="start_calls"
v-model="startCalls"
+ name="allow_groups_start_calls"
:options="startCallOptions"
:placeholder="t('spreed', 'Limit starting calls')"
label="label"
track-by="value"
:disabled="loading || loadingStartCalls"
@input="saveStartCalls" />
- </p>
+ </div>
<p>
<em>{{ t('spreed', 'When a call has started, everyone with access to the conversation can join the call.') }}</em>
</p>
diff --git a/src/components/AdminSettings/Command.vue b/src/components/AdminSettings/Command.vue
index 46fb70aa1..f11110bab 100644
--- a/src/components/AdminSettings/Command.vue
+++ b/src/components/AdminSettings/Command.vue
@@ -41,7 +41,7 @@
</template>
<script>
-import { Fragment } from 'vue-fragment'
+import { Fragment } from 'vue-frag'
export default {
name: 'Command',
diff --git a/src/components/AdminSettings/Commands.vue b/src/components/AdminSettings/Commands.vue
index 148739c82..80cb9ff26 100644
--- a/src/components/AdminSettings/Commands.vue
+++ b/src/components/AdminSettings/Commands.vue
@@ -26,7 +26,6 @@
{{ t('spreed', 'Commands') }}
<small>
{{ t('spreed', 'Beta') }}
- <span class="icon icon-beta-feature" />
</small>
</h2>
@@ -103,12 +102,6 @@ export default {
border: 1px solid var(--color-warning);
border-radius: 16px;
padding: 0 9px;
-
- .icon {
- width: 16px;
- height: 16px;
- margin-bottom: 4px;
- }
}
}
</style>
diff --git a/src/components/AdminSettings/GeneralSettings.vue b/src/components/AdminSettings/GeneralSettings.vue
index 088176fdf..84a6b8add 100644
--- a/src/components/AdminSettings/GeneralSettings.vue
+++ b/src/components/AdminSettings/GeneralSettings.vue
@@ -26,45 +26,39 @@
<h3>{{ t('spreed', 'Default notification settings') }}</h3>
- <p>
+ <div class="paragraph">
<label for="default_group_notification">{{ t('spreed', 'Default group notification') }}</label>
<Multiselect id="default_group_notification"
v-model="defaultGroupNotification"
+ name="default_group_notification"
:options="defaultGroupNotificationOptions"
:placeholder="t('spreed', 'Default group notification for new groups')"
label="label"
track-by="value"
:disabled="loading || loadingDefaultGroupNotification"
@input="saveDefaultGroupNotification" />
- </p>
+ </div>
<h3>{{ t('spreed', 'Integration into other apps') }}</h3>
- <p>
- <input id="conversations_files"
- v-model="conversationsFiles"
- type="checkbox"
- name="conversations_files"
- class="checkbox"
- :disabled="loading || loadingConversationsFiles"
- @change="saveConversationsFiles">
- <label for="conversations_files">{{ t('spreed', 'Allow conversations on files') }}</label>
- </p>
-
- <p>
- <input id="conversations_files_public_shares"
- v-model="conversationsFilesPublicShares"
- type="checkbox"
- name="conversations_files_public_shares"
- class="checkbox"
- :disabled="loading || loadingConversationsFiles || !conversationsFiles"
- @change="saveConversationsFilesPublicShares">
- <label for="conversations_files_public_shares">{{ t('spreed', 'Allow conversations on public shares for files') }}</label>
- </p>
+ <CheckboxRadioSwitch :checked.sync="conversationsFiles"
+ name="conversations_files"
+ :disabled="loading || loadingConversationsFiles"
+ @change="saveConversationsFiles">
+ {{ t('spreed', 'Allow conversations on files') }}
+ </CheckboxRadioSwitch>
+
+ <CheckboxRadioSwitch :checked.sync="conversationsFilesPublicShares"
+ name="conversations_files_public_shares"
+ :disabled="loading || loadingConversationsFiles || !conversationsFiles"
+ @change="saveConversationsFilesPublicShares">
+ {{ t('spreed', 'Allow conversations on public shares for files') }}
+ </CheckboxRadioSwitch>
</div>
</template>
<script>
+import CheckboxRadioSwitch from '@nextcloud/vue/dist/Components/CheckboxRadioSwitch'
import Multiselect from '@nextcloud/vue/dist/Components/Multiselect'
import { loadState } from '@nextcloud/initial-state'
@@ -77,6 +71,7 @@ export default {
name: 'GeneralSettings',
components: {
+ CheckboxRadioSwitch,
Multiselect,
},
@@ -149,7 +144,7 @@ h3 {
margin-top: 24px;
}
-p {
+div.paragraph {
display: flex;
align-items: center;
diff --git a/src/components/AdminSettings/HostedSignalingServer.vue b/src/components/AdminSettings/HostedSignalingServer.vue
index 9f5657a51..ccf77de31 100644
--- a/src/components/AdminSettings/HostedSignalingServer.vue
+++ b/src/components/AdminSettings/HostedSignalingServer.vue
@@ -40,6 +40,7 @@
placeholder="https://cloud.example.org/"
:disabled="loading"
:aria-label="t('spreed', 'URL of this Nextcloud instance')">
+
<h4>{{ t('spreed', 'Full name of the user requesting the trial') }}</h4>
<input v-model="hostedHPBFullName"
type="text"
@@ -47,6 +48,7 @@
placeholder="Jane Doe"
:disabled="loading"
:aria-label="t('spreed', 'Name of the user requesting the trial')">
+
<h4>{{ t('spreed', 'Email of the user') }}</h4>
<input v-model="hostedHPBEmail"
type="text"
@@ -54,10 +56,10 @@
placeholder="jane@example.org"
:disabled="loading"
:aria-label="t('spreed', 'Email of the user')">
+
<h4>{{ t('spreed', 'Language') }}</h4>
<select v-model="hostedHPBLanguage"
name="hosted_hpb_language"
- :placeholder="t('spreed', 'Language')"
:disabled="loading"
:aria-label="t('spreed', 'Language')">
<option v-for="l in languages.commonLanguages" :key="l.code" :value="l.code">
@@ -68,22 +70,23 @@
{{ l.name }}
</option>
</select>
+
<h4>{{ t('spreed', 'Country') }}</h4>
<select v-model="hostedHPBCountry"
name="hosted_hpb_country"
- :placeholder="t('spreed', 'Country')"
:disabled="loading"
:aria-label="t('spreed', 'Country')">
<option v-for="c in countries" :key="c.code" :value="c.code">
{{ c.name }}
</option>
</select>
- <br>
- <button class="button primary"
+
+ <Button type="primary"
:disabled="!hostedHPBFilled || loading"
@click="requestHPBTrial">
{{ t('spreed', 'Request signaling server trial') }}
- </button>
+ </Button>
+
<p v-if="requestError !== ''"
class="warning">
{{ requestError }}
@@ -118,16 +121,18 @@
class="warning">
{{ requestError }}
</p>
- <button class="button delete"
+
+ <Button type="error"
:disabled="loading"
@click="deleteAccount">
{{ t('spreed', 'Delete the signaling server account') }}
- </button>
+ </Button>
</div>
</div>
</template>
<script>
+import Button from '@nextcloud/vue/dist/Components/Button'
import { loadState } from '@nextcloud/initial-state'
import axios from '@nextcloud/axios'
import { generateOcsUrl } from '@nextcloud/router'
@@ -136,6 +141,10 @@ import moment from '@nextcloud/moment'
export default {
name: 'HostedSignalingServer',
+ components: {
+ Button,
+ },
+
data() {
return {
hostedHPBNextcloudUrl: '',
@@ -238,7 +247,7 @@ export default {
this.trialAccount = []
} catch (err) {
- this.deleteError = err?.response?.data?.ocs?.data?.message || t('spreed', 'The account could not be deleted. Please try again later.')
+ this.requestError = err?.response?.data?.ocs?.data?.message || t('spreed', 'The account could not be deleted. Please try again later.')
} finally {
this.loading = false
}
@@ -265,17 +274,4 @@ tr :first-child {
opacity: .5;
}
-.delete {
- background: var(--color-main-background);
- border-color: var(--color-error);
- color: var(--color-error);
-}
-
-.delete:hover,
-.delete:active {
- background: var(--color-error);
- border-color: var(--color-error) !important;
- color: var(--color-main-background);
-}
-
</style>
diff --git a/src/components/AdminSettings/MatterbridgeIntegration.vue b/src/components/AdminSettings/MatterbridgeIntegration.vue
index ea43c164c..b5e1b42eb 100644
--- a/src/components/AdminSettings/MatterbridgeIntegration.vue
+++ b/src/components/AdminSettings/MatterbridgeIntegration.vue
@@ -26,7 +26,6 @@
{{ t('spreed', 'Matterbridge integration') }}
<small>
{{ t('spreed', 'Beta') }}
- <span class="icon icon-beta-feature" />
</small>
</h2>
@@ -58,20 +57,23 @@
</p>
<p>
- <button v-if="isInstalling">
- <span class="icon icon-loading-small" />
+ <Button v-if="isInstalling">
+ <template #icon>
+ <span class="icon icon-loading-small" />
+ </template>
{{ t('spreed', 'Downloading …') }}
- </button>
- <button v-else
+ </Button>
+ <Button v-else
@click="enableMatterbridgeApp">
{{ t('spreed', 'Install Talk Matterbridge') }}
- </button>
+ </Button>
</p>
</template>
</div>
</template>
<script>
+import Button from '@nextcloud/vue/dist/Components/Button'
import { loadState } from '@nextcloud/initial-state'
import { showError } from '@nextcloud/dialogs'
import {
@@ -83,7 +85,9 @@ import {
export default {
name: 'MatterbridgeIntegration',
- components: {},
+ components: {
+ Button,
+ },
data() {
return {
@@ -184,12 +188,6 @@ h2 {
border: 1px solid var(--color-warning);
border-radius: 16px;
padding: 0 9px;
-
- .icon {
- width: 16px;
- height: 16px;
- margin-bottom: 4px;
- }
}
}
diff --git a/src/components/AdminSettings/SIPBridge.vue b/src/components/AdminSettings/SIPBridge.vue
index 3f4b73221..242046375 100644
--- a/src/components/AdminSettings/SIPBridge.vue
+++ b/src/components/AdminSettings/SIPBridge.vue
@@ -73,29 +73,30 @@
:disabled="loading"
:placeholder="t('spreed', 'Phone number (Country)')" />
- <p>
- <button class="button primary"
- :disabled="loading"
- @click="saveSIPSettings">
- {{ saveLabel }}
- </button>
- </p>
+ <Button type="primary"
+ :disabled="loading"
+ @click="saveSIPSettings">
+ {{ t('spreed', 'Save changes') }}
+ </Button>
</template>
</div>
</template>
<script>
+import Button from '@nextcloud/vue/dist/Components/Button'
import Multiselect from '@nextcloud/vue/dist/Components/Multiselect'
import axios from '@nextcloud/axios'
import debounce from 'debounce'
import { generateOcsUrl } from '@nextcloud/router'
import { loadState } from '@nextcloud/initial-state'
+import { showSuccess } from '@nextcloud/dialogs'
import { setSIPSettings } from '../../services/settingsService.js'
export default {
name: 'SIPBridge',
components: {
+ Button,
Multiselect,
},
@@ -106,7 +107,6 @@ export default {
showForm: true,
groups: [],
sipGroups: [],
- saveLabel: t('spreed', 'Save changes'),
dialInInfo: '',
sharedSecret: '',
}
@@ -157,16 +157,14 @@ export default {
await setSIPSettings(groups, this.sharedSecret, this.dialInInfo)
this.loading = false
- this.saveLabel = t('spreed', 'Saved!')
- setTimeout(() => {
- this.saveLabel = t('spreed', 'Save changes')
- }, 5000)
+ showSuccess(t('spreed', 'SIP configuration saved!'))
},
},
}
</script>
<style lang="scss" scoped>
+
.sip-bridge {
&__sip-groups-select,
&__shared-secret,
diff --git a/src/components/AdminSettings/SignalingServer.vue b/src/components/AdminSettings/SignalingServer.vue
index b9bb61d53..693f96631 100644
--- a/src/components/AdminSettings/SignalingServer.vue
+++ b/src/components/AdminSettings/SignalingServer.vue
@@ -38,24 +38,30 @@
@change="updateVerify">
<label :for="'verify' + index">{{ t('spreed', 'Validate SSL certificate') }}</label>
- <a v-show="!loading"
- v-tooltip.auto="t('spreed', 'Delete this server')"
- class="icon icon-delete"
- @click="removeServer" />
+ <Button v-show="!loading"
+ type="tertiary-no-background"
+ :aria-label="t('spreed', 'Delete this server')"
+ @click="removeServer">
+ <template #icon>
+ <Delete :size="20" />
+ </template>
+ </Button>
<span v-if="server">{{ connectionState }}</span>
</div>
</template>
<script>
-import Tooltip from '@nextcloud/vue/dist/Directives/Tooltip'
+import Button from '@nextcloud/vue/dist/Components/Button'
+import Delete from 'vue-material-design-icons/Delete'
import { getWelcomeMessage } from '../../services/signalingService.js'
export default {
name: 'SignalingServer',
- directives: {
- tooltip: Tooltip,
+ components: {
+ Button,
+ Delete,
},
props: {
@@ -164,5 +170,10 @@ export default {
height: 44px;
display: flex;
align-items: center;
+
+ label {
+ margin: 0 20px;
+ display: inline-block;
+ }
}
</style>
diff --git a/src/components/AdminSettings/SignalingServers.vue b/src/components/AdminSettings/SignalingServers.vue
index 8c4e6e02c..8a8c1815c 100644
--- a/src/components/AdminSettings/SignalingServers.vue
+++ b/src/components/AdminSettings/SignalingServers.vue
@@ -21,17 +21,19 @@
-->
<template>
- <div id="signaling_server" class="videocalls section">
+ <div id="signaling_server" class="videocalls section signaling-server">
<h2>
{{ t('spreed', 'High-performance backend') }}
- <span v-if="saved" class="icon icon-checkmark-color" :title="t('spreed', 'Saved')" />
- <a v-else-if="!loading && showAddServerButton"
- v-tooltip.auto="t('spreed', 'Add a new server')"
- class="icon icon-add"
+
+ <Button v-if="!loading && showAddServerButton"
+ class="signaling-server__add-icon"
+ type="tertiary-no-background"
+ :aria-label="t('spreed', 'Add a new high-performance backend server')"
@click="newServer">
- <span class="hidden-visually">{{ t('spreed', 'Add a new server') }}</span>
- </a>
- <span v-else-if="loading" class="icon icon-loading-small" />
+ <template #icon>
+ <Plus :size="20" />
+ </template>
+ </Button>
</h2>
<p class="settings-hint">
@@ -84,7 +86,9 @@
<script>
import SignalingServer from '../../components/AdminSettings/SignalingServer.vue'
-import Tooltip from '@nextcloud/vue/dist/Directives/Tooltip'
+import Button from '@nextcloud/vue/dist/Components/Button'
+import Plus from 'vue-material-design-icons/Plus'
+import { showSuccess } from '@nextcloud/dialogs'
import { loadState } from '@nextcloud/initial-state'
import debounce from 'debounce'
import { SIGNALING } from '../../constants.js'
@@ -92,12 +96,10 @@ import { SIGNALING } from '../../constants.js'
export default {
name: 'SignalingServers',
- directives: {
- tooltip: Tooltip,
- },
-
components: {
+ Button,
SignalingServer,
+ Plus,
},
data() {
@@ -144,6 +146,7 @@ export default {
OCP.AppConfig.setValue('spreed', 'hide_signaling_warning', this.hideWarning ? 'yes' : 'no', {
success() {
+ showSuccess(t('spreed', 'Missing high-performance backend warning hidden'))
self.loading = false
self.toggleSave()
},
@@ -165,6 +168,7 @@ export default {
secret: this.secret,
}), {
success() {
+ showSuccess(t('spreed', 'High-performance backend settings saved'))
self.loading = false
self.toggleSave()
},
@@ -180,3 +184,24 @@ export default {
},
}
</script>
+
+<style lang="scss" scoped>
+@import '../../assets/variables';
+
+.signaling-server {
+ h2 {
+ height: 44px;
+ display: flex;
+ align-items: center;
+ }
+}
+
+.signaling-warning label {
+ margin: 0;
+}
+
+.signaling-warning,
+.signaling-secret {
+ margin-top: 20px;
+}
+</style>
diff --git a/src/components/AdminSettings/StunServer.vue b/src/components/AdminSettings/StunServer.vue
index 9ab2729fe..1d38f26d5 100644
--- a/src/components/AdminSettings/StunServer.vue
+++ b/src/components/AdminSettings/StunServer.vue
@@ -32,22 +32,36 @@
:disabled="loading"
:aria-label="t('spreed', 'STUN server URL')"
@input="update">
- <span v-show="!isValidServer" class="icon icon-error" />
- <a v-show="!loading"
- v-tooltip.auto="t('spreed', 'Delete this server')"
- class="icon icon-delete"
- @click="removeServer" />
+ <Button v-show="!isValidServer"
+ type="tertiary-no-background"
+ :aria-label="t('spreed', 'The server address is invalid')">
+ <template #icon>
+ <AlertCircle />
+ </template>
+ </Button>
+ <Button v-show="!loading"
+ type="tertiary-no-background"
+ :aria-label="t('spreed', 'Delete this server')"
+ @click="removeServer">
+ <template #icon>
+ <Delete :size="20" />
+ </template>
+ </Button>
</div>
</template>
<script>
-import Tooltip from '@nextcloud/vue/dist/Directives/Tooltip'
+import Button from '@nextcloud/vue/dist/Components/Button'
+import AlertCircle from 'vue-material-design-icons/AlertCircle'
+import Delete from 'vue-material-design-icons/Delete'
export default {
name: 'StunServer',
- directives: {
- tooltip: Tooltip,
+ components: {
+ Button,
+ AlertCircle,
+ Delete,
},
props: {
diff --git a/src/components/AdminSettings/StunServers.vue b/src/components/AdminSettings/StunServers.vue
index 1b9d3bb23..5418601cc 100644
--- a/src/components/AdminSettings/StunServers.vue
+++ b/src/components/AdminSettings/StunServers.vue
@@ -21,17 +21,19 @@
-->
<template>
- <div id="stun_server" class="videocalls section">
+ <div id="stun_server" class="videocalls section stun-server">
<h2>
{{ t('spreed', 'STUN servers') }}
- <span v-if="saved" class="icon icon-checkmark-color" :title="t('spreed', 'Saved')" />
- <a v-else-if="!loading"
- v-tooltip.auto="t('spreed', 'Add a new server')"
- class="icon icon-add"
+
+ <Button v-if="!loading"
+ class="stun-server__add-icon"
+ type="tertiary-no-background"
+ :aria-label="t('spreed', 'Add a new STUN server')"
@click="newServer">
- <span class="hidden-visually">{{ t('spreed', 'Add a new server') }}</span>
- </a>
- <span v-else class="icon icon-loading-small" />
+ <template #icon>
+ <Plus :size="20" />
+ </template>
+ </Button>
</h2>
<p class="settings-hint">
@@ -54,19 +56,19 @@
<script>
import StunServer from '../../components/AdminSettings/StunServer.vue'
-import Tooltip from '@nextcloud/vue/dist/Directives/Tooltip'
+import Button from '@nextcloud/vue/dist/Components/Button'
+import Plus from 'vue-material-design-icons/Plus'
import debounce from 'debounce'
import { loadState } from '@nextcloud/initial-state'
+import { showSuccess } from '@nextcloud/dialogs'
export default {
name: 'StunServers',
- directives: {
- tooltip: Tooltip,
- },
-
components: {
+ Button,
StunServer,
+ Plus,
},
data() {
@@ -126,6 +128,7 @@ export default {
OCP.AppConfig.setValue('spreed', 'stun_servers', JSON.stringify(servers), {
success() {
+ showSuccess(t('spreed', 'STUN settings saved'))
self.loading = false
self.toggleSave()
},
@@ -143,16 +146,14 @@ export default {
</script>
<style lang="scss">
-.turn-server {
- height: 44px;
- display: flex;
- align-items: center;
+@import '../../assets/variables';
+
+.stun-server {
+ h2 {
+ height: 44px;
+ display: flex;
+ align-items: center;
+ }
}
-.icon {
- display: inline-block;
- width: 44px;
- height: 44px;
- vertical-align: middle;
-}
</style>
diff --git a/src/components/AdminSettings/TurnServer.vue b/src/components/AdminSettings/TurnServer.vue
index 366b65ddd..d9ce21df0 100644
--- a/src/components/AdminSettings/TurnServer.vue
+++ b/src/components/AdminSettings/TurnServer.vue
@@ -73,23 +73,38 @@
</option>
</select>
- <a v-show="!loading"
- v-tooltip.auto="testResult"
- class="icon"
- :class="testIconClasses"
- @click="testServer" />
- <a v-show="!loading"
- v-tooltip.auto="t('spreed', 'Delete this server')"
- class="icon icon-delete"
- @click="removeServer" />
+ <Button v-show="!loading"
+ type="tertiary-no-background"
+ :aria-label="testResult"
+ @click="testServer">
+ <template #icon>
+ <span v-if="testing" class="icon icon-loading-small" />
+ <AlertCircle v-else-if="testingError" />
+ <Check v-else-if="testingSuccess" />
+ <CategoryMonitoring v-else />
+ </template>
+ </Button>
+ <Button v-show="!loading"
+ type="tertiary-no-background"
+ :aria-label="t('spreed', 'Delete this server')"
+ @click="removeServer">
+ <template #icon>
+ <Delete :size="20" />
+ </template>
+ </Button>
</div>
</template>
<script>
+import Button from '@nextcloud/vue/dist/Components/Button'
import Tooltip from '@nextcloud/vue/dist/Directives/Tooltip'
+import AlertCircle from 'vue-material-design-icons/AlertCircle'
+import Check from 'vue-material-design-icons/Check'
+import Delete from 'vue-material-design-icons/Delete'
import hmacSHA1 from 'crypto-js/hmac-sha1'
import Base64 from 'crypto-js/enc-base64'
import debounce from 'debounce'
+import CategoryMonitoring from '../missingMaterialDesignIcons/CategoryMonitoring.vue'
export default {
name: 'TurnServer',
@@ -98,6 +113,14 @@ export default {
tooltip: Tooltip,
},
+ components: {
+ Button,
+ AlertCircle,
+ CategoryMonitoring,
+ Check,
+ Delete,
+ },
+
props: {
schemes: {
type: String,
diff --git a/src/components/AdminSettings/TurnServers.vue b/src/components/AdminSettings/TurnServers.vue
index 537aba5bd..a4edc5952 100644
--- a/src/components/AdminSettings/TurnServers.vue
+++ b/src/components/AdminSettings/TurnServers.vue
@@ -21,17 +21,19 @@
-->
<template>
- <div id="turn_server" class="videocalls section">
+ <div id="turn_server" class="videocalls section turn-server">
<h2>
{{ t('spreed', 'TURN servers') }}
- <span v-if="saved" class="icon icon-checkmark-color" :title="t('spreed', 'Saved')" />
- <a v-else-if="!loading"
- v-tooltip.auto="t('spreed', 'Add a new server')"
- class="icon icon-add"
+
+ <Button v-if="!loading"
+ class="turn-server__add-icon"
+ type="tertiary-no-background"
+ :aria-label="t('spreed', 'Add a new TURN server')"
@click="newServer">
- <span class="hidden-visually">{{ t('spreed', 'Add a new server') }}</span>
- </a>
- <span v-else class="icon icon-loading-small" />
+ <template #icon>
+ <Plus :size="20" />
+ </template>
+ </Button>
</h2>
<!-- eslint-disable-next-line vue/no-v-html -->
@@ -58,20 +60,20 @@
</template>
<script>
-import Tooltip from '@nextcloud/vue/dist/Directives/Tooltip'
+import Button from '@nextcloud/vue/dist/Components/Button'
import TurnServer from '../../components/AdminSettings/TurnServer.vue'
import { loadState } from '@nextcloud/initial-state'
+import Plus from 'vue-material-design-icons/Plus'
+import { showSuccess } from '@nextcloud/dialogs'
import debounce from 'debounce'
export default {
name: 'TurnServers',
- directives: {
- tooltip: Tooltip,
- },
-
components: {
+ Button,
TurnServer,
+ Plus,
},
data() {
@@ -142,6 +144,7 @@ export default {
this.loading = true
OCP.AppConfig.setValue('spreed', 'turn_servers', JSON.stringify(servers), {
success() {
+ showSuccess(t('spreed', 'TURN settings saved'))
self.loading = false
self.toggleSave()
},
@@ -149,6 +152,7 @@ export default {
},
toggleSave() {
+
this.saved = true
setTimeout(() => {
this.saved = false
@@ -157,3 +161,16 @@ export default {
},
}
</script>
+
+<style lang="scss">
+@import '../../assets/variables';
+
+.turn-server {
+ h2 {
+ height: 44px;
+ display: flex;
+ align-items: center;
+ }
+}
+
+</style>
diff --git a/src/components/AdminSettings/WebServerSetupChecks.vue b/src/components/AdminSettings/WebServerSetupChecks.vue
index 9cb033c9d..4b77f4046 100644
--- a/src/components/AdminSettings/WebServerSetupChecks.vue
+++ b/src/components/AdminSettings/WebServerSetupChecks.vue
@@ -32,35 +32,34 @@
<ul class="web-server-setup-checks">
<li class="background-blur">
{{ t('spreed', 'Files required for background blur can be loaded') }}
- <button v-if="backgroundBlurAvailable === false"
- v-tooltip="backgroundBlurAvailableToolTip"
+ <ButtonVue v-tooltip="backgroundBlurAvailableToolTip"
+ type="tertiary"
+ class="vue-button-inline"
+ :class="{'success-button': backgroundBlurAvailable === true, 'error-button': backgroundBlurAvailable === false}"
:aria-label="backgroundBlurAvailableAriaLabel"
- class="icon"
- :class="backgroundBlurAvailableClasses"
- @click="checkBackgroundBlur" />
- <button v-else-if="backgroundBlurAvailable === true"
- v-tooltip="backgroundBlurAvailableToolTip"
- :aria-label="backgroundBlurAvailableAriaLabel"
- class="icon"
- :class="backgroundBlurAvailableClasses"
- @click="checkBackgroundBlur" />
- <span v-else
- v-tooltip="backgroundBlurAvailableToolTip"
- :aria-label="backgroundBlurAvailableAriaLabel"
- class="icon"
- :class="backgroundBlurAvailableClasses" />
+ @click="checkBackgroundBlur">
+ <template #icon>
+ <AlertCircle v-if="backgroundBlurAvailable === false" :size="20" />
+ <Check v-else-if="backgroundBlurAvailable === true" :size="20" />
+ <span v-else class="icon icon-loading-small" />
+ </template>
+ </ButtonVue>
</li>
</ul>
</div>
</template>
<script>
-import { generateFilePath } from '@nextcloud/router'
+import AlertCircle from 'vue-material-design-icons/AlertCircle'
+import ButtonVue from '@nextcloud/vue/dist/Components/Button'
+import Check from 'vue-material-design-icons/Check'
import Tooltip from '@nextcloud/vue/dist/Directives/Tooltip'
-import { VIRTUAL_BACKGROUND_TYPE } from '../../utils/media/effects/virtual-background/constants.js'
import JitsiStreamBackgroundEffect from '../../utils/media/effects/virtual-background/JitsiStreamBackgroundEffect.js'
import VirtualBackground from '../../utils/media/pipeline/VirtualBackground.js'
+
+import { generateFilePath } from '@nextcloud/router'
import { loadState } from '@nextcloud/initial-state'
+import { VIRTUAL_BACKGROUND_TYPE } from '../../utils/media/effects/virtual-background/constants.js'
export default {
name: 'WebServerSetupChecks',
@@ -69,6 +68,12 @@ export default {
tooltip: Tooltip,
},
+ components: {
+ AlertCircle,
+ ButtonVue,
+ Check,
+ },
+
data() {
return {
backgroundBlurLoaded: undefined,
@@ -81,14 +86,6 @@ export default {
return this.backgroundBlurLoaded
},
- backgroundBlurAvailableClasses() {
- return {
- 'icon-checkmark': this.backgroundBlurAvailable === true,
- 'icon-error': this.backgroundBlurAvailable === false,
- 'icon-loading-small': this.backgroundBlurAvailable === undefined,
- }
- },
-
backgroundBlurAvailableAriaLabel() {
if (this.backgroundBlurAvailable === false) {
return t('spreed', 'Failed')
@@ -182,9 +179,15 @@ export default {
</script>
<style lang="scss" scoped>
-button.icon {
- background-color: transparent;
- border: none;
- width: 44px;
+.vue-button-inline {
+ display: inline-block !important;
+
+ &.success-button {
+ color: var(--color-success);
+ }
+
+ &.error-button {
+ color: var(--color-error);
+ }
}
</style>
diff --git a/src/components/AvatarWrapper/AvatarWrapper.vue b/src/components/AvatarWrapper/AvatarWrapper.vue
index ce9bcf668..3b66086d7 100644
--- a/src/components/AvatarWrapper/AvatarWrapper.vue
+++ b/src/components/AvatarWrapper/AvatarWrapper.vue
@@ -1,7 +1,7 @@
<!--
- - @copyright Copyright (c) 2020 Marco Ambrosini <marcoambrosini@pm.me>
+ - @copyright Copyright (c) 2020 Marco Ambrosini <marcoambrosini@icloud.com>
-
- - @author Marco Ambrosini <marcoambrosini@pm.me>
+ - @author Marco Ambrosini <marcoambrosini@icloud.com>
-
- @license GNU AGPL version 3 or any later version
-
@@ -28,7 +28,7 @@
<Avatar v-else-if="!isGuest"
:user="id"
:display-name="name"
- :menu-container="menuContainer"
+ :menu-container="menuContainerWithFallback"
menu-position="left"
:disable-tooltip="disableTooltip"
:disable-menu="disableMenu"
@@ -97,6 +97,11 @@ export default {
type: Object,
default: undefined,
},
+
+ menuContainer: {
+ type: String,
+ default: undefined,
+ },
},
computed: {
// Determines which icon is displayed
@@ -117,10 +122,10 @@ export default {
const customName = this.name !== t('spreed', 'Guest') ? this.name : '?'
return customName.charAt(0)
},
- menuContainer() {
- return this.$store.getters.getMainContainerSelector()
+ menuContainerWithFallback() {
+ return this.menuContainer ? this.menuContainer : this.$store.getters.getMainContainerSelector()
},
- // Takes the the size prop and makes it a string for the classes
+ // Takes the size prop and makes it a string for the classes
sizeToString() {
return this.size.toString()
},
diff --git a/src/components/AvatarWrapper/AvatarWrapperSmall.vue b/src/components/AvatarWrapper/AvatarWrapperSmall.vue
index 535967e2a..3dc287469 100644
--- a/src/components/AvatarWrapper/AvatarWrapperSmall.vue
+++ b/src/components/AvatarWrapper/AvatarWrapperSmall.vue
@@ -1,7 +1,7 @@
<!--
- - @copyright Copyright (c) 2020 Marco Ambrosini <marcoambrosini@pm.me>
+ - @copyright Copyright (c) 2020 Marco Ambrosini <marcoambrosini@icloud.com>
-
- - @author Marco Ambrosini <marcoambrosini@pm.me>
+ - @author Marco Ambrosini <marcoambrosini@icloud.com>
-
- @license GNU AGPL version 3 or any later version
-
diff --git a/src/components/CallView/CallView.vue b/src/components/CallView/CallView.vue
index bb0ccbd1a..bd25e5eea 100644
--- a/src/components/CallView/CallView.vue
+++ b/src/components/CallView/CallView.vue
@@ -1,7 +1,7 @@
<!--
- - @copyright Copyright (c) 2020 Marco Ambrosini <marcoambrosini@pm.me>
+ - @copyright Copyright (c) 2020 Marco Ambrosini <marcoambrosini@icloud.com>
-
- - @author Marco Ambrosini <marcoambrosini@pm.me>
+ - @author Marco Ambrosini <marcoambrosini@icloud.com>
-
- @license GNU AGPL version 3 or any later version
-
diff --git a/src/components/CallView/Grid/Grid.vue b/src/components/CallView/Grid/Grid.vue
index 41e24cbed..6b70f02bd 100644
--- a/src/components/CallView/Grid/Grid.vue
+++ b/src/components/CallView/Grid/Grid.vue
@@ -1,7 +1,7 @@
<!--
- - @copyright Copyright (c) 2020 Marco Ambrosini <marcoambrosini@pm.me>
+ - @copyright Copyright (c) 2020 Marco Ambrosini <marcoambrosini@icloud.com>
-
- - @author Marco Ambrosini <marcoambrosini@pm.me>
+ - @author Marco Ambrosini <marcoambrosini@icloud.com>
-
- @license GNU AGPL version 3 or any later version
-
@@ -27,13 +27,9 @@
@click="handleClickStripeCollapse">
<ChevronDown v-if="stripeOpen"
fill-color="#ffffff"
- decorative
- title=""
:size="20" />
<ChevronUp v-else
fill-color="#ffffff"
- decorative
- title=""
:size="20" />
</button>
<transition :name="isStripe ? 'slide-down' : ''">
@@ -44,9 +40,7 @@
class="grid-navigation grid-navigation__previous"
:aria-label="t('spreed', 'Previous page of videos')"
@click="handleClickPrevious">
- <ChevronLeft decorative
- fill-color="#ffffff"
- title=""
+ <ChevronLeft fill-color="#ffffff"
:size="20" />
</button>
<div ref="grid"
@@ -110,9 +104,7 @@
:class="{'stripe': isStripe}"
:aria-label="t('spreed', 'Next page of videos')"
@click="handleClickNext">
- <ChevronRight decorative
- fill-color="#ffffff"
- title=""
+ <ChevronRight fill-color="#ffffff"
:size="20" />
</button>
</div>
diff --git a/src/components/CallView/shared/EmptyCallView.vue b/src/components/CallView/shared/EmptyCallView.vue
index 797a02fd0..d2d8fb690 100644
--- a/src/components/CallView/shared/EmptyCallView.vue
+++ b/src/components/CallView/shared/EmptyCallView.vue
@@ -27,15 +27,16 @@
<p v-if="message" class="emptycontent-additional">
{{ message }}
</p>
- <button v-if="showLink"
- class="primary"
+ <ButtonVue v-if="showLink"
+ type="primary"
@click.stop.prevent="copyLinkToConversation">
{{ t('spreed', 'Copy link') }}
- </button>
+ </ButtonVue>
</div>
</template>
<script>
+import ButtonVue from '@nextcloud/vue/dist/Components/Button'
import { showError, showSuccess } from '@nextcloud/dialogs'
import { generateUrl } from '@nextcloud/router'
import { CONVERSATION, PARTICIPANT } from '../../../constants.js'
@@ -44,6 +45,10 @@ export default {
name: 'EmptyCallView',
+ components: {
+ ButtonVue,
+ },
+
props: {
isGrid: {
type: Boolean,
@@ -175,6 +180,8 @@ export default {
align-content: center;
justify-content: center;
text-align: center;
+ z-index: 1; // Otherwise the "Copy link" button is not clickable
+
.icon {
background-size: 64px;
height: 64px;
diff --git a/src/components/CallView/shared/LocalMediaControls.vue b/src/components/CallView/shared/LocalMediaControls.vue
index 67ab6612d..02bf55b03 100644
--- a/src/components/CallView/shared/LocalMediaControls.vue
+++ b/src/components/CallView/shared/LocalMediaControls.vue
@@ -28,90 +28,89 @@
trigger="hover"
:auto-hide="false"
:open="showQualityWarningTooltip">
- <button slot="trigger"
- class="trigger">
- <NetworkStrength2Alert decorative
- fill-color="#e9322d"
- title=""
- :size="20"
- @mouseover="mouseover = true"
- @mouseleave="mouseover = false" />
- </button>
+ <template #trigger>
+ <Button id="quality_warning_button"
+ type="tertiary-no-background"
+ class="trigger"
+ @click="mouseover = !mouseover">
+ <template #icon>
+ <NetworkStrength2Alert fill-color="#e9322d"
+ :size="20" />
+ </template>
+ </Button>
+ </template>
<div class="hint">
<span>{{ qualityWarningTooltip.content }}</span>
<div class="hint__actions">
- <button v-if="qualityWarningTooltip.action"
- class="primary hint__button"
+ <Button v-if="qualityWarningTooltip.action"
+ type="primary"
+ class="hint__button"
@click="executeQualityWarningTooltipAction">
{{ qualityWarningTooltip.actionLabel }}
- </button>
- <button v-if="!isQualityWarningTooltipDismissed"
+ </Button>
+ <Button v-if="!isQualityWarningTooltipDismissed"
+ type="tertiary"
class="hint__button"
@click="dismissQualityWarningTooltip">
{{ t('spreed', 'Dismiss') }}
- </button>
+ </Button>
</div>
</div>
</Popover>
</div>
<div id="muteWrapper">
- <button id="mute"
- v-shortkey.once="['m']"
+ <Button v-shortkey.once="['m']"
v-tooltip="audioButtonTooltip"
+ type="tertiary-no-background"
:aria-label="audioButtonAriaLabel"
:class="audioButtonClass"
@shortkey="toggleAudio"
@click.stop="toggleAudio">
- <Microphone v-if="showMicrophoneOn"
- :size="20"
- title=""
- fill-color="#ffffff"
- decorative />
- <MicrophoneOff v-else
- :size="20"
- title=""
- fill-color="#ffffff"
- decorative />
- </button>
+ <template #icon>
+ <Microphone v-if="showMicrophoneOn"
+ :size="20"
+ fill-color="#ffffff" />
+ <MicrophoneOff v-else
+ :size="20"
+ fill-color="#ffffff" />
+ </template>
+ </Button>
<span v-show="model.attributes.audioAvailable"
ref="volumeIndicator"
class="volume-indicator"
:class="{'microphone-off': !showMicrophoneOn}" />
</div>
- <button id="hideVideo"
- v-shortkey.once="['v']"
+ <Button v-shortkey.once="['v']"
v-tooltip="videoButtonTooltip"
+ type="tertiary-no-background"
:aria-label="videoButtonAriaLabel"
:class="videoButtonClass"
@shortkey="toggleVideo"
@click.stop="toggleVideo">
- <VideoIcon v-if="showVideoOn"
- :size="20"
- title=""
- fill-color="#ffffff"
- decorative />
- <VideoOff v-else
- :size="20"
- title=""
- fill-color="#ffffff"
- decorative />
- </button>
- <button v-if="isVirtualBackgroundAvailable && !showActions"
+ <template #icon>
+ <VideoIcon v-if="showVideoOn"
+ :size="20"
+ fill-color="#ffffff" />
+ <VideoOff v-else
+ :size="20"
+ fill-color="#ffffff" />
+ </template>
+ </Button>
+ <Button v-if="isVirtualBackgroundAvailable && !showActions"
v-tooltip="toggleVirtualBackgroundButtonLabel"
+ type="tertiary-no-background"
:aria-label="toggleVirtualBackgroundButtonLabel"
:class="blurButtonClass"
@click.stop="toggleVirtualBackground">
- <Blur v-if="isVirtualBackgroundEnabled"
- :size="20"
- title=""
- fill-color="#ffffff"
- decorative />
- <BlurOff v-else
- :size="20"
- title=""
- fill-color="#ffffff"
- decorative />
- </button>
+ <template #icon>
+ <Blur v-if="isVirtualBackgroundEnabled"
+ :size="20"
+ fill-color="#ffffff" />
+ <BlurOff v-else
+ :size="20"
+ fill-color="#ffffff" />
+ </template>
+ </Button>
<Actions v-if="!screenSharingButtonHidden"
id="screensharing-button"
v-tooltip="screenSharingButtonTooltip"
@@ -124,101 +123,102 @@
@update:open="screenSharingMenuOpen = true"
@update:close="screenSharingMenuOpen = false">
<!-- Actions button icon -->
- <CancelPresentation v-if="model.attributes.localScreen"
- slot="icon"
- :size="20"
- title=""
- fill-color="#ffffff"
- decorative />
- <PresentToAll v-else
- slot="icon"
- :size="20"
- title=""
- fill-color="#ffffff"
- decorative />
+ <template #icon>
+ <CancelPresentation v-if="model.attributes.localScreen"
+ :size="20"
+ fill-color="#ffffff" />
+ <PresentToAll v-else
+ :size="20"
+ fill-color="#ffffff" />
+ </template>
<!-- /Actions button icon -->
<!-- Actions -->
<ActionButton v-if="!screenSharingMenuOpen"
@click.stop="toggleScreenSharingMenu">
- <PresentToAll slot="icon"
- :size="20"
- title=""
- fill-color="#ffffff"
- decorative />
+ <template #icon>
+ <PresentToAll :size="20"
+ fill-color="#ffffff" />
+ </template>
{{ screenSharingButtonTooltip }}
</ActionButton>
<ActionButton v-if="model.attributes.localScreen"
@click="showScreen">
- <Monitor slot="icon"
- :size="20"
- title=""
- decorative />
+ <template #icon>
+ <Monitor :size="20" />
+ </template>
{{ t('spreed', 'Show your screen') }}
</ActionButton>
<ActionButton v-if="model.attributes.localScreen"
@click="stopScreen">
- <CancelPresentation slot="icon"
- :size="20"
- title=""
- decorative />
+ <template #icon>
+ <CancelPresentation :size="20" />
+ </template>
{{ t('spreed', 'Stop screensharing') }}
</ActionButton>
</Actions>
- <button v-shortkey.once="['r']"
+ <Button v-shortkey.once="['r']"
v-tooltip="t('spreed', 'Lower hand (R)')"
+ type="tertiary-no-background"
class="lower-hand"
:class="model.attributes.raisedHand.state ? '' : 'hidden-visually'"
:tabindex="model.attributes.raisedHand.state ? 0 : -1"
:aria-label="t('spreed', 'Lower hand (R)')"
@shortkey="toggleHandRaised"
@click.stop="toggleHandRaised">
- <!-- The following icon is much bigger than all the others
- so we reduce its size -->
- <HandBackLeft decorative
- title=""
- :size="18"
- fill-color="#ffffff" />
- </button>
+ <template #icon>
+ <!-- The following icon is much bigger than all the others
+ so we reduce its size -->
+ <HandBackLeft :size="18"
+ fill-color="#ffffff" />
+ </template>
+ </Button>
<Actions v-if="showActions"
v-tooltip="t('spreed', 'More actions')"
:container="container"
:aria-label="t('spreed', 'More actions')">
+ <template #icon>
+ <DotsHorizontal :size="20"
+ fill-color="#ffffff" />
+ </template>
+
<ActionButton :close-after-click="true"
@click="toggleHandRaised">
<!-- The following icon is much bigger than all the others
so we reduce its size -->
- <HandBackLeft slot="icon"
- decorative
- title=""
- :size="18" />
+ <template #icon>
+ <HandBackLeft :size="18" />
+ </template>
{{ raiseHandButtonLabel }}
</ActionButton>
<ActionButton v-if="isVirtualBackgroundAvailable"
:close-after-click="true"
@click="toggleVirtualBackground">
- <BlurOff v-if="isVirtualBackgroundEnabled"
- slot="icon"
- :size="20"
- decorative
- title="" />
- <Blur v-else
- slot="icon"
- :size="20"
- decorative
- title="" />
+ <template #icon>
+ <BlurOff v-if="isVirtualBackgroundEnabled"
+ :size="20" />
+ <Blur v-else
+ :size="20" />
+ </template>
{{ toggleVirtualBackgroundButtonLabel }}
</ActionButton>
<!-- Call layout switcher -->
<ActionButton v-if="isInCall"
- :icon="changeViewIconClass"
:close-after-click="true"
@click="changeView">
+ <template #icon>
+ <GridView v-if="!isGrid"
+ :size="20" />
+ <PromotedView v-else
+ :size="20" />
+ </template>
{{ changeViewText }}
</ActionButton>
<ActionSeparator />
- <ActionButton icon="icon-settings"
- :close-after-click="true"
+ <ActionButton :close-after-click="true"
@click="showSettings">
+ <template #icon>
+ <Cog :size="20" />
+ </template>
{{ t('spreed', 'Devices settings') }}
</ActionButton>
</Actions>
@@ -231,16 +231,21 @@ import escapeHtml from 'escape-html'
import { emit } from '@nextcloud/event-bus'
import { showMessage } from '@nextcloud/dialogs'
import CancelPresentation from '../../missingMaterialDesignIcons/CancelPresentation.vue'
+import Cog from 'vue-material-design-icons/Cog'
+import DotsHorizontal from 'vue-material-design-icons/DotsHorizontal'
+import GridView from '../../missingMaterialDesignIcons/GridView.vue'
import HandBackLeft from 'vue-material-design-icons/HandBackLeft'
import Microphone from 'vue-material-design-icons/Microphone'
import MicrophoneOff from 'vue-material-design-icons/MicrophoneOff'
import Monitor from 'vue-material-design-icons/Monitor'
import PresentToAll from '../../missingMaterialDesignIcons/PresentToAll.vue'
+import PromotedView from '../../missingMaterialDesignIcons/PromotedView.vue'
import Video from 'vue-material-design-icons/Video'
import VideoOff from 'vue-material-design-icons/VideoOff'
import Blur from 'vue-material-design-icons/Blur'
import BlurOff from 'vue-material-design-icons/BlurOff'
import Popover from '@nextcloud/vue/dist/Components/Popover'
+import Button from '@nextcloud/vue/dist/Components/Button'
import Tooltip from '@nextcloud/vue/dist/Directives/Tooltip'
import { PARTICIPANT } from '../../../constants.js'
import SpeakingWhileMutedWarner from '../../../utils/webrtc/SpeakingWhileMutedWarner.js'
@@ -265,11 +270,16 @@ export default {
Actions,
ActionSeparator,
ActionButton,
+ Button,
CancelPresentation,
+ Cog,
+ DotsHorizontal,
+ GridView,
HandBackLeft,
Microphone,
MicrophoneOff,
PresentToAll,
+ PromotedView,
VideoIcon: Video,
VideoOff,
Monitor,
@@ -367,7 +377,7 @@ export default {
audioButtonClass() {
return {
- 'audio-disabled': this.isAudioAllowed && this.model.attributes.audioAvailable && !this.model.attributes.audioEnabled,
+ 'audio-enabled': this.isAudioAllowed && this.model.attributes.audioAvailable && this.model.attributes.audioEnabled,
'no-audio-available': !this.isAudioAllowed || !this.model.attributes.audioAvailable,
}
},
@@ -428,7 +438,7 @@ export default {
videoButtonClass() {
return {
- 'video-disabled': this.isVideoAllowed && this.model.attributes.videoAvailable && !this.model.attributes.videoEnabled,
+ 'video-enabled': this.isVideoAllowed && this.model.attributes.videoAvailable && this.model.attributes.videoEnabled,
'no-video-available': !this.isVideoAllowed || !this.model.attributes.videoAvailable,
}
},
@@ -481,7 +491,7 @@ export default {
screenSharingButtonClass() {
return {
- 'screensharing-disabled': this.isScreensharingAllowed && !this.model.attributes.localScreen,
+ 'screensharing-enabled': this.isScreensharingAllowed && this.model.attributes.localScreen,
'no-screensharing-available': !this.isScreensharingAllowed,
}
},
@@ -632,14 +642,6 @@ export default {
}
},
- changeViewIconClass() {
- if (this.isGrid) {
- return 'icon-promoted-view'
- } else {
- return 'icon-grid-view'
- }
- },
-
isGrid() {
return this.$store.getters.isGrid
},
@@ -887,21 +889,15 @@ export default {
.buttons-bar {
display: flex;
align-items: center;
- button, .action-item {
- vertical-align: middle;
- }
}
-.buttons-bar button, .buttons-bar button:active {
- background-color: transparent;
- border: none;
- margin: 0;
- padding: 0 12px;
- width: $clickable-area;
- height: $clickable-area;
- &:active {
- background: transparent;
- }
+.buttons-bar button.lower-hand.hidden-visually {
+ position: absolute;
+ left: -10000px;
+ top: -10000px;
+ width: 1px;
+ height: 1px;
+ overflow: hidden;
}
.buttons-bar #screensharing-menu button {
@@ -909,21 +905,11 @@ export default {
height: auto;
}
-.buttons-bar button.audio-disabled,
-.buttons-bar button.video-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:not(.no-screensharing-available),
-.buttons-bar button.lower-hand {
- &:hover,
- &:focus {
- opacity: 1;
- }
+/* Highlight the media buttons when enabled */
+.buttons-bar button.audio-enabled,
+.buttons-bar button.video-enabled,
+.buttons-bar button.screensharing-enabled {
+ opacity: 1;
}
.buttons-bar button.no-audio-available,
@@ -935,12 +921,6 @@ export default {
}
}
-.buttons-bar button.no-audio-available:active,
-.buttons-bar button.no-video-available:active,
-.buttons-bar button.no-screensharing-available:active {
- background-color: transparent;
-}
-
#muteWrapper {
display: inline-block;
@@ -985,16 +965,6 @@ export default {
}
}
-::v-deep button.action-item,
-::v-deep .action-item__menutoggle {
- // Fix screensharing icon width
- &:hover,
- &:focus,
- &:active {
- background-color: transparent;
- }
-}
-
.trigger {
display: flex;
align-items: center;
diff --git a/src/components/CallView/shared/Video.vue b/src/components/CallView/shared/Video.vue
index 6f60f5b45..73f24decd 100644
--- a/src/components/CallView/shared/Video.vue
+++ b/src/components/CallView/shared/Video.vue
@@ -70,8 +70,6 @@
:key="'placeholderForPromoted'"
class="placeholder-for-promoted">
<AccountCircle v-if="isPromoted || isSelected"
- decorative
- title=""
fill-color="#FFFFFF"
:size="36" />
</div>
diff --git a/src/components/CallView/shared/VideoBackground.vue b/src/components/CallView/shared/VideoBackground.vue
index 23d313549..7025c5c03 100644
--- a/src/components/CallView/shared/VideoBackground.vue
+++ b/src/components/CallView/shared/VideoBackground.vue
@@ -1,7 +1,7 @@
<!--
- - @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@pm.me>
+ - @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@icloud.com>
-
- - @author Marco Ambrosini <marcoambrosini@pm.me>
+ - @author Marco Ambrosini <marcoambrosini@icloud.com>
-
- @license GNU AGPL version 3 or any later version
-
diff --git a/src/components/CallView/shared/VideoBottomBar.vue b/src/components/CallView/shared/VideoBottomBar.vue
index 0dc05a2db..751f15af3 100644
--- a/src/components/CallView/shared/VideoBottomBar.vue
+++ b/src/components/CallView/shared/VideoBottomBar.vue
@@ -1,7 +1,7 @@
<!--
- - @copyright Copyright (c) 2020 Marco Ambrosini <marcoambrosini@pm.me>
+ - @copyright Copyright (c) 2020 Marco Ambrosini <marcoambrosini@icloud.com>
-
- - @author Marco Ambrosini <marcoambrosini@pm.me>
+ - @author Marco Ambrosini <marcoambrosini@icloud.com>
-
- @license GNU AGPL version 3 or any later version
-
@@ -26,8 +26,6 @@
<div v-if="!connectionStateFailedNoRestart && model.attributes.raisedHand.state"
class="bottom-bar__statusIndicator">
<HandBackLeft class="handIndicator"
- decorative
- title=""
size="18px"
fill-color="#ffffff" />
</div>
@@ -54,14 +52,10 @@
@click.stop="forceMute">
<Microphone v-if="showMicrophone"
:size="20"
- title=""
- fill-color="#ffffff"
- decorative />
+ fill-color="#ffffff" />
<MicrophoneOff v-if="showMicrophoneOff"
:size="20"
- title=""
- fill-color="#ffffff"
- decorative />
+ fill-color="#ffffff" />
</button>
<button v-show="!connectionStateFailedNoRestart && model.attributes.videoAvailable"
v-tooltip="videoButtonTooltip"
@@ -69,14 +63,10 @@
@click.stop="toggleVideo">
<VideoIcon v-if="showVideoButton"
:size="20"
- title=""
- fill-color="#ffffff"
- decorative />
+ fill-color="#ffffff" />
<VideoOff v-if="!showVideoButton"
:size="20"
- title=""
- fill-color="#ffffff"
- decorative />
+ fill-color="#ffffff" />
</button>
<button v-show="!connectionStateFailedNoRestart"
v-tooltip="t('spreed', 'Show screen')"
@@ -84,18 +74,14 @@
:class="screenSharingButtonClass"
@click.stop="switchToScreen">
<Monitor :size="20"
- title=""
- fill-color="#ffffff"
- decorative />
+ fill-color="#ffffff" />
</button>
<button v-show="connectionStateFailedNoRestart"
class="iceFailedIndicator"
:class="{ 'not-failed': !connectionStateFailedNoRestart }"
disabled="true">
<AlertCircle :size="20"
- title=""
- fill-color="#ffffff"
- decorative />
+ fill-color="#ffffff" />
</button>
</div>
</transition>
diff --git a/src/components/ConversationIcon.vue b/src/components/ConversationIcon.vue
index c7fe753cf..cbee8980c 100644
--- a/src/components/ConversationIcon.vue
+++ b/src/components/ConversationIcon.vue
@@ -37,12 +37,14 @@
class="conversation-icon__avatar" />
<div v-if="showCall"
class="overlap-icon">
- <span class="icon icon-active-call" />
+ <Video :size="20"
+ :fill-color="'#E9322D'" />
<span class="hidden-visually">{{ t('spreed', 'Call in progress') }}</span>
</div>
<div v-else-if="showFavorite"
class="overlap-icon">
- <span class="icon icon-favorite" />
+ <Star :size="20"
+ :fill-color="'#FFCC00'" />
<span class="hidden-visually">{{ t('spreed', 'Favorite') }}</span>
</div>
</div>
@@ -50,12 +52,16 @@
<script>
import Avatar from '@nextcloud/vue/dist/Components/Avatar'
+import Star from 'vue-material-design-icons/Star'
+import Video from 'vue-material-design-icons/Video'
import { CONVERSATION } from '../constants.js'
export default {
name: 'ConversationIcon',
components: {
Avatar,
+ Star,
+ Video,
},
props: {
/**
@@ -175,18 +181,8 @@ $icon-size: 44px;
top: 0;
left: calc(#{$icon-size} - 12px);
line-height: 100%;
-
- .icon-favorite {
- display: inline-block;
- vertical-align: middle;
- background-image: var(--icon-star-dark-FC0);
- }
-
- .icon-active-call {
- display: inline-block;
- vertical-align: middle;
- background-image: var(--icon-video-E9322D);
- }
+ display: inline-block;
+ vertical-align: middle;
}
}
diff --git a/src/components/ConversationSettings/ConversationPermissionsSettings.vue b/src/components/ConversationSettings/ConversationPermissionsSettings.vue
index f10fdaf24..ff5515d82 100644
--- a/src/components/ConversationSettings/ConversationPermissionsSettings.vue
+++ b/src/components/ConversationSettings/ConversationPermissionsSettings.vue
@@ -1,7 +1,7 @@
<!--
- - @copyright Copyright (c) 2021 Marco Ambrosini <marcoambrosini@pm.me>
+ - @copyright Copyright (c) 2021 Marco Ambrosini <marcoambrosini@icloud.com>
-
- - @author Marco Ambrosini <marcoambrosini@pm.me>
+ - @author Marco Ambrosini <marcoambrosini@icloud.com>
-
- @license GNU AGPL version 3 or any later version
-
@@ -66,14 +66,14 @@
</CheckboxRadioSwitch>
<!-- Edit advanced permissions -->
- <button v-show="showEditButton"
+ <ButtonVue v-show="showEditButton"
+ type="tertiary"
:aria-label="t('spreed', 'Edit permissions')"
- class="nc-button nc-button__main"
@click="showPermissionsEditor = true">
- <Pencil :size="20"
- decorative
- title="" />
- </button>
+ <template #icon>
+ <Pencil :size="20" />
+ </template>
+ </ButtonVue>
</div>
<PermissionEditor v-if="showPermissionsEditor"
:conversation-name="conversationName"
@@ -86,6 +86,7 @@
<script>
import PermissionEditor from '../PermissionsEditor/PermissionsEditor.vue'
+import ButtonVue from '@nextcloud/vue/dist/Components/Button'
import CheckboxRadioSwitch from '@nextcloud/vue/dist/Components/CheckboxRadioSwitch'
import Pencil from 'vue-material-design-icons/Pencil.vue'
import { PARTICIPANT } from '../../constants.js'
@@ -98,6 +99,7 @@ export default {
components: {
PermissionEditor,
+ ButtonVue,
CheckboxRadioSwitch,
Pencil,
},
@@ -234,8 +236,6 @@ export default {
</script>
<style lang="scss" scoped>
-@import '../../assets/buttons';
-
::v-deep .mx-input {
margin: 0;
}
diff --git a/src/components/ConversationSettings/ConversationSettingsDialog.vue b/src/components/ConversationSettings/ConversationSettingsDialog.vue
index 59de98373..d0452fc4e 100644
--- a/src/components/ConversationSettings/ConversationSettingsDialog.vue
+++ b/src/components/ConversationSettings/ConversationSettingsDialog.vue
@@ -27,6 +27,7 @@
:container="container">
<!-- description -->
<AppSettingsSection v-if="showDescription"
+ id="description"
:title="t('spreed', 'Description')">
<Description :editable="canFullModerate"
:description="description"
@@ -38,12 +39,14 @@
</AppSettingsSection>
<!-- Notifications settings -->
- <AppSettingsSection :title="t('spreed', 'Notifications')">
+ <AppSettingsSection id="notifications"
+ :title="t('spreed', 'Notifications')">
<NotificationsSettings :conversation="conversation" />
</AppSettingsSection>
<!-- Devices preview sceren -->
- <AppSettingsSection :title="t('spreed', 'Device check')">
+ <AppSettingsSection id="device-checker"
+ :title="t('spreed', 'Device check')">
<CheckboxRadioSwitch :checked.sync="showDeviceChecker">
{{ t('spreed', 'Always show the device preview screen before joining a call in this conversation.') }}
</CheckboxRadioSwitch>
@@ -51,6 +54,7 @@
<!-- Guest access -->
<AppSettingsSection v-if="canFullModerate"
+ id="guests"
:title="t('spreed', 'Guests access')">
<LinkShareSettings ref="linkShareSettings" />
</AppSettingsSection>
@@ -60,19 +64,23 @@
move lock conversation in destructive actions and create a separate
section for listablesettings -->
<AppSettingsSection v-if="canFullModerate"
+ id="conversation-settings"
:title="t('spreed', 'Conversation settings')">
+ <ExpirationSettings :token="token" />
<ListableSettings :token="token" />
<LockingSettings :token="token" />
</AppSettingsSection>
<!-- Conversation permissions -->
<AppSettingsSection v-if="canFullModerate"
+ id="permissions"
:title="t('spreed', 'Participants permissions')">
<ConversationPermissionsSettings :token="token" />
</AppSettingsSection>
<!-- Meeting settings -->
<AppSettingsSection v-if="canFullModerate"
+ id="meeting"
:title="t('spreed', 'Meeting settings')">
<LobbySettings :token="token" />
<SipSettings v-if="canUserEnableSIP" />
@@ -84,6 +92,7 @@
<!-- Destructive actions -->
<AppSettingsSection v-if="canLeaveConversation || canDeleteConversation"
+ id="dangerzone"
:title="t('spreed', 'Danger zone')">
<DangerZone :conversation="conversation"
:can-leave-conversation="canLeaveConversation"
@@ -97,6 +106,7 @@ import { subscribe, unsubscribe } from '@nextcloud/event-bus'
import { PARTICIPANT, CONVERSATION } from '../../constants.js'
import AppSettingsDialog from '@nextcloud/vue/dist/Components/AppSettingsDialog'
import AppSettingsSection from '@nextcloud/vue/dist/Components/AppSettingsSection'
+import ExpirationSettings from './ExpirationSettings.vue'
import LinkShareSettings from './LinkShareSettings.vue'
import ListableSettings from './ListableSettings.vue'
import LockingSettings from './LockingSettings.vue'
@@ -118,6 +128,7 @@ export default {
components: {
AppSettingsDialog,
AppSettingsSection,
+ ExpirationSettings,
LinkShareSettings,
LobbySettings,
ListableSettings,
diff --git a/src/components/ConversationSettings/DangerZone.vue b/src/components/ConversationSettings/DangerZone.vue
index 56ce4234d..610b9f278 100644
--- a/src/components/ConversationSettings/DangerZone.vue
+++ b/src/components/ConversationSettings/DangerZone.vue
@@ -1,7 +1,7 @@
<!--
- - @copyright Copyright (c) 2021 Marco Ambrosini <marcoambrosini@pm.me>
+ - @copyright Copyright (c) 2021 Marco Ambrosini <marcoambrosini@icloud.com>
-
- - @author Marco Ambrosini <marcoambrosini@pm.me>
+ - @author Marco Ambrosini <marcoambrosini@icloud.com>
-
- @license GNU AGPL version 3 or any later version
-
@@ -178,13 +178,6 @@ export default {
</script>
<style lang="scss" scoped>
-button {
- height: 44px;
- border: none;
- display: block;
- margin: 8px 0;
-}
-
h4 {
font-weight: bold;
}
diff --git a/src/components/ConversationSettings/ExpirationSettings.vue b/src/components/ConversationSettings/ExpirationSettings.vue
new file mode 100644
index 000000000..26aebc658
--- /dev/null
+++ b/src/components/ConversationSettings/ExpirationSettings.vue
@@ -0,0 +1,135 @@
+<!--
+ - @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com>
+ -
+ - @author Joas Schilling <coding@schilljs.com>
+ -
+ - @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>
+ <div class="app-settings-section__hint">
+ {{ t('spreed', 'Expire chat messages after a certain time. Note files shared into the chat will only be unshared from the conversation but are not deleted for the owner.') }}
+ </div>
+ <Multiselect :value="selectedOption"
+ :options="expirationOptions"
+ :allow-empty="false"
+ track-by="id"
+ label="label"
+ :close-on-select="true"
+ @update:value="changeExpiration" />
+ </div>
+</template>
+
+<script>
+import Multiselect from '@nextcloud/vue/dist/Components/Multiselect'
+import { showError, showSuccess } from '@nextcloud/dialogs'
+
+export default {
+ name: 'ExpirationSettings',
+
+ components: {
+ Multiselect,
+ },
+
+ props: {
+ token: {
+ type: String,
+ default: null,
+ },
+ },
+
+ data() {
+ return {
+ overwriteExpiration: undefined,
+ defaultExpirationOptions: [
+ { id: 0, label: t('spreed', 'Off') },
+ { id: 2419200, label: n('spreed', '%n week', '%n weeks', 4) },
+ { id: 604800, label: n('spreed', '%n week', '%n weeks', 1) },
+ { id: 86400, label: n('spreed', '%n day', '%n days', 1) },
+ { id: 28800, label: n('spreed', '%n hour', '%n hours', 8) },
+ { id: 3600, label: n('spreed', '%n hour', '%n hours', 1) },
+ ],
+ }
+ },
+
+ computed: {
+ conversation() {
+ return this.$store.getters.conversation(this.token) || this.$store.getters.dummyConversation
+ },
+
+ expirationOptions() {
+ const expirationOptions = [...this.defaultExpirationOptions]
+
+ const found = expirationOptions.find((option) => {
+ return option.id === this.conversation.messageExpiration
+ })
+ if (!found) {
+ expirationOptions.push({ id: this.conversation.messageExpiration, label: t('spreed', 'Custom expiration time') })
+ }
+
+ return expirationOptions
+ },
+
+ selectedOption() {
+ if (this.overwriteExpiration) {
+ return this.overwriteExpiration
+ }
+
+ const option = this.expirationOptions.find((option) => {
+ return option.id === this.conversation.messageExpiration
+ })
+ if (option) {
+ return option
+ }
+
+ return this.expirationOptions[this.expirationOptions.length - 1]
+ },
+ },
+
+ methods: {
+ async changeExpiration(expiration) {
+ this.overwriteExpiration = expiration
+
+ try {
+ await this.$store.dispatch('setMessageExpiration', {
+ token: this.token,
+ seconds: expiration.id,
+ })
+
+ if (expiration.id === 0) {
+ showSuccess(t('spreed', 'Message expiration disabled'))
+ } else {
+ showSuccess(t('spreed', 'Message expiration set: {duration}', {
+ duration: expiration.label,
+ }))
+ }
+ } catch (error) {
+ showError(t('spreed', 'Error when trying to set the messages expiration'))
+ console.error(error)
+ }
+
+ this.overwriteExpiration = undefined
+ },
+ },
+}
+</script>
+
+<style lang="scss" scoped>
+::v-deep .mx-input {
+ margin: 0;
+}
+</style>
diff --git a/src/components/ConversationSettings/LinkShareSettings.vue b/src/components/ConversationSettings/LinkShareSettings.vue
index 14706ed41..8abb2f0d4 100644
--- a/src/components/ConversationSettings/LinkShareSettings.vue
+++ b/src/components/ConversationSettings/LinkShareSettings.vue
@@ -70,43 +70,50 @@
name="link_share_settings_link_password"
:placeholder="t('spreed', 'Enter a password')"
:disabled="isSaving">
- <button id="link_share_settings_link_password_submit"
+ <Button id="link_share_settings_link_password_submit"
:aria-label="t('spreed', 'Save password')"
:disabled="isSaving"
- type="submit"
- class="icon icon-confirm-fade" />
+ native-type="submit">
+ <template #icon>
+ <ArrowRight />
+ </template>
+ </Button>
</form>
</div>
</div>
<div class="app-settings-subsection">
- <button ref="copyLinkButton"
- @click.prevent="handleCopyLink">
- <ClipboardTextOutline :size="16"
- decorative
- title="" />
+ <Button ref="copyLinkButton"
+ @click.prevent="handleCopyLink"
+ @keydown.enter="handleCopyLink">
+ <template #icon>
+ <ClipboardTextOutline />
+ </template>
{{ t('spreed', 'Copy conversation link') }}
- </button>
+ </Button>
</div>
<div v-if="isSharedPublicly" class="app-settings-subsection">
- <button :disabled="isSendingInvitations"
- @click.prevent="handleResendInvitations">
- <Email :size="16"
- decorative
- title="" />
+ <Button :disabled="isSendingInvitations"
+ @click.prevent="handleResendInvitations"
+ @keydown.enter="handleResendInvitations">
+ <template #icon>
+ <Email />
+ </template>
{{ t('spreed', 'Resend invitations') }}
- </button>
+ </Button>
<span v-if="isSendingInvitations" class="icon-loading-small spinner" />
</div>
</div>
</template>
<script>
+import Button from '@nextcloud/vue/dist/Components/Button'
import { showError, showSuccess } from '@nextcloud/dialogs'
import { CONVERSATION } from '../../constants.js'
import {
setConversationPassword,
} from '../../services/conversationsService.js'
import { generateUrl } from '@nextcloud/router'
+import ArrowRight from 'vue-material-design-icons/ArrowRight'
import ClipboardTextOutline from 'vue-material-design-icons/ClipboardTextOutline'
import Email from 'vue-material-design-icons/Email'
@@ -114,6 +121,8 @@ export default {
name: 'LinkShareSettings',
components: {
+ Button,
+ ArrowRight,
ClipboardTextOutline,
Email,
},
@@ -202,8 +211,11 @@ export default {
async togglePassword() {
if (this.$refs.togglePassword.checked) {
this.showPasswordField = true
- this.$refs.passwordField.focus()
await this.handlePasswordEnable()
+ this.$nextTick(() => {
+ console.error(this.$refs.passwordField)
+ this.$refs.passwordField.focus()
+ })
} else {
this.showPasswordField = false
await this.handlePasswordDisable()
diff --git a/src/components/ConversationSettings/Matterbridge/BridgePart.vue b/src/components/ConversationSettings/Matterbridge/BridgePart.vue
index 19dde6197..80aefd666 100644
--- a/src/components/ConversationSettings/Matterbridge/BridgePart.vue
+++ b/src/components/ConversationSettings/Matterbridge/BridgePart.vue
@@ -185,10 +185,6 @@ export default {
padding-top: 10px;
}
-button {
- display: inline-block;
-}
-
h3 {
display: flex;
margin-bottom: 0;
diff --git a/src/components/ConversationSettings/Matterbridge/MatterbridgeSettings.vue b/src/components/ConversationSettings/Matterbridge/MatterbridgeSettings.vue
index aec78bcf8..5a614651d 100644
--- a/src/components/ConversationSettings/Matterbridge/MatterbridgeSettings.vue
+++ b/src/components/ConversationSettings/Matterbridge/MatterbridgeSettings.vue
@@ -35,7 +35,7 @@
<div class="basic-settings">
<div v-show="!enabled"
class="add-part-wrapper">
- <span class="icon icon-add" />
+ <Plus class="icon" size="20" />
<Multiselect ref="partMultiselect"
v-model="selectedType"
label="displayName"
@@ -64,10 +64,15 @@
{{ t('spreed', 'Enable bridge') }}
({{ processStateText }})
</label>
- <button v-if="enabled"
+ <ButtonVue v-if="enabled"
+ type="tertiary"
v-tooltip.top="{ content: t('spreed', 'Show Matterbridge log') }"
- class="icon icon-edit"
- @click="showLogContent" />
+ :aria-label="t('spreed', 'Show Matterbridge log')"
+ @click="showLogContent">
+ <template #icon>
+ <Message size="20" />
+ </template>
+ </ButtonVue>
<Modal v-if="logModal"
:container="container"
@close="closeLogModal">
@@ -100,8 +105,11 @@ import {
} from '../../../services/matterbridgeService.js'
import { showSuccess } from '@nextcloud/dialogs'
import { imagePath } from '@nextcloud/router'
+import ButtonVue from '@nextcloud/vue/dist/Components/Button'
import Multiselect from '@nextcloud/vue/dist/Components/Multiselect'
+import Message from 'vue-material-design-icons/Message'
import Modal from '@nextcloud/vue/dist/Components/Modal'
+import Plus from 'vue-material-design-icons/Plus'
import BridgePart from './BridgePart.vue'
import Vue from 'vue'
@@ -113,7 +121,10 @@ export default {
components: {
Multiselect,
BridgePart,
+ ButtonVue,
+ Message,
Modal,
+ Plus,
},
mixins: [
@@ -633,6 +644,10 @@ export default {
filter: var(--background-invert-if-dark);
}
+::v-deep .modal-container {
+ height: 700px;
+}
+
.matterbridge-settings {
.loading {
margin-top: 30px;
@@ -695,7 +710,8 @@ export default {
display: inline-block;
width: 40px;
height: 34px;
- background-position: 14px center;
+ padding: 6px 10px 0;
+ vertical-align: middle;
}
.add-part-wrapper {
margin-top: 5px;
@@ -712,21 +728,6 @@ export default {
margin: 0 10px 0 15px;
}
}
- button {
- opacity: 0.5;
- width: 44px;
- height: 44px;
- border-radius: var(--border-radius-pill);
- background-color: transparent;
- border: none;
- margin: 0;
-
- &:hover,
- &:focus {
- opacity: 1;
- background-color: var(--color-background-hover);
- }
- }
}
}
diff --git a/src/components/ConversationSettings/NotificationsSettings.vue b/src/components/ConversationSettings/NotificationsSettings.vue
index 2af6da5c3..89af3722f 100644
--- a/src/components/ConversationSettings/NotificationsSettings.vue
+++ b/src/components/ConversationSettings/NotificationsSettings.vue
@@ -1,7 +1,7 @@
<!--
- - @copyright Copyright (c) 2020 Marco Ambrosini <marcoambrosini@pm.me>
+ - @copyright Copyright (c) 2020 Marco Ambrosini <marcoambrosini@icloud.com>
-
- - @author Marco Ambrosini <marcoambrosini@pm.me>
+ - @author Marco Ambrosini <marcoambrosini@icloud.com>
-
- @license GNU AGPL version 3 or any later version
-
@@ -26,51 +26,39 @@
class="radio-element"
:class="{'radio-element--active': isNotifyAlways}"
@click.prevent.exact="setNotificationLevel(1)">
- <VolumeHigh decorative
- title=""
- :size="20"
+ <VolumeHigh :size="20"
class="radio-element__icon" />
<label class="radio-element__label">
{{ t('spreed', 'All messages') }}
</label>
<Check v-if="isNotifyAlways"
class="check"
- decorative
- title=""
:size="20" />
</a>
<a href="#"
class="radio-element"
:class="{'radio-element--active': isNotifyMention}"
@click.prevent.exact="setNotificationLevel(2)">
- <Account decorative
- title=""
- :size="20"
+ <Account :size="20"
class="radio-element__icon" />
<label class="radio-element__label">
{{ t('spreed', '@-mentions only') }}
</label>
<Check v-if="isNotifyMention"
class="check"
- decorative
- title=""
:size="20" />
</a>
<a href="#"
class="radio-element"
:class="{'radio-element--active': isNotifyNever}"
@click.prevent.exact="setNotificationLevel(3)">
- <VolumeOff decorative
- title=""
- :size="20"
+ <VolumeOff :size="20"
class="radio-element__icon" />
<label class="radio-element__label">
{{ t('spreed', 'Off') }}
</label>
<Check v-if="isNotifyNever"
class="check"
- decorative
- title=""
:size="20" />
</a>
diff --git a/src/components/Description/Description.vue b/src/components/Description/Description.vue
index 45afadbe7..6d9cb9dff 100644
--- a/src/components/Description/Description.vue
+++ b/src/components/Description/Description.vue
@@ -1,7 +1,7 @@
<!--
- - @copyright Copyright (c) 2020 Marco Ambrosini <marcoambrosini@pm.me>
+ - @copyright Copyright (c) 2020 Marco Ambrosini <marcoambrosini@icloud.com>
-
- - @author Marco Ambrosini <marcoambrosini@pm.me>
+ - @author Marco Ambrosini <marcoambrosini@icloud.com>
-
- @license GNU AGPL version 3 or any later version
-
@@ -39,9 +39,7 @@
:aria-label="t('spreed', 'Cancel editing description')"
@click="handleCancelEditing">
<template #icon>
- <Close decorative
- title=""
- :size="20" />
+ <Close :size="20" />
</template>
</Button>
<Button type="primary"
@@ -49,9 +47,7 @@
:disabled="!canSubmit"
@click="handleSubmitDescription">
<template #icon>
- <Check decorative
- title=""
- :size="20" />
+ <Check :size="20" />
</template>
</Button>
<div v-if="showCountDown"
@@ -67,9 +63,7 @@
:aria-label="t('spreed', 'Edit conversation description')"
@click="handleEditDescription">
<template #icon>
- <Pencil decorative
- title=""
- :size="20" />
+ <Pencil :size="20" />
</template>
</Button>
</template>
@@ -249,7 +243,6 @@ export default {
<style lang="scss" scoped>
@import '../../assets/variables';
-@import '../../assets/buttons';
.description {
display: flex;
diff --git a/src/components/DeviceChecker/DeviceChecker.vue b/src/components/DeviceChecker/DeviceChecker.vue
index 3ca0cca16..925c15b9e 100644
--- a/src/components/DeviceChecker/DeviceChecker.vue
+++ b/src/components/DeviceChecker/DeviceChecker.vue
@@ -1,7 +1,7 @@
<!--
- - @copyright Copyright (c) 2021 Marco Ambrosini <marcoambrosini@pm.me>
+ - @copyright Copyright (c) 2021 Marco Ambrosini <marcoambrosini@icloud.com>
-
- - @author Marco Ambrosini <marcoambrosini@pm.me>
+ - @author Marco Ambrosini <marcoambrosini@icloud.com>
-
- @license GNU AGPL version 3 or any later version
-
@@ -67,12 +67,8 @@
@click="toggleAudio">
<template #icon>
<Microphone v-if="audioOn"
- title=""
- decorative
:size="20" />
<MicrophoneOff v-else
- title=""
- decorative
:size="20" />
</template>
</Button>
@@ -90,12 +86,8 @@
@click="toggleVideo">
<template #icon>
<Video v-if="videoOn"
- title=""
- decorative
:size="20" />
<VideoOff v-else
- title=""
- decorative
:size="20" />
</template>
</Button>
@@ -109,13 +101,9 @@
@click="toggleBlur">
<template #icon>
<Blur v-if="blurOn"
- :size="20"
- decorative
- title="" />
+ :size="20" />
<BlurOff v-else
- :size="20"
- decorative
- title="" />
+ :size="20" />
</template>
</Button>
</div>
@@ -127,9 +115,7 @@
class="select-devices"
@click="showDeviceSelection = true">
<template #icon>
- <Cog title=""
- decorative
- :size="20" />
+ <Cog :size="20" />
</template>
{{ t('spreed', 'Choose devices') }}
</Button>
@@ -159,9 +145,7 @@
@click="silentCall= true">
{{ t('spreed', 'The conversation participants will not be notified about this call') }}
<BellOff slot="icon"
- :size="16"
- decorative
- title="" />
+ :size="16" />
</ActionButton>
</template>
<template v-else>
@@ -171,9 +155,7 @@
@click="silentCall= false">
{{ t('spreed', 'The conversation participants will be notified about this call') }}
<Bell slot="icon"
- :size="16"
- decorative
- title="" />
+ :size="16" />
</ActionButton>
</template>
</Actions>
diff --git a/src/components/LeftSidebar/ConversationsList/Conversation.vue b/src/components/LeftSidebar/ConversationsList/Conversation.vue
index fe7edbb56..fc6932361 100644
--- a/src/components/LeftSidebar/ConversationsList/Conversation.vue
+++ b/src/components/LeftSidebar/ConversationsList/Conversation.vue
@@ -45,8 +45,14 @@
</template>
<template v-if="!isSearchResult" slot="actions">
<ActionButton v-if="canFavorite"
- :icon="iconFavorite"
@click.prevent.exact="toggleFavoriteConversation">
+ <Star v-if="item.isFavorite"
+ slot="icon"
+ :size="20" />
+ <Star v-else
+ slot="icon"
+ :size="20"
+ :fill-color="'#FFCC00'" />
{{ labelFavorite }}
</ActionButton>
<ActionButton icon="icon-clippy"
@@ -56,15 +62,14 @@
<ActionButton :close-after-click="true"
@click.prevent.exact="markConversationAsRead">
<template #icon>
- <EyeOutline decorative
- title=""
- :size="16" />
+ <EyeOutline :size="16" />
</template>
{{ t('spreed', 'Mark as read') }}
</ActionButton>
- <ActionButton icon="icon-settings"
- :close-after-click="true"
+ <ActionButton :close-after-click="true"
@click.prevent.exact="showConversationSettings">
+ <Cog slot="icon"
+ :size="20" />
{{ t('spreed', 'Conversation settings') }}
</ActionButton>
<ActionButton v-if="canLeaveConversation"
@@ -75,9 +80,11 @@
</ActionButton>
<ActionButton v-if="canDeleteConversation"
:close-after-click="true"
- icon="icon-delete-critical"
class="critical"
@click.prevent.exact="deleteConversation">
+ <template #icon>
+ <Delete :size="16" />
+ </template>
{{ t('spreed', 'Delete conversation') }}
</ActionButton>
</template>
@@ -87,7 +94,10 @@
<script>
import { showError, showSuccess } from '@nextcloud/dialogs'
import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
+import Cog from 'vue-material-design-icons/Cog'
+import Delete from 'vue-material-design-icons/Delete'
import EyeOutline from 'vue-material-design-icons/EyeOutline'
+import Star from 'vue-material-design-icons/Star'
import ConversationIcon from './../../ConversationIcon.vue'
import { generateUrl } from '@nextcloud/router'
import { emit } from '@nextcloud/event-bus'
@@ -100,7 +110,10 @@ export default {
ActionButton,
ListItem,
ConversationIcon,
+ Cog,
+ Delete,
EyeOutline,
+ Star,
},
props: {
isSearchResult: {
@@ -371,7 +384,7 @@ export default {
}
.critical {
- ::v-deep .action-button__text {
+ ::v-deep .action-button {
color: var(--color-error) !important;
}
}
diff --git a/src/components/LeftSidebar/ConversationsList/ConversationsList.vue b/src/components/LeftSidebar/ConversationsList/ConversationsList.vue
index a0f9563c6..5f3c2c863 100644
--- a/src/components/LeftSidebar/ConversationsList/ConversationsList.vue
+++ b/src/components/LeftSidebar/ConversationsList/ConversationsList.vue
@@ -1,7 +1,7 @@
<!--
- - @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@pm.me>
+ - @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@icloud.com>
-
- - @author Marco Ambrosini <marcoambrosini@pm.me>
+ - @author Marco Ambrosini <marcoambrosini@icloud.com>
-
- @license GNU AGPL version 3 or any later version
-
diff --git a/src/components/LeftSidebar/LeftSidebar.vue b/src/components/LeftSidebar/LeftSidebar.vue
index 1652ddf8a..2ba8d527a 100644
--- a/src/components/LeftSidebar/LeftSidebar.vue
+++ b/src/components/LeftSidebar/LeftSidebar.vue
@@ -1,7 +1,7 @@
<!--
- - @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@pm.me>
+ - @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@icloud.com>
-
- - @author Marco Ambrosini <marcoambrosini@pm.me>
+ - @author Marco Ambrosini <marcoambrosini@icloud.com>
-
- @license GNU AGPL version 3 or any later version
-
@@ -33,65 +33,67 @@
<NewGroupConversation v-if="canStartConversations" />
</div>
<template #list>
- <div ref="scroller"
- class="left-sidebar__list"
- @scroll="debounceHandleScroll">
- <AppNavigationCaption v-if="isSearching"
- :title="t('spreed', 'Conversations')" />
- <li role="presentation">
- <ConversationsList ref="conversationsList"
- :conversations-list="conversationsList"
- :initialised-conversations="initialisedConversations"
- :search-text="searchText"
- @click-search-result="handleClickSearchResult"
- @focus="setFocusedIndex" />
- </li>
- <template v-if="isSearching">
- <template v-if="!listedConversationsLoading && searchResultsListedConversations.length > 0">
- <AppNavigationCaption :title="t('spreed', 'Open conversations')" />
- <Conversation v-for="item of searchResultsListedConversations"
- :key="item.id"
- :item="item"
- :is-search-result="true"
- @click="joinListedConversation(item)" />
+ <li class="left-sidebar__list">
+ <ul ref="scroller"
+ class="scroller"
+ @scroll="debounceHandleScroll">
+ <AppNavigationCaption :class="{'hidden-visually': !isSearching}"
+ :title="t('spreed', 'Conversations')" />
+ <li role="presentation">
+ <ConversationsList ref="conversationsList"
+ :conversations-list="conversationsList"
+ :initialised-conversations="initialisedConversations"
+ :search-text="searchText"
+ @click-search-result="handleClickSearchResult"
+ @focus="setFocusedIndex" />
+ </li>
+ <template v-if="isSearching">
+ <template v-if="!listedConversationsLoading && searchResultsListedConversations.length > 0">
+ <AppNavigationCaption :title="t('spreed', 'Open conversations')" />
+ <Conversation v-for="item of searchResultsListedConversations"
+ :key="item.id"
+ :item="item"
+ :is-search-result="true"
+ @click="joinListedConversation(item)" />
+ </template>
+ <template v-if="searchResultsUsers.length !== 0">
+ <AppNavigationCaption :title="t('spreed', 'Users')" />
+ <li v-if="searchResultsUsers.length !== 0" role="presentation">
+ <ConversationsOptionsList :items="searchResultsUsers"
+ @click="createAndJoinConversation" />
+ </li>
+ </template>
+ <template v-if="!showStartConversationsOptions">
+ <AppNavigationCaption v-if="searchResultsUsers.length === 0"
+ :title="t('spreed', 'Users')" />
+ <Hint v-if="contactsLoading" :hint="t('spreed', 'Loading')" />
+ <Hint v-else :hint="t('spreed', 'No search results')" />
+ </template>
</template>
- <template v-if="searchResultsUsers.length !== 0">
- <AppNavigationCaption :title="t('spreed', 'Users')" />
- <li v-if="searchResultsUsers.length !== 0" role="presentation">
- <ConversationsOptionsList :items="searchResultsUsers"
- @click="createAndJoinConversation" />
- </li>
- </template>
- <template v-if="!showStartConversationsOptions">
- <AppNavigationCaption v-if="searchResultsUsers.length === 0"
- :title="t('spreed', 'Users')" />
+ <template v-if="showStartConversationsOptions">
+ <template v-if="searchResultsGroups.length !== 0">
+ <AppNavigationCaption :title="t('spreed', 'Groups')" />
+ <li v-if="searchResultsGroups.length !== 0" role="presentation">
+ <ConversationsOptionsList :items="searchResultsGroups"
+ @click="createAndJoinConversation" />
+ </li>
+ </template>
+
+ <template v-if="searchResultsCircles.length !== 0">
+ <AppNavigationCaption :title="t('spreed', 'Circles')" />
+ <li v-if="searchResultsCircles.length !== 0" role="presentation">
+ <ConversationsOptionsList :items="searchResultsCircles"
+ @click="createAndJoinConversation" />
+ </li>
+ </template>
+
+ <AppNavigationCaption v-if="sourcesWithoutResults"
+ :title="sourcesWithoutResultsList" />
<Hint v-if="contactsLoading" :hint="t('spreed', 'Loading')" />
<Hint v-else :hint="t('spreed', 'No search results')" />
</template>
- </template>
- <template v-if="showStartConversationsOptions">
- <template v-if="searchResultsGroups.length !== 0">
- <AppNavigationCaption :title="t('spreed', 'Groups')" />
- <li v-if="searchResultsGroups.length !== 0" role="presentation">
- <ConversationsOptionsList :items="searchResultsGroups"
- @click="createAndJoinConversation" />
- </li>
- </template>
-
- <template v-if="searchResultsCircles.length !== 0">
- <AppNavigationCaption :title="t('spreed', 'Circles')" />
- <li v-if="searchResultsCircles.length !== 0" role="presentation">
- <ConversationsOptionsList :items="searchResultsCircles"
- @click="createAndJoinConversation" />
- </li>
- </template>
-
- <AppNavigationCaption v-if="sourcesWithoutResults"
- :title="sourcesWithoutResultsList" />
- <Hint v-if="contactsLoading" :hint="t('spreed', 'Loading')" />
- <Hint v-else :hint="t('spreed', 'No search results')" />
- </template>
- </div>
+ </ul>
+ </li>
<Button v-if="!preventFindingUnread && unreadNum > 0"
class="unread-mention-button"
type="primary"
@@ -506,6 +508,9 @@ export default {
<style lang="scss" scoped>
@import '../../assets/variables';
+.scroller {
+ padding: 0 4px 0 6px;
+}
.new-conversation {
display: flex;
@@ -521,7 +526,7 @@ export default {
width: 100% !important;
overflow-y: auto !important;
overflow-x: hidden !important;
- padding: 0 4px;
+ padding: 0;
}
.unread-mention-button {
diff --git a/src/components/LeftSidebar/NewGroupConversation/Confirmation/Confirmation.vue b/src/components/LeftSidebar/NewGroupConversation/Confirmation/Confirmation.vue
index c07f7d343..a8b7ecab8 100644
--- a/src/components/LeftSidebar/NewGroupConversation/Confirmation/Confirmation.vue
+++ b/src/components/LeftSidebar/NewGroupConversation/Confirmation/Confirmation.vue
@@ -1,7 +1,7 @@
<!--
- - @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@pm.me>
+ - @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@icloud.com>
-
- - @author Marco Ambrosini <marcoambrosini@pm.me>
+ - @author Marco Ambrosini <marcoambrosini@icloud.com>
-
- @license GNU AGPL version 3 or any later version
-
@@ -40,7 +40,7 @@
v-clipboard:error="onError"
type="secondary"
class="confirmation__copy-link">
- <label for="copy-link">{{ t('spreed', 'Copy conversation link') }}</label>
+ {{ t('spreed', 'Copy conversation link') }}
</Button>
<p class="confirmation__warning">
{{ confirmationText }}
diff --git a/src/components/LeftSidebar/NewGroupConversation/NewGroupConversation.vue b/src/components/LeftSidebar/NewGroupConversation/NewGroupConversation.vue
index 49c24b572..75d6f3861 100644
--- a/src/components/LeftSidebar/NewGroupConversation/NewGroupConversation.vue
+++ b/src/components/LeftSidebar/NewGroupConversation/NewGroupConversation.vue
@@ -1,7 +1,7 @@
<!--
- - @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@pm.me>
+ - @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@icloud.com>
-
- - @author Marco Ambrosini <marcoambrosini@pm.me>
+ - @author Marco Ambrosini <marcoambrosini@icloud.com>
-
- @license GNU AGPL version 3 or any later version
-
@@ -25,12 +25,11 @@
v-tooltip.bottom="t('spreed', 'Create a new group conversation')"
type="tertiary"
class="toggle"
- icon=""
:aria-label="t('spreed', 'Create a new group conversation')"
@click="showModal">
- <Plus decorative
- title=""
- :size="20" />
+ <template #icon>
+ <Plus :size="20" />
+ </template>
</Button>
<!-- New group form -->
<Modal v-if="modal"
@@ -399,6 +398,7 @@ export default {
it back */
::v-deep .modal-container {
border-radius: var(--border-radius-large) !important;
+ height: 700px;
}
.navigation {
diff --git a/src/components/LeftSidebar/NewGroupConversation/PasswordProtect/PasswordProtect.vue b/src/components/LeftSidebar/NewGroupConversation/PasswordProtect/PasswordProtect.vue
index 59e193fb3..2b3936f93 100644
--- a/src/components/LeftSidebar/NewGroupConversation/PasswordProtect/PasswordProtect.vue
+++ b/src/components/LeftSidebar/NewGroupConversation/PasswordProtect/PasswordProtect.vue
@@ -1,7 +1,7 @@
<!--
- - @copyright Copyright (c) 2020 Marco Ambrosini <marcoambrosini@pm.me>
+ - @copyright Copyright (c) 2020 Marco Ambrosini <marcoambrosini@icloud.com>
-
- - @author Marco Ambrosini <marcoambrosini@pm.me>
+ - @author Marco Ambrosini <marcoambrosini@icloud.com>
-
- @license GNU AGPL version 3 or any later version
-
diff --git a/src/components/LeftSidebar/NewGroupConversation/SetContacts/ContactSelectionBubble/ContactSelectionBubble.vue b/src/components/LeftSidebar/NewGroupConversation/SetContacts/ContactSelectionBubble/ContactSelectionBubble.vue
index 9b8c5709c..eacca0878 100644
--- a/src/components/LeftSidebar/NewGroupConversation/SetContacts/ContactSelectionBubble/ContactSelectionBubble.vue
+++ b/src/components/LeftSidebar/NewGroupConversation/SetContacts/ContactSelectionBubble/ContactSelectionBubble.vue
@@ -1,7 +1,7 @@
<!--
- - @copyright Copyright (c) 2020 Marco Ambrosini <marcoambrosini@pm.me>
+ - @copyright Copyright (c) 2020 Marco Ambrosini <marcoambrosini@icloud.com>
-
- - @author Marco Ambrosini <marcoambrosini@pm.me>
+ - @author Marco Ambrosini <marcoambrosini@icloud.com>
-
- @license GNU AGPL version 3 or any later version
-
@@ -31,18 +31,27 @@
<span class="contact-selection-bubble__username">
{{ displayName }}
</span>
- <button class="icon-close contact-selection-bubble__remove"
- @click="removeParticipantFromSelection(participant)" />
+ <ButtonVue type="tertiary-no-background"
+ :aria-label="removeLabel"
+ @click="removeParticipantFromSelection(participant)">
+ <template #icon>
+ <Close size="16" />
+ </template>
+ </ButtonVue>
</div>
</template>
<script>
+import ButtonVue from '@nextcloud/vue/dist/Components/Button'
+import Close from 'vue-material-design-icons/Close.vue'
import AvatarWrapperSmall from '../../../../AvatarWrapper/AvatarWrapperSmall.vue'
export default {
name: 'ContactSelectionBubble',
components: {
AvatarWrapperSmall,
+ ButtonVue,
+ Close,
},
props: {
@@ -58,6 +67,10 @@ export default {
// But it causes weird scenarios in formal companies or when people have titles.
return this.participant.label
},
+
+ removeLabel() {
+ return t('spreed', 'Remove participant {name}', { name: this.displayName })
+ },
},
methods: {
@@ -90,18 +103,6 @@ $bubble-height: 24px;
overflow: hidden;
text-overflow: ellipsis;
}
- &__remove {
- margin: 0 0 0 4px;
- border: none;
- border-radius: $bubble-height;
- height: $bubble-height;
- width: $bubble-height;
- background-color: var(--color-primary-active);
- &:active,
- &:focus {
- background-color: transparent !important;
- }
- }
}
</style>
diff --git a/src/components/LeftSidebar/NewGroupConversation/SetContacts/SetContacts.vue b/src/components/LeftSidebar/NewGroupConversation/SetContacts/SetContacts.vue
index 15e9ad3ce..daf41dd04 100644
--- a/src/components/LeftSidebar/NewGroupConversation/SetContacts/SetContacts.vue
+++ b/src/components/LeftSidebar/NewGroupConversation/SetContacts/SetContacts.vue
@@ -1,7 +1,7 @@
<!--
- - @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@pm.me>
+ - @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@icloud.com>
-
- - @author Marco Ambrosini <marcoambrosini@pm.me>
+ - @author Marco Ambrosini <marcoambrosini@icloud.com>
-
- @license GNU AGPL version 3 or any later version
-
@@ -30,6 +30,15 @@
type="text"
:placeholder="t('spreed', 'Search participants')"
@input="handleInput">
+ <Button v-if="isSearching"
+ class="abort-search"
+ type="tertiary-no-background"
+ :aria-label="cancelSearchLabel"
+ @click="abortSearch">
+ <template #icon>
+ <Close :size="20" />
+ </template>
+ </Button>
<transition-group v-if="hasSelectedParticipants"
name="zoom"
tag="div"
@@ -50,6 +59,8 @@
</template>
<script>
+import Button from '@nextcloud/vue/dist/Components/Button'
+import Close from 'vue-material-design-icons/Close.vue'
import CancelableRequest from '../../../../utils/cancelableRequest.js'
import debounce from 'debounce'
import { showError } from '@nextcloud/dialogs'
@@ -60,6 +71,8 @@ import ContactSelectionBubble from './ContactSelectionBubble/ContactSelectionBub
export default {
name: 'SetContacts',
components: {
+ Button,
+ Close,
ParticipantSearchResults,
ContactSelectionBubble,
},
@@ -99,6 +112,14 @@ export default {
displaySearchHint() {
return !this.contactsLoading && this.searchText === ''
},
+
+ isSearching() {
+ return this.searchText !== ''
+ },
+
+ cancelSearchLabel() {
+ return t('spreed', 'Cancel search')
+ },
},
async mounted() {
@@ -123,6 +144,14 @@ export default {
this.debounceFetchSearchResults()
},
+ abortSearch() {
+ this.noResults = false
+ this.contactsLoading = false
+ this.searchResults = []
+ this.searchText = ''
+ this.focusInput()
+ },
+
debounceFetchSearchResults: debounce(function() {
this.fetchSearchResults()
}, 250),
@@ -183,6 +212,12 @@ export default {
margin-top: 20px;
text-align: center;
}
+ .abort-search {
+ position: absolute;
+ right: 0;
+ top: -2px;
+ z-index: 2;
+ }
}
.selected-participants {
diff --git a/src/components/LeftSidebar/NewGroupConversation/SetConversationName/SetConversationName.vue b/src/components/LeftSidebar/NewGroupConversation/SetConversationName/SetConversationName.vue
index f3929ad28..5d0aa619b 100644
--- a/src/components/LeftSidebar/NewGroupConversation/SetConversationName/SetConversationName.vue
+++ b/src/components/LeftSidebar/NewGroupConversation/SetConversationName/SetConversationName.vue
@@ -1,7 +1,7 @@
<!--
- - @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@pm.me>
+ - @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@icloud.com>
-
- - @author Marco Ambrosini <marcoambrosini@pm.me>
+ - @author Marco Ambrosini <marcoambrosini@icloud.com>
-
- @license GNU AGPL version 3 or any later version
-
diff --git a/src/components/LeftSidebar/NewGroupConversation/SetConversationType/SetConversationType.vue b/src/components/LeftSidebar/NewGroupConversation/SetConversationType/SetConversationType.vue
index 4cdf0ff88..9ae49a283 100644
--- a/src/components/LeftSidebar/NewGroupConversation/SetConversationType/SetConversationType.vue
+++ b/src/components/LeftSidebar/NewGroupConversation/SetConversationType/SetConversationType.vue
@@ -1,7 +1,7 @@
<!--
- - @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@pm.me>
+ - @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@icloud.com>
-
- - @author Marco Ambrosini <marcoambrosini@pm.me>
+ - @author Marco Ambrosini <marcoambrosini@icloud.com>
-
- @license GNU AGPL version 3 or any later version
-
diff --git a/src/components/LeftSidebar/SearchBox/SearchBox.vue b/src/components/LeftSidebar/SearchBox/SearchBox.vue
index eb173e369..872b54864 100644
--- a/src/components/LeftSidebar/SearchBox/SearchBox.vue
+++ b/src/components/LeftSidebar/SearchBox/SearchBox.vue
@@ -1,7 +1,7 @@
<!--
- - @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@pm.me>
+ - @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@icloud.com>
-
- - @author Marco Ambrosini <marcoambrosini@pm.me>
+ - @author Marco Ambrosini <marcoambrosini@icloud.com>
-
- @license GNU AGPL version 3 or any later version
-
@@ -28,17 +28,29 @@
type="text"
:placeHolder="placeholderText"
@keypress.enter.prevent="handleSubmit">
- <button v-if="isSearching"
- class="abort-search icon-close"
- @click.prevent="abortSearch" />
+ <Button v-if="isSearching"
+ class="abort-search"
+ type="tertiary-no-background"
+ :aria-label="cancelSearchLabel"
+ @click="abortSearch">
+ <template #icon>
+ <Close :size="20" />
+ </template>
+ </Button>
</form>
</template>
<script>
+import Button from '@nextcloud/vue/dist/Components/Button'
+import Close from 'vue-material-design-icons/Close.vue'
import { EventBus } from '../../../services/EventBus.js'
export default {
name: 'SearchBox',
+ components: {
+ Button,
+ Close,
+ },
props: {
/**
* The placeholder for the input field
@@ -68,6 +80,11 @@ export default {
localValue: '',
}
},
+ computed: {
+ cancelSearchLabel() {
+ return t('spreed', 'Cancel search')
+ },
+ },
watch: {
localValue(localValue) {
this.$emit('update:value', localValue)
@@ -136,10 +153,7 @@ export default {
}
.abort-search {
- margin-left: -34px;
- z-index: 1;
- border: none;
- background-color: transparent
+ margin-left: -44px;
}
</style>
diff --git a/src/components/LobbyScreen.vue b/src/components/LobbyScreen.vue
index 2a0ad32d4..7b7a9490d 100644
--- a/src/components/LobbyScreen.vue
+++ b/src/components/LobbyScreen.vue
@@ -21,7 +21,7 @@
<template>
<div class="lobby">
<div class="lobby emptycontent">
- <div class="icon icon-lobby" />
+ <Lobby :size="64" />
<h2>{{ currentConversationName }}</h2>
<p class="lobby__timer">
@@ -51,6 +51,7 @@
import moment from '@nextcloud/moment'
import RichText from '@juliushaertl/vue-richtext'
import SetGuestUsername from './SetGuestUsername.vue'
+import Lobby from './missingMaterialDesignIcons/Lobby.vue'
export default {
@@ -59,6 +60,7 @@ export default {
components: {
SetGuestUsername,
RichText,
+ Lobby,
},
computed: {
diff --git a/src/components/MessagesList/MessagesGroup/Message/Message.spec.js b/src/components/MessagesList/MessagesGroup/Message/Message.spec.js
index 0c19aea9f..fa6743d26 100644
--- a/src/components/MessagesList/MessagesGroup/Message/Message.spec.js
+++ b/src/components/MessagesList/MessagesGroup/Message/Message.spec.js
@@ -5,6 +5,7 @@ import { cloneDeep } from 'lodash'
import { EventBus } from '../../../../services/EventBus.js'
import storeConfig from '../../../../store/storeConfig.js'
import { CONVERSATION, ATTENDEE, PARTICIPANT } from '../../../../constants.js'
+import ButtonVue from '@nextcloud/vue/dist/Components/Button'
// Components
import Check from 'vue-material-design-icons/Check'
@@ -665,13 +666,13 @@ describe('Message.vue', () => {
expect(wrapper.vm.showReloadButton).toBe(true)
- const reloadButtonIcon = reloadButton.find('button')
- expect(reloadButtonIcon.exists()).toBe(true)
+ const reloadButtonVue = wrapper.findComponent(ButtonVue)
+ expect(reloadButtonVue.exists()).toBe(true)
const retryEvent = jest.fn()
EventBus.$on('retry-message', retryEvent)
- await reloadButtonIcon.trigger('click')
+ await reloadButtonVue.vm.$emit('click')
expect(retryEvent).toHaveBeenCalledWith(123)
})
@@ -805,8 +806,8 @@ describe('Message.vue', () => {
expect(reactionButtons.length).toBe(3)
// Text of the buttons
- expect(reactionButtons.wrappers[0].text()).toBe('❤️ 1')
- expect(reactionButtons.wrappers[1].text()).toBe('👍 7')
+ expect(reactionButtons.wrappers[0].text()).toBe('❤️ 1')
+ expect(reactionButtons.wrappers[1].text()).toBe('👍 7')
})
test('shows reaction buttons with the right emoji count but without emoji placeholder when no chat permission', () => {
@@ -837,8 +838,8 @@ describe('Message.vue', () => {
expect(reactionButtons.length).toBe(2)
// Text of the buttons
- expect(reactionButtons.wrappers[0].text()).toBe('❤️ 1')
- expect(reactionButtons.wrappers[1].text()).toBe('👍 7')
+ expect(reactionButtons.wrappers[0].text()).toBe('❤️ 1')
+ expect(reactionButtons.wrappers[1].text()).toBe('👍 7')
})
test('no emoji picker is mounted when the bottom bar is not shown', () => {
@@ -862,7 +863,7 @@ describe('Message.vue', () => {
const addReactionToMessageAction = jest.fn()
const userHasReactedGetter = jest.fn().mockReturnValue(() => false)
const reactionsLoadedGetter = jest.fn().mockReturnValue(() => true)
- testStoreConfig.modules.quoteReplyStore.actions.addReactionToMessage = addReactionToMessageAction
+ testStoreConfig.modules.messagesStore.actions.addReactionToMessage = addReactionToMessageAction
testStoreConfig.modules.messagesStore.getters.userHasReacted = userHasReactedGetter
testStoreConfig.modules.messagesStore.getters.reactionsLoaded = reactionsLoadedGetter
@@ -905,7 +906,7 @@ describe('Message.vue', () => {
const removeReactionFromMessageAction = jest.fn()
const userHasReactedGetter = jest.fn().mockReturnValue(() => true)
const reactionsLoadedGetter = jest.fn().mockReturnValue(() => true)
- testStoreConfig.modules.quoteReplyStore.actions.removeReactionFromMessage = removeReactionFromMessageAction
+ testStoreConfig.modules.messagesStore.actions.removeReactionFromMessage = removeReactionFromMessageAction
testStoreConfig.modules.messagesStore.getters.userHasReacted = userHasReactedGetter
testStoreConfig.modules.messagesStore.getters.reactionsLoaded = reactionsLoadedGetter
@@ -922,7 +923,7 @@ describe('Message.vue', () => {
})
// Click reaction button upon having already reacted
- await wrapper.find('.reaction-button').trigger('click')
+ await wrapper.find('.reaction-button').getComponent(ButtonVue).vm.$emit('click')
await wrapper.vm.$nextTick()
await wrapper.vm.$nextTick()
diff --git a/src/components/MessagesList/MessagesGroup/Message/Message.vue b/src/components/MessagesList/MessagesGroup/Message/Message.vue
index 7bb314ef6..963536820 100644
--- a/src/components/MessagesList/MessagesGroup/Message/Message.vue
+++ b/src/components/MessagesList/MessagesGroup/Message/Message.vue
@@ -1,7 +1,7 @@
<!--
- - @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@pm.me>
+ - @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@icloud.com>
-
- - @author Marco Ambrosini <marcoambrosini@pm.me>
+ - @author Marco Ambrosini <marcoambrosini@icloud.com>
-
- @license GNU AGPL version 3 or any later version
-
@@ -79,16 +79,14 @@ the main body of the message as well as a quote.
@focus="showReloadButton = true"
@mouseleave="showReloadButton = true"
@blur="showReloadButton = true">
- <Button v-if="sendingErrorCanRetry && showReloadButton"
- class="nc-button nc-button__main--dark"
+ <ButtonVue v-if="sendingErrorCanRetry && showReloadButton"
+ :aria-label="sendingErrorIconTooltip"
@click="handleRetry">
- <Reload decorative
- title=""
- :size="16" />
- </Button>
+ <template #icon>
+ <Reload :size="16" />
+ </template>
+ </ButtonVue>
<AlertCircle v-else
- decorative
- title=""
:size="16" />
</div>
<div v-else-if="isTemporary && !isTemporaryUpload || isDeleting"
@@ -99,17 +97,13 @@ the main body of the message as well as a quote.
v-tooltip.auto="commonReadIconTooltip"
class="message-status"
:aria-label="commonReadIconTooltip">
- <CheckAll decorative
- title=""
- :size="16" />
+ <CheckAll :size="16" />
</div>
<div v-else-if="showSentIcon"
v-tooltip.auto="sentIconTooltip"
class="message-status"
:aria-label="sentIconTooltip">
- <Check decorative
- title=""
- :size="16" />
+ <Check :size="16" />
</div>
</div>
</div>
@@ -122,14 +116,13 @@ the main body of the message as well as a quote.
:key="reaction"
:delay="200"
trigger="hover">
- <button v-if="simpleReactions[reaction]!== 0"
+ <ButtonVue v-if="simpleReactions[reaction]!== 0"
slot="trigger"
class="reaction-button"
:class="{'reaction-button__has-reacted': userHasReacted(reaction)}"
@click="handleReactionClick(reaction)">
- <span class="reaction-button__emoji">{{ reaction }}</span>
- <span> {{ simpleReactions[reaction] }}</span>
- </button>
+ {{ reaction }} {{ simpleReactions[reaction] }}
+ </ButtonVue>
<div v-if="detailedReactions" class="reaction-details">
<span>{{ getReactionSummary(reaction) }}</span>
</div>
@@ -140,14 +133,18 @@ the main body of the message as well as a quote.
:per-line="5"
:container="`#message_${id}`"
@select="handleReactionClick">
- <button class="reaction-button">
- <EmoticonOutline :size="15" />
- </button>
+ <ButtonVue class="reaction-button">
+ <template #icon>
+ <EmoticonOutline :size="15" />
+ </template>
+ </ButtonVue>
</EmojiPicker>
- <button v-else-if="canReact"
+ <ButtonVue v-else-if="canReact"
class="reaction-button">
- <EmoticonOutline :size="15" />
- </button>
+ <template #icon>
+ <EmoticonOutline :size="15" />
+ </template>
+ </ButtonVue>
</div>
</div>
@@ -159,6 +156,7 @@ the main body of the message as well as a quote.
:is-reactions-menu-open.sync="isReactionsMenuOpen"
:message-api-data="messageApiData"
:message-object="messageObject"
+ :is-last-read="isLastReadMessage"
:can-react="canReact"
v-bind="$props"
:previous-message-id="previousMessageId"
@@ -174,6 +172,7 @@ the main body of the message as well as a quote.
</template>
<script>
+import ButtonVue from '@nextcloud/vue/dist/Components/Button'
import Tooltip from '@nextcloud/vue/dist/Directives/Tooltip'
import CallButton from '../../../TopBar/CallButton.vue'
import DeckCard from './MessagePart/DeckCard.vue'
@@ -208,6 +207,7 @@ export default {
},
components: {
+ ButtonVue,
CallButton,
Quote,
RichText,
@@ -786,7 +786,6 @@ export default {
<style lang="scss" scoped>
@import '../../../../assets/variables';
-@import '../../../../assets/buttons';
.message:hover .normal-message-body {
border-radius: 8px;
@@ -926,21 +925,21 @@ export default {
.reaction-button {
// Clear server rules
min-height: 0 !important;
- padding: 0 8px !important;
- font-weight: normal !important;
+ ::v-deep .button-vue__text {
+ font-weight: normal !important;
+ }
margin: 2px;
height: 26px;
- background-color: var(--color-main-background);
+ background-color: var(--color-main-background) !important;
+ margin-right: 8px !important;
&__emoji {
margin: 0 4px 0 0;
}
- &__has-reacted,
- &:hover {
- border-color: var(--color-primary-element);
- background-color: var(--color-primary-element-lighter);
+ &__has-reacted {
+ background-color: var(--color-primary-element-lighter) !important;
}
}
diff --git a/src/components/MessagesList/MessagesGroup/Message/MessagePart/Forwarder.vue b/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/Forwarder.vue
index d1dfc405b..d1a444aeb 100644
--- a/src/components/MessagesList/MessagesGroup/Message/MessagePart/Forwarder.vue
+++ b/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/Forwarder.vue
@@ -1,7 +1,7 @@
<!--
- - @copyright Copyright (c) 2021 Marco Ambrosini <marcoambrosini@pm.me>
+ - @copyright Copyright (c) 2021 Marco Ambrosini <marcoambrosini@icloud.com>
-
- - @author Marco Ambrosini <marcoambrosini@pm.me>
+ - @author Marco Ambrosini <marcoambrosini@icloud.com>
-
- @license GNU AGPL version 3 or any later version
-
@@ -206,9 +206,6 @@ export default {
display: flex;
justify-content: space-between;
padding: 12px;
- button {
- height: 44px;
- }
}
}
diff --git a/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.spec.js b/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.spec.js
index 76475e4e1..6316d5ec7 100644
--- a/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.spec.js
+++ b/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.spec.js
@@ -51,6 +51,7 @@ describe('MessageButtonsBar.vue', () => {
isReplyable: true,
canReact: true,
isReactionsMenuOpen: false,
+ isLastRead: false,
timestamp: new Date('2020-05-07 09:23:00').getTime() / 1000,
token: TOKEN,
systemMessage: '',
diff --git a/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.vue b/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.vue
index e7aed7071..44b0fc925 100644
--- a/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.vue
+++ b/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.vue
@@ -1,7 +1,7 @@
<!--
- - @copyright Copyright (c) 2021 Marco Ambrosini <marcoambrosini@pm.me>
+ - @copyright Copyright (c) 2021 Marco Ambrosini <marcoambrosini@icloud.com>
-
- - @author Marco Ambrosini <marcoambrosini@pm.me>
+ - @author Marco Ambrosini <marcoambrosini@icloud.com>
-
- @license GNU AGPL version 3 or any later version
-
@@ -22,18 +22,22 @@
<template>
<!-- Message Actions -->
<div v-click-outside="handleClickOutside"
- class="message-buttons-bar">
+ class="message-buttons-bar"
+ :class="{ 'message-buttons-bar--last-read' : isLastRead }">
<template v-if="!isReactionsMenuOpen">
<Button v-if="canReact"
type="tertiary"
+ :aria-label="t('spreed', 'Add a reaction to this message')"
@click="openReactionsMenu">
<template #icon>
<EmoticonOutline :size="20" />
</template>
</Button>
<Actions v-show="isReplyable">
- <ActionButton icon="icon-reply"
- @click.stop="handleReply">
+ <ActionButton @click.stop="handleReply">
+ <template #icon>
+ <Reply :size="16" />
+ </template>
{{ t('spreed', 'Reply') }}
</ActionButton>
</Actions>
@@ -56,24 +60,21 @@
<ActionButton :close-after-click="true"
@click.stop="handleMarkAsUnread">
<template #icon>
- <EyeOffOutline decorative
- title=""
- :size="16" />
+ <EyeOffOutline :size="16" />
</template>
{{ t('spreed', 'Mark as unread') }}
</ActionButton>
<ActionLink v-if="linkToFile"
- icon="icon-text"
:href="linkToFile">
+ <File slot="icon"
+ :size="20" />
{{ t('spreed', 'Go to file') }}
</ActionLink>
<ActionButton v-if="!isCurrentGuest && !isFileShare && !isDeletedMessage"
:close-after-click="true"
@click.stop="showForwarder = true">
<Share slot="icon"
- :size="16"
- decorative
- title="" />
+ :size="16" />
{{ t('spreed', 'Forward message') }}
</ActionButton>
<ActionSeparator v-if="messageActions.length > 0" />
@@ -98,18 +99,21 @@
<template v-if="isReactionsMenuOpen">
<Button type="tertiary"
+ :aria-label="t('spreed', 'Close reactions menu')"
@click="closeReactionsMenu">
<template #icon>
<ArrowLeft :size="20" />
</template>
</Button>
<Button type="tertiary"
+ :aria-label="t('spreed', 'React with {emoji}', { emoji: '👍' })"
@click="handleReactionClick('👍')">
<template #icon>
<span>👍</span>
</template>
</Button>
<Button type="tertiary"
+ :aria-label="t('spreed', 'React with {emoji}', { emoji: '❤' })"
@click="handleReactionClick('❤️')">
<template #icon>
<span>❤️</span>
@@ -119,7 +123,8 @@
@select="handleReactionClick"
@after-show="onEmojiPickerOpen"
@after-hide="onEmojiPickerClose">
- <Button type="tertiary">
+ <Button type="tertiary"
+ :aria-label="t('spreed', 'React with another emoji')">
<template #icon>
<Plus :size="20" />
</template>
@@ -140,9 +145,11 @@ import Actions from '@nextcloud/vue/dist/Components/Actions'
import ActionSeparator from '@nextcloud/vue/dist/Components/ActionSeparator'
import EyeOffOutline from 'vue-material-design-icons/EyeOffOutline'
import EmoticonOutline from 'vue-material-design-icons/EmoticonOutline.vue'
+import File from 'vue-material-design-icons/File'
import ArrowLeft from 'vue-material-design-icons/ArrowLeft.vue'
import Plus from 'vue-material-design-icons/Plus.vue'
-import Share from 'vue-material-design-icons/Share'
+import Reply from 'vue-material-design-icons/Reply.vue'
+import Share from 'vue-material-design-icons/Share.vue'
import moment from '@nextcloud/moment'
import { EventBus } from '../../../../../services/EventBus.js'
import { generateUrl } from '@nextcloud/router'
@@ -150,7 +157,7 @@ import {
showError,
showSuccess,
} from '@nextcloud/dialogs'
-import Forwarder from '../MessagePart/Forwarder.vue'
+import Forwarder from './Forwarder.vue'
import Button from '@nextcloud/vue/dist/Components/Button'
import EmojiPicker from '@nextcloud/vue/dist/Components/EmojiPicker'
@@ -162,6 +169,7 @@ export default {
ActionButton,
ActionLink,
EyeOffOutline,
+ File,
Share,
ActionSeparator,
Forwarder,
@@ -169,6 +177,7 @@ export default {
EmoticonOutline,
ArrowLeft,
Plus,
+ Reply,
EmojiPicker,
},
@@ -289,6 +298,16 @@ export default {
type: Boolean,
required: true,
},
+
+ /**
+ * If the MessageButtonsBar belongs to the last read message, we need
+ * to raise it to compensate for the shift in position brought by the
+ * last read marker that's added to the message component.
+ */
+ isLastRead: {
+ type: Boolean,
+ required: true,
+ },
},
data() {
@@ -481,10 +500,15 @@ export default {
background-color: var(--color-main-background);
border-radius: calc($clickable-area / 2);
box-shadow: 0 0 4px 0 var(--color-box-shadow);
+ height: 44px;
& h6 {
margin-left: auto;
}
+
+ &--last-read {
+ bottom: 36px;
+ }
}
</style>
diff --git a/src/components/MessagesList/MessagesGroup/Message/MessagePart/AudioPlayer.vue b/src/components/MessagesList/MessagesGroup/Message/MessagePart/AudioPlayer.vue
index 5136f86ef..e5a977d46 100644
--- a/src/components/MessagesList/MessagesGroup/Message/MessagePart/AudioPlayer.vue
+++ b/src/components/MessagesList/MessagesGroup/Message/MessagePart/AudioPlayer.vue
@@ -1,7 +1,7 @@
<!--
- - @copyright Copyright (c) 2021 Marco Ambrosini <marcoambrosini@pm.me>
+ - @copyright Copyright (c) 2021 Marco Ambrosini <marcoambrosini@icloud.com>
-
- - @author Marco Ambrosini <marcoambrosini@pm.me>
+ - @author Marco Ambrosini <marcoambrosini@icloud.com>
-
- @license GNU AGPL version 3 or any later version
-
diff --git a/src/components/MessagesList/MessagesGroup/Message/MessagePart/Contact.vue b/src/components/MessagesList/MessagesGroup/Message/MessagePart/Contact.vue
index a3d5a17ff..c59215ebe 100644
--- a/src/components/MessagesList/MessagesGroup/Message/MessagePart/Contact.vue
+++ b/src/components/MessagesList/MessagesGroup/Message/MessagePart/Contact.vue
@@ -1,7 +1,7 @@
<!--
- - @copyright Copyright (c) 2021, Marco Ambrosini <marcoambrosini@pm.me>
+ - @copyright Copyright (c) 2021, Marco Ambrosini <marcoambrosini@icloud.com>
-
- - @author Marco Ambrosini <marcoambrosini@pm.me>
+ - @author Marco Ambrosini <marcoambrosini@icloud.com>
-
- @license GNU AGPL version 3 or any later version
-
diff --git a/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue b/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue
index cc8b713ff..17ef3f3ad 100644
--- a/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue
+++ b/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue
@@ -1,9 +1,9 @@
<!--
- @copyright Copyright (c) 2019 Joas Schilling <coding@schilljs.com>
- - @copyright Copyright (c) 2020 Marco Ambrosini <marcoambrosini@pm.me>
+ - @copyright Copyright (c) 2020 Marco Ambrosini <marcoambrosini@icloud.com>
-
- @author Joas Schilling <coding@schilljs.com>
- - @author Marco Ambrosini <marcoambrosini@pm.me>
+ - @author Marco Ambrosini <marcoambrosini@icloud.com>
-
- @license GNU AGPL version 3 or any later version
-
@@ -22,7 +22,7 @@
-->
<template>
- <file-preview v-bind="filePreview"
+ <div v-bind="filePreview"
:tabindex="wrapperTabIndex"
class="file-preview"
:class="{ 'file-preview--viewer-available': isViewerAvailable,
@@ -36,9 +36,7 @@
:class="{'playable': isPlayable}">
<span v-if="isPlayable && !smallPreview" class="play-video-button">
<PlayCircleOutline :size="48"
- decorative
- fill-color="#ffffff"
- title="" />
+ fill-color="#ffffff" />
</span>
<img v-if="!failed"
v-tooltip="previewTooltip"
@@ -61,15 +59,14 @@
:aria-label="removeAriaLabel"
@click="$emit('remove-file', id)">
<template #icon>
- <Close decorative
- title="" />
+ <Close />
</template>
</Button>
<ProgressBar v-if="isTemporaryUpload && !isUploadEditor" :value="uploadProgress" />
<div v-if="shouldShowFileDetail" class="name-container">
{{ fileDetail }}
</div>
- </file-preview>
+ </div>
</template>
<script>
@@ -264,7 +261,6 @@ export default {
if (this.isUploadEditor || this.isTemporaryUpload) {
return {
is: 'div',
- tag: 'div',
}
} else if (this.isVoiceMessage) {
return {
@@ -276,7 +272,6 @@ export default {
}
return {
is: 'a',
- tag: 'a',
href: this.link,
target: '_blank',
rel: 'noopener noreferrer',
@@ -412,7 +407,7 @@ export default {
},
wrapperTabIndex() {
- return this.isUploadEditor ? '0' : ''
+ return this.isUploadEditor ? '0' : undefined
},
removeAriaLabel() {
diff --git a/src/components/MessagesList/MessagesGroup/Message/MessagePart/Location.vue b/src/components/MessagesList/MessagesGroup/Message/MessagePart/Location.vue
index 7d7ed237c..bb002543c 100644
--- a/src/components/MessagesList/MessagesGroup/Message/MessagePart/Location.vue
+++ b/src/components/MessagesList/MessagesGroup/Message/MessagePart/Location.vue
@@ -1,7 +1,7 @@
<!--
- - @copyright Copyright (c) 2021 Marco Ambrosini <marcoambrosini@pm.me>
+ - @copyright Copyright (c) 2021 Marco Ambrosini <marcoambrosini@icloud.com>
-
- - @author Marco Ambrosini <marcoambrosini@pm.me>
+ - @author Marco Ambrosini <marcoambrosini@icloud.com>
-
- @license GNU AGPL version 3 or any later version
-
@@ -24,6 +24,7 @@
target="_blank"
rel="noopener noreferrer"
class="location"
+ :class="{ 'wide': wide}"
:aria-label="linkAriaLabel">
<LMap :zoom="previewZoom"
:center="center"
@@ -93,6 +94,11 @@ export default {
type: String,
default: '',
},
+
+ wide: {
+ type: Boolean,
+ default: false,
+ },
},
data() {
@@ -133,7 +139,14 @@ export default {
white-space: initial;
overflow: hidden;
border-radius: var(--border-radius-large);
- width: 100%;
- height: 100%;
+ height: 300px;
+ max-height: 30vh;
+ margin: 4px;
+
+ &.wide {
+ width: 100%;
+ height: 100%;
+ margin: 0;
+ }
}
</style>
diff --git a/src/components/MessagesList/MessagesGroup/MessagesGroup.vue b/src/components/MessagesList/MessagesGroup/MessagesGroup.vue
index aa1c6748d..aed7f327b 100644
--- a/src/components/MessagesList/MessagesGroup/MessagesGroup.vue
+++ b/src/components/MessagesList/MessagesGroup/MessagesGroup.vue
@@ -1,7 +1,7 @@
<!--
- - @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@pm.me>
+ - @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@icloud.com>
-
- - @author Marco Ambrosini <marcoambrosini@pm.me>
+ - @author Marco Ambrosini <marcoambrosini@icloud.com>
-
- @license GNU AGPL version 3 or any later version
-
diff --git a/src/components/MessagesList/MessagesList.vue b/src/components/MessagesList/MessagesList.vue
index c54764dd0..26d42ff61 100644
--- a/src/components/MessagesList/MessagesList.vue
+++ b/src/components/MessagesList/MessagesList.vue
@@ -1,7 +1,7 @@
<!--
- - @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@pm.me>
+ - @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@icloud.com>
-
- - @author Marco Ambrosini <marcoambrosini@pm.me>
+ - @author Marco Ambrosini <marcoambrosini@icloud.com>
-
- @license GNU AGPL version 3 or any later version
-
@@ -55,9 +55,7 @@ get the messagesList array and loop through the list to generate the messages.
class="scroll-to-bottom"
@click="smoothScrollToBottom">
<template #icon>
- <ChevronDown decorative
- title=""
- :size="20" />
+ <ChevronDown :size="20" />
</template>
</Button>
</transition>
@@ -915,7 +913,7 @@ export default {
}
.scroll-to-bottom {
- position: absolute;
+ position: absolute !important;
bottom: 76px;
right: 24px;
z-index: 2;
diff --git a/src/components/NewMessageForm/AdvancedInput/AdvancedInput.vue b/src/components/NewMessageForm/AdvancedInput/AdvancedInput.vue
index ba9a51052..536a4858d 100644
--- a/src/components/NewMessageForm/AdvancedInput/AdvancedInput.vue
+++ b/src/components/NewMessageForm/AdvancedInput/AdvancedInput.vue
@@ -1,7 +1,7 @@
<!--
- - @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@pm.me>
+ - @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@icloud.com>
-
- - @author Marco Ambrosini <marcoambrosini@pm.me>
+ - @author Marco Ambrosini <marcoambrosini@icloud.com>
-
- @license GNU AGPL version 3 or any later version
-
diff --git a/src/components/NewMessageForm/AudioRecorder/AudioRecorder.vue b/src/components/NewMessageForm/AudioRecorder/AudioRecorder.vue
index 07901057b..80d98a751 100644
--- a/src/components/NewMessageForm/AudioRecorder/AudioRecorder.vue
+++ b/src/components/NewMessageForm/AudioRecorder/AudioRecorder.vue
@@ -1,7 +1,7 @@
<!--
- - @copyright Copyright (c) 2021 Marco Ambrosini <marcoambrosini@pm.me>
+ - @copyright Copyright (c) 2021 Marco Ambrosini <marcoambrosini@icloud.com>
-
- - @author Marco Ambrosini <marcoambrosini@pm.me>
+ - @author Marco Ambrosini <marcoambrosini@icloud.com>
-
- @license GNU AGPL version 3 or any later version
-
@@ -26,12 +26,13 @@
content: startRecordingTooltip,
delay: tooltipDelay,
}"
+ :aria-label="startRecordingTooltip"
type="tertiary"
:disabled="!canStartRecording"
@click="start">
- <Microphone :size="16"
- title=""
- decorative />
+ <template #icon>
+ <Microphone :size="16" />
+ </template>
</Button>
<div v-else class="wrapper">
<Button v-tooltip.auto="{
@@ -39,11 +40,10 @@
delay: tooltipDelay,
}"
type="error"
+ :aria-label="abortRecordingTooltip"
@click="abortRecording">
<template #icon>
- <Close :size="16"
- title=""
- decorative />
+ <Close :size="16" />
</template>
</Button>
<div class="audio-recorder__info">
@@ -56,12 +56,11 @@
delay: tooltipDelay,
}"
type="success"
+ :aria-label="stopRecordingTooltip"
:class="{'audio-recorder__trigger--recording': isRecording}"
@click="stop">
<template #icon>
- <Check :size="16"
- title=""
- decorative />
+ <Check :size="16" />
</template>
</Button>
</div>
@@ -331,8 +330,6 @@ export default {
<style lang="scss" scoped>
-@import '../../../assets/buttons';
-
.audio-recorder {
display: flex;
// Audio record button
diff --git a/src/components/NewMessageForm/NewMessageForm.vue b/src/components/NewMessageForm/NewMessageForm.vue
index db1bda31e..7e88372f1 100644
--- a/src/components/NewMessageForm/NewMessageForm.vue
+++ b/src/components/NewMessageForm/NewMessageForm.vue
@@ -1,7 +1,7 @@
<!--
- - @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@pm.me>
+ - @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@icloud.com>
-
- - @author Marco Ambrosini <marcoambrosini@pm.me>
+ - @author Marco Ambrosini <marcoambrosini@icloud.com>
-
- @license GNU AGPL version 3 or any later version
-
@@ -43,9 +43,7 @@
:aria-label="t('spreed', 'Share files to the conversation')"
:aria-haspopup="true">
<Paperclip slot="icon"
- :size="16"
- decorative
- title="" />
+ :size="16" />
<ActionButton v-if="canUploadFiles"
:close-after-click="true"
icon="icon-upload"
@@ -70,18 +68,19 @@
:aria-label="t('spreed', 'Add emoji')"
type="tertiary-no-background"
:aria-haspopup="true">
- <EmoticonOutline :size="16"
- decorative
- title="" />
+ <template #icon>
+ <EmoticonOutline :size="16" />
+ </template>
</Button>
</EmojiPicker>
<!-- Disabled emoji picker placeholder button -->
<Button v-else
type="tertiary"
+ :aria-label="t('spreed', 'Add emoji')"
:disabled="true">
- <EmoticonOutline :size="16"
- decorative
- title="" />
+ <template #icon>
+ <EmoticonOutline :size="16" />
+ </template>
</Button>
</div>
<div v-if="messageToBeReplied" class="new-message-form__quote">
@@ -97,7 +96,7 @@
:placeholder-text="placeholderText"
:aria-label="placeholderText"
@update:contentEditable="contentEditableToParsed"
- @submit="handleSubmit"
+ @submit="handleSubmit({ silent: false })"
@files-pasted="handlePastedFiles" />
</div>
@@ -115,9 +114,7 @@
@click.prevent="handleSubmit({ silent: true })">
{{ silentSendInfo }}
<BellOff slot="icon"
- :size="16"
- decorative
- title="" />
+ :size="16" />
</ActionButton>
</Actions>
<!-- Send -->
@@ -127,9 +124,9 @@
:title="t('spreed', 'Send message')"
:aria-label="t('spreed', 'Send message')"
@click.prevent="handleSubmit({ silent: false })">
- <Send title=""
- :size="16"
- decorative />
+ <template #icon>
+ <Send :size="16" />
+ </template>
</Button>
</template>
</form>
@@ -564,7 +561,7 @@ export default {
</script>
<style lang="scss" scoped>
-@import '../../assets/buttons';
+@import '../../assets/variables';
.wrapper {
display: flex;
diff --git a/src/components/PermissionsEditor/PermissionsEditor.vue b/src/components/PermissionsEditor/PermissionsEditor.vue
index 375ae1c06..a3a731891 100644
--- a/src/components/PermissionsEditor/PermissionsEditor.vue
+++ b/src/components/PermissionsEditor/PermissionsEditor.vue
@@ -1,7 +1,7 @@
<!--
- - @copyright Copyright (c) 2021 Marco Ambrosini <marcoambrosini@pm.me>
+ - @copyright Copyright (c) 2021 Marco Ambrosini <marcoambrosini@icloud.com>
-
- - @author Marco Ambrosini <marcoambrosini@pm.me>
+ - @author Marco Ambrosini <marcoambrosini@icloud.com>
-
- @license GNU AGPL version 3 or any later version
-
@@ -220,13 +220,6 @@ export default {
</script>
<style lang="scss" scoped>
-@import '../../assets/buttons';
-
-.nc-button {
- width: 100%;
- margin-top: 12px;
-}
-
.wrapper {
padding: 0 24px 24px 24px;
}
diff --git a/src/components/Quote.vue b/src/components/Quote.vue
index 1ad205ce5..743dda865 100644
--- a/src/components/Quote.vue
+++ b/src/components/Quote.vue
@@ -1,7 +1,7 @@
<!--
- - @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@pm.me>
+ - @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@icloud.com>
-
- - @author Marco Ambrosini <marcoambrosini@pm.me>
+ - @author Marco Ambrosini <marcoambrosini@icloud.com>
-
- @license GNU AGPL version 3 or any later version
-
@@ -45,18 +45,20 @@ components.
</blockquote>
</div>
<div v-if="isNewMessageFormQuote" class="quote__main__right">
- <Actions class="quote__main__right__actions">
- <ActionButton icon="icon-close"
- :close-after-click="true"
- @click.stop="handleAbortReply" />
- </Actions>
+ <Button type="tertiary"
+ :aria-label="cancelQuoteLabel"
+ @click="handleAbortReply">
+ <template #icon>
+ <Close :size="20" />
+ </template>
+ </Button>
</div>
</a>
</template>
<script>
-import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
-import Actions from '@nextcloud/vue/dist/Components/Actions'
+import Button from '@nextcloud/vue/dist/Components/Button'
+import Close from 'vue-material-design-icons/Close.vue'
import RichText from '@juliushaertl/vue-richtext'
import FilePreview from './MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue'
import DefaultParameter from './MessagesList/MessagesGroup/Message/MessagePart/DefaultParameter.vue'
@@ -65,8 +67,8 @@ import { EventBus } from '../services/EventBus.js'
export default {
name: 'Quote',
components: {
- Actions,
- ActionButton,
+ Button,
+ Close,
RichText,
},
props: {
@@ -216,6 +218,10 @@ export default {
return this.simpleQuotedMessage
}
},
+
+ cancelQuoteLabel() {
+ return t('spreed', 'Cancel quote')
+ },
},
methods: {
/**
@@ -224,6 +230,7 @@ export default {
*/
handleAbortReply() {
this.$store.dispatch('removeMessageToBeReplied', this.token)
+ EventBus.$emit('focus-chat-input')
},
handleQuoteClick() {
diff --git a/src/components/RightSidebar/Participants/ParticipantsList/Participant/Participant.vue b/src/components/RightSidebar/Participants/ParticipantsList/Participant/Participant.vue
index d5b166989..8a8673e09 100644
--- a/src/components/RightSidebar/Participants/ParticipantsList/Participant/Participant.vue
+++ b/src/components/RightSidebar/Participants/ParticipantsList/Participant/Participant.vue
@@ -28,8 +28,8 @@
'isSearched': isSearched,
'selected': isSelected }"
:aria-label="participantAriaLabel"
- :role="isSearched ? 'listitem' : ''"
- :tabindex="isSearched ? 0 : -1"
+ :role="isSearched ? 'listitem' : undefined"
+ :tabindex="isSearched ? 0 : undefined"
v-on="isSearched ? { click: handleClick, 'keydown.enter': handleClick } : {}"
@keydown.enter="handleClick">
<!-- Participant's avatar -->
@@ -77,22 +77,14 @@
class="participant-row__callstate-icon">
<span class="hidden-visually">{{ callIconTooltip }}</span>
<Microphone v-if="callIcon === 'audio'"
- :size="20"
- title=""
- decorative />
+ :size="20" />
<Phone v-if="callIcon === 'phone'"
- :size="20"
- title=""
- decorative />
+ :size="20" />
<Video v-if="callIcon === 'video'"
- :size="20"
- title=""
- decorative />
+ :size="20" />
<!-- The following icon is much bigger than all the others
so we reduce its size -->
<HandBackLeft v-if="callIcon === 'hand'"
- decorative
- title=""
:size="18" />
</div>
@@ -104,17 +96,13 @@
class="participant-row__actions">
<template #icon>
<LockOpenVariant v-if="actionIcon === 'LockOpenVariant'"
- :size="20"
- decorative />
+ :size="20" />
<Lock v-else-if="actionIcon === 'Lock'"
- :size="20"
- decorative />
+ :size="20" />
<Tune v-else-if="actionIcon === 'Tune'"
- :size="20"
- decorative />
+ :size="20" />
<DotsHorizontal v-else
- :size="20"
- decorative />
+ :size="20" />
</template>
<ActionText v-if="attendeePin"
:title="t('spreed', 'Dial-in PIN')"
@@ -125,9 +113,7 @@
:close-after-click="true"
@click="demoteFromModerator">
<template #icon>
- <Account :size="20"
- title=""
- decorative />
+ <Account :size="20" />
{{ t('spreed', 'Demote from moderator') }}
</template>
</ActionButton>
@@ -135,9 +121,7 @@
:close-after-click="true"
@click="promoteToModerator">
<template #icon>
- <Crown :size="20"
- title=""
- decorative />
+ <Crown :size="20" />
</template>
{{ t('spreed', 'Promote to moderator') }}
</ActionButton>
@@ -149,36 +133,28 @@
:close-after-click="true"
@click="applyDefaultPermissions">
<template #icon>
- <LockReset :size="20"
- title=""
- decorative />
+ <LockReset :size="20" />
</template>
{{ t('spreed', 'Reset custom permissions') }}
</ActionButton>
<ActionButton :close-after-click="true"
@click="grantAllPermissions">
<template #icon>
- <LockOpenVariant :size="20"
- title=""
- decorative />
+ <LockOpenVariant :size="20" />
</template>
{{ t('spreed', 'Grant all permissions') }}
</ActionButton>
<ActionButton :close-after-click="true"
@click="removeAllPermissions">
<template #icon>
- <Lock :size="20"
- title=""
- decorative />
+ <Lock :size="20" />
</template>
{{ t('spreed', 'Remove all permissions') }}
</ActionButton>
<ActionButton :close-after-click="true"
@click="showPermissionsEditor">
<template #icon>
- <Pencil :size="20"
- title=""
- decorative />
+ <Pencil :size="20" />
</template>
{{ t('spreed', 'Edit permissions') }}
</ActionButton>
@@ -193,9 +169,7 @@
:close-after-click="true"
@click="sendCallNotification">
<template #icon>
- <Bell :size="20"
- title=""
- decorative />
+ <Bell :size="20" />
</template>
{{ t('spreed', 'Send call notification') }}
</ActionButton>
diff --git a/src/components/RightSidebar/Participants/ParticipantsList/Participant/ParticipantPermissionsEditor/ParticipantPermissionsEditor.vue b/src/components/RightSidebar/Participants/ParticipantsList/Participant/ParticipantPermissionsEditor/ParticipantPermissionsEditor.vue
index b9d72a9e9..16fc10b93 100644
--- a/src/components/RightSidebar/Participants/ParticipantsList/Participant/ParticipantPermissionsEditor/ParticipantPermissionsEditor.vue
+++ b/src/components/RightSidebar/Participants/ParticipantsList/Participant/ParticipantPermissionsEditor/ParticipantPermissionsEditor.vue
@@ -1,7 +1,7 @@
<!--
- - @copyright Copyright (c) 2021 Marco Ambrosini <marcoambrosini@pm.me>
+ - @copyright Copyright (c) 2021 Marco Ambrosini <marcoambrosini@icloud.com>
-
- - @author Marco Ambrosini <marcoambrosini@pm.me>
+ - @author Marco Ambrosini <marcoambrosini@icloud.com>
-
- @license GNU AGPL version 3 or any later version
-
diff --git a/src/components/RightSidebar/Participants/ParticipantsSearchResults/ParticipantsSearchResults.vue b/src/components/RightSidebar/Participants/ParticipantsSearchResults/ParticipantsSearchResults.vue
index f35fe2755..454ebdf74 100644
--- a/src/components/RightSidebar/Participants/ParticipantsSearchResults/ParticipantsSearchResults.vue
+++ b/src/components/RightSidebar/Participants/ParticipantsSearchResults/ParticipantsSearchResults.vue
@@ -1,7 +1,7 @@
<!--
- - @copyright Copyright (c) 2020 Marco Ambrosini <marcoambrosini@pm.me>
+ - @copyright Copyright (c) 2020 Marco Ambrosini <marcoambrosini@icloud.com>
-
- - @author Marco Ambrosini <marcoambrosini@pm.me>
+ - @author Marco Ambrosini <marcoambrosini@icloud.com>
-
- @license GNU AGPL version 3 or any later version
-
@@ -54,11 +54,10 @@
:key="'integration' + index"
type="tertiary-no-background"
@click="runIntegration(integration)">
- <!-- FIXME: dinamically change the material design icon -->
- <AccountPlus slot="icon"
- decorative
- title=""
- :size="20" />
+ <!-- FIXME: dynamically change the material design icon -->
+ <template #icon>
+ <AccountPlus :size="20" />
+ </template>
{{ integration.label }}
</Button>
</ul>
diff --git a/src/components/RightSidebar/Participants/ParticipantsTab.vue b/src/components/RightSidebar/Participants/ParticipantsTab.vue
index a1af9d333..5346f2a62 100644
--- a/src/components/RightSidebar/Participants/ParticipantsTab.vue
+++ b/src/components/RightSidebar/Participants/ParticipantsTab.vue
@@ -1,7 +1,7 @@
<!--
- - @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@pm.me>
+ - @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@icloud.com>
-
- - @author Marco Ambrosini <marcoambrosini@pm.me>
+ - @author Marco Ambrosini <marcoambrosini@icloud.com>
-
- @license GNU AGPL version 3 or any later version
-
diff --git a/src/components/RightSidebar/RightSidebar.vue b/src/components/RightSidebar/RightSidebar.vue
index 9e1baa3b0..303be8d08 100644
--- a/src/components/RightSidebar/RightSidebar.vue
+++ b/src/components/RightSidebar/RightSidebar.vue
@@ -2,7 +2,7 @@
- @copyright Copyright (c) 2019 Joas Schilling <coding@schilljs.com>
-
- @author Joas Schilling <coding@schilljs.com>
- - @author Marco Ambrosini <marcoambrosini@pm.me>
+ - @author Marco Ambrosini <marcoambrosini@icloud.com>
-
- @license GNU AGPL version 3 or any later version
-
@@ -42,16 +42,20 @@
<AppSidebarTab v-if="showChatInSidebar"
id="chat"
:order="1"
- :name="t('spreed', 'Chat')"
- icon="icon-comment">
+ :name="t('spreed', 'Chat')">
+ <template slot="icon">
+ <Message :size="20" />
+ </template>
<ChatView :is-visible="opened" />
</AppSidebarTab>
<AppSidebarTab v-if="getUserId && !isOneToOne"
id="participants"
ref="participantsTab"
:order="2"
- :name="participantsText"
- icon="icon-contacts-dark">
+ :name="participantsText">
+ <template slot="icon">
+ <AccountMultiple :size="20" />
+ </template>
<ParticipantsTab :is-active="activeTab === 'participants'"
:can-search="canSearchParticipants"
:can-add="canAddParticipants" />
@@ -59,8 +63,10 @@
<AppSidebarTab v-if="!getUserId || showSIPSettings"
id="details-tab"
:order="3"
- :name="t('spreed', 'Details')"
- icon="icon-details">
+ :name="t('spreed', 'Details')">
+ <template slot="icon">
+ <InformationOutline :size="20" />
+ </template>
<SetGuestUsername v-if="!getUserId" />
<SipSettings v-if="showSIPSettings"
:meeting-id="conversation.token"
@@ -69,9 +75,7 @@
<div id="app-settings-header">
<Button type="tertiary" @click="showSettings">
<template #icon>
- <CogIcon decorative
- title=""
- :size="20" />
+ <CogIcon :size="20" />
</template>
{{ t('spreed', 'Settings') }}
</Button>
@@ -82,8 +86,10 @@
id="shared-items"
ref="sharedItemsTab"
:order="4"
- icon="icon-folder-multiple-image"
:name="t('spreed', 'Shared items')">
+ <template slot="icon">
+ <FolderMultipleImage :size="20" />
+ </template>
<SharedItemsTab :active="activeTab === 'shared-items'" />
</AppSidebarTab>
</AppSidebar>
@@ -103,7 +109,11 @@ import SetGuestUsername from '../SetGuestUsername.vue'
import SipSettings from './SipSettings.vue'
import LobbyStatus from './LobbyStatus.vue'
import Button from '@nextcloud/vue/dist/Components/Button'
+import AccountMultiple from 'vue-material-design-icons/AccountMultiple'
import CogIcon from 'vue-material-design-icons/Cog'
+import FolderMultipleImage from 'vue-material-design-icons/FolderMultipleImage'
+import InformationOutline from 'vue-material-design-icons/InformationOutline'
+import Message from 'vue-material-design-icons/Message'
export default {
name: 'RightSidebar',
@@ -117,7 +127,11 @@ export default {
SipSettings,
LobbyStatus,
Button,
+ AccountMultiple,
CogIcon,
+ FolderMultipleImage,
+ InformationOutline,
+ Message,
},
mixins: [
diff --git a/src/components/RightSidebar/SharedItems/SharedItems.vue b/src/components/RightSidebar/SharedItems/SharedItems.vue
index 3bd6f1f01..ffc1a147a 100644
--- a/src/components/RightSidebar/SharedItems/SharedItems.vue
+++ b/src/components/RightSidebar/SharedItems/SharedItems.vue
@@ -1,7 +1,7 @@
<!--
- - @copyright Copyright (c) 2022 Marco Ambrosini <marcoambrosini@pm.me>
+ - @copyright Copyright (c) 2022 Marco Ambrosini <marcoambrosini@icloud.com>
-
- - @author Marco Ambrosini <marcoambrosini@pm.me>
+ - @author Marco Ambrosini <marcoambrosini@icloud.com>
-
- @license GNU AGPL version 3 or any later version
-
@@ -26,7 +26,8 @@
:key="item.id"
class="shared-items__location"
:class="{ 'shared-items__location--nolimit': limit === 0 }">
- <Location v-bind="item.messageParameters.object" />
+ <Location :wide="true"
+ v-bind="item.messageParameters.object" />
</div>
<div v-else-if="type === 'deckcard'"
:key="item.id"
diff --git a/src/components/RightSidebar/SharedItems/SharedItemsBrowser/SharedItemsBrowser.vue b/src/components/RightSidebar/SharedItems/SharedItemsBrowser/SharedItemsBrowser.vue
index 0b37b14f3..b7c86a206 100644
--- a/src/components/RightSidebar/SharedItems/SharedItemsBrowser/SharedItemsBrowser.vue
+++ b/src/components/RightSidebar/SharedItems/SharedItemsBrowser/SharedItemsBrowser.vue
@@ -1,7 +1,7 @@
<!--
- - @copyright Copyright (c) 2022 Marco Ambrosini <marcoambrosini@pm.me>
+ - @copyright Copyright (c) 2022 Marco Ambrosini <marcoambrosini@icloud.com>
-
- - @author Marco Ambrosini <marcoambrosini@pm.me>
+ - @author Marco Ambrosini <marcoambrosini@icloud.com>
-
- @license GNU AGPL version 3 or any later version
-
@@ -164,6 +164,10 @@ export default {
}
}
+::v-deep .modal-container {
+ height: 700px;
+}
+
::v-deep .button-vue {
border-radius: var(--border-radius-large);
&.active {
diff --git a/src/components/RightSidebar/SharedItems/SharedItemsTab.vue b/src/components/RightSidebar/SharedItems/SharedItemsTab.vue
index 67f5683c2..a7af92e02 100644
--- a/src/components/RightSidebar/SharedItems/SharedItemsTab.vue
+++ b/src/components/RightSidebar/SharedItems/SharedItemsTab.vue
@@ -1,7 +1,7 @@
<!--
- - @copyright Copyright (c) 2022 Marco Ambrosini <marcoambrosini@pm.me>
+ - @copyright Copyright (c) 2022 Marco Ambrosini <marcoambrosini@icloud.com>
-
- - @author Marco Ambrosini <marcoambrosini@pm.me>
+ - @author Marco Ambrosini <marcoambrosini@icloud.com>
-
- @license GNU AGPL version 3 or any later version
-
@@ -33,9 +33,7 @@
:wide="true"
@click="showMore(type)">
<template #icon>
- <DotsHorizontal :size="20"
- decorative
- title="" />
+ <DotsHorizontal :size="20" />
</template>
{{ getButtonTitle(type) }}
</Button>
diff --git a/src/components/SetGuestUsername.vue b/src/components/SetGuestUsername.vue
index cb3ac7f36..0987aebbc 100644
--- a/src/components/SetGuestUsername.vue
+++ b/src/components/SetGuestUsername.vue
@@ -1,7 +1,7 @@
<!--
- - @copyright Copyright (c) 2020 Marco Ambrosini <marcoambrosini@pm.me>
+ - @copyright Copyright (c) 2020 Marco Ambrosini <marcoambrosini@icloud.com>
-
- - @author Marco Ambrosini <marcoambrosini@pm.me>
+ - @author Marco Ambrosini <marcoambrosini@icloud.com>
-
- @license GNU AGPL version 3 or any later version
-
@@ -28,9 +28,7 @@
<Button @click.prevent="handleEditUsername">
{{ t('spreed', 'Edit') }}
<template #icon>
- <Pencil :size="20"
- title=""
- decorative />
+ <Pencil :size="20" />
</template>
</Button>
<div v-if="isEditingUsername"
@@ -44,10 +42,11 @@
@keydown.esc="isEditingUsername = !isEditingUsername">
<Button class="username-form__button"
native-type="submit"
+ :aria-label="t('spreed', 'Save name')"
type="tertiary">
- <ArrowRight :size="20"
- title=""
- decorative />
+ <template #icon>
+ <ArrowRight :size="20" />
+ </template>
</Button>
</div>
</form>
diff --git a/src/components/SettingsDialog/SettingsDialog.vue b/src/components/SettingsDialog/SettingsDialog.vue
index e905c5702..700f4e1e5 100644
--- a/src/components/SettingsDialog/SettingsDialog.vue
+++ b/src/components/SettingsDialog/SettingsDialog.vue
@@ -1,7 +1,7 @@
<!--
- - @copyright Copyright (c) 2020 Marco Ambrosini <marcoambrosini@pm.me>
+ - @copyright Copyright (c) 2020 Marco Ambrosini <marcoambrosini@icloud.com>
-
- - @author Marco Ambrosini <marcoambrosini@pm.me>
+ - @author Marco Ambrosini <marcoambrosini@icloud.com>
-
- @license GNU AGPL version 3 or any later version
-
@@ -24,11 +24,13 @@
:show-navigation="true"
first-selected-section="keyboard shortcuts"
:container="container">
- <AppSettingsSection :title="t('spreed', 'Choose devices')"
+ <AppSettingsSection id="devices"
+ :title="t('spreed', 'Choose devices')"
class="app-settings-section">
<MediaDevicesPreview />
</AppSettingsSection>
<AppSettingsSection v-if="!isGuest"
+ id="attachments"
:title="t('spreed', 'Attachments folder')"
class="app-settings-section">
<h3 class="app-settings-section__hint">
@@ -41,26 +43,29 @@
@click="selectAttachmentFolder">
</AppSettingsSection>
<AppSettingsSection v-if="!isGuest"
+ id="privacy"
:title="t('spreed', 'Privacy')"
class="app-settings-section">
<CheckboxRadioSwitch id="read_status_privacy"
:checked="readStatusPrivacyIsPublic"
:disabled="privacyLoading"
- type="checkbox"
+ type="switch"
class="checkbox"
@update:checked="toggleReadStatusPrivacy">
{{ t('spreed', 'Share my read-status and show the read-status of others') }}
</CheckboxRadioSwitch>
</AppSettingsSection>
- <AppSettingsSection :title="t('spreed', 'Sounds')"
+ <AppSettingsSection id="sounds"
+ :title="t('spreed', 'Sounds')"
class="app-settings-section">
- <input id="play_sounds"
+ <CheckboxRadioSwitch id="play_sounds"
:checked="playSounds"
:disabled="playSoundsLoading"
- type="checkbox"
+ type="switch"
class="checkbox"
- @change="togglePlaySounds">
- <label for="play_sounds">{{ t('spreed', 'Play sounds when participants join or leave a call') }}</label>
+ @update:checked="togglePlaySounds">
+ {{ t('spreed', 'Play sounds when participants join or leave a call') }}
+ </CheckboxRadioSwitch>
<em>{{ t('spreed', 'Sounds can currently not be played in Safari browser and iPad and iPhone devices due to technical restrictions by the manufacturer.') }}</em>
<a :href="settingsUrl"
@@ -70,7 +75,8 @@
{{ t('spreed', 'Sounds for chat and call notifications can be adjusted in the personal settings.') }} ↗
</a>
</AppSettingsSection>
- <AppSettingsSection :title="t('spreed', 'Keyboard shortcuts')">
+ <AppSettingsSection id="shortcuts"
+ :title="t('spreed', 'Keyboard shortcuts')">
<em>{{ t('spreed', 'Speed up your Talk experience with these quick shortcuts.') }}</em>
<dl>
diff --git a/src/components/TopBar/CallButton.vue b/src/components/TopBar/CallButton.vue
index 8634254d6..f94e715eb 100644
--- a/src/components/TopBar/CallButton.vue
+++ b/src/components/TopBar/CallButton.vue
@@ -1,7 +1,7 @@
<!--
- - @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@pm.me>
+ - @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@icloud.com>
-
- - @author Marco Ambrosini <marcoambrosini@pm.me>
+ - @author Marco Ambrosini <marcoambrosini@icloud.com>
-
- @license GNU AGPL version 3 or any later version
-
@@ -33,9 +33,9 @@
:disabled="startCallButtonDisabled || loading || blockCalls"
:type="startCallButtonType"
@click="handleClick">
- <Video slot="icon"
- :size="20"
- decorative />
+ <template #icon>
+ <Video :size="20" />
+ </template>
{{ startCallLabel }}
</Button>
<Button v-else-if="showLeaveCallButton && !canEndForAll"
@@ -43,30 +43,28 @@
type="error"
:disabled="loading"
@click="leaveCall(false)">
- <VideoOff slot="icon"
- :size="20"
- decorative />
+ <template #icon>
+ <VideoOff :size="20" />
+ </template>
{{ leaveCallLabel }}
</Button>
<Actions v-else-if="showLeaveCallButton && canEndForAll"
:disabled="loading">
- <template slot="icon">
- <VideoOff :size="16"
- decorative />
+ <template #icon>
+ <VideoOff :size="16" />
<span class="label">{{ leaveCallLabel }}</span>
- <MenuDown :size="16"
- decorative />
+ <MenuDown :size="16" />
</template>
<ActionButton @click="leaveCall(false)">
- <VideoOff slot="icon"
- :size="20"
- decorative />
+ <template #icon>
+ <VideoOff :size="20" />
+ </template>
{{ leaveCallLabel }}
</ActionButton>
<ActionButton @click="leaveCall(true)">
- <VideoOff slot="icon"
- :size="20"
- decorative />
+ <template #icon>
+ <VideoOff :size="20" />
+ </template>
{{ t('spreed', 'End meeting for all') }}
</ActionButton>
</Actions>
@@ -183,14 +181,6 @@ export default {
return t('spreed', 'Leave call')
},
- leaveCallIcon() {
- if (this.loading) {
- return 'icon-loading-small'
- }
-
- return 'icon-leave-call'
- },
-
startCallLabel() {
if (this.hasCall && !this.isInLobby) {
return t('spreed', 'Join call')
@@ -215,18 +205,6 @@ export default {
return ''
},
- startCallIcon() {
- if (this.loading) {
- return 'icon-loading-small'
- }
-
- if (this.hasCall && !this.isInLobby) {
- return 'icon-incoming-call'
- }
-
- return 'icon-start-call'
- },
-
startCallButtonType() {
if (!this.isInLobby) {
if (!this.hasCall) {
diff --git a/src/components/TopBar/TopBar.vue b/src/components/TopBar/TopBar.vue
index 8ae8394e3..9ddac8e28 100644
--- a/src/components/TopBar/TopBar.vue
+++ b/src/components/TopBar/TopBar.vue
@@ -1,7 +1,7 @@
<!--
- - @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@pm.me>
+ - @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@icloud.com>
-
- - @author Marco Ambrosini <marcoambrosini@pm.me>
+ - @author Marco Ambrosini <marcoambrosini@icloud.com>
-
- @license GNU AGPL version 3 or any later version
-
@@ -30,6 +30,7 @@
:hide-call="false" />
<!-- conversation header -->
<a v-if="!isInCall"
+ role="button"
class="conversation-header"
@click="openConversationSettings">
<div class="conversation-header__text"
@@ -71,14 +72,13 @@
<Actions v-if="!isSidebar"
v-shortkey.once="['f']"
class="top-bar__button"
- menu-align="right"
:aria-label="t('spreed', 'Conversation actions')"
:container="container"
@shortkey.native="toggleFullscreen">
- <Cog slot="icon"
- :size="20"
- decorative
- title="" />
+ <span slot="icon"
+ :class="{'top-bar__button__force-white': isInCall}">
+ <Cog :size="20" />
+ </span>
<ActionButton :icon="iconFullscreen"
:aria-label="t('spreed', 'Toggle fullscreen')"
:close-after-click="true"
@@ -87,8 +87,9 @@
</ActionButton>
<ActionSeparator v-if="showModerationOptions" />
<ActionLink v-if="isFileConversation"
- icon="icon-text"
:href="linkToFile">
+ <File slot="icon"
+ :size="20" />
{{ t('spreed', 'Go to file') }}
</ActionLink>
<template v-if="showModerationOptions">
@@ -109,16 +110,16 @@
<ActionButton :close-after-click="true"
@click="forceMuteOthers">
<MicrophoneOff slot="icon"
- :size="20"
- decorative
- title="" />
+ :size="20" />
{{ t('spreed', 'Mute others') }}
</ActionButton>
</template>
<ActionSeparator v-if="showModerationOptions" />
- <ActionButton icon="icon-settings"
- :close-after-click="true"
+ <ActionButton :close-after-click="true"
@click="openConversationSettings">
+ <template #icon>
+ <Cog :size="20" />
+ </template>
{{ t('spreed', 'Conversation settings') }}
</ActionButton>
</Actions>
@@ -131,14 +132,14 @@
@click="openSidebar">
<MessageText slot="icon"
:size="20"
- title=""
- fill-color="#ffffff"
- decorative />
+ fill-color="#ffffff" />
</ActionButton>
<ActionButton v-else
key="openSideBarButtonMenuPeople"
- :icon="iconMenuPeople"
- @click="openSidebar" />
+ @click="openSidebar">
+ <MenuPeople slot="icon"
+ :size="20" />
+ </ActionButton>
</Actions>
</div>
<CounterBubble v-if="!isSidebar && showOpenSidebarButton && isInCall && unreadMessagesCounter > 0"
@@ -158,6 +159,8 @@ import CallButton from './CallButton.vue'
import BrowserStorage from '../../services/BrowserStorage.js'
import ActionLink from '@nextcloud/vue/dist/Components/ActionLink'
import ActionSeparator from '@nextcloud/vue/dist/Components/ActionSeparator'
+import File from 'vue-material-design-icons/File'
+import MenuPeople from '../missingMaterialDesignIcons/MenuPeople.vue'
import MessageText from 'vue-material-design-icons/MessageText'
import MicrophoneOff from 'vue-material-design-icons/MicrophoneOff'
import { CONVERSATION, PARTICIPANT } from '../../constants.js'
@@ -186,6 +189,8 @@ export default {
CounterBubble,
CallButton,
ActionSeparator,
+ File,
+ MenuPeople,
MessageText,
MicrophoneOff,
ConversationIcon,
@@ -246,13 +251,6 @@ export default {
return t('spreed', 'Fullscreen (F)')
},
- iconMenuPeople() {
- if (this.isInCall) {
- return 'forced-white icon-menu-people'
- }
- return 'icon-menu-people'
- },
-
showOpenSidebarButton() {
return !this.$store.getters.getSidebarStatus
},
@@ -542,6 +540,10 @@ export default {
.icon {
margin-right: 4px !important;
}
+
+ &__force-white {
+ color: white;
+ }
}
.unread-messages-counter {
diff --git a/src/components/UploadEditor.vue b/src/components/UploadEditor.vue
index 091532d5d..470385f0b 100644
--- a/src/components/UploadEditor.vue
+++ b/src/components/UploadEditor.vue
@@ -1,7 +1,7 @@
<!--
- - @copyright Copyright (c) 2020 Marco Ambrosini <marcoambrosini@pm.me>
+ - @copyright Copyright (c) 2020 Marco Ambrosini <marcoambrosini@icloud.com>
-
- - @author Marco Ambrosini <marcoambrosini@pm.me>
+ - @author Marco Ambrosini <marcoambrosini@icloud.com>
-
- @license GNU AGPL version 3 or any later version
-
@@ -50,9 +50,7 @@
class="add-more__button"
@click="clickImportInput">
<template #icon>
- <Plus decorative
- title=""
- :size="48" />
+ <Plus :size="48" />
</template>
</Button>
</div>
@@ -194,6 +192,10 @@ export default {
<style lang="scss" scoped>
@import '../assets/variables';
+::v-deep .modal-container {
+ height: 700px;
+}
+
.upload-editor {
height: 100%;
position: relative;
diff --git a/src/components/VolumeIndicator/VolumeIndicator.vue b/src/components/VolumeIndicator/VolumeIndicator.vue
index 05c2222f7..f1796d188 100644
--- a/src/components/VolumeIndicator/VolumeIndicator.vue
+++ b/src/components/VolumeIndicator/VolumeIndicator.vue
@@ -1,7 +1,7 @@
<!--
- - @copyright Copyright (c) 2021 Marco Ambrosini <marcoambrosini@pm.me>
+ - @copyright Copyright (c) 2021 Marco Ambrosini <marcoambrosini@icloud.com>
-
- - @author Marco Ambrosini <marcoambrosini@pm.me>
+ - @author Marco Ambrosini <marcoambrosini@icloud.com>
-
- @license GNU AGPL version 3 or any later version
-
diff --git a/src/components/missingMaterialDesignIcons/CancelPresentation.vue b/src/components/missingMaterialDesignIcons/CancelPresentation.vue
index 4cca75f0d..066fdb504 100644
--- a/src/components/missingMaterialDesignIcons/CancelPresentation.vue
+++ b/src/components/missingMaterialDesignIcons/CancelPresentation.vue
@@ -1,20 +1,20 @@
-<template functional>
- <span :aria-hidden="props.decorative"
- :aria-label="props.title"
- :class="[data.class, data.staticClass]"
+<template>
+ <span :aria-hidden="!title"
+ :aria-label="title"
class="material-design-icon cancel-presentation-icon"
role="img"
- v-bind="data.attrs"
- v-on="listeners">
- <svg :fill="props.fillColor"
+ v-bind="$attrs"
+ @click="$emit('click', $event)">
+ <svg :fill="fillColor"
class="material-design-icon__svg"
- :width="props.size"
- :height="props.size"
+ :width="size"
+ :height="size"
viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none" />
<path d="M21 19.1H3V5h18v14.1zM21 3H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z" />
<path d="M21 19.1H3V5h18v14.1zM21 3H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z" fill="none" />
<path d="M14.59 8L12 10.59 9.41 8 8 9.41 10.59 12 8 14.59 9.41 16 12 13.41 14.59 16 16 14.59 13.41 12 16 9.41z" />
+ <title v-if="title">{{ title }}</title>
</svg>
</span>
</template>
@@ -25,11 +25,7 @@ export default {
props: {
title: {
type: String,
- default: 'Cancel Presentation icon',
- },
- decorative: {
- type: Boolean,
- default: false,
+ default: '',
},
fillColor: {
type: String,
diff --git a/src/components/missingMaterialDesignIcons/CategoryMonitoring.vue b/src/components/missingMaterialDesignIcons/CategoryMonitoring.vue
new file mode 100644
index 000000000..aa6c34b5b
--- /dev/null
+++ b/src/components/missingMaterialDesignIcons/CategoryMonitoring.vue
@@ -0,0 +1,39 @@
+<template>
+ <span :aria-hidden="!title"
+ :aria-label="title"
+ class="material-design-icon category-monitoring-icon"
+ role="img"
+ v-bind="$attrs"
+ @click="$emit('click', $event)">
+ <svg :width="size"
+ :height="size"
+ viewBox="0 0 16 16">
+ <path d="m1 8h4l1.5-4.0935 3 8.1875 1.5-4.094h4"
+ :stroke="fillColor"
+ fill="none"
+ stroke-miterlimit="4"
+ stroke-width="2" />
+ </svg>
+
+ </span>
+</template>
+
+<script>
+export default {
+ name: 'CategoryMonitoring',
+ props: {
+ title: {
+ type: String,
+ default: '',
+ },
+ fillColor: {
+ type: String,
+ default: 'currentColor',
+ },
+ size: {
+ type: Number,
+ default: 24,
+ },
+ },
+}
+</script>
diff --git a/src/components/missingMaterialDesignIcons/GridView.vue b/src/components/missingMaterialDesignIcons/GridView.vue
new file mode 100644
index 000000000..951e6dea4
--- /dev/null
+++ b/src/components/missingMaterialDesignIcons/GridView.vue
@@ -0,0 +1,52 @@
+<template>
+ <span :aria-hidden="!title"
+ :aria-label="title"
+ class="material-design-icon grid-view-icon"
+ role="img"
+ v-bind="$attrs"
+ @click="$emit('click', $event)">
+ <svg :fill="fillColor"
+ class="material-design-icon__svg"
+ :width="size"
+ :height="size"
+ viewBox="0 0 16 16">
+ <g>
+ <rect width="7" height="7" rx="2" />
+ <rect y="9"
+ width="7"
+ height="7"
+ rx="2" />
+ <rect x="9"
+ width="7"
+ height="7"
+ rx="2" />
+ <rect x="9"
+ y="9"
+ width="7"
+ height="7"
+ rx="2" />
+ </g>
+ <title v-if="title">{{ title }}</title>
+ </svg>
+ </span>
+</template>
+
+<script>
+export default {
+ name: 'GridView',
+ props: {
+ title: {
+ type: String,
+ default: '',
+ },
+ fillColor: {
+ type: String,
+ default: 'currentColor',
+ },
+ size: {
+ type: Number,
+ default: 24,
+ },
+ },
+}
+</script>
diff --git a/src/components/missingMaterialDesignIcons/Lobby.vue b/src/components/missingMaterialDesignIcons/Lobby.vue
new file mode 100644
index 000000000..1786e360a
--- /dev/null
+++ b/src/components/missingMaterialDesignIcons/Lobby.vue
@@ -0,0 +1,37 @@
+<template>
+ <span :aria-hidden="!title"
+ :aria-label="title"
+ class="material-design-icon lobby-icon"
+ role="img"
+ v-bind="$attrs"
+ @click="$emit('click', $event)">
+ <svg :fill="fillColor"
+ class="material-design-icon__svg"
+ :width="size"
+ :height="size"
+ viewBox="0 0 24 24">
+ <path d="M2 17h20v2H2zm11.84-9.21c.1-.24.16-.51.16-.79 0-1.1-.9-2-2-2s-2 .9-2 2c0 .28.06.55.16.79C6.25 8.6 3.27 11.93 3 16h18c-.27-4.07-3.25-7.4-7.16-8.21z" />
+ <title v-if="title">{{ title }}</title>
+ </svg>
+ </span>
+</template>
+
+<script>
+export default {
+ name: 'Lobby',
+ props: {
+ title: {
+ type: String,
+ default: '',
+ },
+ fillColor: {
+ type: String,
+ default: 'currentColor',
+ },
+ size: {
+ type: Number,
+ default: 24,
+ },
+ },
+}
+</script>
diff --git a/src/components/missingMaterialDesignIcons/MenuPeople.vue b/src/components/missingMaterialDesignIcons/MenuPeople.vue
new file mode 100644
index 000000000..4b74611bc
--- /dev/null
+++ b/src/components/missingMaterialDesignIcons/MenuPeople.vue
@@ -0,0 +1,37 @@
+<template>
+ <span :aria-hidden="!title"
+ :aria-label="title"
+ class="material-design-icon menu-people-icon"
+ role="img"
+ v-bind="$attrs"
+ @click="$emit('click', $event)">
+ <svg :fill="fillColor"
+ class="material-design-icon__svg"
+ :width="size"
+ :height="size"
+ viewBox="0 0 16 16">
+ <path d="m2 2c-0.554 0-1 0.446-1 1s0.446 1 1 1h12c0.554 0 1-0.446 1-1s-0.446-1-1-1h-12zm9.717 4.0059c-1.247 0-2.1428 1.0199-2.1428 1.998 0 0.9995 0.0726 1.7127 0.5718 2.4981 0.16 0.207 0.347 0.251 0.5 0.43 0.097 0.357 0.171 0.713 0.071 1.07-0.311 0.109-0.607 0.237-0.9065 0.357-0.364-0.195-0.7863-0.357-1.1503-0.5-0.05-0.2-0.0129-0.347 0.0371-0.535 0.0856-0.089 0.163-0.129 0.2558-0.215 0.2642-0.321 0.2793-0.864 0.2793-1.2496 0-0.5712-0.5135-0.9981-1.0703-0.9981-0.6211 0-1.0723 0.5126-1.0723 0.9981h-0.0136c0 0.4996 0.0353 0.8576 0.2851 1.2496 0.0714 0.107 0.1729 0.126 0.25 0.215 0.0481 0.179 0.0859 0.357 0.0352 0.535-0.4569 0.16-0.8863 0.357-1.2832 0.571-0.2999 0.214-0.1668 0.131-0.3574 0.822-0.0886 0.357 0.928 0.521 1.6562 0.578-0.0357 0.196-0.0857 0.457-0.2285 0.957-0.2285 0.893 3.1074 1.213 4.2834 1.213 1.735 0 4.507-0.325 4.269-1.213-0.371-1.385-0.15-1.221-0.701-1.642-0.778-0.467-1.749-0.834-2.568-1.143-0.107-0.398-0.03-0.692 0.07-1.07 0.168-0.179 0.357-0.259 0.514-0.43 0.492-0.6312 0.556-1.7299 0.556-2.4981 0-1.1323-1.019-1.998-2.14-1.998zm-9.717 0.9941c-0.554 0-1 0.446-1 1s0.446 1 1 1h4.2852c0.0891-0.1855 0.2-0.3648 0.3515-0.5195 0.3721-0.3801 0.9171-0.5988 1.4883-0.6192h0.0195c0.1729 0.017 0.3042 0.0597 0.4297 0.1426 0-0.3488 0.0747-0.6853 0.1953-1.0039h-6.7695zm0 5c-0.554 0-1 0.446-1 1s0.446 1 1 1h3.25c-0.0375-0.049-0.0777-0.09-0.1113-0.152-0.1221-0.228-0.1706-0.568-0.1035-0.838l0.0019-0.012 0.0039-0.012c0.0822-0.298 0.0556-0.322 0.1445-0.615 0.0313-0.103 0.1114-0.245 0.1993-0.371h-3.3848z" />
+ <title v-if="title">{{ title }}</title>
+ </svg>
+ </span>
+</template>
+
+<script>
+export default {
+ name: 'MenuPeople',
+ props: {
+ title: {
+ type: String,
+ default: '',
+ },
+ fillColor: {
+ type: String,
+ default: 'currentColor',
+ },
+ size: {
+ type: Number,
+ default: 24,
+ },
+ },
+}
+</script>
diff --git a/src/components/missingMaterialDesignIcons/PresentToAll.vue b/src/components/missingMaterialDesignIcons/PresentToAll.vue
index ab1befec6..40c79135e 100644
--- a/src/components/missingMaterialDesignIcons/PresentToAll.vue
+++ b/src/components/missingMaterialDesignIcons/PresentToAll.vue
@@ -1,18 +1,18 @@
-<template functional>
- <span :aria-hidden="props.decorative"
- :aria-label="props.title"
- :class="[data.class, data.staticClass]"
+<template>
+ <span :aria-hidden="!title"
+ :aria-label="title"
class="material-design-icon present-to-all-icon"
role="img"
- v-bind="data.attrs"
- v-on="listeners">
- <svg :fill="props.fillColor"
+ v-bind="$attrs"
+ @click="$emit('click', $event)">
+ <svg :fill="fillColor"
class="material-design-icon__svg"
- :width="props.size"
- :height="props.size"
+ :width="size"
+ :height="size"
viewBox="0 0 24 24">
<path d="M0 0h24v24H0V0z" fill="none" />
<path d="M21 3H3c-1.11 0-2 .89-2 2v14c0 1.11.89 2 2 2h18c1.11 0 2-.89 2-2V5c0-1.11-.89-2-2-2zm0 16.02H3V4.98h18v14.04zM10 12H8l4-4 4 4h-2v4h-4v-4z" />
+ <title v-if="title">{{ title }}</title>
</svg>
</span>
</template>
@@ -23,11 +23,7 @@ export default {
props: {
title: {
type: String,
- default: 'Present To All icon',
- },
- decorative: {
- type: Boolean,
- default: false,
+ default: '',
},
fillColor: {
type: String,
diff --git a/src/components/missingMaterialDesignIcons/PromotedView.vue b/src/components/missingMaterialDesignIcons/PromotedView.vue
new file mode 100644
index 000000000..e1dee2b16
--- /dev/null
+++ b/src/components/missingMaterialDesignIcons/PromotedView.vue
@@ -0,0 +1,52 @@
+<template>
+ <span :aria-hidden="!title"
+ :aria-label="title"
+ class="material-design-icon promoted-view-icon"
+ role="img"
+ v-bind="$attrs"
+ @click="$emit('click', $event)">
+ <svg :fill="fillColor"
+ class="material-design-icon__svg"
+ :width="size"
+ :height="size"
+ viewBox="0 0 16 16">
+ <rect x="1"
+ y="1"
+ width="13"
+ height="9" />
+ <rect x="1"
+ y="12"
+ width="3"
+ height="3" />
+ <rect x="6"
+ y="12"
+ width="3"
+ height="3" />
+ <rect x="11"
+ y="12"
+ width="3"
+ height="3" />
+ <title v-if="title">{{ title }}</title>
+ </svg>
+ </span>
+</template>
+
+<script>
+export default {
+ name: 'PromotedView',
+ props: {
+ title: {
+ type: String,
+ default: '',
+ },
+ fillColor: {
+ type: String,
+ default: 'currentColor',
+ },
+ size: {
+ type: Number,
+ default: 24,
+ },
+ },
+}
+</script>
diff --git a/src/init.js b/src/init.js
index 4a87fc3b7..f67ee7577 100644
--- a/src/init.js
+++ b/src/init.js
@@ -1,7 +1,7 @@
/**
* @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
*
- * @author Marco Ambrosini <marcoambrosini@pm.me>
+ * @author Marco Ambrosini <marcoambrosini@icloud.com>
*
* @license AGPL-3.0-or-later
*
diff --git a/src/main.js b/src/main.js
index c4592b698..43eae30fd 100644
--- a/src/main.js
+++ b/src/main.js
@@ -5,7 +5,7 @@
*
* @author Joas Schilling <coding@schilljs.com>
*
- * @author Marco Ambrosini <marcoambrosini@pm.me>
+ * @author Marco Ambrosini <marcoambrosini@icloud.com>
*
* @license AGPL-3.0-or-later
*
diff --git a/src/mainFilesSidebar.js b/src/mainFilesSidebar.js
index 4e6653ea4..db86b5dd3 100644
--- a/src/mainFilesSidebar.js
+++ b/src/mainFilesSidebar.js
@@ -5,7 +5,7 @@
*
* @author Joas Schilling <coding@schilljs.com>
*
- * @author Marco Ambrosini <marcoambrosini@pm.me>
+ * @author Marco Ambrosini <marcoambrosini@icloud.com>
*
* @license AGPL-3.0-or-later
*
diff --git a/src/mainFilesSidebarLoader.js b/src/mainFilesSidebarLoader.js
index e2faef26e..7c03d195a 100644
--- a/src/mainFilesSidebarLoader.js
+++ b/src/mainFilesSidebarLoader.js
@@ -1,7 +1,7 @@
/**
- * @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@pm.me>
+ * @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@icloud.com>
*
- * @author Marco Ambrosini <marcoambrosini@pm.me>
+ * @author Marco Ambrosini <marcoambrosini@icloud.com>
*
* @license AGPL-3.0-or-later
*
diff --git a/src/mainPublicShareAuthSidebar.js b/src/mainPublicShareAuthSidebar.js
index 87e7ea0e0..07d251013 100644
--- a/src/mainPublicShareAuthSidebar.js
+++ b/src/mainPublicShareAuthSidebar.js
@@ -106,7 +106,7 @@ function adjustLayout() {
const requestPasswordElement = document.createElement('div')
requestPasswordElement.setAttribute('id', 'request-password')
- document.querySelector('main').appendChild(requestPasswordElement)
+ document.querySelector('.guest-box').appendChild(requestPasswordElement)
const talkSidebarElement = document.createElement('div')
talkSidebarElement.setAttribute('id', 'talk-sidebar')
diff --git a/src/mainPublicShareSidebar.js b/src/mainPublicShareSidebar.js
index f2ba41d77..ee4f468be 100644
--- a/src/mainPublicShareSidebar.js
+++ b/src/mainPublicShareSidebar.js
@@ -21,6 +21,7 @@
import Vue from 'vue'
import VueObserveVisibility from 'vue-observe-visibility'
import PublicShareSidebar from './PublicShareSidebar.vue'
+import PublicShareSidebarTrigger from './PublicShareSidebarTrigger.vue'
import './init.js'
// Store
@@ -98,10 +99,6 @@ if (window.innerWidth > 1111) {
function addTalkSidebarTrigger() {
const talkSidebarTriggerElement = document.createElement('button')
talkSidebarTriggerElement.setAttribute('id', 'talk-sidebar-trigger')
- talkSidebarTriggerElement.setAttribute('class', 'icon-menu-people-white')
- talkSidebarTriggerElement.addEventListener('click', () => {
- sidebarState.isOpen = !sidebarState.isOpen
- })
// The ".header-right" element may not exist in the public share page if
// there are no header actions.
@@ -112,6 +109,17 @@ function addTalkSidebarTrigger() {
}
document.querySelector('.header-right').appendChild(talkSidebarTriggerElement)
+
+ const talkSidebarTriggerVm = new Vue({
+ propsData: {
+ sidebarState,
+ },
+ ...PublicShareSidebarTrigger,
+ })
+ talkSidebarTriggerVm.$on('click', () => {
+ sidebarState.isOpen = !sidebarState.isOpen
+ })
+ talkSidebarTriggerVm.$mount('#talk-sidebar-trigger')
}
addTalkSidebarTrigger()
diff --git a/src/mixins/browserCheck.js b/src/mixins/browserCheck.js
index e55ba14b4..3815ae9fd 100644
--- a/src/mixins/browserCheck.js
+++ b/src/mixins/browserCheck.js
@@ -1,7 +1,7 @@
/**
- * @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@pm.me>
+ * @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@icloud.com>
*
- * @author Marco Ambrosini <marcoambrosini@pm.me>
+ * @author Marco Ambrosini <marcoambrosini@icloud.com>
*
* @license AGPL-3.0-or-later
*
diff --git a/src/mixins/devices.js b/src/mixins/devices.js
index 4647c1631..85f502d6a 100644
--- a/src/mixins/devices.js
+++ b/src/mixins/devices.js
@@ -1,7 +1,7 @@
/**
- * @copyright Copyright (c) 2021 Marco Ambrosini <marcoambrosini@pm.me>
+ * @copyright Copyright (c) 2021 Marco Ambrosini <marcoambrosini@icloud.com>
*
- * @author Marco Ambrosini <marcoambrosini@pm.me>
+ * @author Marco Ambrosini <marcoambrosini@icloud.com>
*
* @license AGPL-3.0-or-later
*
diff --git a/src/mixins/getParticipants.js b/src/mixins/getParticipants.js
index c38a1ce3d..7fded331d 100644
--- a/src/mixins/getParticipants.js
+++ b/src/mixins/getParticipants.js
@@ -1,8 +1,8 @@
/**
- * @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@pm.me>
+ * @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@icloud.com>
*
- * @author Marco Ambrosini <marcoambrosini@pm.me>
+ * @author Marco Ambrosini <marcoambrosini@icloud.com>
*
* @license AGPL-3.0-or-later
*
diff --git a/src/mixins/sessionIssueHandler.js b/src/mixins/sessionIssueHandler.js
index f137e85c9..1d1f9fea1 100644
--- a/src/mixins/sessionIssueHandler.js
+++ b/src/mixins/sessionIssueHandler.js
@@ -1,7 +1,7 @@
/**
- * @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@pm.me>
+ * @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@icloud.com>
*
- * @author Marco Ambrosini <marcoambrosini@pm.me>
+ * @author Marco Ambrosini <marcoambrosini@icloud.com>
*
* @license AGPL-3.0-or-later
*
diff --git a/src/mixins/sharedItems.js b/src/mixins/sharedItems.js
index d5b1dfad7..bf8772caa 100644
--- a/src/mixins/sharedItems.js
+++ b/src/mixins/sharedItems.js
@@ -1,7 +1,7 @@
/**
- * @copyright Copyright (c) 2022 Marco Ambrosini <marcoambrosini@pm.me>
+ * @copyright Copyright (c) 2022 Marco Ambrosini <marcoambrosini@icloud.com>
*
- * @author Marco Ambrosini <marcoambrosini@pm.me>
+ * @author Marco Ambrosini <marcoambrosini@icloud.com>
*
* @license AGPL-3.0-or-later
*
diff --git a/src/mixins/talkHashCheck.js b/src/mixins/talkHashCheck.js
index 7b3c13ee7..fb2c61d20 100644
--- a/src/mixins/talkHashCheck.js
+++ b/src/mixins/talkHashCheck.js
@@ -1,7 +1,7 @@
/**
- * @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@pm.me>
+ * @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@icloud.com>
*
- * @author Marco Ambrosini <marcoambrosini@pm.me>
+ * @author Marco Ambrosini <marcoambrosini@icloud.com>
*
* @license AGPL-3.0-or-later
*
diff --git a/src/mixins/userStatus.js b/src/mixins/userStatus.js
index bfa91eba8..5526b71eb 100644
--- a/src/mixins/userStatus.js
+++ b/src/mixins/userStatus.js
@@ -1,7 +1,7 @@
/**
- * @copyright Copyright (c) 2020 Marco Ambrosini <marcoambrosini@pm.me>
+ * @copyright Copyright (c) 2020 Marco Ambrosini <marcoambrosini@icloud.com>
*
- * @author Marco Ambrosini <marcoambrosini@pm.me>
+ * @author Marco Ambrosini <marcoambrosini@icloud.com>
*
* @license AGPL-3.0-or-later
*
diff --git a/src/mixins/video.js b/src/mixins/video.js
index 6a41f221c..bae9ba00e 100644
--- a/src/mixins/video.js
+++ b/src/mixins/video.js
@@ -1,7 +1,7 @@
/**
- * @copyright Copyright (c) 2020 Marco Ambrosini <marcoambrosini@pm.me>
+ * @copyright Copyright (c) 2020 Marco Ambrosini <marcoambrosini@icloud.com>
*
- * @author Marco Ambrosini <marcoambrosini@pm.me>
+ * @author Marco Ambrosini <marcoambrosini@icloud.com>
*
* @license AGPL-3.0-or-later
*
diff --git a/src/router/router.js b/src/router/router.js
index 16ab7e9f9..bd7fe2e60 100644
--- a/src/router/router.js
+++ b/src/router/router.js
@@ -1,7 +1,7 @@
/**
- * @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@pm.me>
+ * @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@icloud.com>
*
- * @author Marco Ambrosini <marcoambrosini@pm.me>
+ * @author Marco Ambrosini <marcoambrosini@icloud.com>
*
* @license AGPL-3.0-or-later
*
diff --git a/src/services/EventBus.js b/src/services/EventBus.js
index 0e1eb19af..fc47ce47d 100644
--- a/src/services/EventBus.js
+++ b/src/services/EventBus.js
@@ -1,7 +1,7 @@
/**
- * @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@pm.me>
+ * @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@icloud.com>
*
- * @author Marco Ambrosini <marcoambrosini@pm.me>
+ * @author Marco Ambrosini <marcoambrosini@icloud.com>
*
* @license AGPL-3.0-or-later
*
diff --git a/src/services/conversationsService.js b/src/services/conversationsService.js
index bf9b3e7d1..3845cb8c3 100644
--- a/src/services/conversationsService.js
+++ b/src/services/conversationsService.js
@@ -1,7 +1,7 @@
/**
- * @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@pm.me>
+ * @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@icloud.com>
*
- * @author Marco Ambrosini <marcoambrosini@pm.me>
+ * @author Marco Ambrosini <marcoambrosini@icloud.com>
*
* @license AGPL-3.0-or-later
*
@@ -390,6 +390,18 @@ const setCallPermissions = async (token, permissions) => {
})
}
+/**
+ * Set the message expiration
+ *
+ * @param {string} token conversation token
+ * @param {number} seconds the seconds for the message expiration, 0 to disable
+ */
+const setMessageExpiration = async (token, seconds) => {
+ return await axios.post(generateOcsUrl('apps/spreed/api/v4/room/{token}/message-expiration', { token }), {
+ seconds,
+ })
+}
+
const validatePassword = async (password) => {
return await axios.post(generateOcsUrl('apps/password_policy/api/v1/validate'), {
password,
@@ -422,5 +434,6 @@ export {
clearConversationHistory,
setConversationPermissions,
setCallPermissions,
+ setMessageExpiration,
validatePassword,
}
diff --git a/src/services/filesIntegrationServices.js b/src/services/filesIntegrationServices.js
index 8b4964359..d3b4b2d84 100644
--- a/src/services/filesIntegrationServices.js
+++ b/src/services/filesIntegrationServices.js
@@ -1,7 +1,7 @@
/**
- * @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@pm.me>
+ * @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@icloud.com>
*
- * @author Marco Ambrosini <marcoambrosini@pm.me>
+ * @author Marco Ambrosini <marcoambrosini@icloud.com>
*
* @license AGPL-3.0-or-later
*
diff --git a/src/services/filesSharingServices.js b/src/services/filesSharingServices.js
index 13be694a3..2b9b2f1fd 100644
--- a/src/services/filesSharingServices.js
+++ b/src/services/filesSharingServices.js
@@ -1,7 +1,7 @@
/**
- * @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@pm.me>
+ * @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@icloud.com>
*
- * @author Marco Ambrosini <marcoambrosini@pm.me>
+ * @author Marco Ambrosini <marcoambrosini@icloud.com>
*
* @license AGPL-3.0-or-later
*
diff --git a/src/services/messagesService.js b/src/services/messagesService.js
index 1cddecac3..2f1705273 100644
--- a/src/services/messagesService.js
+++ b/src/services/messagesService.js
@@ -1,7 +1,7 @@
/**
- * @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@pm.me>
+ * @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@icloud.com>
*
- * @author Marco Ambrosini <marcoambrosini@pm.me>
+ * @author Marco Ambrosini <marcoambrosini@icloud.com>
*
* @license AGPL-3.0-or-later
*
diff --git a/src/services/participantsService.js b/src/services/participantsService.js
index c4771c25e..b820b46e8 100644
--- a/src/services/participantsService.js
+++ b/src/services/participantsService.js
@@ -1,7 +1,7 @@
/**
- * @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@pm.me>
+ * @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@icloud.com>
*
- * @author Marco Ambrosini <marcoambrosini@pm.me>
+ * @author Marco Ambrosini <marcoambrosini@icloud.com>
*
* @license AGPL-3.0-or-later
*
diff --git a/src/services/sharedItemsService.js b/src/services/sharedItemsService.js
index a0537ecf9..45c0e8224 100644
--- a/src/services/sharedItemsService.js
+++ b/src/services/sharedItemsService.js
@@ -1,7 +1,7 @@
/**
- * @copyright Copyright (c) 2022 Marco Ambrosini <marcoambrosini@pm.me>
+ * @copyright Copyright (c) 2022 Marco Ambrosini <marcoambrosini@icloud.com>
*
- * @author Marco Ambrosini <marcoambrosini@pm.me>
+ * @author Marco Ambrosini <marcoambrosini@icloud.com>
*
* @license AGPL-3.0-or-later
*
diff --git a/src/store/audioRecorderStore.js b/src/store/audioRecorderStore.js
index 71a67deb1..80201439b 100644
--- a/src/store/audioRecorderStore.js
+++ b/src/store/audioRecorderStore.js
@@ -1,7 +1,7 @@
/**
- * @copyright Copyright (c) 2021 Marco Ambrosini <marcoambrosini@pm.me>
+ * @copyright Copyright (c) 2021 Marco Ambrosini <marcoambrosini@icloud.com>
*
- * @author Marco Ambrosini <marcoambrosini@pm.me>
+ * @author Marco Ambrosini <marcoambrosini@icloud.com>
*
* @license AGPL-3.0-or-later
*
diff --git a/src/store/callViewStore.js b/src/store/callViewStore.js
index 9b4f792fd..3fcbec3c3 100644
--- a/src/store/callViewStore.js
+++ b/src/store/callViewStore.js
@@ -1,7 +1,7 @@
/**
- * @copyright Copyright (c) 2020 Marco Ambrosini <marcoambrosini@pm.me>
+ * @copyright Copyright (c) 2020 Marco Ambrosini <marcoambrosini@icloud.com>
*
- * @author Marco Ambrosini <marcoambrosini@pm.me>
+ * @author Marco Ambrosini <marcoambrosini@icloud.com>
*
* @license AGPL-3.0-or-later
*
diff --git a/src/store/conversationsStore.js b/src/store/conversationsStore.js
index 6bafa7650..52e3e284a 100644
--- a/src/store/conversationsStore.js
+++ b/src/store/conversationsStore.js
@@ -1,7 +1,7 @@
/**
- * @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@pm.me>
+ * @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@icloud.com>
*
- * @author Marco Ambrosini <marcoambrosini@pm.me>
+ * @author Marco Ambrosini <marcoambrosini@icloud.com>
*
* @license AGPL-3.0-or-later
*
@@ -40,6 +40,7 @@ import {
setNotificationCalls,
setConversationPermissions,
setCallPermissions,
+ setMessageExpiration,
} from '../services/conversationsService.js'
import { getCurrentUser } from '@nextcloud/auth'
// eslint-disable-next-line import/extensions
@@ -160,6 +161,10 @@ const mutations = {
setCallPermissions(state, { token, permissions }) {
Vue.set(state.conversations[token], 'callPermissions', permissions)
},
+
+ setMessageExpiration(state, { token, seconds }) {
+ Vue.set(state.conversations[token], 'messageExpiration', seconds)
+ },
}
const actions = {
@@ -500,6 +505,11 @@ const actions = {
context.commit('setConversationPermissions', { token, permissions })
},
+ async setMessageExpiration({ commit }, { token, seconds }) {
+ await setMessageExpiration(token, seconds)
+ commit('setMessageExpiration', { token, seconds })
+ },
+
async setCallPermissions(context, { token, permissions }) {
await setCallPermissions(token, permissions)
context.commit('setCallPermissions', { token, permissions })
diff --git a/src/store/fileUploadStore.js b/src/store/fileUploadStore.js
index defe9bc45..38810179a 100644
--- a/src/store/fileUploadStore.js
+++ b/src/store/fileUploadStore.js
@@ -1,7 +1,7 @@
/**
- * @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@pm.me>
+ * @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@icloud.com>
*
- * @author Marco Ambrosini <marcoambrosini@pm.me>
+ * @author Marco Ambrosini <marcoambrosini@icloud.com>
*
* @license AGPL-3.0-or-later
*
diff --git a/src/store/index.js b/src/store/index.js
index e87ffcedb..721e3d977 100644
--- a/src/store/index.js
+++ b/src/store/index.js
@@ -1,7 +1,7 @@
/**
- * @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@pm.me>
+ * @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@icloud.com>
*
- * @author Marco Ambrosini <marcoambrosini@pm.me>
+ * @author Marco Ambrosini <marcoambrosini@icloud.com>
*
* @license AGPL-3.0-or-later
*
diff --git a/src/store/integrationsStore.js b/src/store/integrationsStore.js
index 21b262f92..cd16aefc4 100644
--- a/src/store/integrationsStore.js
+++ b/src/store/integrationsStore.js
@@ -1,7 +1,7 @@
/**
- * @copyright Copyright (c) 2020 Marco Ambrosini <marcoambrosini@pm.me>
+ * @copyright Copyright (c) 2020 Marco Ambrosini <marcoambrosini@icloud.com>
*
- * @author Marco Ambrosini <marcoambrosini@pm.me>
+ * @author Marco Ambrosini <marcoambrosini@icloud.com>
*
* @license AGPL-3.0-or-later
*
diff --git a/src/store/messagesStore.js b/src/store/messagesStore.js
index 0d8b2e977..95c3b9848 100644
--- a/src/store/messagesStore.js
+++ b/src/store/messagesStore.js
@@ -1,7 +1,7 @@
/**
- * @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@pm.me>
+ * @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@icloud.com>
*
- * @author Marco Ambrosini <marcoambrosini@pm.me>
+ * @author Marco Ambrosini <marcoambrosini@icloud.com>
*
* @license AGPL-3.0-or-later
*
@@ -976,7 +976,7 @@ const actions = {
* @param {object} data.messageToBeForwarded the message object;
*/
async forwardMessage(context, { messageToBeForwarded }) {
- const response = await postNewMessage(messageToBeForwarded)
+ const response = await postNewMessage(messageToBeForwarded, { silent: false })
return response
},
diff --git a/src/store/newGroupConversationStore.js b/src/store/newGroupConversationStore.js
index 2682f2e87..67e8aab15 100644
--- a/src/store/newGroupConversationStore.js
+++ b/src/store/newGroupConversationStore.js
@@ -1,7 +1,7 @@
/**
- * @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@pm.me>
+ * @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@icloud.com>
*
- * @author Marco Ambrosini <marcoambrosini@pm.me>
+ * @author Marco Ambrosini <marcoambrosini@icloud.com>
*
* @license AGPL-3.0-or-later
*
diff --git a/src/store/quoteReplyStore.js b/src/store/quoteReplyStore.js
index 533100ce0..ba279b02a 100644
--- a/src/store/quoteReplyStore.js
+++ b/src/store/quoteReplyStore.js
@@ -1,7 +1,7 @@
/**
- * @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@pm.me>
+ * @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@icloud.com>
*
- * @author Marco Ambrosini <marcoambrosini@pm.me>
+ * @author Marco Ambrosini <marcoambrosini@icloud.com>
*
* @license AGPL-3.0-or-later
*
diff --git a/src/store/reactionsStore.js b/src/store/reactionsStore.js
index 195a1ee2d..cf42a137e 100644
--- a/src/store/reactionsStore.js
+++ b/src/store/reactionsStore.js
@@ -1,7 +1,7 @@
/**
- * @copyright Copyright (c) 2022 Marco Ambrosini <marcoambrosini@pm.me>
+ * @copyright Copyright (c) 2022 Marco Ambrosini <marcoambrosini@icloud.com>
*
- * @author Marco Ambrosini <marcoambrosini@pm.me>
+ * @author Marco Ambrosini <marcoambrosini@icloud.com>
*
* @license AGPL-3.0-or-later
*
diff --git a/src/store/sharedItemsStore.js b/src/store/sharedItemsStore.js
index ed0e90a58..0e17dc456 100644
--- a/src/store/sharedItemsStore.js
+++ b/src/store/sharedItemsStore.js
@@ -1,7 +1,7 @@
/**
- * @copyright Copyright (c) 2022 Marco Ambrosini <marcoambrosini@pm.me>
+ * @copyright Copyright (c) 2022 Marco Ambrosini <marcoambrosini@icloud.com>
*
- * @author Marco Ambrosini <marcoambrosini@pm.me>
+ * @author Marco Ambrosini <marcoambrosini@icloud.com>
*
* @license AGPL-3.0-or-later
*
diff --git a/src/store/sidebarStore.js b/src/store/sidebarStore.js
index 1ff007677..454d049ed 100644
--- a/src/store/sidebarStore.js
+++ b/src/store/sidebarStore.js
@@ -1,7 +1,7 @@
/**
- * @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@pm.me>
+ * @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@icloud.com>
*
- * @author Marco Ambrosini <marcoambrosini@pm.me>
+ * @author Marco Ambrosini <marcoambrosini@icloud.com>
*
* @license AGPL-3.0-or-later
*
diff --git a/src/store/storeConfig.js b/src/store/storeConfig.js
index 656ca3e5a..1a0ebc24f 100644
--- a/src/store/storeConfig.js
+++ b/src/store/storeConfig.js
@@ -1,7 +1,7 @@
/**
- * @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@pm.me>
+ * @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@icloud.com>
*
- * @author Marco Ambrosini <marcoambrosini@pm.me>
+ * @author Marco Ambrosini <marcoambrosini@icloud.com>
*
* @license AGPL-3.0-or-later
*
diff --git a/src/store/tokenStore.js b/src/store/tokenStore.js
index 197a51303..b5187f006 100644
--- a/src/store/tokenStore.js
+++ b/src/store/tokenStore.js
@@ -1,7 +1,7 @@
/**
- * @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@pm.me>
+ * @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@icloud.com>
*
- * @author Marco Ambrosini <marcoambrosini@pm.me>
+ * @author Marco Ambrosini <marcoambrosini@icloud.com>
*
* @license AGPL-3.0-or-later
*
diff --git a/src/store/windowVisibilityStore.js b/src/store/windowVisibilityStore.js
index 7d568ad6b..109ccb4f1 100644
--- a/src/store/windowVisibilityStore.js
+++ b/src/store/windowVisibilityStore.js
@@ -1,7 +1,7 @@
/**
- * @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@pm.me>
+ * @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@icloud.com>
*
- * @author Marco Ambrosini <marcoambrosini@pm.me>
+ * @author Marco Ambrosini <marcoambrosini@icloud.com>
*
* @license AGPL-3.0-or-later
*
diff --git a/src/utils/cancelableRequest.js b/src/utils/cancelableRequest.js
index 518db9f38..3a529e4fc 100644
--- a/src/utils/cancelableRequest.js
+++ b/src/utils/cancelableRequest.js
@@ -1,7 +1,7 @@
/**
- * @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@pm.me>
+ * @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@icloud.com>
*
- * @author Marco Ambrosini <marcoambrosini@pm.me>
+ * @author Marco Ambrosini <marcoambrosini@icloud.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
diff --git a/src/utils/fileUpload.js b/src/utils/fileUpload.js
index db7a4858a..b1b41d2ef 100644
--- a/src/utils/fileUpload.js
+++ b/src/utils/fileUpload.js
@@ -1,7 +1,7 @@
/**
- * @copyright Copyright (c) 2020 Marco Ambrosini <marcoambrosini@pm.me>
+ * @copyright Copyright (c) 2020 Marco Ambrosini <marcoambrosini@icloud.com>
*
- * @author Marco Ambrosini <marcoambrosini@pm.me>
+ * @author Marco Ambrosini <marcoambrosini@icloud.com>
*
* @license AGPL-3.0-or-later
*
diff --git a/src/utils/media/pipeline/BlackVideoEnforcer.js b/src/utils/media/pipeline/BlackVideoEnforcer.js
new file mode 100644
index 000000000..76dcd3af8
--- /dev/null
+++ b/src/utils/media/pipeline/BlackVideoEnforcer.js
@@ -0,0 +1,169 @@
+/**
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+import TrackSinkSource from './TrackSinkSource.js'
+
+/**
+ * Processor node to enforce zero-information-content on a disabled or ended
+ * video track.
+ *
+ * A single input track slot with the default id is accepted. A single output
+ * track slot with the default id is provided. The input track must be a video
+ * track. The output track will be a video track.
+ *
+ * When the input track is enabled it is just bypassed to the output. However,
+ * if the input track is disabled or stopped a black video track is generated
+ * and set as the output instead; the black video track will be initially
+ * enabled and later automatically disabled, unless anything changes in the
+ * input and causes a different output track, even another black video track, to
+ * be set (a previous black video track is not reused, a new one is always
+ * generated). If the input track is removed the black video will be initially
+ * set as the output too, but then it will be also removed instead of disabled.
+ *
+ * --------------------
+ * | |
+ * ---> | BlackVideoEnforcer | --->
+ * | |
+ * --------------------
+ */
+export default class BlackVideoEnforcer extends TrackSinkSource {
+
+ constructor() {
+ super()
+
+ this._addInputTrackSlot()
+ this._addOutputTrackSlot()
+ }
+
+ _handleInputTrack(trackId, newTrack, oldTrack) {
+ if (oldTrack && this._startBlackVideoWhenTrackEndedHandler) {
+ oldTrack.removeEventListener('ended', this._startBlackVideoWhenTrackEndedHandler)
+ this._startBlackVideoWhenTrackEndedHandler = null
+ }
+
+ if (newTrack) {
+ this._disableRemoveTrackWhenEnded(newTrack)
+
+ this._startBlackVideoWhenTrackEndedHandler = () => {
+ this._startBlackVideo(newTrack.getSettings())
+ }
+ newTrack.addEventListener('ended', this._startBlackVideoWhenTrackEndedHandler)
+ }
+
+ this._stopBlackVideo()
+
+ if (newTrack && newTrack.enabled) {
+ this._setOutputTrack('default', this.getInputTrack())
+
+ return
+ }
+
+ const trackSettings = newTrack ? newTrack.getSettings() : oldTrack?.getSettings()
+ this._startBlackVideo(trackSettings)
+ }
+
+ _handleInputTrackEnabled(trackId, enabled) {
+ // Same enabled state as before, nothing to do
+ if ((enabled && !this._outputStream)
+ || (!enabled && this._outputStream)) {
+ return
+ }
+
+ if (enabled) {
+ this._stopBlackVideo()
+
+ this._setOutputTrack('default', this.getInputTrack())
+
+ return
+ }
+
+ if (this._outputStream) {
+ this._setOutputTrackEnabled('default', false)
+
+ return
+ }
+
+ this._startBlackVideo(this.getInputTrack().getSettings())
+ }
+
+ _startBlackVideo(trackSettings) {
+ if (this._outputStream) {
+ return
+ }
+
+ const { width, height } = trackSettings ?? { width: 640, height: 480 }
+
+ const outputCanvasElement = document.createElement('canvas')
+ outputCanvasElement.width = parseInt(width, 10)
+ outputCanvasElement.height = parseInt(height, 10)
+ const outputCanvasContext = outputCanvasElement.getContext('2d')
+
+ this._outputStream = outputCanvasElement.captureStream()
+
+ outputCanvasContext.fillStyle = 'black'
+ outputCanvasContext.fillRect(0, 0, outputCanvasElement.width, outputCanvasElement.height)
+
+ // Sometimes Chromium does not render one or more frames to the stream
+ // captured from a canvas, so repeat the drawing several times for
+ // several seconds to work around that.
+ this._renderInterval = setInterval(() => {
+ outputCanvasContext.fillRect(0, 0, outputCanvasElement.width, outputCanvasElement.height)
+ }, 100)
+
+ this._setOutputTrack('default', this._outputStream.getVideoTracks()[0])
+
+ this._disableOrRemoveOutputTrackTimeout = setTimeout(() => {
+ clearTimeout(this._disableOrRemoveOutputTrackTimeout)
+ this._disableOrRemoveOutputTrackTimeout = null
+
+ clearInterval(this._renderInterval)
+ this._renderInterval = null
+
+ if (this.getInputTrack()) {
+ this._setOutputTrackEnabled('default', false)
+ } else {
+ this._stopBlackVideo()
+ this._setOutputTrack('default', null)
+ }
+ }, 5000)
+ }
+
+ _stopBlackVideo() {
+ if (!this._outputStream) {
+ return
+ }
+
+ clearTimeout(this._disableOrRemoveOutputTrackTimeout)
+ this._disableOrRemoveOutputTrackTimeout = null
+
+ clearInterval(this._renderInterval)
+ this._renderInterval = null
+
+ this._outputStream.getTracks().forEach(track => {
+ this._disableRemoveTrackWhenEnded(track)
+
+ track.stop()
+ })
+
+ this._outputStream = null
+ }
+
+}
diff --git a/src/utils/media/pipeline/BlackVideoEnforcer.spec.js b/src/utils/media/pipeline/BlackVideoEnforcer.spec.js
new file mode 100644
index 000000000..70c20f1da
--- /dev/null
+++ b/src/utils/media/pipeline/BlackVideoEnforcer.spec.js
@@ -0,0 +1,1505 @@
+/**
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+import BlackVideoEnforcer from './BlackVideoEnforcer.js'
+
+/**
+ * Helper function to create MediaStreamTrack mocks with just the attributes and
+ * methods used by BlackVideoEnforcer.
+ *
+ * @param {string} id the ID of the track
+ */
+function newMediaStreamTrackMock(id) {
+ /**
+ * MediaStreamTrackMock constructor.
+ */
+ function MediaStreamTrackMock() {
+ this._endedEventHandlers = []
+ this._width = 720
+ this._height = 540
+ this.id = id
+ this.enabled = true
+ this.addEventListener = jest.fn((eventName, eventHandler) => {
+ if (eventName !== 'ended') {
+ return
+ }
+
+ this._endedEventHandlers.push(eventHandler)
+ })
+ this.removeEventListener = jest.fn((eventName, eventHandler) => {
+ if (eventName !== 'ended') {
+ return
+ }
+
+ const index = this._endedEventHandlers.indexOf(eventHandler)
+ if (index !== -1) {
+ this._endedEventHandlers.splice(index, 1)
+ }
+ })
+ this.stop = jest.fn(() => {
+ for (let i = 0; i < this._endedEventHandlers.length; i++) {
+ const handler = this._endedEventHandlers[i]
+ handler.apply(handler)
+ }
+ })
+ this.getSettings = jest.fn(() => {
+ return {
+ width: this._width,
+ height: this._height,
+ }
+ })
+ }
+ return new MediaStreamTrackMock()
+}
+
+describe('BlackVideoEnforcer', () => {
+ let blackVideoEnforcer
+ let outputTrackSetHandler
+ let outputTrackEnabledHandler
+ let expectedTrackEnabledStateInOutputTrackSetEvent
+ let blackVideoTrackCount
+ let blackVideoTracks
+
+ beforeAll(() => {
+ const originalCreateElement = document.createElement
+ jest.spyOn(document, 'createElement').mockImplementation((tagName, options) => {
+ if (tagName !== 'canvas') {
+ return originalCreateElement(tagName, options)
+ }
+
+ return new function() {
+ this.getContext = jest.fn(() => {
+ return {
+ fillRect: jest.fn(),
+ }
+ })
+ this.captureStream = jest.fn(() => {
+ const blackVideoTrackLocal = newMediaStreamTrackMock('blackVideoTrack' + blackVideoTrackCount)
+ blackVideoTracks[blackVideoTrackCount] = blackVideoTrackLocal
+ blackVideoTrackCount++
+
+ blackVideoTrackLocal._width = this.width
+ blackVideoTrackLocal._height = this.height
+
+ return {
+ getVideoTracks: jest.fn(() => {
+ return [blackVideoTrackLocal]
+ }),
+ getTracks: jest.fn(() => {
+ return [blackVideoTrackLocal]
+ }),
+ }
+ })
+ }()
+ })
+ })
+
+ beforeEach(() => {
+ jest.useFakeTimers()
+
+ blackVideoTrackCount = 0
+ blackVideoTracks = []
+
+ blackVideoEnforcer = new BlackVideoEnforcer()
+
+ expectedTrackEnabledStateInOutputTrackSetEvent = undefined
+
+ outputTrackSetHandler = jest.fn((blackVideoEnforcer, trackId, track) => {
+ if (expectedTrackEnabledStateInOutputTrackSetEvent !== undefined) {
+ expect(track.enabled).toBe(expectedTrackEnabledStateInOutputTrackSetEvent)
+ }
+ })
+ outputTrackEnabledHandler = jest.fn()
+
+ blackVideoEnforcer.on('outputTrackSet', outputTrackSetHandler)
+ blackVideoEnforcer.on('outputTrackEnabled', outputTrackEnabledHandler)
+ })
+
+ afterEach(() => {
+ clearTimeout(blackVideoEnforcer._disableOrRemoveOutputTrackTimeout)
+ clearInterval(blackVideoEnforcer._renderInterval)
+ })
+
+ afterAll(() => {
+ jest.restoreAllMocks()
+ })
+
+ const DISABLE_OR_REMOVE_TIMEOUT = 5000
+
+ const STOPPED = true
+
+ /**
+ * Checks that a black video track has the expected attributes.
+ *
+ * @param {number} index the index of the black video track to check.
+ * @param {number} width the expected width of the black video track.
+ * @param {number} height the expected height of the black video track.
+ * @param {boolean} stopped whether the black video track is expected to
+ * have been stopped already or not.
+ */
+ function assertBlackVideoTrack(index, width, height, stopped = false) {
+ expect(blackVideoTracks[index].getSettings().width).toBe(width)
+ expect(blackVideoTracks[index].getSettings().height).toBe(height)
+ expect(blackVideoTracks[index].stop).toHaveBeenCalledTimes(stopped ? 1 : 0)
+ }
+
+ describe('set input track', () => {
+ test('sets input track as its output track when setting enabled input track', () => {
+ const inputTrack = newMediaStreamTrackMock('input')
+
+ expectedTrackEnabledStateInOutputTrackSetEvent = true
+
+ blackVideoEnforcer._setInputTrack('default', inputTrack)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(1)
+ expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', inputTrack)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTrackCount).toBe(0)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT * 5)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(0)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTrackCount).toBe(0)
+ })
+
+ test('sets black video track as its output track when setting disabled input track', () => {
+ const inputTrack = newMediaStreamTrackMock('input')
+
+ expectedTrackEnabledStateInOutputTrackSetEvent = true
+
+ inputTrack.enabled = false
+ blackVideoEnforcer._setInputTrack('default', inputTrack)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(1)
+ expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', blackVideoTracks[0])
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTrackCount).toBe(1)
+ assertBlackVideoTrack(0, 720, 540)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT - 1)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(0)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+
+ jest.advanceTimersByTime(1)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(0)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(1)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', false)
+ expect(blackVideoTrackCount).toBe(1)
+ assertBlackVideoTrack(0, 720, 540)
+ })
+ })
+
+ describe('enable/disable input track', () => {
+ test('sets black video track as its output track if input track is disabled', () => {
+ const inputTrack = newMediaStreamTrackMock('input')
+
+ blackVideoEnforcer._setInputTrack('default', inputTrack)
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ expectedTrackEnabledStateInOutputTrackSetEvent = true
+
+ inputTrack.enabled = false
+ blackVideoEnforcer._setInputTrackEnabled('default', false)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(1)
+ expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', blackVideoTracks[0])
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTrackCount).toBe(1)
+ assertBlackVideoTrack(0, 720, 540)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT - 1)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(0)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+
+ jest.advanceTimersByTime(1)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(0)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(1)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', false)
+ expect(blackVideoTrackCount).toBe(1)
+ assertBlackVideoTrack(0, 720, 540)
+ })
+
+ test('sets input track as its output track if input track is enabled', () => {
+ const inputTrack = newMediaStreamTrackMock('input')
+
+ inputTrack.enabled = false
+ blackVideoEnforcer._setInputTrack('default', inputTrack)
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ expectedTrackEnabledStateInOutputTrackSetEvent = true
+
+ inputTrack.enabled = true
+ blackVideoEnforcer._setInputTrackEnabled('default', true)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(1)
+ expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', inputTrack)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTrackCount).toBe(1)
+ assertBlackVideoTrack(0, 720, 540, STOPPED)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT * 5)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(0)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTrackCount).toBe(1)
+ assertBlackVideoTrack(0, 720, 540, STOPPED)
+ })
+
+ test('sets input track as its output track if input track is later enabled', () => {
+ const inputTrack = newMediaStreamTrackMock('input')
+
+ inputTrack.enabled = false
+ blackVideoEnforcer._setInputTrack('default', inputTrack)
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT * 2)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ expectedTrackEnabledStateInOutputTrackSetEvent = true
+
+ inputTrack.enabled = true
+ blackVideoEnforcer._setInputTrackEnabled('default', true)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(1)
+ expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', inputTrack)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTrackCount).toBe(1)
+ assertBlackVideoTrack(0, 720, 540, STOPPED)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT * 5)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(0)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTrackCount).toBe(1)
+ assertBlackVideoTrack(0, 720, 540, STOPPED)
+ })
+
+ test('does nothing if input track is enabled again', () => {
+ const inputTrack = newMediaStreamTrackMock('input')
+
+ blackVideoEnforcer._setInputTrack('default', inputTrack)
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ blackVideoEnforcer._setInputTrackEnabled('default', true)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(0)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTrackCount).toBe(0)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT * 5)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(0)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTrackCount).toBe(0)
+ })
+
+ test('does nothing if input track is disabled again', () => {
+ const inputTrack = newMediaStreamTrackMock('input')
+
+ inputTrack.enabled = false
+ blackVideoEnforcer._setInputTrack('default', inputTrack)
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ blackVideoEnforcer._setInputTrackEnabled('default', false)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(0)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTrackCount).toBe(1)
+ assertBlackVideoTrack(0, 720, 540)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2 - 1)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(0)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+
+ jest.advanceTimersByTime(1)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(0)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(1)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', false)
+ expect(blackVideoTrackCount).toBe(1)
+ assertBlackVideoTrack(0, 720, 540)
+ })
+ })
+
+ describe('remove input track', () => {
+ test('sets black video track as its output track and later removes output track when removing enabled input track', () => {
+ const inputTrack = newMediaStreamTrackMock('input')
+
+ blackVideoEnforcer._setInputTrack('default', inputTrack)
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ expectedTrackEnabledStateInOutputTrackSetEvent = true
+
+ blackVideoEnforcer._setInputTrack('default', null)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(1)
+ expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', blackVideoTracks[0])
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTrackCount).toBe(1)
+ assertBlackVideoTrack(0, 720, 540)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT - 1)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(0)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTracks[0].stop).toHaveBeenCalledTimes(0)
+
+ expectedTrackEnabledStateInOutputTrackSetEvent = undefined
+
+ jest.advanceTimersByTime(1)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(1)
+ expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', null)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTrackCount).toBe(1)
+ assertBlackVideoTrack(0, 720, 540, STOPPED)
+ })
+
+ test('sets input track as its output track when setting enabled input track after removing enabled input track', () => {
+ const inputTrack = newMediaStreamTrackMock('input')
+ const inputTrack2 = newMediaStreamTrackMock('input2')
+
+ blackVideoEnforcer._setInputTrack('default', inputTrack)
+
+ blackVideoEnforcer._setInputTrack('default', null)
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ expectedTrackEnabledStateInOutputTrackSetEvent = true
+
+ inputTrack2._width = 320
+ inputTrack2._height = 180
+ blackVideoEnforcer._setInputTrack('default', inputTrack2)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(1)
+ expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', inputTrack2)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTrackCount).toBe(1)
+ assertBlackVideoTrack(0, 720, 540, STOPPED)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT * 5)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(0)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTrackCount).toBe(1)
+ assertBlackVideoTrack(0, 720, 540, STOPPED)
+ })
+
+ test('sets black video track as its output track when setting disabled input track after removing enabled input track', () => {
+ const inputTrack = newMediaStreamTrackMock('input')
+ const inputTrack2 = newMediaStreamTrackMock('input2')
+
+ blackVideoEnforcer._setInputTrack('default', inputTrack)
+
+ blackVideoEnforcer._setInputTrack('default', null)
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ expectedTrackEnabledStateInOutputTrackSetEvent = true
+
+ inputTrack2.enabled = false
+ inputTrack2._width = 320
+ inputTrack2._height = 180
+ blackVideoEnforcer._setInputTrack('default', inputTrack2)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(1)
+ expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', blackVideoTracks[1])
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTrackCount).toBe(2)
+ assertBlackVideoTrack(0, 720, 540, STOPPED)
+ assertBlackVideoTrack(1, 320, 180)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT - 1)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(0)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+
+ jest.advanceTimersByTime(1)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(0)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(1)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', false)
+ expect(blackVideoTrackCount).toBe(2)
+ assertBlackVideoTrack(0, 720, 540, STOPPED)
+ assertBlackVideoTrack(1, 320, 180)
+ })
+
+ test('sets black video track as its output track and later removes output track when removing disabled input track', () => {
+ const inputTrack = newMediaStreamTrackMock('input')
+
+ inputTrack.enabled = false
+ blackVideoEnforcer._setInputTrack('default', inputTrack)
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ expectedTrackEnabledStateInOutputTrackSetEvent = true
+
+ blackVideoEnforcer._setInputTrack('default', null)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(1)
+ expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', blackVideoTracks[1])
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTrackCount).toBe(2)
+ assertBlackVideoTrack(0, 720, 540, STOPPED)
+ assertBlackVideoTrack(1, 720, 540)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT - 1)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(0)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTracks[1].stop).toHaveBeenCalledTimes(0)
+
+ expectedTrackEnabledStateInOutputTrackSetEvent = undefined
+
+ jest.advanceTimersByTime(1)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(1)
+ expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', null)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTrackCount).toBe(2)
+ assertBlackVideoTrack(0, 720, 540, STOPPED)
+ assertBlackVideoTrack(1, 720, 540, STOPPED)
+ })
+
+ test('sets black video track as its output track and later removes output track when later removing disabled input track', () => {
+ const inputTrack = newMediaStreamTrackMock('input')
+
+ inputTrack.enabled = false
+ blackVideoEnforcer._setInputTrack('default', inputTrack)
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT * 5)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ expectedTrackEnabledStateInOutputTrackSetEvent = true
+
+ blackVideoEnforcer._setInputTrack('default', null)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(1)
+ expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', blackVideoTracks[1])
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTrackCount).toBe(2)
+ assertBlackVideoTrack(0, 720, 540, STOPPED)
+ assertBlackVideoTrack(1, 720, 540)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT - 1)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(0)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTracks[1].stop).toHaveBeenCalledTimes(0)
+
+ expectedTrackEnabledStateInOutputTrackSetEvent = undefined
+
+ jest.advanceTimersByTime(1)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(1)
+ expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', null)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTrackCount).toBe(2)
+ assertBlackVideoTrack(0, 720, 540, STOPPED)
+ assertBlackVideoTrack(1, 720, 540, STOPPED)
+ })
+
+ test('sets black video track as its output track and later removes output track when removing null track', () => {
+ expectedTrackEnabledStateInOutputTrackSetEvent = true
+
+ blackVideoEnforcer._setInputTrack('default', null)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(1)
+ expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', blackVideoTracks[0])
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTrackCount).toBe(1)
+ assertBlackVideoTrack(0, 640, 480)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT - 1)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(0)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTracks[0].stop).toHaveBeenCalledTimes(0)
+
+ expectedTrackEnabledStateInOutputTrackSetEvent = undefined
+
+ jest.advanceTimersByTime(1)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(1)
+ expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', null)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTrackCount).toBe(1)
+ assertBlackVideoTrack(0, 640, 480, STOPPED)
+ })
+
+ test('sets black video track as its output track and later removes output track when removing null track again after removing enabled input track', () => {
+ const inputTrack = newMediaStreamTrackMock('input')
+
+ blackVideoEnforcer._setInputTrack('default', inputTrack)
+
+ blackVideoEnforcer._setInputTrack('default', null)
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ expectedTrackEnabledStateInOutputTrackSetEvent = true
+
+ blackVideoEnforcer._setInputTrack('default', null)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(1)
+ expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', blackVideoTracks[1])
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTrackCount).toBe(2)
+ assertBlackVideoTrack(0, 720, 540, STOPPED)
+ assertBlackVideoTrack(1, 640, 480)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT - 1)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(0)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTracks[1].stop).toHaveBeenCalledTimes(0)
+
+ expectedTrackEnabledStateInOutputTrackSetEvent = undefined
+
+ jest.advanceTimersByTime(1)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(1)
+ expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', null)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTrackCount).toBe(2)
+ assertBlackVideoTrack(0, 720, 540, STOPPED)
+ assertBlackVideoTrack(1, 640, 480, STOPPED)
+ })
+ })
+
+ describe('stop input track', () => {
+ test('sets black video track as its output track when stopping enabled input track', () => {
+ const inputTrack = newMediaStreamTrackMock('input')
+
+ blackVideoEnforcer._setInputTrack('default', inputTrack)
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ expectedTrackEnabledStateInOutputTrackSetEvent = true
+
+ inputTrack.stop()
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(1)
+ expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', blackVideoTracks[0])
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTrackCount).toBe(1)
+ assertBlackVideoTrack(0, 720, 540)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT - 1)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(0)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+
+ jest.advanceTimersByTime(1)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(0)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(1)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', false)
+ expect(blackVideoTrackCount).toBe(1)
+ assertBlackVideoTrack(0, 720, 540)
+ })
+
+ test('sets black video track as its output track when stopping initially disabled and then enabled input track', () => {
+ const inputTrack = newMediaStreamTrackMock('input')
+
+ inputTrack.enabled = false
+ blackVideoEnforcer._setInputTrack('default', inputTrack)
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 4)
+
+ blackVideoEnforcer._setInputTrackEnabled('default', true)
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 4)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ expectedTrackEnabledStateInOutputTrackSetEvent = true
+
+ inputTrack.stop()
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(1)
+ expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', blackVideoTracks[1])
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTrackCount).toBe(2)
+ assertBlackVideoTrack(0, 720, 540, STOPPED)
+ assertBlackVideoTrack(1, 720, 540)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT - 1)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(0)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+
+ jest.advanceTimersByTime(1)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(0)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(1)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', false)
+ expect(blackVideoTrackCount).toBe(2)
+ assertBlackVideoTrack(0, 720, 540, STOPPED)
+ assertBlackVideoTrack(1, 720, 540)
+ })
+
+ test('does nothing when stopping disabled input track', () => {
+ const inputTrack = newMediaStreamTrackMock('input')
+
+ inputTrack.enabled = false
+ blackVideoEnforcer._setInputTrack('default', inputTrack)
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ inputTrack.stop()
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(0)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTrackCount).toBe(1)
+ assertBlackVideoTrack(0, 720, 540)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2 - 1)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(0)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+
+ jest.advanceTimersByTime(1)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(0)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(1)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', false)
+ expect(blackVideoTrackCount).toBe(1)
+ assertBlackVideoTrack(0, 720, 540)
+ })
+
+ test('does nothing when stopping initially enabled and then disabled input track', () => {
+ const inputTrack = newMediaStreamTrackMock('input')
+
+ blackVideoEnforcer._setInputTrack('default', inputTrack)
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 4)
+
+ blackVideoEnforcer._setInputTrackEnabled('default', false)
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 4)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ inputTrack.stop()
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(0)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTrackCount).toBe(1)
+ assertBlackVideoTrack(0, 720, 540)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT * 3 / 4 - 1)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(0)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+
+ jest.advanceTimersByTime(1)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(0)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(1)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', false)
+ expect(blackVideoTrackCount).toBe(1)
+ assertBlackVideoTrack(0, 720, 540)
+ })
+
+ test('sets black video track as its output track and later removes output track when stopping enabled input track and then removing it', () => {
+ const inputTrack = newMediaStreamTrackMock('input')
+
+ blackVideoEnforcer._setInputTrack('default', inputTrack)
+
+ inputTrack.stop()
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ expectedTrackEnabledStateInOutputTrackSetEvent = true
+
+ blackVideoEnforcer._setInputTrack('default', null)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(1)
+ expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', blackVideoTracks[1])
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTrackCount).toBe(2)
+ assertBlackVideoTrack(0, 720, 540, STOPPED)
+ assertBlackVideoTrack(1, 720, 540)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT - 1)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(0)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTracks[1].stop).toHaveBeenCalledTimes(0)
+
+ expectedTrackEnabledStateInOutputTrackSetEvent = undefined
+
+ jest.advanceTimersByTime(1)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(1)
+ expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', null)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTrackCount).toBe(2)
+ assertBlackVideoTrack(0, 720, 540, STOPPED)
+ assertBlackVideoTrack(1, 720, 540, STOPPED)
+ })
+
+ test('sets black video track as its output track and later removes output track when stopping disabled input track and then removing it', () => {
+ const inputTrack = newMediaStreamTrackMock('input')
+
+ inputTrack.enabled = false
+ blackVideoEnforcer._setInputTrack('default', inputTrack)
+
+ inputTrack.stop()
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ expectedTrackEnabledStateInOutputTrackSetEvent = true
+
+ blackVideoEnforcer._setInputTrack('default', null)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(1)
+ expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', blackVideoTracks[1])
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTrackCount).toBe(2)
+ assertBlackVideoTrack(0, 720, 540, STOPPED)
+ assertBlackVideoTrack(1, 720, 540)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT - 1)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(0)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTracks[1].stop).toHaveBeenCalledTimes(0)
+
+ expectedTrackEnabledStateInOutputTrackSetEvent = undefined
+
+ jest.advanceTimersByTime(1)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(1)
+ expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', null)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTrackCount).toBe(2)
+ assertBlackVideoTrack(0, 720, 540, STOPPED)
+ assertBlackVideoTrack(1, 720, 540, STOPPED)
+ })
+
+ test('sets input track as its output track when stopping enabled input track and then replacing it with another enabled input track', () => {
+ const inputTrack = newMediaStreamTrackMock('input')
+ const inputTrack2 = newMediaStreamTrackMock('input2')
+
+ blackVideoEnforcer._setInputTrack('default', inputTrack)
+
+ inputTrack.stop()
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ expectedTrackEnabledStateInOutputTrackSetEvent = true
+
+ inputTrack2._width = 320
+ inputTrack2._height = 180
+ blackVideoEnforcer._setInputTrack('default', inputTrack2)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(1)
+ expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', inputTrack2)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTrackCount).toBe(1)
+ assertBlackVideoTrack(0, 720, 540, STOPPED)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT * 5)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(0)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTrackCount).toBe(1)
+ assertBlackVideoTrack(0, 720, 540, STOPPED)
+ })
+
+ test('sets black video track as its output track when stopping enabled input track and then replacing it with another disabled input track', () => {
+ const inputTrack = newMediaStreamTrackMock('input')
+ const inputTrack2 = newMediaStreamTrackMock('input2')
+
+ blackVideoEnforcer._setInputTrack('default', inputTrack)
+
+ inputTrack.stop()
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ expectedTrackEnabledStateInOutputTrackSetEvent = true
+
+ inputTrack2.enabled = false
+ inputTrack2._width = 320
+ inputTrack2._height = 180
+ blackVideoEnforcer._setInputTrack('default', inputTrack2)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(1)
+ expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', blackVideoTracks[1])
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTrackCount).toBe(2)
+ assertBlackVideoTrack(0, 720, 540, STOPPED)
+ assertBlackVideoTrack(1, 320, 180)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT - 1)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(0)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+
+ jest.advanceTimersByTime(1)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(0)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(1)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', false)
+ expect(blackVideoTrackCount).toBe(2)
+ assertBlackVideoTrack(0, 720, 540, STOPPED)
+ assertBlackVideoTrack(1, 320, 180)
+ })
+
+ test('sets input track as its output track when stopping disabled input track and then replacing it with another enabled input track', () => {
+ const inputTrack = newMediaStreamTrackMock('input')
+ const inputTrack2 = newMediaStreamTrackMock('input2')
+
+ inputTrack.enabled = false
+ blackVideoEnforcer._setInputTrack('default', inputTrack)
+
+ inputTrack.stop()
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ expectedTrackEnabledStateInOutputTrackSetEvent = true
+
+ inputTrack2._width = 320
+ inputTrack2._height = 180
+ blackVideoEnforcer._setInputTrack('default', inputTrack2)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(1)
+ expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', inputTrack2)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTrackCount).toBe(1)
+ assertBlackVideoTrack(0, 720, 540, STOPPED)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT * 5)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(0)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTrackCount).toBe(1)
+ assertBlackVideoTrack(0, 720, 540, STOPPED)
+ })
+
+ test('sets black video track as its output track when stopping disabled input track and then replacing it with another disabled input track', () => {
+ const inputTrack = newMediaStreamTrackMock('input')
+ const inputTrack2 = newMediaStreamTrackMock('input2')
+
+ inputTrack.enabled = false
+ blackVideoEnforcer._setInputTrack('default', inputTrack)
+
+ inputTrack.stop()
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ expectedTrackEnabledStateInOutputTrackSetEvent = true
+
+ inputTrack2.enabled = false
+ inputTrack2._width = 320
+ inputTrack2._height = 180
+ blackVideoEnforcer._setInputTrack('default', inputTrack2)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(1)
+ expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', blackVideoTracks[1])
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTrackCount).toBe(2)
+ assertBlackVideoTrack(0, 720, 540, STOPPED)
+ assertBlackVideoTrack(1, 320, 180)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT - 1)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(0)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+
+ jest.advanceTimersByTime(1)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(0)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(1)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', false)
+ expect(blackVideoTrackCount).toBe(2)
+ assertBlackVideoTrack(0, 720, 540, STOPPED)
+ assertBlackVideoTrack(1, 320, 180)
+ })
+
+ test('does nothing when stopping a previously removed enabled input track', () => {
+ const inputTrack = newMediaStreamTrackMock('input')
+
+ blackVideoEnforcer._setInputTrack('default', inputTrack)
+
+ blackVideoEnforcer._setInputTrack('default', null)
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT * 5)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ inputTrack.stop()
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(0)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTrackCount).toBe(1)
+ assertBlackVideoTrack(0, 720, 540, STOPPED)
+ })
+
+ test('does nothing when stopping a previously removed disabled input track', () => {
+ const inputTrack = newMediaStreamTrackMock('input')
+
+ inputTrack.enabled = false
+ blackVideoEnforcer._setInputTrack('default', inputTrack)
+
+ blackVideoEnforcer._setInputTrack('default', null)
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT * 5)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ inputTrack.stop()
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(0)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTrackCount).toBe(2)
+ assertBlackVideoTrack(0, 720, 540, STOPPED)
+ assertBlackVideoTrack(1, 720, 540, STOPPED)
+ })
+
+ test('does nothing when stopping a previously replaced enabled input track', () => {
+ const inputTrack = newMediaStreamTrackMock('input')
+ const inputTrack2 = newMediaStreamTrackMock('input2')
+
+ blackVideoEnforcer._setInputTrack('default', inputTrack)
+
+ inputTrack2._width = 320
+ inputTrack2._height = 180
+ blackVideoEnforcer._setInputTrack('default', inputTrack2)
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT * 5)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ inputTrack.stop()
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(0)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTrackCount).toBe(0)
+ })
+
+ test('does nothing when stopping a previously replaced disabled input track', () => {
+ const inputTrack = newMediaStreamTrackMock('input')
+ const inputTrack2 = newMediaStreamTrackMock('input2')
+
+ inputTrack.enabled = false
+ blackVideoEnforcer._setInputTrack('default', inputTrack)
+
+ inputTrack2._width = 320
+ inputTrack2._height = 180
+ blackVideoEnforcer._setInputTrack('default', inputTrack2)
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT * 5)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ inputTrack.stop()
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(0)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTrackCount).toBe(1)
+ assertBlackVideoTrack(0, 720, 540, STOPPED)
+ })
+
+ test('sets black video track as its output track when stopping input track after setting it again', () => {
+ const inputTrack = newMediaStreamTrackMock('input')
+
+ blackVideoEnforcer._setInputTrack('default', inputTrack)
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 4)
+
+ inputTrack._width = 320
+ inputTrack._height = 180
+ blackVideoEnforcer._setInputTrack('default', inputTrack)
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 4)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ inputTrack.stop()
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(1)
+ expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', blackVideoTracks[0])
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTrackCount).toBe(1)
+ assertBlackVideoTrack(0, 320, 180)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT - 1)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(0)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+
+ jest.advanceTimersByTime(1)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(0)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(1)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', false)
+ expect(blackVideoTrackCount).toBe(1)
+ assertBlackVideoTrack(0, 320, 180)
+ })
+ })
+
+ describe('update input track', () => {
+ test('sets input track as its output track when setting same enabled input track again', () => {
+ const inputTrack = newMediaStreamTrackMock('input')
+
+ blackVideoEnforcer._setInputTrack('default', inputTrack)
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ expectedTrackEnabledStateInOutputTrackSetEvent = true
+
+ inputTrack._width = 320
+ inputTrack._height = 180
+ blackVideoEnforcer._setInputTrack('default', inputTrack)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(1)
+ expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', inputTrack)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTrackCount).toBe(0)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT * 5)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(0)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTrackCount).toBe(0)
+ })
+
+ test('sets black video track as its output track when setting same disabled input track again', () => {
+ const inputTrack = newMediaStreamTrackMock('input')
+
+ inputTrack.enabled = false
+ blackVideoEnforcer._setInputTrack('default', inputTrack)
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ expectedTrackEnabledStateInOutputTrackSetEvent = true
+
+ inputTrack._width = 320
+ inputTrack._height = 180
+ blackVideoEnforcer._setInputTrack('default', inputTrack)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(1)
+ expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', blackVideoTracks[1])
+ expect(blackVideoTrackCount).toBe(2)
+ assertBlackVideoTrack(0, 720, 540, STOPPED)
+ assertBlackVideoTrack(1, 320, 180)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT - 1)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(0)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+
+ jest.advanceTimersByTime(1)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(0)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(1)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', false)
+ expect(blackVideoTrackCount).toBe(2)
+ assertBlackVideoTrack(0, 720, 540, STOPPED)
+ assertBlackVideoTrack(1, 320, 180)
+ })
+
+ test('sets black video track as its output track when setting same now disabled input track again', () => {
+ const inputTrack = newMediaStreamTrackMock('input')
+
+ blackVideoEnforcer._setInputTrack('default', inputTrack)
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ expectedTrackEnabledStateInOutputTrackSetEvent = true
+
+ inputTrack.enabled = false
+ inputTrack._width = 320
+ inputTrack._height = 180
+ blackVideoEnforcer._setInputTrack('default', inputTrack)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(1)
+ expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', blackVideoTracks[0])
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTrackCount).toBe(1)
+ assertBlackVideoTrack(0, 320, 180)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT - 1)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(0)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+
+ jest.advanceTimersByTime(1)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(0)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(1)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', false)
+ expect(blackVideoTrackCount).toBe(1)
+ assertBlackVideoTrack(0, 320, 180)
+ })
+
+ test('sets input track as its output track when setting same now enabled input track again', () => {
+ const inputTrack = newMediaStreamTrackMock('input')
+
+ inputTrack.enabled = false
+ blackVideoEnforcer._setInputTrack('default', inputTrack)
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ expectedTrackEnabledStateInOutputTrackSetEvent = true
+
+ inputTrack.enabled = true
+ inputTrack._width = 320
+ inputTrack._height = 180
+ blackVideoEnforcer._setInputTrack('default', inputTrack)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(1)
+ expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', inputTrack)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTrackCount).toBe(1)
+ assertBlackVideoTrack(0, 720, 540, STOPPED)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT * 5)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(0)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTrackCount).toBe(1)
+ assertBlackVideoTrack(0, 720, 540, STOPPED)
+ })
+
+ test('sets input track as its output track when setting another enabled input track', () => {
+ const inputTrack = newMediaStreamTrackMock('input')
+ const inputTrack2 = newMediaStreamTrackMock('input2')
+
+ blackVideoEnforcer._setInputTrack('default', inputTrack)
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ expectedTrackEnabledStateInOutputTrackSetEvent = true
+
+ blackVideoEnforcer._setInputTrack('default', inputTrack2)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(1)
+ expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', inputTrack2)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTrackCount).toBe(0)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT * 5)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(0)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTrackCount).toBe(0)
+ })
+
+ test('sets black video track as its output track when setting another disabled input track', () => {
+ const inputTrack = newMediaStreamTrackMock('input')
+ const inputTrack2 = newMediaStreamTrackMock('input2')
+
+ inputTrack.enabled = false
+ blackVideoEnforcer._setInputTrack('default', inputTrack)
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ expectedTrackEnabledStateInOutputTrackSetEvent = true
+
+ inputTrack2.enabled = false
+ inputTrack2._width = 320
+ inputTrack2._height = 180
+ blackVideoEnforcer._setInputTrack('default', inputTrack2)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(1)
+ expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', blackVideoTracks[1])
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTrackCount).toBe(2)
+ assertBlackVideoTrack(0, 720, 540, STOPPED)
+ assertBlackVideoTrack(1, 320, 180)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT - 1)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(0)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+
+ jest.advanceTimersByTime(1)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(0)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(1)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', false)
+ expect(blackVideoTrackCount).toBe(2)
+ assertBlackVideoTrack(0, 720, 540, STOPPED)
+ assertBlackVideoTrack(1, 320, 180)
+ })
+
+ test('sets black video track as its output track when setting another now disabled input track', () => {
+ const inputTrack = newMediaStreamTrackMock('input')
+ const inputTrack2 = newMediaStreamTrackMock('input2')
+
+ blackVideoEnforcer._setInputTrack('default', inputTrack)
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ expectedTrackEnabledStateInOutputTrackSetEvent = true
+
+ inputTrack2.enabled = false
+ inputTrack2._width = 320
+ inputTrack2._height = 180
+ blackVideoEnforcer._setInputTrack('default', inputTrack2)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(1)
+ expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', blackVideoTracks[0])
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTrackCount).toBe(1)
+ assertBlackVideoTrack(0, 320, 180)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT - 1)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(0)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+
+ jest.advanceTimersByTime(1)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(0)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(1)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', false)
+ expect(blackVideoTrackCount).toBe(1)
+ assertBlackVideoTrack(0, 320, 180)
+ })
+
+ test('sets input track as its output track when setting another now enabled input track', () => {
+ const inputTrack = newMediaStreamTrackMock('input')
+ const inputTrack2 = newMediaStreamTrackMock('input2')
+
+ inputTrack.enabled = false
+ blackVideoEnforcer._setInputTrack('default', inputTrack)
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT / 2)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ expectedTrackEnabledStateInOutputTrackSetEvent = true
+
+ inputTrack2.enabled = true
+ blackVideoEnforcer._setInputTrack('default', inputTrack2)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(1)
+ expect(outputTrackSetHandler).toHaveBeenCalledWith(blackVideoEnforcer, 'default', inputTrack2)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTrackCount).toBe(1)
+ assertBlackVideoTrack(0, 720, 540, STOPPED)
+
+ outputTrackSetHandler.mockClear()
+ outputTrackEnabledHandler.mockClear()
+
+ jest.advanceTimersByTime(DISABLE_OR_REMOVE_TIMEOUT * 5)
+
+ expect(outputTrackSetHandler).toHaveBeenCalledTimes(0)
+ expect(outputTrackEnabledHandler).toHaveBeenCalledTimes(0)
+ expect(blackVideoTrackCount).toBe(1)
+ assertBlackVideoTrack(0, 720, 540, STOPPED)
+ })
+ })
+})
diff --git a/src/utils/webrtc/simplewebrtc/localmedia.js b/src/utils/webrtc/simplewebrtc/localmedia.js
index ab99bf681..7ae5df837 100644
--- a/src/utils/webrtc/simplewebrtc/localmedia.js
+++ b/src/utils/webrtc/simplewebrtc/localmedia.js
@@ -7,6 +7,7 @@ const mockconsole = require('mockconsole')
// Only mediaDevicesManager is used, but it can not be assigned here due to not
// being initialized yet.
const webrtcIndex = require('../index.js')
+const BlackVideoEnforcer = require('../../media/pipeline/BlackVideoEnforcer.js').default
const MediaDevicesSource = require('../../media/pipeline/MediaDevicesSource.js').default
const SpeakingMonitor = require('../../media/pipeline/SpeakingMonitor.js').default
const TrackConstrainer = require('../../media/pipeline/TrackConstrainer.js').default
@@ -39,6 +40,7 @@ function LocalMedia(opts) {
this._localMediaActive = false
this.localStreams = []
+ this.sentStreams = []
this.localScreens = []
if (!webrtcIndex.mediaDevicesManager.isSupported()) {
@@ -57,6 +59,8 @@ function LocalMedia(opts) {
this.emit('virtualBackgroundLoadFailed')
})
+ this._blackVideoEnforcer = new BlackVideoEnforcer()
+
this._speakingMonitor = new SpeakingMonitor()
this._speakingMonitor.on('speaking', () => {
this.emit('speaking')
@@ -78,6 +82,10 @@ function LocalMedia(opts) {
this._trackToStream.addInputTrackSlot('audio')
this._trackToStream.addInputTrackSlot('video')
+ this._trackToSentStream = new TrackToStream()
+ this._trackToSentStream.addInputTrackSlot('audio')
+ this._trackToSentStream.addInputTrackSlot('video')
+
this._handleStreamSetBound = this._handleStreamSet.bind(this)
this._handleTrackReplacedBound = this._handleTrackReplaced.bind(this)
this._handleTrackEnabledBound = this._handleTrackEnabled.bind(this)
@@ -87,12 +95,16 @@ function LocalMedia(opts) {
this._audioTrackEnabler.connectTrackSink('default', this._speakingMonitor)
this._audioTrackEnabler.connectTrackSink('default', this._trackToStream, 'audio')
+ this._audioTrackEnabler.connectTrackSink('default', this._trackToSentStream, 'audio')
this._videoTrackEnabler.connectTrackSink('default', this._videoTrackConstrainer)
this._videoTrackConstrainer.connectTrackSink('default', this._virtualBackground)
this._virtualBackground.connectTrackSink('default', this._trackToStream, 'video')
+ this._virtualBackground.connectTrackSink('default', this._blackVideoEnforcer, 'default')
+
+ this._blackVideoEnforcer.connectTrackSink('default', this._trackToSentStream, 'video')
}
util.inherits(LocalMedia, WildEmitter)
@@ -166,6 +178,7 @@ LocalMedia.prototype.start = function(mediaConstraints, cb, context) {
this._mediaDevicesSource.start(retryNoVideoCallback).then(() => {
self.localStreams.push(self._trackToStream.getStream())
+ self.sentStreams.push(self._trackToSentStream.getStream())
self.emit('localStream', self._trackToStream.getStream())
@@ -173,6 +186,10 @@ LocalMedia.prototype.start = function(mediaConstraints, cb, context) {
self._trackToStream.on('trackReplaced', self._handleTrackReplacedBound)
self._trackToStream.on('trackEnabled', self._handleTrackEnabledBound)
+ self._trackToSentStream.on('streamSet', self._handleStreamSetBound)
+ self._trackToSentStream.on('trackReplaced', self._handleTrackReplacedBound)
+ self._trackToSentStream.on('trackEnabled', self._handleTrackEnabledBound)
+
self._localMediaActive = true
if (cb) {
@@ -190,6 +207,10 @@ LocalMedia.prototype.start = function(mediaConstraints, cb, context) {
self._trackToStream.on('trackReplaced', self._handleTrackReplacedBound)
self._trackToStream.on('trackEnabled', self._handleTrackEnabledBound)
+ self._trackToSentStream.on('streamSet', self._handleStreamSetBound)
+ self._trackToSentStream.on('trackReplaced', self._handleTrackReplacedBound)
+ self._trackToSentStream.on('trackEnabled', self._handleTrackEnabledBound)
+
self._localMediaActive = true
if (cb) {
@@ -204,7 +225,7 @@ LocalMedia.prototype._handleStreamSet = function(trackToStream, newStream, oldSt
}
if (newStream) {
- this.localStreams.push(newStream)
+ trackToStream === this._trackToStream ? this.localStreams.push(newStream) : this.sentStreams.push(newStream)
}
// "streamSet" is always emitted along with "trackReplaced", so the
@@ -212,16 +233,24 @@ LocalMedia.prototype._handleStreamSet = function(trackToStream, newStream, oldSt
}
LocalMedia.prototype._handleTrackReplaced = function(trackToStream, newTrack, oldTrack) {
- // "localStreamChanged" is expected to be emitted also when the tracks of
- // the stream change, even if the stream itself is the same.
- this.emit('localStreamChanged', trackToStream.getStream())
- this.emit('localTrackReplaced', newTrack, oldTrack, trackToStream.getStream())
+ if (trackToStream === this._trackToStream) {
+ // "localStreamChanged" is expected to be emitted also when the tracks
+ // of the stream change, even if the stream itself is the same.
+ this.emit('localStreamChanged', trackToStream.getStream())
+ this.emit('localTrackReplaced', newTrack, oldTrack, trackToStream.getStream())
+ } else {
+ this.emit('sentTrackReplaced', newTrack, oldTrack, trackToStream.getStream())
+ }
}
LocalMedia.prototype._handleTrackEnabled = function(trackToStream, track) {
// MediaStreamTrack does not emit an event when the enabled property
// changes, so it needs to be explicitly notified.
- this.emit('localTrackEnabledChanged', track, trackToStream.getStream())
+ if (trackToStream === this._trackToStream) {
+ this.emit('localTrackEnabledChanged', track, trackToStream.getStream())
+ } else {
+ this.emit('sentTrackEnabledChanged', track, trackToStream.getStream())
+ }
}
LocalMedia.prototype.stop = function() {
@@ -231,6 +260,10 @@ LocalMedia.prototype.stop = function() {
this._trackToStream.off('trackReplaced', this._handleTrackReplacedBound)
this._trackToStream.off('trackEnabled', this._handleTrackEnabledBound)
+ this._trackToSentStream.off('streamSet', this._handleStreamSetBound)
+ this._trackToSentStream.off('trackReplaced', this._handleTrackReplacedBound)
+ this._trackToSentStream.off('trackEnabled', this._handleTrackEnabledBound)
+
this.stopStream()
this.stopScreenShare()
@@ -239,12 +272,16 @@ LocalMedia.prototype.stop = function() {
LocalMedia.prototype.stopStream = function() {
const stream = this._trackToStream.getStream()
+ const sentStream = this._trackToSentStream.getStream()
this._mediaDevicesSource.stop()
if (stream) {
this._removeStream(stream)
}
+ if (sentStream) {
+ this._removeStream(sentStream)
+ }
}
LocalMedia.prototype.startScreenShare = function(mode, constraints, cb) {
@@ -431,12 +468,22 @@ LocalMedia.prototype._removeStream = function(stream) {
if (idx > -1) {
this.localStreams.splice(idx, 1)
this.emit('localStreamStopped', stream)
- } else {
- idx = this.localScreens.indexOf(stream)
- if (idx > -1) {
- this.localScreens.splice(idx, 1)
- this.emit('localScreenStopped', stream)
- }
+
+ return
+ }
+
+ idx = this.sentStreams.indexOf(stream)
+ if (idx > -1) {
+ this.sentStreams.splice(idx, 1)
+ this.emit('sentStreamStopped', stream)
+
+ return
+ }
+
+ idx = this.localScreens.indexOf(stream)
+ if (idx > -1) {
+ this.localScreens.splice(idx, 1)
+ this.emit('localScreenStopped', stream)
}
}
diff --git a/src/utils/webrtc/simplewebrtc/peer.js b/src/utils/webrtc/simplewebrtc/peer.js
index edf7885ea..df67298fd 100644
--- a/src/utils/webrtc/simplewebrtc/peer.js
+++ b/src/utils/webrtc/simplewebrtc/peer.js
@@ -70,7 +70,7 @@ function Peer(options) {
if (sender.track) {
// The stream is not known, but it is only used when the
// track is added, so it can be ignored here.
- self.handleLocalTrackEnabledChanged(sender.track, null)
+ self.handleSentTrackEnabledChanged(sender.track, null)
}
})
@@ -122,7 +122,7 @@ function Peer(options) {
this.broadcaster = options.broadcaster
}
} else {
- this.parent.localStreams.forEach(function(stream) {
+ this.parent.sentStreams.forEach(function(stream) {
stream.getTracks().forEach(function(track) {
if (track.kind !== 'video' || self.sendVideoIfAvailable) {
self.pc.addTrack(track, stream)
@@ -130,13 +130,13 @@ function Peer(options) {
})
})
- this.handleLocalTrackReplacedBound = this.handleLocalTrackReplaced.bind(this)
+ this.handleSentTrackReplacedBound = this.handleSentTrackReplaced.bind(this)
// TODO What would happen if the track is replaced while the peer is
// still negotiating the offer and answer?
- this.parent.on('localTrackReplaced', this.handleLocalTrackReplacedBound)
+ this.parent.on('sentTrackReplaced', this.handleSentTrackReplacedBound)
- this.handleLocalTrackEnabledChangedBound = this.handleLocalTrackEnabledChanged.bind(this)
- this.parent.on('localTrackEnabledChanged', this.handleLocalTrackEnabledChangedBound)
+ this.handleSentTrackEnabledChangedBound = this.handleSentTrackEnabledChanged.bind(this)
+ this.parent.on('sentTrackEnabledChanged', this.handleSentTrackEnabledChangedBound)
}
}
@@ -675,13 +675,13 @@ Peer.prototype.end = function() {
}
this.pc.close()
this.handleStreamRemoved()
- this.parent.off('localTrackReplaced', this.handleLocalTrackReplacedBound)
- this.parent.off('localTrackEnabledChanged', this.handleLocalTrackEnabledChangedBound)
+ this.parent.off('sentTrackReplaced', this.handleSentTrackReplacedBound)
+ this.parent.off('sentTrackEnabledChanged', this.handleSentTrackEnabledChangedBound)
this.parent.emit('peerEnded', this)
}
-Peer.prototype.handleLocalTrackReplaced = function(newTrack, oldTrack, stream) {
+Peer.prototype.handleSentTrackReplaced = function(newTrack, oldTrack, stream) {
this._pendingReplaceTracksQueue.push({ newTrack, oldTrack, stream })
this._processPendingReplaceTracks()
@@ -864,14 +864,14 @@ Peer.prototype._replaceTrack = async function(newTrack, oldTrack, stream) {
return Promise.allSettled(replaceTrackPromises)
}
-Peer.prototype.handleLocalTrackEnabledChanged = function(track, stream) {
+Peer.prototype.handleSentTrackEnabledChanged = function(track, stream) {
const sender = this.pc.getSenders().find(sender => sender.track === track)
const stoppedSender = this.pc.getSenders().find(sender => sender.trackDisabled === track)
if (track.enabled && stoppedSender) {
- this.handleLocalTrackReplacedBound(track, track, stream)
+ this.handleSentTrackReplacedBound(track, track, stream)
} else if (!track.enabled && sender) {
- this.handleLocalTrackReplacedBound(track, track, stream)
+ this.handleSentTrackReplacedBound(track, track, stream)
}
}
diff --git a/src/views/AdminSettings.vue b/src/views/AdminSettings.vue
index 6f55acd09..cb570915b 100644
--- a/src/views/AdminSettings.vue
+++ b/src/views/AdminSettings.vue
@@ -64,3 +64,35 @@ export default {
},
}
</script>
+
+<style lang="scss" scoped>
+::v-deep {
+ input {
+ width: 300px;
+ vertical-align: middle;
+ }
+
+ select {
+ vertical-align: middle;
+ }
+
+ .icon-delete,
+ .icon-checkmark-color,
+ .icon-category-monitoring,
+ .icon-checkmark,
+ .icon-error {
+ display: inline-block;
+ width: 44px;
+ height: 44px;
+ vertical-align: middle;
+ }
+
+ .icon-checkmark-color.hidden {
+ display: none;
+ }
+
+ .error {
+ border-color: var(--color-error);
+ }
+}
+</style>
diff --git a/src/views/MainView.vue b/src/views/MainView.vue
index 18f4c8380..aad48cab3 100644
--- a/src/views/MainView.vue
+++ b/src/views/MainView.vue
@@ -70,6 +70,7 @@ export default {
</script>
<style lang="scss" scoped>
+@import '../assets/variables';
.main-view {
height: 100%;
diff --git a/src/views/RoomSelector.vue b/src/views/RoomSelector.vue
index 8274b8424..1657a0ae4 100644
--- a/src/views/RoomSelector.vue
+++ b/src/views/RoomSelector.vue
@@ -168,6 +168,10 @@ export default {
<style lang="scss" scoped>
+::v-deep .modal-container {
+ height: 700px;
+}
+
.talk-modal {
height: 80vh;
}