diff --git a/frontend/src/components/common/user-avatar/__snapshots__/guest-user-avatar.spec.tsx.snap b/frontend/src/components/common/user-avatar/__snapshots__/guest-user-avatar.spec.tsx.snap new file mode 100644 index 000000000..527579f37 --- /dev/null +++ b/frontend/src/components/common/user-avatar/__snapshots__/guest-user-avatar.spec.tsx.snap @@ -0,0 +1,16 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`GuestUserAvatar renders the guest user avatar correctly 1`] = ` +
+ + BootstrapIconMock_Person + + common.guestUser + + +
+`; diff --git a/frontend/src/components/common/user-avatar/__snapshots__/user-avatar.spec.tsx.snap b/frontend/src/components/common/user-avatar/__snapshots__/user-avatar.spec.tsx.snap index ecddb7e72..234e5a785 100644 --- a/frontend/src/components/common/user-avatar/__snapshots__/user-avatar.spec.tsx.snap +++ b/frontend/src/components/common/user-avatar/__snapshots__/user-avatar.spec.tsx.snap @@ -105,6 +105,40 @@ exports[`UserAvatar renders the user avatar in size sm 1`] = ` `; +exports[`UserAvatar uses custom photo component if provided 1`] = ` +
+ +
+ Custom Photo +
+ + test + +
+
+`; + +exports[`UserAvatar uses custom photo component preferred over photoUrl 1`] = ` +
+ +
+ Custom Photo +
+ + test + +
+
+`; + exports[`UserAvatar uses identicon when empty photoUrl is given 1`] = `
null) +jest.mock('@dicebear/core', () => ({ + createAvatar: jest.fn(() => ({ + toDataUri: jest.fn(() => 'data:image/x-other,identicon-mock') + })) +})) + +describe('GuestUserAvatar', () => { + beforeEach(async () => { + await mockI18n() + }) + + it('renders the guest user avatar correctly', () => { + const view = render() + expect(view.container).toMatchSnapshot() + }) +}) diff --git a/frontend/src/components/common/user-avatar/guest-user-avatar.tsx b/frontend/src/components/common/user-avatar/guest-user-avatar.tsx new file mode 100644 index 000000000..e5a29e52e --- /dev/null +++ b/frontend/src/components/common/user-avatar/guest-user-avatar.tsx @@ -0,0 +1,21 @@ +/* + * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +import React from 'react' +import type { UserAvatarProps } from './user-avatar' +import { UserAvatar } from './user-avatar' +import { useTranslatedText } from '../../../hooks/common/use-translated-text' +import { Person as IconPerson } from 'react-bootstrap-icons' + +export type GuestUserAvatarProps = Omit + +/** + * The avatar component for an anonymous user. + * @param props The properties of the guest user avatar ({@link UserAvatarProps}) + */ +export const GuestUserAvatar: React.FC = (props) => { + const label = useTranslatedText('common.guestUser') + return } {...props} /> +} diff --git a/frontend/src/components/common/user-avatar/user-avatar.spec.tsx b/frontend/src/components/common/user-avatar/user-avatar.spec.tsx index d90b80af4..29c84952e 100644 --- a/frontend/src/components/common/user-avatar/user-avatar.spec.tsx +++ b/frontend/src/components/common/user-avatar/user-avatar.spec.tsx @@ -59,4 +59,16 @@ describe('UserAvatar', () => { const view = render() expect(view.container).toMatchSnapshot() }) + + it('uses custom photo component if provided', () => { + const view = render(Custom Photo
} />) + expect(view.container).toMatchSnapshot() + }) + + it('uses custom photo component preferred over photoUrl', () => { + const view = render( + Custom Photo} photoUrl={user.photoUrl} /> + ) + expect(view.container).toMatchSnapshot() + }) }) diff --git a/frontend/src/components/common/user-avatar/user-avatar.tsx b/frontend/src/components/common/user-avatar/user-avatar.tsx index 2447d39c9..182dbb045 100644 --- a/frontend/src/components/common/user-avatar/user-avatar.tsx +++ b/frontend/src/components/common/user-avatar/user-avatar.tsx @@ -15,6 +15,7 @@ export interface UserAvatarProps { photoUrl?: string displayName: string username?: string | null + photoComponent?: React.ReactNode } /** @@ -24,6 +25,8 @@ export interface UserAvatarProps { * @param size The size in which the user image should be shown. * @param additionalClasses Additional CSS classes that will be added to the container. * @param showName true when the name should be displayed alongside the image, false otherwise. Defaults to true. + * @param username The username to use for generating the fallback avatar image. + * @param photoComponent A custom component to use as the user's photo. */ export const UserAvatar: React.FC = ({ photoUrl, @@ -31,7 +34,8 @@ export const UserAvatar: React.FC = ({ size, additionalClasses = '', showName = true, - username + username, + photoComponent }) => { const imageSize = useMemo(() => { switch (size) { @@ -56,15 +60,17 @@ export const UserAvatar: React.FC = ({ return ( - {/* eslint-disable-next-line @next/next/no-img-element */} - {imgDescription} + {photoComponent ?? ( + // eslint-disable-next-line @next/next/no-img-element + {imgDescription} + )} {showName && {displayName}} ) diff --git a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/permissions-sidebar-entry/permissions-modal/permission-owner-info.tsx b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/permissions-sidebar-entry/permissions-modal/permission-owner-info.tsx index d738bab88..c078fcc9d 100644 --- a/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/permissions-sidebar-entry/permissions-modal/permission-owner-info.tsx +++ b/frontend/src/components/editor-page/sidebar/specific-sidebar-entries/permissions-sidebar-entry/permissions-modal/permission-owner-info.tsx @@ -11,6 +11,7 @@ import type { PermissionDisabledProps } from './permission-disabled.prop' import React, { Fragment } from 'react' import { Button } from 'react-bootstrap' import { Pencil as IconPencil } from 'react-bootstrap-icons' +import { GuestUserAvatar } from '../../../../../common/user-avatar/guest-user-avatar' export interface PermissionOwnerInfoProps { onEditOwner: () => void @@ -30,7 +31,7 @@ export const PermissionOwnerInfo: React.FC } return (