You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

App.vue 6.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. <!--
  2. - @copyright Copyright (c) 2020 Georg Ehrke <oc.list@georgehrke.com>
  3. - @author Georg Ehrke <oc.list@georgehrke.com>
  4. -
  5. - @license GNU AGPL version 3 or any later version
  6. -
  7. - This program is free software: you can redistribute it and/or modify
  8. - it under the terms of the GNU Affero General Public License as
  9. - published by the Free Software Foundation, either version 3 of the
  10. - License, or (at your option) any later version.
  11. -
  12. - This program is distributed in the hope that it will be useful,
  13. - but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. - GNU Affero General Public License for more details.
  16. -
  17. - You should have received a copy of the GNU Affero General Public License
  18. - along with this program. If not, see <http://www.gnu.org/licenses/>.
  19. -
  20. -->
  21. <template>
  22. <li>
  23. <div id="user-status-menu-item">
  24. <span id="user-status-menu-item__header">{{ displayName }}</span>
  25. <Actions
  26. id="user-status-menu-item__subheader"
  27. :default-icon="statusIcon"
  28. :menu-title="visibleMessage">
  29. <ActionButton
  30. v-for="status in statuses"
  31. :key="status.type"
  32. :icon="status.icon"
  33. :close-after-click="true"
  34. @click.prevent.stop="changeStatus(status.type)">
  35. {{ status.label }}
  36. </ActionButton>
  37. <ActionButton
  38. icon="icon-rename"
  39. :close-after-click="true"
  40. @click.prevent.stop="openModal">
  41. {{ $t('user_status', 'Set custom status') }}
  42. </ActionButton>
  43. </Actions>
  44. <SetStatusModal
  45. v-if="isModalOpen"
  46. @close="closeModal" />
  47. </div>
  48. </li>
  49. </template>
  50. <script>
  51. import { getCurrentUser } from '@nextcloud/auth'
  52. import SetStatusModal from './components/SetStatusModal'
  53. import Actions from '@nextcloud/vue/dist/Components/Actions'
  54. import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
  55. import { mapState } from 'vuex'
  56. import { showError } from '@nextcloud/dialogs'
  57. import { getAllStatusOptions } from './services/statusOptionsService'
  58. import { sendHeartbeat } from './services/heartbeatService'
  59. import debounce from 'debounce'
  60. export default {
  61. name: 'App',
  62. components: {
  63. Actions,
  64. ActionButton,
  65. SetStatusModal,
  66. },
  67. data() {
  68. return {
  69. isModalOpen: false,
  70. statuses: getAllStatusOptions(),
  71. heartbeatInterval: null,
  72. setAwayTimeout: null,
  73. mouseMoveListener: null,
  74. isAway: false,
  75. }
  76. },
  77. computed: {
  78. ...mapState({
  79. statusType: state => state.userStatus.status,
  80. statusIsUserDefined: state => state.userStatus.statusIsUserDefined,
  81. customIcon: state => state.userStatus.icon,
  82. customMessage: state => state.userStatus.message,
  83. }),
  84. /**
  85. * The display-name of the current user
  86. *
  87. * @returns {String}
  88. */
  89. displayName() {
  90. return getCurrentUser().displayName
  91. },
  92. /**
  93. * The message displayed in the top right corner
  94. *
  95. * @returns {String}
  96. */
  97. visibleMessage() {
  98. if (this.customIcon && this.customMessage) {
  99. return `${this.customIcon} ${this.customMessage}`
  100. }
  101. if (this.customMessage) {
  102. return this.customMessage
  103. }
  104. if (this.statusIsUserDefined) {
  105. switch (this.statusType) {
  106. case 'online':
  107. return this.$t('user_status', 'Online')
  108. case 'away':
  109. return this.$t('user_status', 'Away')
  110. case 'dnd':
  111. return this.$t('user_status', 'Do not disturb')
  112. case 'invisible':
  113. return this.$t('user_status', 'Invisible')
  114. case 'offline':
  115. return this.$t('user_status', 'Offline')
  116. }
  117. }
  118. return this.$t('user_status', 'Set status')
  119. },
  120. /**
  121. * The status indicator icon
  122. *
  123. * @returns {String|null}
  124. */
  125. statusIcon() {
  126. switch (this.statusType) {
  127. case 'online':
  128. return 'icon-user-status-online'
  129. case 'away':
  130. return 'icon-user-status-away'
  131. case 'dnd':
  132. return 'icon-user-status-dnd'
  133. case 'invisible':
  134. case 'offline':
  135. return 'icon-user-status-invisible'
  136. }
  137. return ''
  138. },
  139. },
  140. /**
  141. * Loads the current user's status from initial state
  142. * and stores it in Vuex
  143. */
  144. mounted() {
  145. this.$store.dispatch('loadStatusFromInitialState')
  146. if (OC.config.session_keepalive) {
  147. // Send the latest status to the server every 5 minutes
  148. this.heartbeatInterval = setInterval(this._backgroundHeartbeat.bind(this), 1000 * 60 * 5)
  149. this.setAwayTimeout = () => {
  150. this.isAway = true
  151. }
  152. // Catch mouse movements, but debounce to once every 30 seconds
  153. this.mouseMoveListener = debounce(() => {
  154. const wasAway = this.isAway
  155. this.isAway = false
  156. // Reset the two minute counter
  157. clearTimeout(this.setAwayTimeout)
  158. // If the user did not move the mouse within two minutes,
  159. // mark them as away
  160. setTimeout(this.setAwayTimeout, 1000 * 60 * 2)
  161. if (wasAway) {
  162. this._backgroundHeartbeat()
  163. }
  164. }, 1000 * 2, true)
  165. window.addEventListener('mousemove', this.mouseMoveListener, {
  166. capture: true,
  167. passive: true,
  168. })
  169. this._backgroundHeartbeat()
  170. }
  171. },
  172. /**
  173. * Some housekeeping before destroying the component
  174. */
  175. beforeDestroy() {
  176. window.removeEventListener('mouseMove', this.mouseMoveListener)
  177. clearInterval(this.heartbeatInterval)
  178. },
  179. methods: {
  180. /**
  181. * Opens the modal to set a custom status
  182. */
  183. openModal() {
  184. this.isModalOpen = true
  185. },
  186. /**
  187. * Closes the modal
  188. */
  189. closeModal() {
  190. this.isModalOpen = false
  191. },
  192. /**
  193. * Changes the user-status
  194. *
  195. * @param {String} statusType (online / away / dnd / invisible)
  196. */
  197. async changeStatus(statusType) {
  198. try {
  199. await this.$store.dispatch('setStatus', { statusType })
  200. } catch (err) {
  201. showError(this.$t('user_status', 'There was an error saving the new status'))
  202. console.debug(err)
  203. }
  204. },
  205. /**
  206. * Sends the status heartbeat to the server
  207. *
  208. * @returns {Promise<void>}
  209. * @private
  210. */
  211. async _backgroundHeartbeat() {
  212. await sendHeartbeat(this.isAway)
  213. await this.$store.dispatch('reFetchStatusFromServer')
  214. },
  215. },
  216. }
  217. </script>
  218. <style lang="scss">
  219. #user-status-menu-item {
  220. &__header {
  221. display: block;
  222. align-items: center;
  223. color: var(--color-main-text);
  224. padding: 10px 12px 5px 12px;
  225. box-sizing: border-box;
  226. opacity: 1;
  227. white-space: nowrap;
  228. width: 100%;
  229. text-align: center;
  230. max-width: 250px;
  231. text-overflow: ellipsis;
  232. min-width: 175px;
  233. }
  234. &__subheader {
  235. width: 100%;
  236. > button {
  237. background-color: var(--color-main-background);
  238. background-size: 16px;
  239. border: 0;
  240. border-radius: 0;
  241. font-weight: normal;
  242. font-size: 0.875em;
  243. padding-left: 40px;
  244. &:hover,
  245. &:focus {
  246. box-shadow: inset 4px 0 var(--color-primary-element);
  247. }
  248. }
  249. }
  250. }
  251. </style>