@nextcloud/vue
Version:
Nextcloud vue components
686 lines (685 loc) • 22.9 kB
JavaScript
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