diff options
Diffstat (limited to 'src')
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; } |