fix(permissions): show guest avatar when note owner is anonymous

Signed-off-by: Erik Michelson <github@erik.michelson.eu>
This commit is contained in:
Erik Michelson 2024-09-15 22:04:23 +02:00 committed by Philip Molares
parent 62dfe4df72
commit ebf8e3a759
7 changed files with 127 additions and 11 deletions

View file

@ -0,0 +1,16 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`GuestUserAvatar renders the guest user avatar correctly 1`] = `
<div>
<span
class="d-inline-flex align-items-center "
>
BootstrapIconMock_Person
<span
class="ms-2 me-1 user-line-name"
>
common.guestUser
</span>
</span>
</div>
`;

View file

@ -105,6 +105,40 @@ exports[`UserAvatar renders the user avatar in size sm 1`] = `
</div> </div>
`; `;
exports[`UserAvatar uses custom photo component if provided 1`] = `
<div>
<span
class="d-inline-flex align-items-center "
>
<div>
Custom Photo
</div>
<span
class="ms-2 me-1 user-line-name"
>
test
</span>
</span>
</div>
`;
exports[`UserAvatar uses custom photo component preferred over photoUrl 1`] = `
<div>
<span
class="d-inline-flex align-items-center "
>
<div>
Custom Photo
</div>
<span
class="ms-2 me-1 user-line-name"
>
test
</span>
</span>
</div>
`;
exports[`UserAvatar uses identicon when empty photoUrl is given 1`] = ` exports[`UserAvatar uses identicon when empty photoUrl is given 1`] = `
<div> <div>
<span <span

View file

@ -0,0 +1,26 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { mockI18n } from '../../../test-utils/mock-i18n'
import { render } from '@testing-library/react'
import { GuestUserAvatar } from './guest-user-avatar'
jest.mock('@dicebear/identicon', () => 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(<GuestUserAvatar />)
expect(view.container).toMatchSnapshot()
})
})

View file

@ -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<UserAvatarProps, 'displayName' | 'photoUrl' | 'username'>
/**
* The avatar component for an anonymous user.
* @param props The properties of the guest user avatar ({@link UserAvatarProps})
*/
export const GuestUserAvatar: React.FC<GuestUserAvatarProps> = (props) => {
const label = useTranslatedText('common.guestUser')
return <UserAvatar displayName={label} photoComponent={<IconPerson />} {...props} />
}

View file

@ -59,4 +59,16 @@ describe('UserAvatar', () => {
const view = render(<UserAvatar displayName={'test'} photoUrl={''} />) const view = render(<UserAvatar displayName={'test'} photoUrl={''} />)
expect(view.container).toMatchSnapshot() expect(view.container).toMatchSnapshot()
}) })
it('uses custom photo component if provided', () => {
const view = render(<UserAvatar displayName={'test'} photoComponent={<div>Custom Photo</div>} />)
expect(view.container).toMatchSnapshot()
})
it('uses custom photo component preferred over photoUrl', () => {
const view = render(
<UserAvatar displayName={'test'} photoComponent={<div>Custom Photo</div>} photoUrl={user.photoUrl} />
)
expect(view.container).toMatchSnapshot()
})
}) })

View file

@ -15,6 +15,7 @@ export interface UserAvatarProps {
photoUrl?: string photoUrl?: string
displayName: string displayName: string
username?: string | null 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 size The size in which the user image should be shown.
* @param additionalClasses Additional CSS classes that will be added to the container. * @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 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<UserAvatarProps> = ({ export const UserAvatar: React.FC<UserAvatarProps> = ({
photoUrl, photoUrl,
@ -31,7 +34,8 @@ export const UserAvatar: React.FC<UserAvatarProps> = ({
size, size,
additionalClasses = '', additionalClasses = '',
showName = true, showName = true,
username username,
photoComponent
}) => { }) => {
const imageSize = useMemo(() => { const imageSize = useMemo(() => {
switch (size) { switch (size) {
@ -56,15 +60,17 @@ export const UserAvatar: React.FC<UserAvatarProps> = ({
return ( return (
<span className={'d-inline-flex align-items-center ' + additionalClasses}> <span className={'d-inline-flex align-items-center ' + additionalClasses}>
{/* eslint-disable-next-line @next/next/no-img-element */} {photoComponent ?? (
<img // eslint-disable-next-line @next/next/no-img-element
src={avatarUrl} <img
className={`rounded ${styles['user-image']}`} src={avatarUrl}
alt={imgDescription} className={`rounded ${styles['user-image']}`}
title={imgDescription} alt={imgDescription}
height={imageSize} title={imgDescription}
width={imageSize} height={imageSize}
/> width={imageSize}
/>
)}
{showName && <span className={`ms-2 me-1 ${styles['user-line-name']}`}>{displayName}</span>} {showName && <span className={`ms-2 me-1 ${styles['user-line-name']}`}>{displayName}</span>}
</span> </span>
) )

View file

@ -11,6 +11,7 @@ import type { PermissionDisabledProps } from './permission-disabled.prop'
import React, { Fragment } from 'react' import React, { Fragment } from 'react'
import { Button } from 'react-bootstrap' import { Button } from 'react-bootstrap'
import { Pencil as IconPencil } from 'react-bootstrap-icons' import { Pencil as IconPencil } from 'react-bootstrap-icons'
import { GuestUserAvatar } from '../../../../../common/user-avatar/guest-user-avatar'
export interface PermissionOwnerInfoProps { export interface PermissionOwnerInfoProps {
onEditOwner: () => void onEditOwner: () => void
@ -30,7 +31,7 @@ export const PermissionOwnerInfo: React.FC<PermissionOwnerInfoProps & Permission
const buttonTitle = useTranslatedText('editor.modal.permissions.ownerChange.button') const buttonTitle = useTranslatedText('editor.modal.permissions.ownerChange.button')
if (!noteOwner) { if (!noteOwner) {
return null return <GuestUserAvatar />
} }
return ( return (