nextcloud/core/src/views/AccountMenu.vue
Grigorii K. Shartsev 8ca4a7a036 refactor(user_status): migrate to Vue 3
Signed-off-by: Grigorii K. Shartsev <me@shgk.me>
2025-11-26 14:09:33 +01:00

269 lines
6.5 KiB
Vue

<!--
- SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<NcHeaderMenu
id="user-menu"
class="account-menu"
is-nav
:aria-label="t('core', 'Settings menu')"
:description="avatarDescription">
<template #trigger>
<!-- The `key` is a hack as NcAvatar does not handle updating the preloaded status on show status change -->
<NcAvatar
:key="String(showUserStatus)"
class="account-menu__avatar"
disable-menu
disable-tooltip
:hide-user-status="!showUserStatus"
:user="currentUserId"
:preloaded-user-status="userStatus" />
</template>
<ul class="account-menu__list">
<AccountMenuProfileEntry
:id="profileEntry.id"
:name="profileEntry.name"
:href="profileEntry.href"
:active="profileEntry.active" />
<AccountMenuEntry
v-for="entry in otherEntries"
:id="entry.id"
:key="entry.id"
:name="entry.name"
:href="entry.href"
:active="entry.active"
:icon="entry.icon" />
</ul>
</NcHeaderMenu>
</template>
<script lang="ts">
import { getCurrentUser } from '@nextcloud/auth'
import axios from '@nextcloud/axios'
import { getCapabilities } from '@nextcloud/capabilities'
import { emit, subscribe } from '@nextcloud/event-bus'
import { loadState } from '@nextcloud/initial-state'
import { t } from '@nextcloud/l10n'
import { generateOcsUrl } from '@nextcloud/router'
import { defineComponent } from 'vue'
import NcAvatar from '@nextcloud/vue/components/NcAvatar'
import NcHeaderMenu from '@nextcloud/vue/components/NcHeaderMenu'
import AccountMenuEntry from '../components/AccountMenu/AccountMenuEntry.vue'
import AccountMenuProfileEntry from '../components/AccountMenu/AccountMenuProfileEntry.vue'
import logger from '../logger.js'
interface ISettingsNavigationEntry {
/**
* id of the entry, used as HTML ID, for example, "settings"
*/
id: string
/**
* Label of the entry, for example, "Personal Settings"
*/
name: string
/**
* Icon of the entry, for example, "/apps/settings/img/personal.svg"
*/
icon: string
/**
* Type of the entry
*/
type: 'settings' | 'link' | 'guest'
/**
* Link of the entry, for example, "/settings/user"
*/
href: string
/**
* Whether the entry is active
*/
active: boolean
/**
* Order of the entry
*/
order: number
/**
* Number of unread pf this items
*/
unread: number
/**
* Classes for custom styling
*/
classes: string
}
// See: apps/user_status/src/services/statusOptionsService.js
// TODO: either import this again from the user_status app when core is migrated to Vue 3
// Or get rid of the forbidden import
const USER_DEFINABLE_STATUSES = [{
type: 'online',
label: t('user_status', 'Online'),
}, {
type: 'away',
label: t('user_status', 'Away'),
}, {
type: 'busy',
label: t('user_status', 'Busy'),
}, {
type: 'dnd',
label: t('user_status', 'Do not disturb'),
subline: t('user_status', 'Mute all notifications'),
}, {
type: 'invisible',
label: t('user_status', 'Invisible'),
subline: t('user_status', 'Appear offline'),
}]
export default defineComponent({
name: 'AccountMenu',
components: {
AccountMenuEntry,
AccountMenuProfileEntry,
NcAvatar,
NcHeaderMenu,
},
setup() {
const settingsNavEntries = loadState<Record<string, ISettingsNavigationEntry>>('core', 'settingsNavEntries', {})
const { profile: profileEntry, ...otherEntries } = settingsNavEntries
return {
currentDisplayName: getCurrentUser()?.displayName ?? getCurrentUser()!.uid,
currentUserId: getCurrentUser()!.uid,
profileEntry,
otherEntries,
t,
}
},
data() {
return {
showUserStatus: false,
userStatus: {
status: null,
icon: null,
message: null,
},
}
},
computed: {
translatedUserStatus() {
return {
...this.userStatus,
status: this.translateStatus(this.userStatus.status),
}
},
avatarDescription() {
const description = [
t('core', 'Avatar of {displayName}', { displayName: this.currentDisplayName }),
...Object.values(this.translatedUserStatus).filter(Boolean),
].join(' — ')
return description
},
},
async created() {
if (!getCapabilities()?.user_status?.enabled) {
return
}
const url = generateOcsUrl('/apps/user_status/api/v1/user_status')
try {
const response = await axios.get(url)
const { status, icon, message } = response.data.ocs.data
this.userStatus = { status, icon, message }
} catch (error) {
logger.error('Failed to load user status', { error })
}
this.showUserStatus = true
},
mounted() {
subscribe('user_status:status.updated', this.handleUserStatusUpdated)
emit('core:user-menu:mounted')
},
methods: {
handleUserStatusUpdated(state) {
if (this.currentUserId === state.userId) {
this.userStatus = {
status: state.status,
icon: state.icon,
message: state.message,
}
}
},
translateStatus(status) {
const statusMap = Object.fromEntries(USER_DEFINABLE_STATUSES.map(({ type, label }) => [type, label]))
if (statusMap[status]) {
return statusMap[status]
}
return status
},
},
})
</script>
<style lang="scss" scoped>
:deep(#header-menu-user-menu) {
padding: 0 !important;
}
.account-menu {
:deep(*) {
// do not apply the alpha mask on the avatar div
mask: none !important;
}
&__avatar {
--account-menu-outline: var(--border-width-input) solid color-mix(in srgb, var(--color-background-plain-text), transparent 75%);
outline: var(--account-menu-outline);
position: fixed;
&:hover {
--account-menu-outline: none;
// Add hover styles similar to the focus-visible style
border: var(--border-width-input-focused) solid var(--color-background-plain-text);
}
}
&__list {
display: inline-flex;
flex-direction: column;
padding-block: var(--default-grid-baseline) 0;
padding-inline: 0 var(--default-grid-baseline);
> :deep(li) {
box-sizing: border-box;
// basically "fit-content"
flex: 0 1;
}
}
// Ensure we do not waste space, as the header menu sets a default width of 350px
:deep(.header-menu__content) {
width: fit-content !important;
}
:deep(button) {
// Normally header menus are slightly translucent when not active
// this is generally ok but for the avatar this is weird so fix the opacity
opacity: 1 !important;
// The avatar is just the "icon" of the button
// So we add the focus-visible manually
&:focus-visible {
.account-menu__avatar {
--account-menu-outline: none;
border: var(--border-width-input-focused) solid var(--color-background-plain-text);
}
}
}
}
</style>