feature: add identicon generation to users without photo

Signed-off-by: Erik Michelson <github@erik.michelson.eu>
This commit is contained in:
Erik Michelson 2023-10-07 13:45:24 +02:00
parent 55398e2428
commit a8b3b117dc
17 changed files with 210 additions and 23 deletions

View file

@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
@ -7,5 +7,5 @@
export interface UserInfo {
username: string
displayName: string
photo: string
photoUrl: string
}

View file

@ -104,3 +104,47 @@ exports[`UserAvatar renders the user avatar in size sm 1`] = `
</span>
</div>
`;
exports[`UserAvatar uses identicon when empty photoUrl is given 1`] = `
<div>
<span
class="d-inline-flex align-items-center "
>
<img
alt="common.avatarOf"
class="rounded user-image"
height="20"
src="data:image/x-other,identicon-mock"
title="common.avatarOf"
width="20"
/>
<span
class="ms-2 me-1 user-line-name"
>
test
</span>
</span>
</div>
`;
exports[`UserAvatar uses identicon when no photoUrl is given 1`] = `
<div>
<span
class="d-inline-flex align-items-center "
>
<img
alt="common.avatarOf"
class="rounded user-image"
height="20"
src="data:image/x-other,identicon-mock"
title="common.avatarOf"
width="20"
/>
<span
class="ms-2 me-1 user-line-name"
>
test
</span>
</span>
</div>
`;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

View file

@ -1,3 +0,0 @@
SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
SPDX-License-Identifier: CC0-1.0

View file

@ -0,0 +1,28 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useMemo } from 'react'
import { createAvatar } from '@dicebear/core'
import identicon from '@dicebear/identicon'
/**
* Returns the correct avatar url for a user.
* When an empty or no photoUrl is given, a random avatar is generated from the displayName.
*
* @param photoUrl The photo url of the user to use. Maybe empty or not set.
* @param displayName The display name of the user to use as input to the random avatar.
* @return The correct avatar url for the user.
*/
export const useAvatarUrl = (photoUrl: string | undefined, displayName: string): string => {
return useMemo(() => {
if (photoUrl && photoUrl.trim() !== '') {
return photoUrl
}
const avatar = createAvatar(identicon, {
seed: displayName
})
return avatar.toDataUriSync()
}, [photoUrl, displayName])
}

View file

@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
@ -19,5 +19,5 @@ export interface UserAvatarForUserProps extends Omit<UserAvatarProps, 'photoUrl'
* @param props remaining avatar props
*/
export const UserAvatarForUser: React.FC<UserAvatarForUserProps> = ({ user, ...props }) => {
return <UserAvatar displayName={user.displayName} photoUrl={user.photo} {...props} />
return <UserAvatar displayName={user.displayName} photoUrl={user.photoUrl} {...props} />
}

View file

@ -7,12 +7,20 @@ import type { UserInfo } from '../../../api/users/types'
import { mockI18n } from '../../../test-utils/mock-i18n'
import { UserAvatarForUser } from './user-avatar-for-user'
import { render } from '@testing-library/react'
import { UserAvatar } from './user-avatar'
jest.mock('@dicebear/identicon', () => null)
jest.mock('@dicebear/core', () => ({
createAvatar: jest.fn(() => ({
toDataUriSync: jest.fn(() => 'data:image/x-other,identicon-mock')
}))
}))
describe('UserAvatar', () => {
const user: UserInfo = {
username: 'boatface',
displayName: 'Boaty McBoatFace',
photo: 'https://example.com/test.png'
photoUrl: 'https://example.com/test.png'
}
beforeEach(async () => {
@ -41,4 +49,14 @@ describe('UserAvatar', () => {
const view = render(<UserAvatarForUser user={user} showName={false} />)
expect(view.container).toMatchSnapshot()
})
it('uses identicon when no photoUrl is given', () => {
const view = render(<UserAvatar displayName={'test'} />)
expect(view.container).toMatchSnapshot()
})
it('uses identicon when empty photoUrl is given', () => {
const view = render(<UserAvatar displayName={'test'} photoUrl={''} />)
expect(view.container).toMatchSnapshot()
})
})

View file

@ -1,15 +1,15 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useTranslatedText } from '../../../hooks/common/use-translated-text'
import { ShowIf } from '../show-if/show-if'
import defaultAvatar from './default-avatar.png'
import styles from './user-avatar.module.scss'
import React, { useCallback, useMemo } from 'react'
import { OverlayTrigger, Tooltip } from 'react-bootstrap'
import type { OverlayInjectedProps } from 'react-bootstrap/Overlay'
import { useAvatarUrl } from './hooks/use-avatar-url'
export interface UserAvatarProps {
size?: 'sm' | 'lg'
@ -45,9 +45,7 @@ export const UserAvatar: React.FC<UserAvatarProps> = ({
}
}, [size])
const avatarUrl = useMemo(() => {
return photoUrl || defaultAvatar.src
}, [photoUrl])
const avatarUrl = useAvatarUrl(photoUrl, displayName)
const imageTranslateOptions = useMemo(
() => ({

View file

@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
@ -15,7 +15,7 @@ export const fetchAndSetUser: () => Promise<void> = async () => {
setUser({
username: me.username,
displayName: me.displayName,
photo: me.photo,
photoUrl: me.photoUrl,
authProvider: me.authProvider,
email: me.email
})

View file

@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
@ -15,7 +15,7 @@ const handler = (req: NextApiRequest, res: NextApiResponse): void => {
}
respondToMatchingRequest<LoginUserInfo>(HttpMethod.GET, req, res, {
username: 'mock',
photo: '/public/img/avatar.png',
photoUrl: '/public/img/avatar.png',
displayName: 'Mock User',
authProvider: 'local',
email: 'mock@hedgedoc.test'

View file

@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
@ -11,7 +11,7 @@ const handler = (req: NextApiRequest, res: NextApiResponse): void => {
respondToMatchingRequest<UserInfo>(HttpMethod.GET, req, res, {
username: 'erik',
displayName: 'Erik',
photo: '/public/img/avatar.png'
photoUrl: '/public/img/avatar.png'
})
}

View file

@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
@ -11,7 +11,7 @@ const handler = (req: NextApiRequest, res: NextApiResponse): void => {
respondToMatchingRequest<UserInfo>(HttpMethod.GET, req, res, {
username: 'molly',
displayName: 'Molly',
photo: '/public/img/avatar.png'
photoUrl: '/public/img/avatar.png'
})
}

View file

@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
@ -11,7 +11,7 @@ const handler = (req: NextApiRequest, res: NextApiResponse): void => {
respondToMatchingRequest<UserInfo>(HttpMethod.GET, req, res, {
username: 'tilman',
displayName: 'Tilman',
photo: '/public/img/avatar.png'
photoUrl: '/public/img/avatar.png'
})
}