refactor: split avatar component to handle displaynames

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
Tilman Vatteroth 2023-03-24 13:16:35 +01:00
parent 3a06f84af1
commit e97a426680
9 changed files with 88 additions and 44 deletions

View file

@ -0,0 +1,23 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { UserInfo } from '../../../api/users/types'
import type { UserAvatarProps } from './user-avatar'
import { UserAvatar } from './user-avatar'
import React from 'react'
export interface UserAvatarForUserProps extends Omit<UserAvatarProps, 'photoUrl' | 'displayName'> {
user: UserInfo
}
/**
* Renders the avatar image of a user, optionally altogether with their name.
*
* @param user The user object with the display name and photo.
* @param props remaining avatar props
*/
export const UserAvatarForUser: React.FC<UserAvatarForUserProps> = ({ user, ...props }) => {
return <UserAvatar displayName={user.displayName} photoUrl={user.photo} {...props} />
}

View file

@ -4,15 +4,14 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { getUser } from '../../../api/users'
import type { UserInfo } from '../../../api/users/types'
import { AsyncLoadingBoundary } from '../async-loading-boundary/async-loading-boundary'
import type { UserAvatarProps } from './user-avatar'
import { UserAvatar } from './user-avatar'
import React from 'react'
import React, { Fragment, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useAsync } from 'react-use'
export interface UserAvatarForUsernameProps extends Omit<UserAvatarProps, 'user'> {
export interface UserAvatarForUsernameProps extends Omit<UserAvatarProps, 'photoUrl' | 'displayName'> {
username: string | null
}
@ -27,20 +26,21 @@ export interface UserAvatarForUsernameProps extends Omit<UserAvatarProps, 'user'
*/
export const UserAvatarForUsername: React.FC<UserAvatarForUsernameProps> = ({ username, ...props }) => {
const { t } = useTranslation()
const { error, value, loading } = useAsync(async (): Promise<UserInfo> => {
if (username) {
return await getUser(username)
}
return {
displayName: t('common.guestUser'),
photo: `public/img/avatar.png`,
username: ''
}
const { error, value, loading } = useAsync(async (): Promise<{ displayName: string; photo?: string }> => {
return username
? await getUser(username)
: {
displayName: t('common.guestUser')
}
}, [username, t])
const avatar = useMemo(() => {
return !value ? <Fragment /> : <UserAvatar displayName={value.displayName} photoUrl={value.photo} {...props} />
}, [props, value])
return (
<AsyncLoadingBoundary loading={loading || !value} error={error} componentName={'UserAvatarForUsername'}>
<UserAvatar user={value as UserInfo} {...props} />
{avatar}
</AsyncLoadingBoundary>
)
}

View file

@ -5,7 +5,7 @@
*/
import type { UserInfo } from '../../../api/users/types'
import { mockI18n } from '../../markdown-renderer/test-utils/mock-i18n'
import { UserAvatar } from './user-avatar'
import { UserAvatarForUser } from './user-avatar-for-user'
import { render } from '@testing-library/react'
describe('UserAvatar', () => {
@ -20,25 +20,25 @@ describe('UserAvatar', () => {
})
it('renders the user avatar correctly', () => {
const view = render(<UserAvatar user={user} />)
const view = render(<UserAvatarForUser user={user} />)
expect(view.container).toMatchSnapshot()
})
describe('renders the user avatar in size', () => {
it('sm', () => {
const view = render(<UserAvatar user={user} size={'sm'} />)
const view = render(<UserAvatarForUser user={user} size={'sm'} />)
expect(view.container).toMatchSnapshot()
})
it('lg', () => {
const view = render(<UserAvatar user={user} size={'lg'} />)
const view = render(<UserAvatarForUser user={user} size={'lg'} />)
expect(view.container).toMatchSnapshot()
})
})
it('adds additionalClasses props to wrapping span', () => {
const view = render(<UserAvatar user={user} additionalClasses={'testClass'} />)
const view = render(<UserAvatarForUser user={user} additionalClasses={'testClass'} />)
expect(view.container).toMatchSnapshot()
})
it('does not show names if showName prop is false', () => {
const view = render(<UserAvatar user={user} showName={false} />)
const view = render(<UserAvatarForUser user={user} showName={false} />)
expect(view.container).toMatchSnapshot()
})
})

View file

@ -3,7 +3,6 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { UserInfo } from '../../../api/users/types'
import { ShowIf } from '../show-if/show-if'
import defaultAvatar from './default-avatar.png'
import styles from './user-avatar.module.scss'
@ -16,7 +15,8 @@ export interface UserAvatarProps {
size?: 'sm' | 'lg'
additionalClasses?: string
showName?: boolean
user: UserInfo
photoUrl?: string
displayName: string
}
/**
@ -27,7 +27,13 @@ export interface UserAvatarProps {
* @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.
*/
export const UserAvatar: React.FC<UserAvatarProps> = ({ user, size, additionalClasses = '', showName = true }) => {
export const UserAvatar: React.FC<UserAvatarProps> = ({
photoUrl,
displayName,
size,
additionalClasses = '',
showName = true
}) => {
const { t } = useTranslation()
const imageSize = useMemo(() => {
@ -42,18 +48,18 @@ export const UserAvatar: React.FC<UserAvatarProps> = ({ user, size, additionalCl
}, [size])
const avatarUrl = useMemo(() => {
return user.photo !== '' ? user.photo : defaultAvatar.src
}, [user.photo])
return photoUrl || defaultAvatar.src
}, [photoUrl])
const imgDescription = useMemo(() => t('common.avatarOf', { name: user.displayName }), [t, user])
const imgDescription = useMemo(() => t('common.avatarOf', { name: displayName }), [t, displayName])
const tooltip = useCallback(
(props: OverlayInjectedProps) => (
<Tooltip id={user.displayName} {...props}>
{user.displayName}
(overlayInjectedProps: OverlayInjectedProps) => (
<Tooltip id={displayName} {...overlayInjectedProps}>
{displayName}
</Tooltip>
),
[user]
[displayName]
)
return (
@ -69,7 +75,7 @@ export const UserAvatar: React.FC<UserAvatarProps> = ({ user, size, additionalCl
/>
<ShowIf condition={showName}>
<OverlayTrigger overlay={tooltip}>
<span className={`ms-2 me-1 ${styles['user-line-name']}`}>{user.displayName}</span>
<span className={`ms-2 me-1 ${styles['user-line-name']}`}>{displayName}</span>
</OverlayTrigger>
</ShowIf>
</span>