diff options
author | Georg Ehrke <developer@georgehrke.com> | 2020-06-02 12:48:37 +0200 |
---|---|---|
committer | Georg Ehrke <developer@georgehrke.com> | 2020-07-31 16:45:27 +0200 |
commit | 0fad921840eb801492522af6ef795231163cff20 (patch) | |
tree | ddab0d1567d81eeb8d956ec98196180ad296cabd /apps/user_status/src | |
parent | fce6df06e2bd1d68ee5614621ae7f92c6f7fa53d (diff) | |
download | nextcloud-server-0fad921840eb801492522af6ef795231163cff20.tar.gz nextcloud-server-0fad921840eb801492522af6ef795231163cff20.zip |
Add user-status app
Signed-off-by: Georg Ehrke <developer@georgehrke.com>
Diffstat (limited to 'apps/user_status/src')
18 files changed, 1691 insertions, 0 deletions
diff --git a/apps/user_status/src/App.vue b/apps/user_status/src/App.vue new file mode 100644 index 00000000000..e8c3021c7ec --- /dev/null +++ b/apps/user_status/src/App.vue @@ -0,0 +1,271 @@ +<!-- + - @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> + <li> + <div id="user-status-menu-item"> + <span id="user-status-menu-item__header">{{ displayName }}</span> + <Actions + id="user-status-menu-item__subheader" + :default-icon="statusIcon" + :menu-title="visibleMessage"> + <ActionButton + v-for="status in statuses" + :key="status.type" + :icon="status.icon" + :close-after-click="true" + @click.prevent.stop="changeStatus(status.type)"> + {{ status.label }} + </ActionButton> + <ActionButton + icon="icon-rename" + :close-after-click="true" + @click.prevent.stop="openModal"> + {{ $t('user_status', 'Set custom status') }} + </ActionButton> + </Actions> + <SetStatusModal + v-if="isModalOpen" + @close="closeModal" /> + </div> + </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' + +export default { + name: 'App', + components: { + Actions, + ActionButton, + SetStatusModal, + }, + data() { + return { + isModalOpen: false, + statuses: getAllStatusOptions(), + heartbeatInterval: null, + setAwayTimeout: null, + mouseMoveListener: null, + isAway: false, + } + }, + 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 + * + * @returns {String} + */ + 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 + */ + mounted() { + this.$store.dispatch('loadStatusFromInitialState') + + if (OC.config.session_keepalive) { + // Send the latest status to the server every 5 minutes + this.heartbeatInterval = setInterval(this._backgroundHeartbeat.bind(this), 1000 * 60 * 5) + this.setAwayTimeout = () => { + this.isAway = true + } + // Catch mouse movements, but debounce to once every 30 seconds + this.mouseMoveListener = debounce(() => { + const wasAway = this.isAway + this.isAway = false + // Reset the two minute counter + clearTimeout(this.setAwayTimeout) + // If the user did not move the mouse within two minutes, + // mark them as away + setTimeout(this.setAwayTimeout, 1000 * 60 * 2) + + if (wasAway) { + this._backgroundHeartbeat() + } + }, 1000 * 2, true) + window.addEventListener('mousemove', this.mouseMoveListener, { + capture: true, + passive: true, + }) + + this._backgroundHeartbeat() + } + }, + /** + * Some housekeeping before destroying the component + */ + beforeDestroy() { + window.removeEventListener('mouseMove', this.mouseMoveListener) + clearInterval(this.heartbeatInterval) + }, + methods: { + /** + * Opens the modal to set a custom status + */ + openModal() { + this.isModalOpen = true + }, + /** + * Closes the modal + */ + 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 + * + * @returns {Promise<void>} + * @private + */ + async _backgroundHeartbeat() { + await sendHeartbeat(this.isAway) + await this.$store.dispatch('reFetchStatusFromServer') + }, + }, +} +</script> + +<style lang="scss"> +#user-status-menu-item { + &__header { + display: block; + align-items: center; + color: var(--color-main-text); + padding: 10px 12px 5px 12px; + box-sizing: border-box; + opacity: 1; + white-space: nowrap; + width: 100%; + text-align: center; + max-width: 250px; + text-overflow: ellipsis; + min-width: 175px; + } + + &__subheader { + width: 100%; + + > button { + background-color: var(--color-main-background); + background-size: 16px; + border: 0; + border-radius: 0; + font-weight: normal; + font-size: 0.875em; + padding-left: 40px; + + &:hover, + &:focus { + box-shadow: inset 4px 0 var(--color-primary-element); + } + } + } +} +</style> diff --git a/apps/user_status/src/components/ClearAtSelect.vue b/apps/user_status/src/components/ClearAtSelect.vue new file mode 100644 index 00000000000..af0db698ad9 --- /dev/null +++ b/apps/user_status/src/components/ClearAtSelect.vue @@ -0,0 +1,102 @@ +<!-- + - @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> + <div class="clear-at-select"> + <span + class="clear-at-select__label"> + {{ $t('user_select', 'Clear status after') }} + </span> + <Multiselect + label="label" + :value="option" + :options="options" + open-direction="top" + @select="select" /> + </div> +</template> + +<script> +import Multiselect from '@nextcloud/vue/dist/Components/Multiselect' +import { getAllClearAtOptions } from '../services/clearAtOptionsService' +import { clearAtFilter } from '../filters/clearAtFilter' + +export default { + name: 'ClearAtSelect', + components: { + Multiselect, + }, + props: { + clearAt: { + type: Object, + default: null, + }, + }, + data() { + return { + options: getAllClearAtOptions(), + } + }, + computed: { + /** + * Returns an object of the currently selected option + * + * @returns {Object} + */ + option() { + return { + clearAt: this.clearAt, + label: clearAtFilter(this.clearAt), + } + }, + }, + methods: { + /** + * Triggered when the user selects a new option. + * + * @param {Object=} option The new selected option + */ + select(option) { + if (!option) { + return + } + + this.$emit('selectClearAt', option.clearAt) + }, + }, +} +</script> + +<style lang="scss" scoped> +.clear-at-select { + display: flex; + margin-bottom: 10px; + align-items: center; + + &__label { + margin-right: 10px; + } + + .multiselect { + flex-grow: 1; + } +} +</style> diff --git a/apps/user_status/src/components/CustomMessageInput.vue b/apps/user_status/src/components/CustomMessageInput.vue new file mode 100644 index 00000000000..04bc2f026da --- /dev/null +++ b/apps/user_status/src/components/CustomMessageInput.vue @@ -0,0 +1,65 @@ +<!-- + - @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> + <form + class="custom-input__form" + @submit.prevent> + <input + :placeholder="$t('user_status', 'What\'s your status?')" + type="text" + :value="message" + @change="change"> + </form> +</template> + +<script> +export default { + name: 'CustomMessageInput', + props: { + message: { + type: String, + required: true, + default: () => '', + }, + }, + methods: { + /** + * Notifies the parent component about a changed input + * + * @param {Event} event The Change Event + */ + change(event) { + this.$emit('change', event.target.value) + }, + }, +} +</script> + +<style lang="scss" scoped> +.custom-input__form { + flex-grow: 1; + + input { + width: 100%; + border-radius: 0 var(--border-radius) var(--border-radius) 0; + } +} +</style> diff --git a/apps/user_status/src/components/PredefinedStatus.vue b/apps/user_status/src/components/PredefinedStatus.vue new file mode 100644 index 00000000000..c7fd4d63fed --- /dev/null +++ b/apps/user_status/src/components/PredefinedStatus.vue @@ -0,0 +1,111 @@ +<!-- + - @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> + <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> +</template> + +<script> +import { clearAtFilter } from '../filters/clearAtFilter' + +export default { + name: 'PredefinedStatus', + filters: { + clearAtFilter, + }, + props: { + messageId: { + type: String, + required: true, + }, + icon: { + type: String, + required: true, + }, + message: { + type: String, + required: true, + }, + clearAt: { + type: Object, + required: false, + default: null, + }, + }, + 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: 44px; + + &:hover, + &:focus { + background-color: var(--color-background-hover); + } + + &__icon { + flex-basis: 40px; + text-align: center; + } + + &__message { + font-weight: bold; + padding: 0 6px; + } + + &__clear-at { + opacity: .7; + + &::before { + content: ' - '; + } + } +} +</style> diff --git a/apps/user_status/src/components/PredefinedStatusesList.vue b/apps/user_status/src/components/PredefinedStatusesList.vue new file mode 100644 index 00000000000..844fdbbdfe3 --- /dev/null +++ b/apps/user_status/src/components/PredefinedStatusesList.vue @@ -0,0 +1,90 @@ +<!-- + - @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> + <div + v-if="hasLoaded" + class="predefined-statuses-list"> + <PredefinedStatus + v-for="status in predefinedStatuses" + :key="status.id" + :message-id="status.id" + :icon="status.icon" + :message="status.message" + :clear-at="status.clearAt" + @select="selectStatus(status)" /> + </div> + <div + v-else + class="predefined-statuses-list"> + <div class="icon icon-loading-small" /> + </div> +</template> + +<script> +import PredefinedStatus from './PredefinedStatus' +import { mapState } from 'vuex' + +export default { + name: 'PredefinedStatusesList', + components: { + PredefinedStatus, + }, + computed: { + ...mapState({ + predefinedStatuses: state => state.predefinedStatuses.predefinedStatuses, + }), + /** + * Indicator whether the predefined statuses have already been loaded + * + * @returns {boolean} + */ + hasLoaded() { + return this.predefinedStatuses.length > 0 + }, + }, + /** + * Loads all predefined statuses from the server + * when this component is mounted + */ + mounted() { + this.$store.dispatch('loadAllPredefinedStatuses') + }, + methods: { + /** + * Emits an event when the user selects a status + * + * @param {Object} status The selected status + */ + selectStatus(status) { + this.$emit('selectStatus', status) + }, + }, +} +</script> + +<style lang="scss" scoped> +.predefined-statuses-list { + display: flex; + flex-direction: column; + margin-bottom: 10px; +} +</style> diff --git a/apps/user_status/src/components/SetStatusModal.vue b/apps/user_status/src/components/SetStatusModal.vue new file mode 100644 index 00000000000..46c289d9e81 --- /dev/null +++ b/apps/user_status/src/components/SetStatusModal.vue @@ -0,0 +1,236 @@ +<!-- + - @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> + <Modal + size="normal" + :title="$t('user_status', 'Set a custom status')" + @close="closeModal"> + <div class="set-status-modal"> + <div class="set-status-modal__header"> + <h3>{{ $t('user_status', 'Set a custom status') }}</h3> + </div> + <div class="set-status-modal__custom-input"> + <EmojiPicker @select="setIcon"> + <button + class="custom-input__emoji-button"> + {{ visibleIcon }} + </button> + </EmojiPicker> + <CustomMessageInput + :message="message" + @change="setMessage" /> + </div> + <PredefinedStatusesList + @selectStatus="selectPredefinedMessage" /> + <ClearAtSelect + :clear-at="clearAt" + @selectClearAt="setClearAt" /> + <div class="status-buttons"> + <button class="status-buttons__select" @click="clearStatus"> + {{ $t('user_status', 'Clear custom status') }} + </button> + <button class="status-buttons__primary primary" @click="saveStatus"> + {{ $t('user_status', 'Set status') }} + </button> + </div> + </div> + </Modal> +</template> + +<script> +import EmojiPicker from '@nextcloud/vue/dist/Components/EmojiPicker' +import Modal from '@nextcloud/vue/dist/Components/Modal' +import PredefinedStatusesList from './PredefinedStatusesList' +import CustomMessageInput from './CustomMessageInput' +import ClearAtSelect from './ClearAtSelect' +import { showError } from '@nextcloud/dialogs' + +export default { + name: 'SetStatusModal', + components: { + EmojiPicker, + Modal, + CustomMessageInput, + PredefinedStatusesList, + ClearAtSelect, + }, + data() { + return { + icon: null, + message: null, + clearAt: null, + } + }, + computed: { + /** + * Returns the user-set icon or a smiley in case no icon is set + * + * @returns {String} + */ + visibleIcon() { + return this.icon || '😀' + }, + }, + /** + * 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 + + if (this.$store.state.userStatus.clearAt !== null) { + this.clearAt = { + type: '_time', + time: this.$store.state.userStatus.clearAt, + } + } + }, + methods: { + /** + * Closes the Set Status modal + */ + closeModal() { + this.$emit('close') + }, + /** + * Sets a new icon + * + * @param {String} icon The new icon + */ + setIcon(icon) { + this.messageId = null + this.icon = icon + }, + /** + * Sets a new message + * + * @param {String} message The new message + */ + setMessage(message) { + this.messageId = null + this.message = message + }, + /** + * Sets a new clearAt value + * + * @param {Object} clearAt The new clearAt object + */ + setClearAt(clearAt) { + this.clearAt = clearAt + }, + /** + * Sets new icon/message/clearAt based on a predefined message + * + * @param {Object} status The predefined status object + */ + selectPredefinedMessage(status) { + this.messageId = status.id + this.clearAt = status.clearAt + this.icon = status.icon + this.message = status.message + }, + /** + * Saves the status and closes the + * + * @returns {Promise<void>} + */ + async saveStatus() { + try { + this.isSavingStatus = true + + if (this.messageId !== null) { + await this.$store.dispatch('setPredefinedMessage', { + messageId: this.messageId, + clearAt: this.clearAt, + }) + } else { + await this.$store.dispatch('setCustomMessage', { + message: this.message, + icon: this.icon, + clearAt: this.clearAt, + }) + } + } catch (err) { + showError(this.$t('user_status', 'There was an error saving the status')) + console.debug(err) + this.isSavingStatus = false + return + } + + this.isSavingStatus = false + this.closeModal() + }, + /** + * + * @returns {Promise<void>} + */ + async clearStatus() { + try { + this.isSavingStatus = true + + await this.$store.dispatch('clearMessage') + } catch (err) { + showError(this.$t('user_status', 'There was an error clearing the status')) + console.debug(err) + this.isSavingStatus = false + return + } + + this.isSavingStatus = false + this.closeModal() + }, + }, +} +</script> + +<style lang="scss" scoped> +.set-status-modal { + min-width: 500px; + min-height: 200px; + padding: 8px 20px 20px 20px; + + &__custom-input { + display: flex; + width: 100%; + margin-bottom: 10px; + + .custom-input__emoji-button { + flex-basis: 40px; + width: 40px; + flex-grow: 0; + border-radius: var(--border-radius) 0 0 var(--border-radius); + height: 34px; + margin-right: 0; + border-right: none; + } + } + + .status-buttons { + display: flex; + + button { + flex-basis: 50%; + } + } +} +</style> diff --git a/apps/user_status/src/filters/clearAtFilter.js b/apps/user_status/src/filters/clearAtFilter.js new file mode 100644 index 00000000000..22579baa82a --- /dev/null +++ b/apps/user_status/src/filters/clearAtFilter.js @@ -0,0 +1,68 @@ +/** + * @copyright Copyright (c) 2020 Georg Ehrke + * + * @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/>. + * + */ +import { translate as t } from '@nextcloud/l10n' +import moment from '@nextcloud/moment' +import { dateFactory } from '../services/dateService' + +/** + * Formats a clearAt object to be human readable + * + * @param {Object} clearAt The clearAt object + * @returns {string|null} + */ +const clearAtFilter = (clearAt) => { + if (clearAt === null) { + return t('user_status', 'Don\'t clear') + } + + if (clearAt.type === 'end-of') { + switch (clearAt.time) { + case 'day': + return t('user_status', 'Today') + case 'week': + return t('user_status', 'This week') + + default: + return null + } + } + + if (clearAt.type === 'period') { + return moment.duration(clearAt.time * 1000).humanize() + } + + // This is not an officially supported type + // but only used internally to show the remaining time + // in the Set Status Modal + if (clearAt.type === '_time') { + const momentNow = moment(dateFactory()) + const momentClearAt = moment(clearAt.time, 'X') + + return moment.duration(momentNow.diff(momentClearAt)).humanize() + } + + return null +} + +export { + clearAtFilter, +} diff --git a/apps/user_status/src/main-user-status-menu.js b/apps/user_status/src/main-user-status-menu.js new file mode 100644 index 00000000000..795f41df4e7 --- /dev/null +++ b/apps/user_status/src/main-user-status-menu.js @@ -0,0 +1,23 @@ +import Vue from 'vue' +import { getRequestToken } from '@nextcloud/auth' +import App from './App' +import store from './store' + +// eslint-disable-next-line camelcase +__webpack_nonce__ = btoa(getRequestToken()) + +// Correct the root of the app for chunk loading +// OC.linkTo matches the apps folders +// OC.generateUrl ensure the index.php (or not) +// eslint-disable-next-line +__webpack_public_path__ = OC.linkTo('user_status', 'js/') + +Vue.prototype.t = t +Vue.prototype.$t = t + +const app = new Vue({ + render: h => h(App), + store, +}).$mount('li[data-id="user_status-menuitem"]') + +export { app } diff --git a/apps/user_status/src/services/clearAtOptionsService.js b/apps/user_status/src/services/clearAtOptionsService.js new file mode 100644 index 00000000000..83289f9059f --- /dev/null +++ b/apps/user_status/src/services/clearAtOptionsService.js @@ -0,0 +1,68 @@ +/** + * @copyright Copyright (c) 2020 Georg Ehrke + * + * @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/>. + * + */ +import { translate as t } from '@nextcloud/l10n' + +/** + * Returns an array + * + * @returns {Object[]} + */ +const getAllClearAtOptions = () => { + return [{ + label: t('user_status', 'Don\'t clear'), + clearAt: null, + }, { + label: t('user_status', '30 minutes'), + clearAt: { + type: 'period', + time: 1800, + }, + }, { + label: t('user_status', '1 hour'), + clearAt: { + type: 'period', + time: 3600, + }, + }, { + label: t('user_status', '4 hours'), + clearAt: { + type: 'period', + time: 14400, + }, + }, { + label: t('user_status', 'Today'), + clearAt: { + type: 'end-of', + time: 'day', + }, + }, { + label: t('user_status', 'This week'), + clearAt: { + type: 'end-of', + time: 'week', + }, + }] +} + +export { + getAllClearAtOptions, +} diff --git a/apps/user_status/src/services/clearAtService.js b/apps/user_status/src/services/clearAtService.js new file mode 100644 index 00000000000..12328d3b399 --- /dev/null +++ b/apps/user_status/src/services/clearAtService.js @@ -0,0 +1,63 @@ +/** + * @copyright Copyright (c) 2020 Georg Ehrke + * + * @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/>. + * + */ +import { + dateFactory, +} from './dateService' +import moment from '@nextcloud/moment' + +/** + * Calculates the actual clearAt timestamp + * + * @param {Object|null} clearAt The clear-at config + * @returns {Number|null} + */ +const getTimestampForClearAt = (clearAt) => { + if (clearAt === null) { + return null + } + + const date = dateFactory() + + if (clearAt.type === 'period') { + date.setSeconds(date.getSeconds() + clearAt.time) + return Math.floor(date.getTime() / 1000) + } + if (clearAt.type === 'end-of') { + switch (clearAt.time) { + case 'day': + case 'week': + return Number(moment(date).endOf(clearAt.time).format('X')) + } + } + // This is not an officially supported type + // but only used internally to show the remaining time + // in the Set Status Modal + if (clearAt.type === '_time') { + return clearAt.time + } + + return null +} + +export { + getTimestampForClearAt, +} diff --git a/apps/user_status/src/services/dateService.js b/apps/user_status/src/services/dateService.js new file mode 100644 index 00000000000..641244dada3 --- /dev/null +++ b/apps/user_status/src/services/dateService.js @@ -0,0 +1,34 @@ +/** + * @copyright Copyright (c) 2020 Georg Ehrke + * + * @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/>. + * + */ + +/** + * Returns a new Date object + * + * @returns {Date} + */ +const dateFactory = () => { + return new Date() +} + +export { + dateFactory, +} diff --git a/apps/user_status/src/services/heartbeatService.js b/apps/user_status/src/services/heartbeatService.js new file mode 100644 index 00000000000..ca3a7de6d03 --- /dev/null +++ b/apps/user_status/src/services/heartbeatService.js @@ -0,0 +1,40 @@ +/** + * @copyright Copyright (c) 2020 Georg Ehrke + * + * @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/>. + * + */ +import HttpClient from '@nextcloud/axios' +import { generateUrl } from '@nextcloud/router' + +/** + * Sends a heartbeat + * + * @param {Boolean} isAway Whether or not the user is active + * @returns {Promise<void>} + */ +const sendHeartbeat = async(isAway) => { + const url = generateUrl('/apps/user_status/heartbeat') + await HttpClient.put(url, { + status: isAway ? 'away' : 'online', + }) +} + +export { + sendHeartbeat, +} diff --git a/apps/user_status/src/services/predefinedStatusService.js b/apps/user_status/src/services/predefinedStatusService.js new file mode 100644 index 00000000000..116fccb0c56 --- /dev/null +++ b/apps/user_status/src/services/predefinedStatusService.js @@ -0,0 +1,39 @@ +/** + * @copyright Copyright (c) 2020 Georg Ehrke + * + * @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/>. + * + */ +import HttpClient from '@nextcloud/axios' +import { generateOcsUrl } from '@nextcloud/router' + +/** + * Fetches all predefined statuses from the server + * + * @returns {Promise<void>} + */ +const fetchAllPredefinedStatuses = async() => { + const url = generateOcsUrl('apps/user_status/api/v1', 2) + '/predefined_statuses?format=json' + const response = await HttpClient.get(url) + + return response.data.ocs.data +} + +export { + fetchAllPredefinedStatuses, +} diff --git a/apps/user_status/src/services/statusOptionsService.js b/apps/user_status/src/services/statusOptionsService.js new file mode 100644 index 00000000000..f429d6b189f --- /dev/null +++ b/apps/user_status/src/services/statusOptionsService.js @@ -0,0 +1,52 @@ +/** + * @copyright Copyright (c) 2020 Georg Ehrke + * + * @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/>. + * + */ +import { translate as t } from '@nextcloud/l10n' + +/** + * Returns a list of all user-definable statuses + * + * @returns {Object[]} + */ +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: 'dnd', + label: t('user_status', 'Do not disturb'), + icon: 'icon-user-status-dnd', + + }, { + type: 'invisible', + label: t('user_status', 'Invisible'), + icon: 'icon-user-status-invisible', + }] +} + +export { + getAllStatusOptions, +} diff --git a/apps/user_status/src/services/statusService.js b/apps/user_status/src/services/statusService.js new file mode 100644 index 00000000000..206ff4ee647 --- /dev/null +++ b/apps/user_status/src/services/statusService.js @@ -0,0 +1,98 @@ +/** + * @copyright Copyright (c) 2020 Georg Ehrke + * + * @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/>. + * + */ +import HttpClient from '@nextcloud/axios' +import { generateOcsUrl } from '@nextcloud/router' + +/** + * Fetches the current user-status + * + * @returns {Promise<Object>} + */ +const fetchCurrentStatus = async() => { + const url = generateOcsUrl('apps/user_status/api/v1', 2) + 'user_status' + const response = await HttpClient.get(url) + + return response.data.ocs.data +} + +/** + * Sets the status + * + * @param {String} statusType The status (online / away / dnd / invisible) + * @returns {Promise<void>} + */ +const setStatus = async(statusType) => { + const url = generateOcsUrl('apps/user_status/api/v1', 2) + 'user_status/status' + await HttpClient.put(url, { + statusType, + }) +} + +/** + * Sets a message based on our predefined statuses + * + * @param {String} messageId The id of the message, taken from predefined status service + * @param {Number|null} clearAt When to automatically clean the status + * @returns {Promise<void>} + */ +const setPredefinedMessage = async(messageId, clearAt = null) => { + const url = generateOcsUrl('apps/user_status/api/v1', 2) + 'user_status/message/predefined?format=json' + await HttpClient.put(url, { + messageId, + clearAt, + }) +} + +/** + * Sets a custom message + * + * @param {String} message The user-defined message + * @param {String|null} statusIcon The user-defined icon + * @param {Number|null} clearAt When to automatically clean the status + * @returns {Promise<void>} + */ +const setCustomMessage = async(message, statusIcon = null, clearAt = null) => { + const url = generateOcsUrl('apps/user_status/api/v1', 2) + 'user_status/message/custom?format=json' + await HttpClient.put(url, { + message, + statusIcon, + clearAt, + }) +} + +/** + * Clears the current status of the user + * + * @returns {Promise<void>} + */ +const clearMessage = async() => { + const url = generateOcsUrl('apps/user_status/api/v1', 2) + 'user_status/message?format=json' + await HttpClient.delete(url) +} + +export { + fetchCurrentStatus, + setStatus, + setCustomMessage, + setPredefinedMessage, + clearMessage, +} diff --git a/apps/user_status/src/store/index.js b/apps/user_status/src/store/index.js new file mode 100644 index 00000000000..d810cae5444 --- /dev/null +++ b/apps/user_status/src/store/index.js @@ -0,0 +1,35 @@ +/** + * @copyright Copyright (c) 2020 Georg Ehrke + * + * @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/>. + * + */ +import Vue from 'vue' +import Vuex from 'vuex' +import predefinedStatuses from './predefinedStatuses' +import userStatus from './userStatus' + +Vue.use(Vuex) + +export default new Vuex.Store({ + modules: { + predefinedStatuses, + userStatus, + }, + strict: true, +}) diff --git a/apps/user_status/src/store/predefinedStatuses.js b/apps/user_status/src/store/predefinedStatuses.js new file mode 100644 index 00000000000..f7174bf8bfc --- /dev/null +++ b/apps/user_status/src/store/predefinedStatuses.js @@ -0,0 +1,64 @@ +/** + * @copyright Copyright (c) 2020 Georg Ehrke + * + * @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/>. + * + */ +import { fetchAllPredefinedStatuses } from '../services/predefinedStatusService' + +const state = { + predefinedStatuses: [], +} + +const mutations = { + + /** + * Adds a predefined status to the state + * + * @param {Object} state The Vuex state + * @param {Object} status The status to add + */ + addPredefinedStatus(state, status) { + state.predefinedStatuses.push(status) + }, +} + +const getters = {} + +const actions = { + + /** + * Loads all predefined statuses from the server + * + * @param {Object} vuex The Vuex components + * @param {Function} vuex.commit The Vuex commit function + */ + async loadAllPredefinedStatuses({ state, commit }) { + if (state.predefinedStatuses.length > 0) { + return + } + + const statuses = await fetchAllPredefinedStatuses() + for (const status of statuses) { + commit('addPredefinedStatus', status) + } + }, + +} + +export default { state, mutations, getters, actions } diff --git a/apps/user_status/src/store/userStatus.js b/apps/user_status/src/store/userStatus.js new file mode 100644 index 00000000000..ebe2aea2047 --- /dev/null +++ b/apps/user_status/src/store/userStatus.js @@ -0,0 +1,232 @@ +/** + * @copyright Copyright (c) 2020 Georg Ehrke + * + * @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/>. + * + */ +import { + fetchCurrentStatus, + setStatus, + setPredefinedMessage, + setCustomMessage, + clearMessage, +} from '../services/statusService' +import { loadState } from '@nextcloud/initial-state' +import { getTimestampForClearAt } from '../services/clearAtService' + +const state = { + // Status (online / away / dnd / invisible / offline) + status: null, + // Whether or not 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 or not 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 = { + + /** + * Sets a new status + * + * @param {Object} state The Vuex state + * @param {Object} data The destructuring object + * @param {String} data.statusType The new status type + */ + setStatus(state, { statusType }) { + state.status = statusType + state.statusIsUserDefined = true + }, + + /** + * Sets a message using a predefined message + * + * @param {Object} state The Vuex state + * @param {Object} data The destructuring object + * @param {String} data.messageId The messageId + * @param {Number|null} data.clearAt When to automatically clear the status + * @param {String} data.message The message + * @param {String} data.icon The icon + */ + setPredefinedMessage(state, { messageId, clearAt, message, icon }) { + state.messageId = messageId + state.messageIsPredefined = true + + state.message = message + state.icon = icon + state.clearAt = clearAt + }, + + /** + * Sets a custom message + * + * @param {Object} state The Vuex state + * @param {Object} data The destructuring object + * @param {String} data.message The message + * @param {String} data.icon The icon + * @param {Number} data.clearAt When to automatically clear the status + */ + setCustomMessage(state, { message, icon, clearAt }) { + state.messageId = null + state.messageIsPredefined = false + + state.message = message + state.icon = icon + state.clearAt = clearAt + }, + + /** + * Clears the status + * + * @param {Object} state The Vuex state + */ + clearMessage(state) { + state.messageId = null + state.messageIsPredefined = false + + state.message = null + state.icon = null + state.clearAt = null + }, + + /** + * 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 + */ + loadStatusFromServer(state, { status, statusIsUserDefined, message, icon, clearAt, messageIsPredefined, messageId }) { + state.status = status + state.statusIsUserDefined = statusIsUserDefined + state.message = message + state.icon = icon + state.clearAt = clearAt + state.messageIsPredefined = messageIsPredefined + state.messageId = messageId + }, +} + +const getters = {} + +const actions = { + + /** + * Sets a new status + * + * @param {Object} vuex The Vuex destructuring object + * @param {Function} vuex.commit The Vuex commit function + * @param {Object} data The data destructuring object + * @param {String} data.statusType The new status type + * @returns {Promise<void>} + */ + async setStatus({ commit }, { statusType }) { + await setStatus(statusType) + commit('setStatus', { statusType }) + }, + + /** + * Sets a message using a predefined message + * + * @param {Object} vuex The Vuex destructuring object + * @param {Function} vuex.commit The Vuex commit function + * @param {Object} vuex.rootState The Vuex root state + * @param {Object} data The data destructuring object + * @param {String} data.messageId The messageId + * @param {Object|null} data.clearAt When to automatically clear the status + * @returns {Promise<void>} + */ + async setPredefinedMessage({ commit, rootState }, { messageId, clearAt }) { + const resolvedClearAt = getTimestampForClearAt(clearAt) + + await setPredefinedMessage(messageId, resolvedClearAt) + const status = rootState.predefinedStatuses.predefinedStatuses.find((status) => status.id === messageId) + const { message, icon } = status + + commit('setPredefinedMessage', { messageId, clearAt: resolvedClearAt, message, icon }) + }, + + /** + * Sets a custom message + * + * @param {Object} vuex The Vuex destructuring object + * @param {Function} vuex.commit The Vuex commit function + * @param {Object} data The data destructuring object + * @param {String} data.message The message + * @param {String} data.icon The icon + * @param {Object|null} data.clearAt When to automatically clear the status + * @returns {Promise<void>} + */ + async setCustomMessage({ commit }, { message, icon, clearAt }) { + const resolvedClearAt = getTimestampForClearAt(clearAt) + + await setCustomMessage(message, icon, resolvedClearAt) + commit('setCustomMessage', { message, icon, clearAt: resolvedClearAt }) + }, + + /** + * Clears the status + * + * @param {Object} vuex The Vuex destructuring object + * @param {Function} vuex.commit The Vuex commit function + * @returns {Promise<void>} + */ + async clearMessage({ commit }) { + await clearMessage() + commit('clearMessage') + }, + + /** + * Re-fetches the status from the server + * + * @param {Object} vuex The Vuex destructuring object + * @param {Function} vuex.commit The Vuex commit function + * @returns {Promise<void>} + */ + async reFetchStatusFromServer({ commit }) { + const status = await fetchCurrentStatus() + commit('loadStatusFromServer', status) + }, + + /** + * Loads the server from the initial state + * + * @param {Object} vuex The Vuex destructuring object + * @param {Function} vuex.commit The Vuex commit function + */ + loadStatusFromInitialState({ commit }) { + const status = loadState('user_status', 'status') + commit('loadStatusFromServer', status) + }, +} + +export default { state, mutations, getters, actions } |