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 */}
-
+ {photoComponent ?? (
+ // eslint-disable-next-line @next/next/no-img-element
+
+ )}
{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 (