diff options
author | John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com> | 2020-10-01 22:10:10 +0200 |
---|---|---|
committer | John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com> | 2020-10-01 22:19:05 +0200 |
commit | f240794f2b9990e6a56b6dd7067c219e7fe1d591 (patch) | |
tree | 0074ff88a0e57231d5ec9e22d55018b3f697376c /apps/user_status/src | |
parent | 5451dcbec504c3b0a9b57c0794e71868a9b6254e (diff) | |
download | nextcloud-server-f240794f2b9990e6a56b6dd7067c219e7fe1d591.tar.gz nextcloud-server-f240794f2b9990e6a56b6dd7067c219e7fe1d591.zip |
Move online status into modal
Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
Diffstat (limited to 'apps/user_status/src')
-rw-r--r-- | apps/user_status/src/UserStatus.vue (renamed from apps/user_status/src/App.vue) | 217 | ||||
-rw-r--r-- | apps/user_status/src/components/OnlineStatusSelect.vue | 103 | ||||
-rw-r--r-- | apps/user_status/src/components/SetStatusModal.vue | 54 | ||||
-rw-r--r-- | apps/user_status/src/main-user-status-menu.js | 17 | ||||
-rw-r--r-- | apps/user_status/src/mixins/OnlineStatusMixin.js | 110 |
5 files changed, 331 insertions, 170 deletions
diff --git a/apps/user_status/src/App.vue b/apps/user_status/src/UserStatus.vue index ff927505bae..51761582595 100644 --- a/apps/user_status/src/App.vue +++ b/apps/user_status/src/UserStatus.vue @@ -20,70 +20,59 @@ --> <template> - <li :class="{ inline }"> - <div id="user-status-menu-item"> + <li> + <div class="user-status-menu-item"> + <!-- Username display --> <span v-if="!inline" - id="user-status-menu-item__header" + class="user-status-menu-item__header" :title="displayName"> {{ displayName }} </span> - <Actions - id="user-status-menu-item__subheader" - :default-icon="statusIcon" - container="header" - :menu-title="visibleMessage" - :title="visibleMessage"> - <ActionButton - v-for="status in statuses" - :key="status.type" - :icon="status.icon" - :close-after-click="true" - :title="status.label" - @click.prevent.stop="changeStatus(status.type)"> - {{ status.subline }} - </ActionButton> - <ActionButton - icon="icon-rename" - :close-after-click="true" - :title="$t('user_status', 'Set status message')" - @click.prevent.stop="openModal" /> - </Actions> - <SetStatusModal - v-if="isModalOpen" - @close="closeModal" /> + + <!-- 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" /> + {{ visibleMessage }} + </toggle> </div> + + <!-- Status management modal --> + <SetStatusModal + v-if="isModalOpen" + @close="closeModal" /> </li> </template> <script> import { getCurrentUser } from '@nextcloud/auth' -import SetStatusModal from './components/SetStatusModal' -import Actions from '@nextcloud/vue/dist/Components/Actions' -import ActionButton from '@nextcloud/vue/dist/Components/ActionButton' -import { mapState } from 'vuex' -import { showError } from '@nextcloud/dialogs' -import { getAllStatusOptions } from './services/statusOptionsService' -import { sendHeartbeat } from './services/heartbeatService' import debounce from 'debounce' +import { sendHeartbeat } from './services/heartbeatService' +import OnlineStatusMixin from './mixins/OnlineStatusMixin' + export default { - name: 'App', + name: 'UserStatus', + components: { - Actions, - ActionButton, - SetStatusModal, + SetStatusModal: () => import(/* webpackChunkName: 'user-status-modal' */'./components/SetStatusModal'), }, + mixins: [OnlineStatusMixin], + props: { inline: { type: Boolean, default: false, }, }, + data() { return { isModalOpen: false, - statuses: getAllStatusOptions(), heartbeatInterval: null, setAwayTimeout: null, mouseMoveListener: null, @@ -91,12 +80,6 @@ export default { } }, computed: { - ...mapState({ - statusType: state => state.userStatus.status, - statusIsUserDefined: state => state.userStatus.statusIsUserDefined, - customIcon: state => state.userStatus.icon, - customMessage: state => state.userStatus.message, - }), /** * The display-name of the current user * @@ -105,64 +88,8 @@ export default { displayName() { return getCurrentUser().displayName }, - /** - * The message displayed in the top right corner - * - * @returns {String} - */ - visibleMessage() { - if (this.customIcon && this.customMessage) { - return `${this.customIcon} ${this.customMessage}` - } - if (this.customMessage) { - return this.customMessage - } - - if (this.statusIsUserDefined) { - switch (this.statusType) { - case 'online': - return this.$t('user_status', 'Online') - - case 'away': - return this.$t('user_status', 'Away') - - case 'dnd': - return this.$t('user_status', 'Do not disturb') - - case 'invisible': - return this.$t('user_status', 'Invisible') - - case 'offline': - return this.$t('user_status', 'Offline') - } - } - - return this.$t('user_status', 'Set status') - }, - /** - * The status indicator icon - * - * @returns {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 '' - }, }, + /** * Loads the current user's status from initial state * and stores it in Vuex @@ -198,6 +125,7 @@ export default { this._backgroundHeartbeat() } }, + /** * Some housekeeping before destroying the component */ @@ -205,6 +133,7 @@ export default { window.removeEventListener('mouseMove', this.mouseMoveListener) clearInterval(this.heartbeatInterval) }, + methods: { /** * Opens the modal to set a custom status @@ -218,19 +147,7 @@ export default { closeModal() { this.isModalOpen = false }, - /** - * Changes the user-status - * - * @param {String} statusType (online / away / dnd / invisible) - */ - async changeStatus(statusType) { - try { - await this.$store.dispatch('setStatus', { statusType }) - } catch (err) { - showError(this.$t('user_status', 'There was an error saving the new status')) - console.debug(err) - } - }, + /** * Sends the status heartbeat to the server * @@ -248,65 +165,55 @@ export default { <style lang="scss"> $max-width-user-status: 200px; -li:not(.inline) #user-status-menu-item { +.user-status-menu-item { &__header { display: block; + overflow: hidden; box-sizing: border-box; - color: var(--color-text-maxcontrast); + max-width: $max-width-user-status; padding: 10px 12px 5px 38px; - opacity: 1; - white-space: nowrap; text-align: left; - max-width: $max-width-user-status; - overflow: hidden; + white-space: nowrap; text-overflow: ellipsis; + opacity: 1; + color: var(--color-text-maxcontrast); } - &__subheader { - width: 100%; - - button.action-item__menutoggle { - display: block; - box-sizing: border-box; - background-color: var(--color-main-background); - background-position: 12px center; + &__toggle { + &-icon { + width: 16px; + height: 16px; + margin-right: 10px; + opacity: 1 !important; background-size: 16px; + } + + // In dashboard + &--inline { + width: auto; + min-width: 44px; + height: 44px; + margin: 0; border: 0; - border-radius: 0; + border-radius: var(--border-radius-pill); + background-color: var(--color-background-translucent); + font-size: inherit; font-weight: normal; - padding-left: 38px; - opacity: 1; - max-width: $max-width-user-status; - overflow: hidden; - text-overflow: ellipsis; + -webkit-backdrop-filter: var(--background-blur); + backdrop-filter: var(--background-blur); + + &:active, &:hover, &:focus { - box-shadow: inset 4px 0 var(--color-primary-element); + background-color: var(--color-background-hover); } } } } -.inline #user-status-menu-item__subheader { - width: 100%; - - button.action-item__menutoggle { - background-size: 16px; - border: 0; - border-radius: var(--border-radius-pill); - font-weight: normal; - padding-left: 40px; - - &.icon-loading-small { - &::after { - left: 21px; - } - } - } +li { + list-style-type: none; } - li { - list-style-type: none; - } </style> diff --git a/apps/user_status/src/components/OnlineStatusSelect.vue b/apps/user_status/src/components/OnlineStatusSelect.vue new file mode 100644 index 00000000000..b03af75681e --- /dev/null +++ b/apps/user_status/src/components/OnlineStatusSelect.vue @@ -0,0 +1,103 @@ +<!-- + - @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/>. + - + --> +<template> + <div class="user-status-online-select"> + <input :id="id" + :checked="checked" + class="user-status-online-select__input" + type="radio" + name="user-status-online" + @change="onChange"> + <label :for="id" :class="icon" class="user-status-online-select__label"> + <slot /> + </label> + </div> +</template> + +<script> +export default { + name: 'OnlineStatusSelect', + + props: { + checked: { + type: Boolean, + default: false, + }, + icon: { + type: String, + required: true, + }, + type: { + type: String, + required: true, + }, + }, + + computed: { + id() { + return `user-status-online-status-${this.type}` + }, + }, + + methods: { + onChange() { + this.$emit('select', this.type) + }, + }, +} +</script> + +<style lang="scss" scoped> +$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 { + display: block; + margin: $label-padding; + padding: $label-padding; + padding-left: $icon-size + $label-padding * 2; + border: 2px solid var(--color-main-background); + border-radius: var(--border-radius-large); + background-color: var(--color-background-hover); + background-position: $label-padding center; + background-size: $icon-size; + } + + &__input:checked + &__label, + &__input:focus + &__label, + &__label:hover { + border-color: var(--color-primary); + } +} + +</style> diff --git a/apps/user_status/src/components/SetStatusModal.vue b/apps/user_status/src/components/SetStatusModal.vue index c0652ce990b..96223ba7e01 100644 --- a/apps/user_status/src/components/SetStatusModal.vue +++ b/apps/user_status/src/components/SetStatusModal.vue @@ -22,11 +22,26 @@ <template> <Modal size="normal" - :title="$t('user_status', 'Set status message')" + :title="$t('user_status', 'Set status')" @close="closeModal"> <div class="set-status-modal"> + <!-- Status selector --> <div class="set-status-modal__header"> - <h3>{{ $t('user_status', 'Set status message') }}</h3> + <h3>{{ $t('user_status', 'Online status') }}</h3> + </div> + <div class="set-status-modal__online-status"> + <OnlineStatusSelect v-for="status in statuses" + :key="status.type" + v-bind="status" + :checked="status.type === statusType" + @select="changeStatus"> + {{ status.label }} + </OnlineStatusSelect> + </div> + + <!-- Status message --> + <div class="set-status-modal__header"> + <h3>{{ $t('user_status', 'Status message') }}</h3> </div> <div class="set-status-modal__custom-input"> <EmojiPicker @select="setIcon"> @@ -57,27 +72,36 @@ </template> <script> +import { showError } from '@nextcloud/dialogs' import EmojiPicker from '@nextcloud/vue/dist/Components/EmojiPicker' import Modal from '@nextcloud/vue/dist/Components/Modal' + +import { getAllStatusOptions } from '../services/statusOptionsService' +import OnlineStatusMixin from '../mixins/OnlineStatusMixin' import PredefinedStatusesList from './PredefinedStatusesList' import CustomMessageInput from './CustomMessageInput' import ClearAtSelect from './ClearAtSelect' -import { showError } from '@nextcloud/dialogs' +import OnlineStatusSelect from './OnlineStatusSelect' export default { name: 'SetStatusModal', + components: { + ClearAtSelect, + CustomMessageInput, EmojiPicker, Modal, - CustomMessageInput, + OnlineStatusSelect, PredefinedStatusesList, - ClearAtSelect, }, + mixins: [OnlineStatusMixin], + data() { return { + clearAt: null, icon: null, message: null, - clearAt: null, + statuses: getAllStatusOptions(), } }, computed: { @@ -90,6 +114,7 @@ export default { return this.icon || '😀' }, }, + /** * Loads the current status when a user opens dialog */ @@ -209,6 +234,18 @@ export default { min-height: 200px; padding: 8px 20px 20px 20px; + &__header { + text-align: center; + font-weight: bold; + } + + &__online-status { + display: grid; + // Space between the two sections + margin-bottom: 40px; + grid-template-columns: 1fr 1fr; + } + &__custom-input { display: flex; width: 100%; @@ -216,12 +253,12 @@ export default { .custom-input__emoji-button { flex-basis: 40px; - width: 40px; flex-grow: 0; - border-radius: var(--border-radius) 0 0 var(--border-radius); + width: 40px; height: 34px; margin-right: 0; border-right: none; + border-radius: var(--border-radius) 0 0 var(--border-radius); } } @@ -233,4 +270,5 @@ export default { } } } + </style> diff --git a/apps/user_status/src/main-user-status-menu.js b/apps/user_status/src/main-user-status-menu.js index 322585c3f0c..12fda36e85b 100644 --- a/apps/user_status/src/main-user-status-menu.js +++ b/apps/user_status/src/main-user-status-menu.js @@ -21,7 +21,7 @@ */ import Vue from 'vue' import { getRequestToken } from '@nextcloud/auth' -import App from './App' +import UserStatus from './UserStatus' import store from './store' // eslint-disable-next-line camelcase @@ -36,18 +36,23 @@ __webpack_public_path__ = OC.linkTo('user_status', 'js/') Vue.prototype.t = t Vue.prototype.$t = t -const app = new Vue({ - render: h => h(App), +// 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, -}).$mount('li[data-id="user_status-menuitem"]') +}) +// Register dashboard status document.addEventListener('DOMContentLoaded', function() { if (!OCA.Dashboard) { return } OCA.Dashboard.registerStatus('status', (el) => { - const Dashboard = Vue.extend(App) + const Dashboard = Vue.extend(UserStatus) return new Dashboard({ propsData: { inline: true, @@ -56,5 +61,3 @@ document.addEventListener('DOMContentLoaded', function() { }).$mount(el) }) }) - -export { app } diff --git a/apps/user_status/src/mixins/OnlineStatusMixin.js b/apps/user_status/src/mixins/OnlineStatusMixin.js new file mode 100644 index 00000000000..ceba40f05e7 --- /dev/null +++ b/apps/user_status/src/mixins/OnlineStatusMixin.js @@ -0,0 +1,110 @@ +/** + * @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/>. + * + */ +import { mapState } from 'vuex' +import { showError } from '@nextcloud/dialogs' + +export default { + computed: { + ...mapState({ + statusType: state => state.userStatus.status, + statusIsUserDefined: state => state.userStatus.statusIsUserDefined, + customIcon: state => state.userStatus.icon, + customMessage: state => state.userStatus.message, + }), + + /** + * The message displayed in the top right corner + * + * @returns {String} + */ + visibleMessage() { + if (this.customIcon && this.customMessage) { + return `${this.customIcon} ${this.customMessage}` + } + + if (this.customMessage) { + return this.customMessage + } + + if (this.statusIsUserDefined) { + switch (this.statusType) { + case 'online': + return this.$t('user_status', 'Online') + + case 'away': + return this.$t('user_status', 'Away') + + case 'dnd': + return this.$t('user_status', 'Do not disturb') + + case 'invisible': + return this.$t('user_status', 'Invisible') + + case 'offline': + return this.$t('user_status', 'Offline') + } + } + + return this.$t('user_status', 'Set status') + }, + + /** + * The status indicator icon + * + * @returns {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: { + /** + * Changes the user-status + * + * @param {String} statusType (online / away / dnd / invisible) + */ + async changeStatus(statusType) { + try { + await this.$store.dispatch('setStatus', { statusType }) + } catch (err) { + showError(this.$t('user_status', 'There was an error saving the new status')) + console.debug(err) + } + }, + }, +} |