diff --git a/src/components/NcAvatar/NcAvatar.vue b/src/components/NcAvatar/NcAvatar.vue index 5823d6149e..f33bf0ad95 100644 --- a/src/components/NcAvatar/NcAvatar.vue +++ b/src/components/NcAvatar/NcAvatar.vue @@ -196,7 +196,8 @@ export default { + v-bind="item.ncActionComponentProps" + v-on="item.ncActionComponentHandlers"> @@ -232,16 +233,19 @@ import NcActions from '../NcActions/index.js' import NcActionLink from '../NcActionLink/index.js' import NcActionRouter from '../NcActionRouter/index.js' import NcActionText from '../NcActionText/index.js' +import NcActionButton from '../NcActionButton/index.js' import NcButton from '../NcButton/index.js' import NcIconSvgWrapper from '../NcIconSvgWrapper/index.js' import NcLoadingIcon from '../NcLoadingIcon/index.js' import NcUserStatusIcon from '../NcUserStatusIcon/index.js' import usernameToColor from '../../functions/usernameToColor/index.js' +import { getEnabledContactsMenuActions } from '../../functions/contactsMenu/index.ts' import { getAvatarUrl } from '../../utils/getAvatarUrl.ts' import { getUserStatusText } from '../../utils/UserStatus.ts' import { userStatus } from '../../mixins/index.js' import { t } from '../../l10n.js' import { getRoute } from '../../components/NcRichText/autolink.js' +import logger from '../../utils/logger.js' import axios from '@nextcloud/axios' import DotsHorizontal from 'vue-material-design-icons/DotsHorizontal.vue' @@ -419,6 +423,7 @@ export default { isAvatarLoaded: false, isMenuLoaded: false, contactsMenuLoading: false, + contactsMenuData: {}, contactsMenuActions: [], contactsMenuOpenState: false, } @@ -566,6 +571,25 @@ export default { } }) + for (const action of getEnabledContactsMenuActions(this.contactsMenuData)) { + try { + actions.push({ + ncActionComponent: NcActionButton, + ncActionComponentProps: {}, + ncActionComponentHandlers: { + click: () => 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, + }) + } + } + /** * @param {string} html The HTML to escape */ @@ -663,6 +687,7 @@ export default { 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 (e) { this.contactsMenuOpenState = false diff --git a/src/functions/contactsMenu/index.ts b/src/functions/contactsMenu/index.ts new file mode 100644 index 0000000000..b9e73a3d2b --- /dev/null +++ b/src/functions/contactsMenu/index.ts @@ -0,0 +1,64 @@ +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import logger from '../../utils/logger.js' + +// Taken from \OC\Contacts\ContactsMenu\Entry::jsonSerialize +export interface ContactsMenuEntry { + id: number|string|null, + fullName: string, + avatar: string|null, + topAction: object|null, + actions: object[], + lastMessage: '', + emailAddresses: string[], + profileTitle: string|null, + profileUrl: string|null, + status: string|null, + statusMessage: null|string, + statusMessageTimestamp: null|number, + statusIcon: null|string, + isUser: boolean, + uid: null|string, +} + +export interface ContactsMenuAction { + id: string, + displayName: (entry: ContactsMenuEntry) => string, + enabled: (entry: ContactsMenuEntry) => boolean, + iconSvg: (entry: ContactsMenuEntry) => string, + callback: (entry: ContactsMenuEntry) => void, +} + +/** + * Register a contacts and avatar menu action that will invoke the given callback on click. + * + * @param {ContactsMenuAction} action The action to register + */ +export function registerContactsMenuAction(action: ContactsMenuAction): void { + window._nc_contacts_menu_hooks ??= {} + + if (window._nc_contacts_menu_hooks[action.id]) { + logger.error(`ContactsMenu action for id ${action.id} has already been registered`, { + action, + }) + return + } + + window._nc_contacts_menu_hooks[action.id] = action +} + +/** + * Get all registered and enabled contacts menu actions for the given menu entry. + * + * @param {ContactsMenuEntry} entry The contacts menu entry object as returned by the backend + */ +export function getEnabledContactsMenuActions(entry: ContactsMenuEntry): ContactsMenuAction[] { + if (!window._nc_contacts_menu_hooks) { + return [] + } + + return Object.values(window._nc_contacts_menu_hooks).filter((action) => action.enabled(entry)) +} diff --git a/src/functions/index.ts b/src/functions/index.ts index bc0b822256..5e5bed97b6 100644 --- a/src/functions/index.ts +++ b/src/functions/index.ts @@ -8,4 +8,5 @@ export * from './dialog/index.ts' export * from './emoji/index.ts' export * from './reference/index.js' export * from './isDarkTheme/index.ts' +export * from './contactsMenu/index.ts' export { default as usernameToColor } from './usernameToColor/index.js' diff --git a/src/globals.d.ts b/src/globals.d.ts index 1af6488de7..305a457e4e 100644 --- a/src/globals.d.ts +++ b/src/globals.d.ts @@ -3,6 +3,8 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ +import type { ContactsMenuAction } from './functions/contactsMenu/index.ts' + declare const PRODUCTION: boolean declare const SCOPE_VERSION: string @@ -14,3 +16,9 @@ declare module '*?raw' { const content: string export default content } + +declare global { + interface Window { + _nc_contacts_menu_hooks: { [id: string]: ContactsMenuAction }, + } +}