server/apps/user_status/src/App.vue

272 lines
6.5 KiB
Vue

<!--
- @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>