Improve fallback of Group Avatars not loading
This commit is contained in:
parent
e5cf1dfa85
commit
36be68cdcc
5 changed files with 88 additions and 28 deletions
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
|
||||||
|
|
||||||
|
import GroupHeaderImage from 'soapbox/features/group/components/group-header-image';
|
||||||
import GroupMemberCount from 'soapbox/features/group/components/group-member-count';
|
import GroupMemberCount from 'soapbox/features/group/components/group-member-count';
|
||||||
import GroupPrivacy from 'soapbox/features/group/components/group-privacy';
|
import GroupPrivacy from 'soapbox/features/group/components/group-privacy';
|
||||||
import GroupRelationship from 'soapbox/features/group/components/group-relationship';
|
import GroupRelationship from 'soapbox/features/group/components/group-relationship';
|
||||||
|
@ -10,17 +10,11 @@ import { HStack, Stack, Text } from './ui';
|
||||||
|
|
||||||
import type { Group as GroupEntity } from 'soapbox/types/entities';
|
import type { Group as GroupEntity } from 'soapbox/types/entities';
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
groupHeader: { id: 'group.header.alt', defaultMessage: 'Group header' },
|
|
||||||
});
|
|
||||||
|
|
||||||
interface IGroupCard {
|
interface IGroupCard {
|
||||||
group: GroupEntity
|
group: GroupEntity
|
||||||
}
|
}
|
||||||
|
|
||||||
const GroupCard: React.FC<IGroupCard> = ({ group }) => {
|
const GroupCard: React.FC<IGroupCard> = ({ group }) => {
|
||||||
const intl = useIntl();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
className='relative h-[240px] rounded-lg border border-solid border-gray-300 bg-white dark:border-primary-800 dark:bg-primary-900'
|
className='relative h-[240px] rounded-lg border border-solid border-gray-300 bg-white dark:border-primary-800 dark:bg-primary-900'
|
||||||
|
@ -28,12 +22,10 @@ const GroupCard: React.FC<IGroupCard> = ({ group }) => {
|
||||||
>
|
>
|
||||||
{/* Group Cover Image */}
|
{/* Group Cover Image */}
|
||||||
<Stack grow className='relative basis-1/2 rounded-t-lg bg-primary-100 dark:bg-gray-800'>
|
<Stack grow className='relative basis-1/2 rounded-t-lg bg-primary-100 dark:bg-gray-800'>
|
||||||
{group.header && (
|
<GroupHeaderImage
|
||||||
<img
|
group={group}
|
||||||
className='absolute inset-0 h-full w-full rounded-t-lg object-cover'
|
className='absolute inset-0 h-full w-full rounded-t-lg object-cover'
|
||||||
src={group.header} alt={intl.formatMessage(messages.groupHeader)}
|
/>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
{/* Group Avatar */}
|
{/* Group Avatar */}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import React, { useRef } from 'react';
|
||||||
|
|
||||||
import { useSettings } from 'soapbox/hooks';
|
import { useSettings } from 'soapbox/hooks';
|
||||||
|
|
||||||
interface IStillImage {
|
export interface IStillImage {
|
||||||
/** Image alt text. */
|
/** Image alt text. */
|
||||||
alt?: string
|
alt?: string
|
||||||
/** Extra class names for the outer <div> container. */
|
/** Extra class names for the outer <div> container. */
|
||||||
|
|
|
@ -1,34 +1,54 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
import StillImage from 'soapbox/components/still-image';
|
import StillImage, { IStillImage } from 'soapbox/components/still-image';
|
||||||
|
|
||||||
|
import Icon from '../icon/icon';
|
||||||
|
|
||||||
const AVATAR_SIZE = 42;
|
const AVATAR_SIZE = 42;
|
||||||
|
|
||||||
interface IAvatar {
|
interface IAvatar extends Pick<IStillImage, 'src' | 'onError' | 'className'> {
|
||||||
/** URL to the avatar image. */
|
|
||||||
src: string
|
|
||||||
/** Width and height of the avatar in pixels. */
|
/** Width and height of the avatar in pixels. */
|
||||||
size?: number
|
size?: number
|
||||||
/** Extra class names for the div surrounding the avatar image. */
|
|
||||||
className?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Round profile avatar for accounts. */
|
/** Round profile avatar for accounts. */
|
||||||
const Avatar = (props: IAvatar) => {
|
const Avatar = (props: IAvatar) => {
|
||||||
const { src, size = AVATAR_SIZE, className } = props;
|
const { src, size = AVATAR_SIZE, className } = props;
|
||||||
|
|
||||||
|
const [isAvatarMissing, setIsAvatarMissing] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const handleLoadFailure = () => setIsAvatarMissing(true);
|
||||||
|
|
||||||
const style: React.CSSProperties = React.useMemo(() => ({
|
const style: React.CSSProperties = React.useMemo(() => ({
|
||||||
width: size,
|
width: size,
|
||||||
height: size,
|
height: size,
|
||||||
}), [size]);
|
}), [size]);
|
||||||
|
|
||||||
|
if (isAvatarMissing) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
}}
|
||||||
|
className={clsx('flex items-center justify-center rounded-full bg-gray-200 dark:bg-gray-900', className)}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
src={require('@tabler/icons/photo-off.svg')}
|
||||||
|
className='h-4 w-4 text-gray-500 dark:text-gray-700'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StillImage
|
<StillImage
|
||||||
className={clsx('rounded-full', className)}
|
className={clsx('rounded-full', className)}
|
||||||
style={style}
|
style={style}
|
||||||
src={src}
|
src={src}
|
||||||
alt='Avatar'
|
alt='Avatar'
|
||||||
|
onError={handleLoadFailure}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
50
app/soapbox/features/group/components/group-header-image.tsx
Normal file
50
app/soapbox/features/group/components/group-header-image.tsx
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import { Icon } from 'soapbox/components/ui';
|
||||||
|
|
||||||
|
import type { Group } from 'soapbox/schemas';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
header: { id: 'group.header.alt', defaultMessage: 'Group header' },
|
||||||
|
});
|
||||||
|
|
||||||
|
interface IGroupHeaderImage {
|
||||||
|
group?: Group | false | null
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const GroupHeaderImage: React.FC<IGroupHeaderImage> = ({ className, group }) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const [isHeaderMissing, setIsHeaderMissing] = useState<boolean>(false);
|
||||||
|
|
||||||
|
if (!group || !group.header) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isHeaderMissing) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx(className, 'flex items-center justify-center bg-gray-200 dark:bg-gray-800/30')}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
src={require('@tabler/icons/photo-off.svg')}
|
||||||
|
className='h-6 w-6 text-gray-500 dark:text-gray-700'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
className={className}
|
||||||
|
src={group.header}
|
||||||
|
alt={intl.formatMessage(messages.header)}
|
||||||
|
onError={() => setIsHeaderMissing(true)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GroupHeaderImage;
|
|
@ -4,6 +4,7 @@ import { Link } from 'react-router-dom';
|
||||||
import GroupAvatar from 'soapbox/components/groups/group-avatar';
|
import GroupAvatar from 'soapbox/components/groups/group-avatar';
|
||||||
import { HStack, Stack, Text } from 'soapbox/components/ui';
|
import { HStack, Stack, Text } from 'soapbox/components/ui';
|
||||||
import GroupActionButton from 'soapbox/features/group/components/group-action-button';
|
import GroupActionButton from 'soapbox/features/group/components/group-action-button';
|
||||||
|
import GroupHeaderImage from 'soapbox/features/group/components/group-header-image';
|
||||||
import GroupMemberCount from 'soapbox/features/group/components/group-member-count';
|
import GroupMemberCount from 'soapbox/features/group/components/group-member-count';
|
||||||
import GroupPrivacy from 'soapbox/features/group/components/group-privacy';
|
import GroupPrivacy from 'soapbox/features/group/components/group-privacy';
|
||||||
|
|
||||||
|
@ -31,13 +32,10 @@ const GroupGridItem = forwardRef((props: IGroup, ref: React.ForwardedRef<HTMLDiv
|
||||||
ref={ref}
|
ref={ref}
|
||||||
style={{ minHeight: 180 }}
|
style={{ minHeight: 180 }}
|
||||||
>
|
>
|
||||||
{group.header && (
|
<GroupHeaderImage
|
||||||
<img
|
group={group}
|
||||||
src={group.header}
|
className='absolute inset-0 object-cover'
|
||||||
alt='Group cover'
|
/>
|
||||||
className='absolute inset-0 object-cover'
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className='absolute inset-x-0 bottom-0 flex justify-center rounded-b-lg bg-gradient-to-t from-gray-900 to-transparent pb-8 pt-12 transition-opacity duration-500'
|
className='absolute inset-x-0 bottom-0 flex justify-center rounded-b-lg bg-gradient-to-t from-gray-900 to-transparent pb-8 pt-12 transition-opacity duration-500'
|
||||||
|
|
Loading…
Reference in a new issue