aboutsummaryrefslogtreecommitdiffstats
path: root/apps/user_status/src/App.vue
diff options
context:
space:
mode:
Diffstat (limited to 'apps/user_status/src/App.vue')
-rw-r--r--apps/user_status/src/App.vue271
1 files changed, 271 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>