diff options
Diffstat (limited to 'apps/user_status/src')
24 files changed, 745 insertions, 1006 deletions
diff --git a/apps/user_status/src/UserStatus.vue b/apps/user_status/src/UserStatus.vue index a4b7b9552f3..07d81aad95c 100644 --- a/apps/user_status/src/UserStatus.vue +++ b/apps/user_status/src/UserStatus.vue @@ -1,80 +1,70 @@ <!-- - - @copyright Copyright (c) 2020 Georg Ehrke <oc.list@georgehrke.com> - - @author Georg Ehrke <oc.list@georgehrke.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/>. - - - --> + - SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> <template> - <component :is="elementTag"> - <div class="user-status-menu-item"> - <!-- Username display --> - <a v-if="!inline" - class="user-status-menu-item__header" - :href="profilePageLink" - @click="loadProfilePage"> - <div class="user-status-menu-item__header-content"> - <div class="user-status-menu-item__header-content-displayname">{{ displayName }}</div> - <div v-if="!loadingProfilePage" class="user-status-menu-item__header-content-placeholder" /> - <div v-else class="icon-loading-small" /> - </div> - <div v-if="profileEnabled"> - {{ t('user_status', 'View profile') }} - </div> - </a> - - <!-- Status modal toggle --> - <toggle :is="inline ? 'button' : 'a'" - :class="{'user-status-menu-item__toggle--inline': inline}" - class="user-status-menu-item__toggle" - href="#" - @click.prevent.stop="openModal"> - <span :class="statusIcon" class="user-status-menu-item__toggle-icon" /> + <Fragment> + <NcListItem v-if="!inline" + class="user-status-menu-item" + compact + :name="visibleMessage" + @click.stop="openModal"> + <template #icon> + <NcUserStatusIcon class="user-status-icon" + :status="statusType" + aria-hidden="true" /> + </template> + </NcListItem> + + <div v-else> + <!-- Dashboard Status --> + <NcButton @click.stop="openModal"> + <template #icon> + <NcUserStatusIcon class="user-status-icon" + :status="statusType" + aria-hidden="true" /> + </template> {{ visibleMessage }} - </toggle> + </NcButton> </div> - <!-- Status management modal --> <SetStatusModal v-if="isModalOpen" + :inline="inline" @close="closeModal" /> - </component> + </Fragment> </template> <script> -import { generateUrl } from '@nextcloud/router' import { getCurrentUser } from '@nextcloud/auth' -import { loadState } from '@nextcloud/initial-state' import { subscribe, unsubscribe } from '@nextcloud/event-bus' +import { Fragment } from 'vue-frag' +import NcButton from '@nextcloud/vue/components/NcButton' +import NcListItem from '@nextcloud/vue/components/NcListItem' +import NcUserStatusIcon from '@nextcloud/vue/components/NcUserStatusIcon' import debounce from 'debounce' -import { sendHeartbeat } from './services/heartbeatService' -import OnlineStatusMixin from './mixins/OnlineStatusMixin' - -const { profileEnabled } = loadState('user_status', 'profileEnabled', false) +import { sendHeartbeat } from './services/heartbeatService.js' +import OnlineStatusMixin from './mixins/OnlineStatusMixin.js' export default { name: 'UserStatus', components: { - SetStatusModal: () => import(/* webpackChunkName: 'user-status-modal' */'./components/SetStatusModal'), + Fragment, + NcButton, + NcListItem, + NcUserStatusIcon, + SetStatusModal: () => import(/* webpackChunkName: 'user-status-modal' */'./components/SetStatusModal.vue'), }, mixins: [OnlineStatusMixin], props: { + /** + * Whether the component should be rendered as a Dashboard Status or a User Menu Entries + * true = Dashboard Status + * false = User Menu Entries + */ inline: { type: Boolean, default: false, @@ -83,44 +73,19 @@ export default { data() { return { - displayName: getCurrentUser().displayName, heartbeatInterval: null, isAway: false, isModalOpen: false, - loadingProfilePage: false, mouseMoveListener: null, - profileEnabled, setAwayTimeout: null, } }, - computed: { - elementTag() { - return this.inline ? 'div' : 'li' - }, - /** - * The profile page link - * - * @return {string | null} - */ - profilePageLink() { - if (this.profileEnabled) { - return generateUrl('/u/{userId}', { userId: getCurrentUser().uid }) - } - // Since an anchor element is used rather than a button, - // this hack removes href if the profile is disabled so that disabling pointer-events is not needed to prevent a click from opening a page - // and to allow the hover event for styling - return null - }, - }, /** * Loads the current user's status from initial state * and stores it in Vuex */ mounted() { - subscribe('settings:display-name:updated', this.handleDisplayNameUpdate) - subscribe('settings:profile-enabled:updated', this.handleProfileEnabledUpdate) - this.$store.dispatch('loadStatusFromInitialState') if (OC.config.session_keepalive) { @@ -157,28 +122,12 @@ export default { * Some housekeeping before destroying the component */ beforeDestroy() { - unsubscribe('settings:display-name:updated', this.handleDisplayNameUpdate) - unsubscribe('settings:profile-enabled:updated', this.handleProfileEnabledUpdate) window.removeEventListener('mouseMove', this.mouseMoveListener) clearInterval(this.heartbeatInterval) unsubscribe('user_status:status.updated', this.handleUserStatusUpdated) }, methods: { - handleDisplayNameUpdate(displayName) { - this.displayName = displayName - }, - - handleProfileEnabledUpdate(profileEnabled) { - this.profileEnabled = profileEnabled - }, - - loadProfilePage() { - if (this.profileEnabled) { - this.loadingProfilePage = true - } - }, - /** * Opens the modal to set a custom status */ @@ -211,7 +160,7 @@ export default { } }, handleUserStatusUpdated(state) { - if (OC.getCurrentUser().uid === state.userId) { + if (getCurrentUser()?.uid === state.userId) { this.$store.dispatch('setStatusFromObject', { status: state.status, icon: state.icon, @@ -224,97 +173,12 @@ export default { </script> <style lang="scss" scoped> -.user-status-menu-item { - &__header { - display: flex !important; - flex-direction: column !important; - width: auto !important; - height: 44px * 1.5 !important; - padding: 10px 12px 5px 12px !important; - align-items: flex-start !important; - color: var(--color-main-text) !important; - - &:focus-visible { - padding: 6px 8px 1px 8px !important; - margin: 2px !important; - } - - &:not([href]) { - height: var(--header-menu-item-height) !important; - color: var(--color-text-maxcontrast) !important; - cursor: default !important; - - & * { - cursor: default !important; - } - - &:hover { - background-color: transparent !important; - } - } - - &-content { - display: inline-flex !important; - font-weight: bold !important; - gap: 0 10px !important; - width: auto; - - &-displayname { - width: auto; - } - - &-placeholder { - width: 16px !important; - height: 24px !important; - margin-right: 10px !important; - visibility: hidden !important; - } - } - - span { - color: var(--color-text-maxcontrast) !important; - } - } - - &__toggle { - &-icon { - width: 16px; - height: 16px; - margin-right: 10px; - opacity: 1 !important; - background-size: 16px; - vertical-align: middle !important; - } - - // In dashboard - &--inline { - width: auto; - min-width: 44px; - height: 44px; - margin: 0; - border: 0; - border-radius: var(--border-radius-pill); - background-color: var(--color-main-background-blur); - font-size: inherit; - font-weight: normal; - - -webkit-backdrop-filter: var(--background-blur); - backdrop-filter: var(--background-blur); - - &:active, - &:hover, - &:focus { - background-color: var(--color-background-hover); - } - &:focus { - box-shadow: 0 0 0 2px var(--color-main-text) !important; - } - } - } +.user-status-icon { + width: 20px; + height: 20px; + margin: calc((var(--default-clickable-area) - 20px) / 2); // 20px icon size + opacity: 1 !important; + background-size: 20px; + vertical-align: middle !important; } - -li { - list-style-type: none; -} - </style> diff --git a/apps/user_status/src/components/ClearAtSelect.vue b/apps/user_status/src/components/ClearAtSelect.vue index 620b72a3a41..91b816dc04a 100644 --- a/apps/user_status/src/components/ClearAtSelect.vue +++ b/apps/user_status/src/components/ClearAtSelect.vue @@ -1,47 +1,33 @@ <!-- - - @copyright Copyright (c) 2020 Georg Ehrke <oc.list@georgehrke.com> - - @author Georg Ehrke <oc.list@georgehrke.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/>. - - - --> + - SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> <template> <div class="clear-at-select"> <label class="clear-at-select__label" for="clearStatus"> {{ $t('user_status', 'Clear status after') }} </label> - <NcMultiselect id="clearStatus" - label="label" - :value="option" + <NcSelect input-id="clearStatus" + class="clear-at-select__select" :options="options" - open-direction="top" - @select="select" /> + :value="option" + :clearable="false" + placement="top" + label-outside + @option:selected="select" /> </div> </template> <script> -import NcMultiselect from '@nextcloud/vue/dist/Components/NcMultiselect' -import { getAllClearAtOptions } from '../services/clearAtOptionsService' -import { clearAtFilter } from '../filters/clearAtFilter' +import NcSelect from '@nextcloud/vue/components/NcSelect' +import { getAllClearAtOptions } from '../services/clearAtOptionsService.js' +import { clearAtFilter } from '../filters/clearAtFilter.js' export default { name: 'ClearAtSelect', components: { - NcMultiselect, + NcSelect, }, props: { clearAt: { @@ -87,16 +73,13 @@ export default { <style lang="scss" scoped> .clear-at-select { display: flex; - margin-bottom: 10px; + gap: calc(2 * var(--default-grid-baseline)); align-items: center; + margin-block: 0 calc(2 * var(--default-grid-baseline)); - &__label { - margin-right: 10px; - } - - .multiselect { + &__select { flex-grow: 1; - min-width: 130px; + min-width: 215px; } } </style> diff --git a/apps/user_status/src/components/CustomMessageInput.vue b/apps/user_status/src/components/CustomMessageInput.vue index 4fd649dc166..fb129281430 100644 --- a/apps/user_status/src/components/CustomMessageInput.vue +++ b/apps/user_status/src/components/CustomMessageInput.vue @@ -1,56 +1,40 @@ <!-- - - @copyright Copyright (c) 2020 Georg Ehrke <oc.list@georgehrke.com> - - @author Georg Ehrke <oc.list@georgehrke.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/>. - - - --> + - SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> <template> - <div class="custom-input"> + <div class="custom-input" role="group"> <NcEmojiPicker container=".custom-input" @select="setIcon"> - <NcButton class="custom-input__emoji-button" type="tertiary"> - {{ visibleIcon }} + <NcButton type="tertiary" + :aria-label="t('user_status', 'Emoji for your status message')"> + <template #icon> + {{ visibleIcon }} + </template> </NcButton> </NcEmojiPicker> <div class="custom-input__container"> - <label class="hidden-visually" for="user_status_message"> - {{ t('user_status', 'What is your status?') }} - </label> - <input id="user_status_message" - ref="input" + <NcTextField ref="input" maxlength="80" :disabled="disabled" - :placeholder="$t('user_status', 'What is your status?')" - type="text" + :placeholder="t('user_status', 'What is your status?')" :value="message" - @change="onChange" - @keyup="onKeyup" - @paste="onKeyup"> + type="text" + :label="t('user_status', 'What is your status?')" + @input="onChange" /> </div> </div> </template> <script> -import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' -import NcEmojiPicker from '@nextcloud/vue/dist/Components/NcEmojiPicker.js' +import NcButton from '@nextcloud/vue/components/NcButton' +import NcEmojiPicker from '@nextcloud/vue/components/NcEmojiPicker' +import NcTextField from '@nextcloud/vue/components/NcTextField' export default { name: 'CustomMessageInput', components: { + NcTextField, NcButton, NcEmojiPicker, }, @@ -73,8 +57,7 @@ export default { emits: [ 'change', - 'submit', - 'icon-selected', + 'select-icon', ], computed: { @@ -98,12 +81,8 @@ export default { * * @param {Event} event The Change Event */ - onKeyup(event) { - this.$emit('change', event.target.value) - }, - onChange(event) { - this.$emit('submit', event.target.value) + this.$emit('change', event.target.value) }, setIcon(icon) { @@ -116,28 +95,12 @@ export default { <style lang="scss" scoped> .custom-input { display: flex; + align-items: flex-end; + gap: var(--default-grid-baseline); width: 100%; - &__emoji-button { - min-height: 36px; - padding: 0; - border: 2px solid var(--color-border-maxcontrast); - border-right: none; - border-radius: var(--border-radius) 0 0 var(--border-radius); - - &:hover { - border-color: var(--color-primary-element); - } - } - &__container { width: 100%; - - input { - width: 100%; - margin: 0; - border-radius: 0 var(--border-radius) var(--border-radius) 0; - } } } </style> diff --git a/apps/user_status/src/components/OnlineStatusSelect.vue b/apps/user_status/src/components/OnlineStatusSelect.vue index 52b6022fb44..0abcc8d68e6 100644 --- a/apps/user_status/src/components/OnlineStatusSelect.vue +++ b/apps/user_status/src/components/OnlineStatusSelect.vue @@ -1,53 +1,40 @@ <!-- - - @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com> - - - - @author John Molakvoæ <skjnldsv@protonmail.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/>. - - - --> + - SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> <template> <div class="user-status-online-select"> <input :id="id" :checked="checked" - class="user-status-online-select__input" + class="hidden-visually user-status-online-select__input" type="radio" name="user-status-online" @change="onChange"> <label :for="id" class="user-status-online-select__label"> + <NcUserStatusIcon :status="type" + class="user-status-online-select__icon" + aria-hidden="true" /> {{ label }} - <span :class="icon" role="img" /> <em class="user-status-online-select__subline">{{ subline }}</em> </label> </div> </template> <script> +import NcUserStatusIcon from '@nextcloud/vue/components/NcUserStatusIcon' + export default { name: 'OnlineStatusSelect', + components: { + NcUserStatusIcon, + }, + props: { checked: { type: Boolean, default: false, }, - icon: { - type: String, - required: true, - }, type: { type: String, required: true, @@ -77,56 +64,42 @@ export default { </script> <style lang="scss" scoped> -@use 'sass:math'; -$icon-size: 24px; -$label-padding: 8px; - .user-status-online-select { - // Inputs are here for keyboard navigation, they are not visually visible - &__input { - position: absolute; - top: auto; - left: -10000px; - overflow: hidden; - width: 1px; - height: 1px; - } - &__label { - position: relative; - display: block; - margin: $label-padding; - padding: $label-padding; - padding-left: $icon-size + $label-padding * 2; - border: 2px solid var(--color-main-background); + box-sizing: inherit; + display: grid; + grid-template-columns: var(--default-clickable-area) 1fr 2fr; + align-items: center; + gap: var(--default-grid-baseline); + min-height: var(--default-clickable-area); + padding: var(--default-grid-baseline); border-radius: var(--border-radius-large); background-color: var(--color-background-hover); - background-position: $label-padding center; - background-size: $icon-size; - span, - & { + &, & * { cursor: pointer; } - span { - position: absolute; - top: calc(50% - math.div($icon-size, 2)); - left: $label-padding; - display: block; - width: $icon-size; - height: $icon-size; + &:hover { + background-color: var(--color-background-dark); } } - &__input:checked + &__label, - &__input:focus + &__label, - &__label:hover { - border-color: var(--color-primary); + &__icon { + flex-shrink: 0; + max-width: 34px; + max-height: 100%; } - &__label:active { - border-color: var(--color-border-dark); + &__input:checked + &__label { + outline: 2px solid var(--color-main-text); + background-color: var(--color-background-dark); + box-shadow: 0 0 0 4px var(--color-main-background); + } + + &__input:focus-visible + &__label { + outline: 2px solid var(--color-primary-element) !important; + background-color: var(--color-background-dark); } &__subline { @@ -134,5 +107,4 @@ $label-padding: 8px; color: var(--color-text-lighter); } } - </style> diff --git a/apps/user_status/src/components/PredefinedStatus.vue b/apps/user_status/src/components/PredefinedStatus.vue index bca94b41a76..b12892d4add 100644 --- a/apps/user_status/src/components/PredefinedStatus.vue +++ b/apps/user_status/src/components/PredefinedStatus.vue @@ -1,43 +1,31 @@ <!-- - - @copyright Copyright (c) 2020 Georg Ehrke <oc.list@georgehrke.com> - - @author Georg Ehrke <oc.list@georgehrke.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/>. - - - --> + - SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> <template> - <div class="predefined-status" - tabindex="0" - @keyup.enter="select" - @keyup.space="select" - @click="select"> - <span class="predefined-status__icon"> - {{ icon }} - </span> - <span class="predefined-status__message"> - {{ message }} - </span> - <span class="predefined-status__clear-at"> - {{ clearAt | clearAtFilter }} - </span> - </div> + <li class="predefined-status"> + <input :id="id" + class="hidden-visually predefined-status__input" + type="radio" + name="predefined-status" + :checked="selected" + @change="select"> + <label class="predefined-status__label" :for="id"> + <span aria-hidden="true" class="predefined-status__label--icon"> + {{ icon }} + </span> + <span class="predefined-status__label--message"> + {{ message }} + </span> + <span class="predefined-status__label--clear-at"> + {{ clearAt | clearAtFilter }} + </span> + </label> + </li> </template> <script> -import { clearAtFilter } from '../filters/clearAtFilter' +import { clearAtFilter } from '../filters/clearAtFilter.js' export default { name: 'PredefinedStatus', @@ -62,6 +50,16 @@ export default { required: false, default: null, }, + selected: { + type: Boolean, + required: false, + default: false, + }, + }, + computed: { + id() { + return `user-status-predefined-status-${this.messageId}` + }, }, methods: { /** @@ -76,39 +74,55 @@ export default { <style lang="scss" scoped> .predefined-status { - display: flex; - flex-wrap: nowrap; - justify-content: flex-start; - flex-basis: 100%; - border-radius: var(--border-radius); - align-items: center; - min-height: 44px; + &__label { + display: flex; + flex-wrap: nowrap; + justify-content: flex-start; + flex-basis: 100%; + border-radius: var(--border-radius); + align-items: center; + min-height: var(--default-clickable-area); + padding-inline: var(--default-grid-baseline); - &:hover, - &:focus { - background-color: var(--color-background-hover); - } + &, & * { + cursor: pointer; + } - &:active{ - background-color: var(--color-background-dark); - } + &:hover { + background-color: var(--color-background-dark); + } - &__icon { - flex-basis: 40px; - text-align: center; - } + &--icon { + flex-basis: var(--default-clickable-area); + text-align: center; + } - &__message { - font-weight: bold; - padding: 0 6px; - } + &--message { + font-weight: bold; + padding: 0 6px; + } - &__clear-at { - opacity: .7; + &--clear-at { + color: var(--color-text-maxcontrast); - &::before { - content: ' – '; + &::before { + content: ' – '; + } } } + + &__input:checked + &__label, + &__label:active { + outline: 2px solid var(--color-main-text); + box-shadow: 0 0 0 4px var(--color-main-background); + background-color: var(--color-background-dark); + border-radius: var(--border-radius-large); + } + + &__input:focus-visible + &__label { + outline: 2px solid var(--color-primary-element) !important; + background-color: var(--color-background-dark); + border-radius: var(--border-radius-large); + } } </style> diff --git a/apps/user_status/src/components/PredefinedStatusesList.vue b/apps/user_status/src/components/PredefinedStatusesList.vue index cff03289715..cdf359dce76 100644 --- a/apps/user_status/src/components/PredefinedStatusesList.vue +++ b/apps/user_status/src/components/PredefinedStatusesList.vue @@ -1,35 +1,21 @@ <!-- - - @copyright Copyright (c) 2020 Georg Ehrke <oc.list@georgehrke.com> - - @author Georg Ehrke <oc.list@georgehrke.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/>. - - - --> + - SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> <template> - <div v-if="hasLoaded" - class="predefined-statuses-list"> + <ul v-if="statusesHaveLoaded" + class="predefined-statuses-list" + :aria-label="t('user_status', 'Predefined statuses')"> <PredefinedStatus v-for="status in predefinedStatuses" :key="status.id" :message-id="status.id" :icon="status.icon" :message="status.message" :clear-at="status.clearAt" + :selected="lastSelected === status.id" @select="selectStatus(status)" /> - </div> + </ul> <div v-else class="predefined-statuses-list"> <div class="icon icon-loading-small" /> @@ -37,32 +23,41 @@ </template> <script> -import PredefinedStatus from './PredefinedStatus' -import { mapState } from 'vuex' +import PredefinedStatus from './PredefinedStatus.vue' +import { mapGetters, mapState } from 'vuex' export default { name: 'PredefinedStatusesList', components: { PredefinedStatus, }, + data() { + return { + lastSelected: null, + } + }, computed: { ...mapState({ predefinedStatuses: state => state.predefinedStatuses.predefinedStatuses, + messageId: state => state.userStatus.messageId, }), - /** - * Indicator whether the predefined statuses have already been loaded - * - * @return {boolean} - */ - hasLoaded() { - return this.predefinedStatuses.length > 0 - }, + ...mapGetters(['statusesHaveLoaded']), }, + + watch: { + messageId: { + immediate: true, + handler() { + this.lastSelected = this.messageId + }, + }, + }, + /** * Loads all predefined statuses from the server * when this component is mounted */ - mounted() { + created() { this.$store.dispatch('loadAllPredefinedStatuses') }, methods: { @@ -72,6 +67,7 @@ export default { * @param {object} status The selected status */ selectStatus(status) { + this.lastSelected = status.id this.$emit('select-status', status) }, }, @@ -82,6 +78,7 @@ export default { .predefined-statuses-list { display: flex; flex-direction: column; - margin-bottom: 10px; + gap: var(--default-grid-baseline); + margin-block: 0 calc(2 * var(--default-grid-baseline)); } </style> diff --git a/apps/user_status/src/components/PreviousStatus.vue b/apps/user_status/src/components/PreviousStatus.vue new file mode 100644 index 00000000000..58d6ebd294b --- /dev/null +++ b/apps/user_status/src/components/PreviousStatus.vue @@ -0,0 +1,106 @@ +<!-- + - SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> +<template> + <div class="predefined-status backup-status" + tabindex="0" + @keyup.enter="select" + @keyup.space="select" + @click="select"> + <span class="predefined-status__icon"> + {{ icon }} + </span> + <span class="predefined-status__message"> + {{ message }} + </span> + <span class="predefined-status__clear-at"> + {{ $t('user_status', 'Previously set') }} + </span> + + <div class="backup-status__reset-button"> + <NcButton @click="select"> + {{ $t('user_status', 'Reset status') }} + </NcButton> + </div> + </div> +</template> + +<script> +import NcButton from '@nextcloud/vue/components/NcButton' + +export default { + name: 'PreviousStatus', + + components: { + NcButton, + }, + + props: { + icon: { + type: [String, null], + required: true, + }, + message: { + type: String, + required: true, + }, + }, + methods: { + /** + * Emits an event when the user clicks the row + */ + select() { + this.$emit('select') + }, + }, +} +</script> + +<style lang="scss" scoped> +.predefined-status { + display: flex; + flex-wrap: nowrap; + justify-content: flex-start; + flex-basis: 100%; + border-radius: var(--border-radius); + align-items: center; + min-height: var(--default-clickable-area); + padding-inline: var(--default-grid-baseline); + + &:hover, + &:focus { + background-color: var(--color-background-hover); + } + + &:active{ + background-color: var(--color-background-dark); + } + + &__icon { + flex-basis: var(--default-clickable-area); + text-align: center; + } + + &__message { + font-weight: bold; + padding: 0 6px; + } + + &__clear-at { + color: var(--color-text-maxcontrast); + + &::before { + content: ' – '; + } + } +} + +.backup-status { + &__reset-button { + justify-content: flex-end; + display: flex; + flex-grow: 1; + } +} +</style> diff --git a/apps/user_status/src/components/SetStatusModal.vue b/apps/user_status/src/components/SetStatusModal.vue index d7adc99da14..8624ed19e94 100644 --- a/apps/user_status/src/components/SetStatusModal.vue +++ b/apps/user_status/src/components/SetStatusModal.vue @@ -1,34 +1,22 @@ <!-- - - @copyright Copyright (c) 2020 Georg Ehrke <oc.list@georgehrke.com> - - @author Georg Ehrke <oc.list@georgehrke.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/>. - - - --> + - SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> <template> <NcModal size="normal" - :title="$t('user_status', 'Set status')" + label-id="user_status-set-dialog" + dark + :set-return-focus="setReturnFocus" @close="closeModal"> <div class="set-status-modal"> <!-- Status selector --> - <div class="set-status-modal__header"> - <h2>{{ $t('user_status', 'Online status') }}</h2> - </div> - <div class="set-status-modal__online-status"> + <h2 id="user_status-set-dialog" class="set-status-modal__header"> + {{ $t('user_status', 'Online status') }} + </h2> + <div class="set-status-modal__online-status" + role="radiogroup" + :aria-label="$t('user_status', 'Online status')"> <OnlineStatusSelect v-for="status in statuses" :key="status.type" v-bind="status" @@ -36,48 +24,66 @@ @select="changeStatus" /> </div> - <!-- Status message --> - <div class="set-status-modal__header"> - <h2>{{ $t('user_status', 'Status message') }}</h2> - </div> - <div class="set-status-modal__custom-input"> - <CustomMessageInput ref="customMessageInput" - :icon="icon" - :message="message" - @change="setMessage" - @submit="saveStatus" - @select-icon="setIcon" /> - </div> - <PredefinedStatusesList @select-status="selectPredefinedMessage" /> - <ClearAtSelect :clear-at="clearAt" - @select-clear-at="setClearAt" /> - <div class="status-buttons"> - <NcButton :wide="true" - type="tertiary" - :text="$t('user_status', 'Clear status message')" - :disabled="isSavingStatus" - @click="clearStatus"> - {{ $t('user_status', 'Clear status message') }} - </NcButton> - <NcButton :wide="true" - type="primary" - :text="$t('user_status', 'Set status message')" - :disabled="isSavingStatus" - @click="saveStatus"> - {{ $t('user_status', 'Set status message') }} - </NcButton> - </div> + <!-- Status message form --> + <form @submit.prevent="saveStatus" @reset="clearStatus"> + <h3 class="set-status-modal__header"> + {{ $t('user_status', 'Status message') }} + </h3> + <div class="set-status-modal__custom-input"> + <CustomMessageInput ref="customMessageInput" + :icon="icon" + :message="editedMessage" + @change="setMessage" + @select-icon="setIcon" /> + <NcButton v-if="messageId === 'vacationing'" + :href="absencePageUrl" + target="_blank" + type="secondary" + :aria-label="$t('user_status', 'Set absence period')"> + {{ $t('user_status', 'Set absence period and replacement') + ' ↗' }} + </NcButton> + </div> + <div v-if="hasBackupStatus" + class="set-status-modal__automation-hint"> + {{ $t('user_status', 'Your status was set automatically') }} + </div> + <PreviousStatus v-if="hasBackupStatus" + :icon="backupIcon" + :message="backupMessage" + @select="revertBackupFromServer" /> + <PredefinedStatusesList @select-status="selectPredefinedMessage" /> + <ClearAtSelect :clear-at="clearAt" + @select-clear-at="setClearAt" /> + <div class="status-buttons"> + <NcButton :wide="true" + type="tertiary" + native-type="reset" + :aria-label="$t('user_status', 'Clear status message')" + :disabled="isSavingStatus"> + {{ $t('user_status', 'Clear status message') }} + </NcButton> + <NcButton :wide="true" + type="primary" + native-type="submit" + :aria-label="$t('user_status', 'Set status message')" + :disabled="isSavingStatus"> + {{ $t('user_status', 'Set status message') }} + </NcButton> + </div> + </form> </div> </NcModal> </template> <script> import { showError } from '@nextcloud/dialogs' -import NcModal from '@nextcloud/vue/dist/Components/NcModal' -import NcButton from '@nextcloud/vue/dist/Components/NcButton' +import { generateUrl } from '@nextcloud/router' +import NcModal from '@nextcloud/vue/components/NcModal' +import NcButton from '@nextcloud/vue/components/NcButton' import { getAllStatusOptions } from '../services/statusOptionsService.js' import OnlineStatusMixin from '../mixins/OnlineStatusMixin.js' import PredefinedStatusesList from './PredefinedStatusesList.vue' +import PreviousStatus from './PreviousStatus.vue' import CustomMessageInput from './CustomMessageInput.vue' import ClearAtSelect from './ClearAtSelect.vue' import OnlineStatusSelect from './OnlineStatusSelect.vue' @@ -91,29 +97,100 @@ export default { NcModal, OnlineStatusSelect, PredefinedStatusesList, + PreviousStatus, NcButton, }, mixins: [OnlineStatusMixin], + props: { + /** + * Whether the component should be rendered as a Dashboard Status or a User Menu Entries + * true = Dashboard Status + * false = User Menu Entries + */ + inline: { + type: Boolean, + default: false, + }, + }, + data() { return { clearAt: null, - icon: null, - message: '', - messageId: '', + editedMessage: '', + predefinedMessageId: null, isSavingStatus: false, statuses: getAllStatusOptions(), } }, + computed: { + messageId() { + return this.$store.state.userStatus.messageId + }, + icon() { + return this.$store.state.userStatus.icon + }, + message() { + return this.$store.state.userStatus.message || '' + }, + hasBackupStatus() { + return this.messageId && (this.backupIcon || this.backupMessage) + }, + backupIcon() { + return this.$store.state.userBackupStatus.icon || '' + }, + backupMessage() { + return this.$store.state.userBackupStatus.message || '' + }, + + absencePageUrl() { + return generateUrl('settings/user/availability#absence') + }, + + resetButtonText() { + if (this.backupIcon && this.backupMessage) { + return this.$t('user_status', 'Reset status to "{icon} {message}"', { + icon: this.backupIcon, + message: this.backupMessage, + }) + } else if (this.backupMessage) { + return this.$t('user_status', 'Reset status to "{message}"', { + message: this.backupMessage, + }) + } else if (this.backupIcon) { + return this.$t('user_status', 'Reset status to "{icon}"', { + icon: this.backupIcon, + }) + } + + return this.$t('user_status', 'Reset status') + }, + + setReturnFocus() { + if (this.inline) { + return undefined + } + return document.querySelector('[aria-controls="header-menu-user-menu"]') ?? undefined + }, + }, + + watch: { + message: { + immediate: true, + handler(newValue) { + this.editedMessage = newValue + }, + }, + }, + /** * Loads the current status when a user opens dialog */ mounted() { - this.messageId = this.$store.state.userStatus.messageId - this.icon = this.$store.state.userStatus.icon - this.message = this.$store.state.userStatus.message || '' + this.$store.dispatch('fetchBackupFromServer') + this.predefinedMessageId = this.$store.state.userStatus.messageId if (this.$store.state.userStatus.clearAt !== null) { this.clearAt = { type: '_time', @@ -134,8 +211,12 @@ export default { * @param {string} icon The new icon */ setIcon(icon) { - this.messageId = null - this.icon = icon + this.predefinedMessageId = null + this.$store.dispatch('setCustomMessage', { + message: this.message, + icon, + clearAt: this.clearAt, + }) this.$nextTick(() => { this.$refs.customMessageInput.focus() }) @@ -146,8 +227,8 @@ export default { * @param {string} message The new message */ setMessage(message) { - this.messageId = null - this.message = message + this.predefinedMessageId = null + this.editedMessage = message }, /** * Sets a new clearAt value @@ -163,10 +244,12 @@ export default { * @param {object} status The predefined status object */ selectPredefinedMessage(status) { - this.messageId = status.id + this.predefinedMessageId = status.id this.clearAt = status.clearAt - this.icon = status.icon - this.message = status.message + this.$store.dispatch('setPredefinedMessage', { + messageId: status.id, + clearAt: status.clearAt, + }) }, /** * Saves the status and closes the @@ -181,15 +264,15 @@ export default { try { this.isSavingStatus = true - if (this.messageId !== undefined && this.messageId !== null) { - await this.$store.dispatch('setPredefinedMessage', { - messageId: this.messageId, + if (this.predefinedMessageId === null) { + await this.$store.dispatch('setCustomMessage', { + message: this.editedMessage, + icon: this.icon, clearAt: this.clearAt, }) } else { - await this.$store.dispatch('setCustomMessage', { - message: this.message, - icon: this.icon, + this.$store.dispatch('setPredefinedMessage', { + messageId: this.predefinedMessageId, clearAt: this.clearAt, }) } @@ -220,8 +303,30 @@ export default { } this.isSavingStatus = false + this.predefinedMessageId = null this.closeModal() }, + /** + * + * @return {Promise<void>} + */ + async revertBackupFromServer() { + try { + this.isSavingStatus = true + + await this.$store.dispatch('revertBackupFromServer', { + messageId: this.messageId, + }) + } catch (err) { + showError(this.$t('user_status', 'There was an error reverting the status')) + console.debug(err) + this.isSavingStatus = false + return + } + + this.isSavingStatus = false + this.predefinedMessageId = this.$store.state.userStatus?.messageId + }, }, } </script> @@ -231,27 +336,48 @@ export default { .set-status-modal { padding: 8px 20px 20px 20px; + &, & * { + box-sizing: border-box; + } + &__header { + font-size: 21px; text-align: center; - font-weight: bold; - margin: 15px 0; + height: fit-content; + min-height: var(--default-clickable-area); + line-height: var(--default-clickable-area); + overflow-wrap: break-word; + margin-block: 0 calc(2 * var(--default-grid-baseline)); } &__online-status { - display: grid; - grid-template-columns: 1fr 1fr; + display: flex; + flex-direction: column; + gap: calc(2 * var(--default-grid-baseline)); + margin-block: 0 calc(2 * var(--default-grid-baseline)); } &__custom-input { display: flex; + flex-direction: column; + align-items: center; + gap: var(--default-grid-baseline); + width: 100%; + padding-inline-start: var(--default-grid-baseline); + margin-block: 0 calc(2 * var(--default-grid-baseline)); + } + + &__automation-hint { + display: flex; width: 100%; - margin-bottom: 10px; + margin-block: 0 calc(2 * var(--default-grid-baseline)); + color: var(--color-text-maxcontrast); } .status-buttons { display: flex; padding: 3px; - padding-left:0; + padding-inline-start:0; gap: 3px; } } diff --git a/apps/user_status/src/dashboard.js b/apps/user_status/src/dashboard.js deleted file mode 100644 index 9c3d94151d0..00000000000 --- a/apps/user_status/src/dashboard.js +++ /dev/null @@ -1,44 +0,0 @@ -/** - * @copyright Copyright (c) 2020 Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.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 Vue from 'vue' -import { getRequestToken } from '@nextcloud/auth' -import { translate, translatePlural } from '@nextcloud/l10n' -import Dashboard from './views/Dashboard' - -// eslint-disable-next-line camelcase -__webpack_nonce__ = btoa(getRequestToken()) - -Vue.prototype.t = translate -Vue.prototype.n = translatePlural -Vue.prototype.OC = OC -Vue.prototype.OCA = OCA - -document.addEventListener('DOMContentLoaded', function() { - OCA.Dashboard.register('user_status', (el) => { - const View = Vue.extend(Dashboard) - new View({ - propsData: {}, - }).$mount(el) - }) - -}) diff --git a/apps/user_status/src/filters/clearAtFilter.js b/apps/user_status/src/filters/clearAtFilter.js index 9a99b9ec69f..5f62385a978 100644 --- a/apps/user_status/src/filters/clearAtFilter.js +++ b/apps/user_status/src/filters/clearAtFilter.js @@ -1,28 +1,11 @@ /** - * @copyright Copyright (c) 2020 Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.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/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ import { translate as t } from '@nextcloud/l10n' import moment from '@nextcloud/moment' -import { dateFactory } from '../services/dateService' +import { dateFactory } from '../services/dateService.js' /** * Formats a clearAt object to be human readable diff --git a/apps/user_status/src/menu.js b/apps/user_status/src/menu.js index 66010102f10..34e5e6eabb1 100644 --- a/apps/user_status/src/menu.js +++ b/apps/user_status/src/menu.js @@ -1,65 +1,38 @@ /** - * @copyright Copyright (c) 2020 Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Julius Härtl <jus@bitgrid.net> - * - * @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/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ +import { getCSPNonce } from '@nextcloud/auth' +import { subscribe } from '@nextcloud/event-bus' import Vue from 'vue' -import { getRequestToken } from '@nextcloud/auth' -import UserStatus from './UserStatus' -import store from './store' -import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar' -import { loadState } from '@nextcloud/initial-state' + +import UserStatus from './UserStatus.vue' +import store from './store/index.js' // eslint-disable-next-line camelcase -__webpack_nonce__ = btoa(getRequestToken()) +__webpack_nonce__ = getCSPNonce() Vue.prototype.t = t Vue.prototype.$t = t -const avatarDiv = document.getElementById('avatardiv-menu') -const userStatusData = loadState('user_status', 'status') -const propsData = { - preloadedUserStatus: { - message: userStatusData.message, - icon: userStatusData.icon, - status: userStatusData.status, - }, - user: avatarDiv.dataset.user, - displayName: avatarDiv.dataset.displayname, - disableMenu: true, - disableTooltip: true, -} +const mountPoint = document.getElementById('user_status-menu-entry') -const NcAvatarInMenu = Vue.extend(NcAvatar) -new NcAvatarInMenu({ propsData }).$mount('#avatardiv-menu') +const mountMenuEntry = () => { + const mountPoint = document.getElementById('user_status-menu-entry') + // eslint-disable-next-line no-new + new Vue({ + el: mountPoint, + render: h => h(UserStatus), + store, + }) +} -// Register settings menu entry -export default new Vue({ - el: 'li[data-id="user_status-menuitem"]', - // eslint-disable-next-line vue/match-component-file-name - name: 'UserStatusRoot', - render: h => h(UserStatus), - store, -}) +if (mountPoint) { + mountMenuEntry() +} else { + subscribe('core:user-menu:mounted', mountMenuEntry) +} // Register dashboard status document.addEventListener('DOMContentLoaded', function() { diff --git a/apps/user_status/src/mixins/OnlineStatusMixin.js b/apps/user_status/src/mixins/OnlineStatusMixin.js index d1e3a9111fa..5670eb4dc06 100644 --- a/apps/user_status/src/mixins/OnlineStatusMixin.js +++ b/apps/user_status/src/mixins/OnlineStatusMixin.js @@ -1,23 +1,6 @@ /** - * @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com> - * - * @author John Molakvoæ <skjnldsv@protonmail.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/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ import { mapState } from 'vuex' @@ -52,6 +35,7 @@ export default { return this.$t('user_status', 'Online') case 'away': + case 'busy': return this.$t('user_status', 'Away') case 'dnd': @@ -67,30 +51,6 @@ export default { return this.$t('user_status', 'Set status') }, - - /** - * The status indicator icon - * - * @return {string | null} - */ - statusIcon() { - switch (this.statusType) { - case 'online': - return 'icon-user-status-online' - - case 'away': - return 'icon-user-status-away' - - case 'dnd': - return 'icon-user-status-dnd' - - case 'invisible': - case 'offline': - return 'icon-user-status-invisible' - } - - return '' - }, }, methods: { diff --git a/apps/user_status/src/services/clearAtOptionsService.js b/apps/user_status/src/services/clearAtOptionsService.js index 2849e6170f2..af0059bfb7f 100644 --- a/apps/user_status/src/services/clearAtOptionsService.js +++ b/apps/user_status/src/services/clearAtOptionsService.js @@ -1,23 +1,6 @@ /** - * @copyright Copyright (c) 2020 Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.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/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ import { translate as t } from '@nextcloud/l10n' diff --git a/apps/user_status/src/services/clearAtService.js b/apps/user_status/src/services/clearAtService.js index a02905b90c9..f23d267ad02 100644 --- a/apps/user_status/src/services/clearAtService.js +++ b/apps/user_status/src/services/clearAtService.js @@ -1,28 +1,11 @@ /** - * @copyright Copyright (c) 2020 Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.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/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ import { dateFactory, -} from './dateService' +} from './dateService.js' import moment from '@nextcloud/moment' /** diff --git a/apps/user_status/src/services/dateService.js b/apps/user_status/src/services/dateService.js index 2d110db5998..26a61d4a3e2 100644 --- a/apps/user_status/src/services/dateService.js +++ b/apps/user_status/src/services/dateService.js @@ -1,23 +1,6 @@ /** - * @copyright Copyright (c) 2020 Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.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/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ const dateFactory = () => { diff --git a/apps/user_status/src/services/heartbeatService.js b/apps/user_status/src/services/heartbeatService.js index 6a033f7789b..fda1a1ffc9f 100644 --- a/apps/user_status/src/services/heartbeatService.js +++ b/apps/user_status/src/services/heartbeatService.js @@ -1,23 +1,6 @@ /** - * @copyright Copyright (c) 2020 Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.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/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ import HttpClient from '@nextcloud/axios' diff --git a/apps/user_status/src/services/predefinedStatusService.js b/apps/user_status/src/services/predefinedStatusService.js index 0a4cb55b573..b423c6e0cc4 100644 --- a/apps/user_status/src/services/predefinedStatusService.js +++ b/apps/user_status/src/services/predefinedStatusService.js @@ -1,23 +1,6 @@ /** - * @copyright Copyright (c) 2020 Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.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/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ import HttpClient from '@nextcloud/axios' diff --git a/apps/user_status/src/services/statusOptionsService.js b/apps/user_status/src/services/statusOptionsService.js index 7bd50bf7aff..6c23645e5be 100644 --- a/apps/user_status/src/services/statusOptionsService.js +++ b/apps/user_status/src/services/statusOptionsService.js @@ -1,24 +1,6 @@ /** - * @copyright Copyright (c) 2020 Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Jan C. Borchardt <hey@jancborchardt.net> - * - * @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/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ import { translate as t } from '@nextcloud/l10n' @@ -32,22 +14,20 @@ const getAllStatusOptions = () => { return [{ type: 'online', label: t('user_status', 'Online'), - icon: 'icon-user-status-online', }, { type: 'away', label: t('user_status', 'Away'), - icon: 'icon-user-status-away', + }, { + type: 'busy', + label: t('user_status', 'Busy'), }, { type: 'dnd', label: t('user_status', 'Do not disturb'), subline: t('user_status', 'Mute all notifications'), - icon: 'icon-user-status-dnd', - }, { type: 'invisible', label: t('user_status', 'Invisible'), subline: t('user_status', 'Appear offline'), - icon: 'icon-user-status-invisible', }] } diff --git a/apps/user_status/src/services/statusService.js b/apps/user_status/src/services/statusService.js index f4bda930303..6504411c996 100644 --- a/apps/user_status/src/services/statusService.js +++ b/apps/user_status/src/services/statusService.js @@ -1,23 +1,6 @@ /** - * @copyright Copyright (c) 2020 Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.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/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ import HttpClient from '@nextcloud/axios' @@ -36,6 +19,19 @@ const fetchCurrentStatus = async () => { } /** + * Fetches the current user-status + * + * @param {string} userId Id of the user to fetch the status + * @return {Promise<object>} + */ +const fetchBackupStatus = async (userId) => { + const url = generateOcsUrl('apps/user_status/api/v1/statuses/{userId}', { userId: '_' + userId }) + const response = await HttpClient.get(url) + + return response.data.ocs.data +} + +/** * Sets the status * * @param {string} statusType The status (online / away / dnd / invisible) @@ -90,10 +86,25 @@ const clearMessage = async () => { await HttpClient.delete(url) } +/** + * Revert the automated status + * + * @param {string} messageId ID of the message to revert + * @return {Promise<object>} + */ +const revertToBackupStatus = async (messageId) => { + const url = generateOcsUrl('apps/user_status/api/v1/user_status/revert/{messageId}', { messageId }) + const response = await HttpClient.delete(url) + + return response.data.ocs.data +} + export { fetchCurrentStatus, + fetchBackupStatus, setStatus, setCustomMessage, setPredefinedMessage, clearMessage, + revertToBackupStatus, } diff --git a/apps/user_status/src/store/index.js b/apps/user_status/src/store/index.js index c2c270d14d8..d9cfe674165 100644 --- a/apps/user_status/src/store/index.js +++ b/apps/user_status/src/store/index.js @@ -1,29 +1,13 @@ /** - * @copyright Copyright (c) 2020 Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.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/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ import Vue from 'vue' import Vuex, { Store } from 'vuex' -import predefinedStatuses from './predefinedStatuses' -import userStatus from './userStatus' +import predefinedStatuses from './predefinedStatuses.js' +import userStatus from './userStatus.js' +import userBackupStatus from './userBackupStatus.js' Vue.use(Vuex) @@ -31,6 +15,7 @@ export default new Store({ modules: { predefinedStatuses, userStatus, + userBackupStatus, }, strict: true, }) diff --git a/apps/user_status/src/store/predefinedStatuses.js b/apps/user_status/src/store/predefinedStatuses.js index e41ba24f7dd..6d592ca627e 100644 --- a/apps/user_status/src/store/predefinedStatuses.js +++ b/apps/user_status/src/store/predefinedStatuses.js @@ -1,26 +1,9 @@ /** - * @copyright Copyright (c) 2020 Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.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/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ -import { fetchAllPredefinedStatuses } from '../services/predefinedStatusService' +import { fetchAllPredefinedStatuses } from '../services/predefinedStatusService.js' const state = { predefinedStatuses: [], @@ -35,11 +18,15 @@ const mutations = { * @param {object} status The status to add */ addPredefinedStatus(state, status) { - state.predefinedStatuses.push(status) + state.predefinedStatuses = [...state.predefinedStatuses, status] }, } -const getters = {} +const getters = { + statusesHaveLoaded(state) { + return state.predefinedStatuses.length > 0 + }, +} const actions = { diff --git a/apps/user_status/src/store/userBackupStatus.js b/apps/user_status/src/store/userBackupStatus.js new file mode 100644 index 00000000000..78e5318de9d --- /dev/null +++ b/apps/user_status/src/store/userBackupStatus.js @@ -0,0 +1,102 @@ +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { + fetchBackupStatus, + revertToBackupStatus, +} from '../services/statusService.js' +import { getCurrentUser } from '@nextcloud/auth' +import { emit } from '@nextcloud/event-bus' + +const state = { + // Status (online / away / dnd / invisible / offline) + status: null, + // Whether the status is user-defined + statusIsUserDefined: null, + // A custom message set by the user + message: null, + // The icon selected by the user + icon: null, + // When to automatically clean the status + clearAt: null, + // Whether the message is predefined + // (and can automatically be translated by Nextcloud) + messageIsPredefined: null, + // The id of the message in case it's predefined + messageId: null, +} + +const mutations = { + /** + * Loads the status from initial state + * + * @param {object} state The Vuex state + * @param {object} data The destructuring object + * @param {string} data.status The status type + * @param {boolean} data.statusIsUserDefined Whether or not this status is user-defined + * @param {string} data.message The message + * @param {string} data.icon The icon + * @param {number} data.clearAt When to automatically clear the status + * @param {boolean} data.messageIsPredefined Whether or not the message is predefined + * @param {string} data.messageId The id of the predefined message + */ + loadBackupStatusFromServer(state, { status, statusIsUserDefined, message, icon, clearAt, messageIsPredefined, messageId }) { + state.status = status + state.message = message + state.icon = icon + + // Don't overwrite certain values if the refreshing comes in via short updates + // E.g. from talk participant list which only has the status, message and icon + if (typeof statusIsUserDefined !== 'undefined') { + state.statusIsUserDefined = statusIsUserDefined + } + if (typeof clearAt !== 'undefined') { + state.clearAt = clearAt + } + if (typeof messageIsPredefined !== 'undefined') { + state.messageIsPredefined = messageIsPredefined + } + if (typeof messageId !== 'undefined') { + state.messageId = messageId + } + }, +} + +const getters = {} + +const actions = { + /** + * Re-fetches the status from the server + * + * @param {object} vuex The Vuex destructuring object + * @param {Function} vuex.commit The Vuex commit function + * @return {Promise<void>} + */ + async fetchBackupFromServer({ commit }) { + try { + const status = await fetchBackupStatus(getCurrentUser()?.uid) + commit('loadBackupStatusFromServer', status) + } catch (e) { + // Ignore missing user backup status + } + }, + + async revertBackupFromServer({ commit }, { messageId }) { + const status = await revertToBackupStatus(messageId) + if (status) { + commit('loadBackupStatusFromServer', {}) + commit('loadStatusFromServer', status) + emit('user_status:status.updated', { + status: status.status, + message: status.message, + icon: status.icon, + clearAt: status.clearAt, + userId: getCurrentUser()?.uid, + }) + } + }, +} + +export default { state, mutations, getters, actions } diff --git a/apps/user_status/src/store/userStatus.js b/apps/user_status/src/store/userStatus.js index 6d8b5bd1e1f..9bc86ab5062 100644 --- a/apps/user_status/src/store/userStatus.js +++ b/apps/user_status/src/store/userStatus.js @@ -1,23 +1,6 @@ /** - * @copyright Copyright (c) 2020 Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.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/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ import { @@ -26,16 +9,16 @@ import { setPredefinedMessage, setCustomMessage, clearMessage, -} from '../services/statusService' +} from '../services/statusService.js' import { loadState } from '@nextcloud/initial-state' import { getCurrentUser } from '@nextcloud/auth' -import { getTimestampForClearAt } from '../services/clearAtService' +import { getTimestampForClearAt } from '../services/clearAtService.js' import { emit } from '@nextcloud/event-bus' const state = { // Status (online / away / dnd / invisible / offline) status: null, - // Whether or not the status is user-defined + // Whether the status is user-defined statusIsUserDefined: null, // A custom message set by the user message: null, @@ -43,7 +26,7 @@ const state = { icon: null, // When to automatically clean the status clearAt: null, - // Whether or not the message is predefined + // Whether the message is predefined // (and can automatically be translated by Nextcloud) messageIsPredefined: null, // The id of the message in case it's predefined diff --git a/apps/user_status/src/views/Dashboard.vue b/apps/user_status/src/views/Dashboard.vue deleted file mode 100644 index ef5a9832ebc..00000000000 --- a/apps/user_status/src/views/Dashboard.vue +++ /dev/null @@ -1,121 +0,0 @@ -<!-- - - @copyright Copyright (c) 2020 Georg Ehrke <oc.list@georgehrke.com> - - @author Georg Ehrke <oc.list@georgehrke.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> - <NcDashboardWidget id="user-status_panel" - :items="items" - :loading="loading" - :empty-content-message="t('user_status', 'No recent status changes')"> - <template #default="{ item }"> - <NcDashboardWidgetItem :main-text="item.mainText" - :sub-text="item.subText"> - <template #avatar> - <NcAvatar class="item-avatar" - :size="44" - :user="item.avatarUsername" - :display-name="item.mainText" - :show-user-status="false" - :show-user-status-compact="false" /> - </template> - </NcDashboardWidgetItem> - </template> - <template #emptyContentIcon> - <div class="icon-user-status-dark" /> - </template> - </NcDashboardWidget> -</template> - -<script> -import { loadState } from '@nextcloud/initial-state' -import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar' -import NcDashboardWidget from '@nextcloud/vue/dist/Components/NcDashboardWidget' -import NcDashboardWidgetItem from '@nextcloud/vue/dist/Components/NcDashboardWidgetItem' -import moment from '@nextcloud/moment' - -export default { - name: 'Dashboard', - components: { - NcAvatar, - NcDashboardWidget, - NcDashboardWidgetItem, - }, - data() { - return { - statuses: [], - loading: true, - } - }, - computed: { - items() { - return this.statuses.map((item) => { - const icon = item.icon || '' - let message = item.message || '' - if (message === '') { - if (item.status === 'away') { - message = t('user_status', 'Away') - } - if (item.status === 'dnd') { - message = t('user_status', 'Do not disturb') - } - } - const status = item.icon !== '' ? `${icon} ${message}` : message - - let subText - if (item.icon === null && message === '' && item.timestamp === null) { - subText = '' - } else if (item.icon === null && message === '' && item.timestamp !== null) { - subText = moment(item.timestamp, 'X').fromNow() - } else if (item.timestamp !== null) { - subText = this.t('user_status', '{status}, {timestamp}', { - status, - timestamp: moment(item.timestamp, 'X').fromNow(), - }, null, { escape: false, sanitize: false }) - } else { - subText = status - } - - return { - mainText: item.displayName, - subText, - avatarUsername: item.userId, - } - }) - }, - }, - mounted() { - try { - this.statuses = loadState('user_status', 'dashboard_data') - this.loading = false - } catch (e) { - console.error(e) - } - }, -} -</script> - -<style lang="scss"> -.icon-user-status-dark { - width: 64px; - height: 64px; - background-size: 64px; - filter: var(--background-invert-if-dark); -} -</style> |