Improved Inbox view UI (#21600)

ref https://linear.app/ghost/issue/AP-521/improve-inbox-view-alignmentstructure

- Improved the layout, spacing and typography of Inbox view
- Removed attachment counters for images in Inbox view
- Added a very very small variant of `APAvatar`
This commit is contained in:
Djordje Vlaisavljevic 2024-11-13 11:45:37 +00:00 committed by GitHub
parent ead408ed20
commit 5863c40306
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 30 additions and 38 deletions

View File

@ -76,9 +76,9 @@ const Inbox: React.FC<InboxProps> = ({}) => {
</div>
) : activities.length > 0 ? (
<>
<div className={`mx-auto flex items-start ${layout === 'inbox' ? 'max-w-6xl gap-14' : 'gap-8'}`}>
<div className={`mx-auto flex items-start gap-8`}>
<div className='flex w-full min-w-0 items-start'>
<ul className={`mx-auto flex ${layout === 'inbox' ? 'w-full max-w-full' : 'max-w-[500px]'} flex-col`}>
<ul className={`mx-auto flex w-full flex-col ${layout === 'inbox' ? 'xxxl:max-w-[800px]' : 'max-w-[500px]'}`}>
{activities.map((activity, index) => (
<li
key={activity.id}
@ -106,7 +106,7 @@ const Inbox: React.FC<InboxProps> = ({}) => {
)}
</ul>
</div>
<div className={`sticky top-[135px] ml-auto w-full max-w-[300px] max-lg:hidden ${layout === 'inbox' ? '' : ' xxxl:fixed xxxl:right-[40px]'}`}>
<div className='sticky top-[135px] ml-auto w-full max-w-[300px] max-lg:hidden xxxl:sticky xxxl:right-[40px]'>
<h2 className='mb-2 text-lg font-semibold'>You might also like...</h2>
{isLoadingSuggested ? (
<LoadingIndicator size="sm" />

View File

@ -5,6 +5,7 @@ import {Button, Heading, Icon, Menu, MenuItem, showToast} from '@tryghost/admin-
import APAvatar from '../global/APAvatar';
import FeedItemStats from './FeedItemStats';
import clsx from 'clsx';
import getRelativeTimestamp from '../../utils/get-relative-timestamp';
import getUsername from '../../utils/get-username';
import stripHtml from '../../utils/strip-html';
@ -83,7 +84,7 @@ export function renderFeedAttachment(object: ObjectProperties, layout: string) {
</div>;
default:
if (object.image) {
return <img alt='attachment' className='my-3 max-h-[280px] w-full rounded-md object-cover outline outline-1 -outline-offset-1 outline-black/10' src={object.image} />;
return <img alt='attachment' className='my-3 max-h-[280px] w-full rounded-md object-cover outline outline-1 -outline-offset-1 outline-black/[0.05]' src={object.image} />;
}
return null;
}
@ -92,20 +93,16 @@ export function renderFeedAttachment(object: ObjectProperties, layout: string) {
function renderInboxAttachment(object: ObjectProperties) {
const attachment = getAttachment(object);
const videoAttachmentStyles = 'ml-8 shrink-0 rounded-md h-[80px] w-[120px] relative';
const imageAttachmentStyles = clsx('object-cover outline outline-1 -outline-offset-1 outline-black/[0.05]', videoAttachmentStyles);
if (!attachment) {
return null;
}
if (Array.isArray(attachment)) {
const attachmentCount = attachment.length;
return (
<div className='min-w-[120px]'>
<div className='relative'>
<img className={`h-[80px] w-[120px] rounded-md object-cover outline outline-1 -outline-offset-1 outline-black/10`} src={attachment[0].url} />
<div className='absolute bottom-1 right-1 z-10 rounded-full border border-[rgba(255,255,255,0.25)] bg-black px-2 py-0.5 font-semibold text-white'>+ {attachmentCount - 1}</div>
</div>
</div>
<img className={imageAttachmentStyles} src={attachment[0].url} />
);
}
@ -114,14 +111,12 @@ function renderInboxAttachment(object: ObjectProperties) {
case 'image/png':
case 'image/gif':
return (
<div className='min-w-[120px]'>
<img className={`h-[80px] w-[120px] rounded-md object-cover outline outline-1 -outline-offset-1 outline-black/10`} src={attachment.url} />
</div>
<img className={imageAttachmentStyles} src={attachment.url} />
);
case 'video/mp4':
case 'video/webm':
return (
<div className='relative h-[80px]'>
<div className={videoAttachmentStyles}>
<video className='h-[80px] w-full rounded object-cover' src={attachment.url} />
<div className='absolute inset-0 rounded bg-grey-900 opacity-50'></div>
<div className='absolute inset-0 flex items-center justify-center'>
@ -133,7 +128,7 @@ function renderInboxAttachment(object: ObjectProperties) {
case 'audio/mpeg':
case 'audio/ogg':
return (
<div className='min-w-[160px]'>
<div className='ml-8 w-[120px]'>
<div className='relative mb-4 mt-2 w-full'>
<audio className='w-full' src={attachment.url} controls/>
</div>
@ -141,9 +136,7 @@ function renderInboxAttachment(object: ObjectProperties) {
);
default:
if (object.image) {
return <div className='min-h-[80px] min-w-[120px]'>
<img className={`h-[80px] w-[120px] rounded-md object-cover outline outline-1 -outline-offset-1 outline-black/10`} src={object.image} />
</div>;
return <img className={imageAttachmentStyles} src={object.image} />;
}
return null;
}
@ -391,31 +384,25 @@ const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, comment
return (
<>
{object && (
<div className='group/article relative -mx-4 -my-px flex min-w-0 cursor-pointer justify-between rounded-md p-4 hover:bg-grey-75' data-layout='inbox' data-object-id={object.id} onClick={onClick}>
<div className='flex w-full min-w-0 flex-col items-start justify-between gap-1 pr-4'>
<div className='z-10 flex w-full min-w-0 items-start gap-2 group-hover/article:border-transparent'>
<APAvatar author={author} size='xs'/>
<span className='min-w-0 truncate break-all font-semibold' data-test-activity-heading>{author.name}</span>
<span className='min-w-0 truncate text-grey-700'>{getUsername(author)}</span>
{/* <div className='flex gap-2'>
<span className='truncate min-w-0 break-all font-semibold' data-test-activity-heading>{author.name}</span>
<span className='min-w-0 truncate text-grey-700'>{getUsername(author)}</span>
</div> */}
<span className='shrink-0 whitespace-nowrap text-grey-700 before:mr-1 before:content-["·"]' title={`${timestamp}`}>{getRelativeTimestamp(date)}</span>
<div className='group/article relative -mx-4 -my-px flex min-h-[112px] min-w-0 cursor-pointer items-center justify-between rounded-md p-4 hover:bg-grey-75' data-layout='inbox' data-object-id={object.id} onClick={onClick}>
<div className='flex min-h-[73px] w-full min-w-0 flex-col items-start justify-start'>
<div className='z-10 mb-1 flex w-full min-w-0 items-center gap-1.5 text-base text-grey-700 group-hover/article:border-transparent'>
<APAvatar author={author} size='2xs'/>
<span className='min-w-0 truncate break-all font-medium text-grey-900' data-test-activity-heading>{author.name}</span>
<span className='min-w-0 truncate'>{getUsername(author)}</span>
<span className='shrink-0 whitespace-nowrap before:mr-1 before:content-["·"]' title={`${timestamp}`}>{getRelativeTimestamp(date)}</span>
</div>
<Heading className='line-clamp-1 font-semibold leading-normal' level={5} data-test-activity-heading>
<Heading className='mb-1 line-clamp-1 w-full max-w-[600px] text-[1.6rem] font-semibold leading-snug' level={5} data-test-activity-heading>
{object.name ? object.name : (
<span dangerouslySetInnerHTML={{
__html: object.content.length > 30
? stripHtml(object.content).substring(0, 50) + '...'
: stripHtml(object.content)
__html: stripHtml(object.content)
}}></span>
)}
</Heading>
<div dangerouslySetInnerHTML={({__html: stripHtml(object.content)})} className='ap-note-content w-full truncate text-[1.5rem] text-grey-700'></div>
<div dangerouslySetInnerHTML={({__html: stripHtml(object.content)})} className='ap-note-content w-full max-w-[600px] truncate text-base leading-normal text-grey-700'></div>
</div>
{renderInboxAttachment(object)}
<div className='invisible absolute right-2 top-[9px] z-[49] flex flex-col gap-2 rounded-lg bg-white p-2 shadow-md-heavy group-hover/article:visible'>
<div className='invisible absolute right-4 top-[9px] z-[49] flex flex-col gap-2 rounded-lg bg-white p-2 shadow-md-heavy group-hover/article:visible'>
<FeedItemStats
commentCount={commentCount}
layout={layout}

View File

@ -3,7 +3,7 @@ import clsx from 'clsx';
import {ActorProperties} from '@tryghost/admin-x-framework/api/activitypub';
import {Icon} from '@tryghost/admin-x-design-system';
type AvatarSize = 'xs' | 'sm' | 'lg';
type AvatarSize = '2xs' | 'xs' | 'sm' | 'lg';
export type AvatarBadge = 'user-fill' | 'heart-fill' | 'comment-fill' | undefined;
interface APAvatarProps {
@ -37,6 +37,11 @@ const APAvatar: React.FC<APAvatarProps> = ({author, size, badge}) => {
}
switch (size) {
case '2xs':
iconSize = 10;
containerClass = clsx('h-4 w-4 rounded ', containerClass);
imageClass = 'z-10 rounded w-4 h-4 object-cover';
break;
case 'xs':
iconSize = 12;
containerClass = clsx('h-5 w-5 rounded ', containerClass);