UNPKG

@nextcloud/vue

Version:
686 lines (685 loc) 22.9 kB
import '../assets/NcAvatar-f8SJKMDw.css'; import { getCurrentUser } from "@nextcloud/auth"; import axios from "@nextcloud/axios"; import { getBuilder } from "@nextcloud/browser-storage"; import { unsubscribe, subscribe } from "@nextcloud/event-bus"; import { generateOcsUrl, generateUrl } from "@nextcloud/router"; import { vOnClickOutside } from "@vueuse/components"; import { N as NcActions, I as IconDotsHorizontal } from "./NcActions-DWmvh7-Y.mjs"; import { g as getRoute } from "./autolink-U5pBzLgI.mjs"; import "../composables/useFormatDateTime/index.mjs"; import "../composables/useHotKey/index.mjs"; import { useIsDarkTheme } from "../composables/useIsDarkTheme/index.mjs"; import "../composables/useIsFullscreen/index.mjs"; import "../composables/useIsMobile/index.mjs"; import { getEnabledContactsMenuActions } from "../functions/contactsMenu/index.mjs"; import { usernameToColor } from "../functions/usernameToColor/index.mjs"; import { r as register, i as t10, a as t } from "./_l10n-DrTiip5c.mjs"; import "escape-html"; import "striptags"; import { resolveComponent, resolveDirective, withDirectives, createElementBlock, openBlock, normalizeStyle, normalizeClass, renderSlot, createBlock, createCommentVNode, withCtx, createSlots, Fragment, renderList, resolveDynamicComponent, mergeProps, createTextVNode, toDisplayString, createVNode, createElementVNode } from "vue"; import { g as getAvatarUrl } from "./NcMentionBubble.vue_vue_type_style_index_0_scoped_45238efd_lang-D6LzDiYf.mjs"; import { _ as _export_sfc } from "./_plugin-vue_export-helper-1tPrXgE0.mjs"; import { getCapabilities } from "@nextcloud/capabilities"; import { l as logger } from "./logger-D3RVzcfQ.mjs"; import { N as NcUserStatusIcon, g as getUserStatusText } from "./NcUserStatusIcon-CGEf7fej.mjs"; import { N as NcActionButton } from "./NcActionButton-pKOSrlGE.mjs"; import { N as NcActionLink } from "./NcActionLink-vEvKSV4N.mjs"; import { N as NcActionRouter } from "./NcActionRouter-oT-YU_jf.mjs"; import { N as NcActionText } from "./NcActionText-uKvLcEY6.mjs"; import { N as NcButton } from "./NcButton-Dc8V4Urj.mjs"; import { N as NcIconSvgWrapper } from "./NcIconSvgWrapper-BvLanNaW.mjs"; import { N as NcLoadingIcon } from "./NcLoadingIcon-b_ajZ_nQ.mjs"; register(t10); const userStatus = { data() { return { hasStatus: false, userStatus: { status: null, message: null, icon: null } }; }, methods: { /** * Fetches the user-status from the server * * @param {string} userId UserId of the user to fetch the status for * * @return {Promise<void>} */ async fetchUserStatus(userId) { if (!userId) { return; } const capabilities = getCapabilities(); if (!Object.hasOwn(capabilities, "user_status") || !capabilities.user_status.enabled) { return; } if (!getCurrentUser()) { return; } try { const { data } = await axios.get(generateOcsUrl("apps/user_status/api/v1/statuses/{userId}", { userId })); this.setUserStatus(data.ocs.data); } catch (e) { if (e.response.status === 404 && e.response.data.ocs?.data?.length === 0) { return; } logger.error("Failed to fetch user status", { error: e }); } }, /** * Sets the user status * * @param {string} status user's status * @param {string} message user's message * @param {string} icon user's icon */ setUserStatus({ status, message, icon }) { this.userStatus.status = status || ""; this.userStatus.message = message || ""; this.userStatus.icon = icon || ""; this.hasStatus = !!status; } } }; const browserStorage = getBuilder("nextcloud").persist().build(); function getUserHasAvatar(userId) { const flag = browserStorage.getItem("user-has-avatar." + userId); if (typeof flag === "string") { return Boolean(flag); } return null; } function setUserHasAvatar(userId, flag) { if (userId) { browserStorage.setItem("user-has-avatar." + userId, flag); } } const _sfc_main = { name: "NcAvatar", directives: { /** @type {import('vue').ObjectDirective} */ ClickOutside: vOnClickOutside }, components: { IconDotsHorizontal, NcActions, NcButton, NcIconSvgWrapper, NcLoadingIcon, NcUserStatusIcon }, mixins: [userStatus], props: { /** * Set a custom url to the avatar image * either the url, user or displayName property must be defined */ url: { type: String, default: void 0 }, /** * Set a css icon-class for an icon to be used instead of the avatar. */ iconClass: { type: String, default: void 0 }, /** * Set the user id to fetch the avatar * either the url, user or displayName property must be defined */ user: { type: String, default: void 0 }, /** * Do not show the user status on the avatar. */ hideStatus: { type: Boolean, default: false }, /** * Show the verbose user status (e.g. "online" / "away") instead of just the status icon. */ verboseStatus: { type: Boolean, default: false }, /** * When the user status was preloaded via another source it can be handed in with this property to save the request. * If this property is not set the status will be fetched automatically. * If a preloaded no-status is available provide this object with properties "status", "icon" and "message" set to null. */ preloadedUserStatus: { type: Object, default: void 0 }, /** * Is the user a guest user (then we have to user a different endpoint) */ isGuest: { type: Boolean, default: false }, /** * Set a display name that will be rendered as a tooltip * either the url, user or displayName property must be defined * specify just the displayname to generate a placeholder avatar without * trying to fetch the avatar based on the user id */ displayName: { type: String, default: void 0 }, /** * Set a size in px for the rendered avatar */ size: { type: Number, default: 32 }, /** * Do not automatically generate a placeholder avatars if there is no real avatar is available. */ noPlaceholder: { type: Boolean, default: false }, /** * Disable the tooltip */ disableTooltip: { type: Boolean, default: false }, /** * Disable the menu */ disableMenu: { type: Boolean, default: false }, /** * Declares a custom tooltip when not null * Fallback will be the displayName * * requires disableTooltip not to be set to true */ tooltipMessage: { type: String, default: null }, /** * Declares username is not a user's name, when true. * Prevents loading user's avatar from server and forces generating colored initials, * i.e. if the user is a group */ isNoUser: { type: Boolean, default: false }, /** * Selector for the popover menu container */ menuContainer: { type: [Boolean, String, Object, Element], default: "body" } }, setup() { const isDarkTheme = useIsDarkTheme(); return { isDarkTheme }; }, data() { return { avatarUrlLoaded: null, avatarSrcSetLoaded: null, userDoesNotExist: false, isAvatarLoaded: false, isMenuLoaded: false, contactsMenuLoading: false, contactsMenuData: {}, contactsMenuActions: [], contactsMenuOpenState: false }; }, computed: { avatarAriaLabel() { if (!this.hasMenu) { return; } if (this.canDisplayUserStatus || this.showUserStatusIconOnAvatar) { return t("Avatar of {displayName}, {status}", { displayName: this.displayName ?? this.user, status: getUserStatusText(this.userStatus.status) }); } return t("Avatar of {displayName}", { displayName: this.displayName ?? this.user }); }, canDisplayUserStatus() { return !this.hideStatus && this.hasStatus && ["online", "away", "busy", "dnd"].includes(this.userStatus.status); }, showUserStatusIconOnAvatar() { return !this.hideStatus && !this.verboseStatus && this.hasStatus && this.userStatus.status !== "dnd" && this.userStatus.icon; }, /** * The user identifier, either the display name if set or the user property * If both properties are not set an empty string is returned */ userIdentifier() { if (this.isDisplayNameDefined) { return this.displayName; } if (this.isUserDefined) { return this.user; } return ""; }, isUserDefined() { return typeof this.user !== "undefined"; }, isDisplayNameDefined() { return typeof this.displayName !== "undefined"; }, isUrlDefined() { return typeof this.url !== "undefined"; }, hasMenu() { if (this.disableMenu) { return false; } if (this.isMenuLoaded) { return this.menu.length > 0; } return !(this.user === getCurrentUser()?.uid || this.userDoesNotExist || this.url); }, /** * True if initials should be shown as the user icon fallback */ showInitials() { return !this.noPlaceholder && this.userDoesNotExist && !(this.iconClass || this.$slots.icon); }, avatarStyle() { return { "--avatar-size": this.size + "px", lineHeight: this.showInitials ? this.size + "px" : 0, fontSize: Math.round(this.size * 0.45) + "px" }; }, initialsWrapperStyle() { const { r, g, b } = usernameToColor(this.userIdentifier); return { backgroundColor: `rgba(${r}, ${g}, ${b}, 0.1)` }; }, initialsStyle() { const { r, g, b } = usernameToColor(this.userIdentifier); return { color: `rgb(${r}, ${g}, ${b})` }; }, tooltip() { if (this.disableTooltip) { return null; } if (this.tooltipMessage) { return this.tooltipMessage; } return this.displayName; }, /** * Get the (max. two) initials of the user as uppcase string */ initials() { let initials = "?"; if (this.showInitials) { const user = this.userIdentifier.trim(); if (user === "") { return initials; } const filteredChars = user.match(/[\p{L}\p{N}\s]/gu); if (!filteredChars) { return initials; } const filtered = filteredChars.join(""); const idx = filtered.lastIndexOf(" "); initials = String.fromCodePoint(filtered.codePointAt(0)); if (idx !== -1) { initials = initials.concat(String.fromCodePoint(filtered.codePointAt(idx + 1))); } } return initials.toLocaleUpperCase(); }, menu() { const actions = this.contactsMenuActions.map((item) => { const route = getRoute(this.$router, item.hyperlink); return { ncActionComponent: route ? NcActionRouter : NcActionLink, ncActionComponentProps: route ? { to: route, icon: item.icon } : { href: item.hyperlink, icon: item.icon }, text: item.title }; }); for (const action of getEnabledContactsMenuActions(this.contactsMenuData)) { try { actions.push({ ncActionComponent: NcActionButton, ncActionComponentProps: { onClick: () => action.callback(this.contactsMenuData) }, text: action.displayName(this.contactsMenuData), iconSvg: action.iconSvg(this.contactsMenuData) }); } catch (error) { logger.error(`Failed to render ContactsMenu action ${action.id}`, { error, action }); } } function escape(html) { const text = document.createTextNode(html); const p = document.createElement("p"); p.appendChild(text); return p.innerHTML; } if (!this.hideStatus && (this.userStatus.icon || this.userStatus.message)) { const emojiIcon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> <text x="50%" y="50%" text-anchor="middle" style="dominant-baseline: central; font-size: 85%">${escape(this.userStatus.icon)}</text> </svg>`; return [{ ncActionComponent: NcActionText, ncActionComponentProps: {}, iconSvg: this.userStatus.icon ? emojiIcon : void 0, text: `${this.userStatus.message}` }].concat(actions); } return actions; } }, watch: { url() { this.userDoesNotExist = false; this.loadAvatarUrl(); }, user() { this.userDoesNotExist = false; this.isMenuLoaded = false; this.loadAvatarUrl(); } }, mounted() { this.loadAvatarUrl(); subscribe("settings:avatar:updated", this.loadAvatarUrl); subscribe("settings:display-name:updated", this.loadAvatarUrl); if (!this.hideStatus && this.user && !this.isNoUser) { if (!this.preloadedUserStatus) { this.fetchUserStatus(this.user); } else { this.setUserStatus(this.preloadedUserStatus); } subscribe("user_status:status.updated", this.handleUserStatusUpdated); } else if (!this.hideStatus && this.preloadedUserStatus) { this.setUserStatus(this.preloadedUserStatus); } }, beforeUnmount() { unsubscribe("settings:avatar:updated", this.loadAvatarUrl); unsubscribe("settings:display-name:updated", this.loadAvatarUrl); unsubscribe("user_status:status.updated", this.handleUserStatusUpdated); }, methods: { t, handleUserStatusUpdated(state) { if (this.user === state.userId) { this.userStatus = { status: state.status, icon: state.icon, message: state.message }; this.hasStatus = state.status !== null; } }, /** * Toggle the popover menu on click or enter * * @param {KeyboardEvent|MouseEvent} event the UI event */ async toggleMenu(event) { if (event.type === "keydown" && event.key !== "Enter") { return; } if (!this.contactsMenuOpenState) { await this.fetchContactsMenu(); } this.contactsMenuOpenState = !this.contactsMenuOpenState; }, closeMenu() { this.contactsMenuOpenState = false; }, async fetchContactsMenu() { this.contactsMenuLoading = true; try { const user = encodeURIComponent(this.user); const { data } = await axios.post(generateUrl("contactsmenu/findOne"), `shareType=0&shareWith=${user}`); this.contactsMenuData = data; this.contactsMenuActions = data.topAction ? [data.topAction].concat(data.actions) : data.actions; } catch { this.contactsMenuOpenState = false; } this.contactsMenuLoading = false; this.isMenuLoaded = true; }, /** * Handle avatar loading if user or url defined */ loadAvatarUrl() { this.isAvatarLoaded = false; if (!this.isUrlDefined && (!this.isUserDefined || this.isNoUser || this.iconClass || this.$slots.icon)) { this.isAvatarLoaded = true; this.userDoesNotExist = true; return; } if (this.isUrlDefined) { this.updateImageIfValid(this.url); return; } if (this.size <= 64) { const avatarUrl = this.avatarUrlGenerator(this.user, 64); const srcset = [ avatarUrl + " 1x", this.avatarUrlGenerator(this.user, 512) + " 8x" ].join(", "); this.updateImageIfValid(avatarUrl, srcset); } else { const avatarUrl = this.avatarUrlGenerator(this.user, 512); this.updateImageIfValid(avatarUrl); } }, /** * Generate an avatar url from the server's avatar endpoint * * @param {string} user the user id * @param {number} size the desired size * @return {string} */ avatarUrlGenerator(user, size) { let avatarUrl = getAvatarUrl(user, { size, isDarkTheme: this.isDarkTheme, isGuest: this.isGuest }); if (user === getCurrentUser()?.uid && typeof oc_userconfig !== "undefined") { avatarUrl += "?v=" + window.oc_userconfig.avatar.version; } return avatarUrl; }, /** * Check if the provided url is valid and update Avatar if so * * @param {string} url the avatar url * @param {Array} srcset the avatar srcset */ updateImageIfValid(url, srcset = null) { const userHasAvatar = getUserHasAvatar(this.user); if (this.isUserDefined && typeof userHasAvatar === "boolean") { this.isAvatarLoaded = true; this.avatarUrlLoaded = url; if (srcset) { this.avatarSrcSetLoaded = srcset; } if (userHasAvatar === false) { this.userDoesNotExist = true; } return; } const img = new Image(); img.onload = () => { this.avatarUrlLoaded = url; if (srcset) { this.avatarSrcSetLoaded = srcset; } this.isAvatarLoaded = true; setUserHasAvatar(this.user, true); }; img.onerror = (error) => { logger.debug("[NcAvatar] Invalid avatar url", { error, url }); this.avatarUrlLoaded = null; this.avatarSrcSetLoaded = null; this.userDoesNotExist = true; this.isAvatarLoaded = false; setUserHasAvatar(this.user, false); }; if (srcset) { img.srcset = srcset; } img.src = url; } } }; const _hoisted_1 = ["title"]; const _hoisted_2 = ["src", "srcset"]; const _hoisted_3 = { key: 2, class: "avatardiv__user-status avatardiv__user-status--icon" }; function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) { const _component_NcLoadingIcon = resolveComponent("NcLoadingIcon"); const _component_IconDotsHorizontal = resolveComponent("IconDotsHorizontal"); const _component_NcButton = resolveComponent("NcButton"); const _component_NcIconSvgWrapper = resolveComponent("NcIconSvgWrapper"); const _component_NcActions = resolveComponent("NcActions"); const _component_NcUserStatusIcon = resolveComponent("NcUserStatusIcon"); const _directive_click_outside = resolveDirective("click-outside"); return withDirectives((openBlock(), createElementBlock("span", { class: normalizeClass(["avatardiv popovermenu-wrapper", { "avatardiv--unknown": $data.userDoesNotExist, "avatardiv--with-menu": $options.hasMenu, "avatardiv--with-menu-loading": $data.contactsMenuLoading }]), style: normalizeStyle($options.avatarStyle), title: $options.tooltip }, [ renderSlot(_ctx.$slots, "icon", {}, () => [ $props.iconClass ? (openBlock(), createElementBlock("span", { key: 0, class: normalizeClass([$props.iconClass, "avatar-class-icon"]) }, null, 2)) : $data.isAvatarLoaded && !$data.userDoesNotExist ? (openBlock(), createElementBlock("img", { key: 1, src: $data.avatarUrlLoaded, srcset: $data.avatarSrcSetLoaded, alt: "" }, null, 8, _hoisted_2)) : createCommentVNode("", true) ], true), $options.hasMenu && $options.menu.length === 0 ? (openBlock(), createBlock(_component_NcButton, { key: 0, "aria-label": $options.avatarAriaLabel, class: "action-item action-item__menutoggle", variant: "tertiary-no-background", onClick: $options.toggleMenu }, { icon: withCtx(() => [ $data.contactsMenuLoading ? (openBlock(), createBlock(_component_NcLoadingIcon, { key: 0 })) : (openBlock(), createBlock(_component_IconDotsHorizontal, { key: 1, size: 20 })) ]), _: 1 }, 8, ["aria-label", "onClick"])) : $options.hasMenu ? (openBlock(), createBlock(_component_NcActions, { key: 1, open: $data.contactsMenuOpenState, "onUpdate:open": _cache[0] || (_cache[0] = ($event) => $data.contactsMenuOpenState = $event), "aria-label": $options.avatarAriaLabel, container: $props.menuContainer, "force-menu": "", "manual-open": "", variant: "tertiary-no-background", onClick: $options.toggleMenu }, createSlots({ default: withCtx(() => [ (openBlock(true), createElementBlock(Fragment, null, renderList($options.menu, (item, key) => { return openBlock(), createBlock(resolveDynamicComponent(item.ncActionComponent), mergeProps({ key }, { ref_for: true }, item.ncActionComponentProps), createSlots({ default: withCtx(() => [ createTextVNode(" " + toDisplayString(item.text), 1) ]), _: 2 }, [ item.iconSvg ? { name: "icon", fn: withCtx(() => [ createVNode(_component_NcIconSvgWrapper, { svg: item.iconSvg }, null, 8, ["svg"]) ]), key: "0" } : void 0 ]), 1040); }), 128)) ]), _: 2 }, [ $data.contactsMenuLoading ? { name: "icon", fn: withCtx(() => [ createVNode(_component_NcLoadingIcon) ]), key: "0" } : void 0 ]), 1032, ["open", "aria-label", "container", "onClick"])) : createCommentVNode("", true), $options.showUserStatusIconOnAvatar ? (openBlock(), createElementBlock("span", _hoisted_3, toDisplayString(_ctx.userStatus.icon), 1)) : $options.canDisplayUserStatus ? (openBlock(), createBlock(_component_NcUserStatusIcon, { key: 3, class: "avatardiv__user-status", status: _ctx.userStatus.status, "aria-hidden": String($options.hasMenu) }, null, 8, ["status", "aria-hidden"])) : createCommentVNode("", true), $options.showInitials ? (openBlock(), createElementBlock("span", { key: 4, style: normalizeStyle($options.initialsWrapperStyle), class: "avatardiv__initials-wrapper" }, [ createElementVNode("span", { style: normalizeStyle($options.initialsStyle), class: "avatardiv__initials" }, toDisplayString($options.initials), 5) ], 4)) : createCommentVNode("", true) ], 14, _hoisted_1)), [ [_directive_click_outside, $options.closeMenu] ]); } const NcAvatar = /* @__PURE__ */ _export_sfc(_sfc_main, [["render", _sfc_render], ["__scopeId", "data-v-d7dc2a1f"]]); export { NcAvatar as N, userStatus as u }; //# sourceMappingURL=NcAvatar-DmUGApWA.mjs.map