Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions packages/shared/src/components/cards/common/SourceButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ interface SourceButtonProps {
className?: string;
style?: CSSProperties;
size?: ProfileImageSize;
// Defaults to a circle; callers (e.g. notifications) can pass a size token to
// render a rounded square instead.
rounded?: ProfileImageSize | 'full';
pureTextTooltip?: boolean;
tooltipPosition?: HoverCardContentProps['side'];
}
Expand All @@ -34,6 +37,7 @@ export default function SourceButton({
tooltipPosition = 'bottom',
pureTextTooltip = false,
size = ProfileImageSize.Medium,
rounded = 'full',
className,
...props
}: SourceButtonProps): ReactElement {
Expand All @@ -45,9 +49,9 @@ export default function SourceButton({
{...props}
className={className}
size={size}
rounded="full"
rounded={rounded}
user={{
id: source.id,
id: source.id ?? source.handle,
image: source.image,
username: source.handle,
}}
Expand All @@ -62,9 +66,9 @@ export default function SourceButton({
<ProfileImageLink
{...props}
className={className}
picture={{ size, rounded: 'full' }}
picture={{ size, rounded }}
user={{
id: source.id,
id: source.id ?? source.handle,
image: source.image,
permalink: source.permalink,
username: source.handle,
Expand All @@ -84,9 +88,9 @@ export default function SourceButton({
<ProfileImageLink
{...props}
className={className}
picture={{ size, rounded: 'full' }}
picture={{ size, rounded }}
user={{
id: source.id,
id: source.id ?? source.handle,
image: source.image,
permalink: source.permalink,
username: source.handle,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,36 +1,20 @@
import type { ReactElement } from 'react';
import React from 'react';
import classNames from 'classnames';
import { useObjectPurify } from '../../hooks/useDomPurify';
import classed from '../../lib/classed';
import NotificationItemIcon from './NotificationIcon';
import NotificationItemAvatar from './NotificationItemAvatar';
import styles from './InAppNotification.module.css';
import { NotificationItemLead } from './NotificationItemLead';
import { getNotificationLeadAvatar } from './leadAvatar';
import type { NewNotification } from '../../graphql/notifications';

const NotificationWrapper = classed(
'div',
'relative flex h-full w-full flex-row overflow-hidden rounded-16 p-3 pr-10 hover:bg-surface-hover focus:bg-theme-active',
);
const NotificationLink = classed('a', 'absolute inset-0 h-full w-full');
const NotificationAvatar = classed(
'span',
classNames(
styles.inAppNotificationAvatar,
'flex flex-row items-start gap-[0.375rem]',
),
);
const NotificationText = classed(
'p',
'flex flex-col flex-1 ml-4 w-full text-left typo-callout multi-truncate line-clamp-3 h-16',
);

interface InAppNotificationItemProps extends NewNotification {
onClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void;
}

// Real-time popup, laid out like a feed row (NotificationItem): the avatar with
// its colored category badge leads, then the headline with the actor's name
// bold and the rest regular for a scannable hierarchy.
export function InAppNotificationItem({
icon,
type,
title,
avatars,
targetUrl,
Expand All @@ -42,19 +26,28 @@ export function InAppNotificationItem({
return null;
}

const [avatar] = avatars ?? [];
const avatar = getNotificationLeadAvatar(avatars);

return (
<NotificationWrapper>
<NotificationLink href={targetUrl} onClick={onClick} />
<NotificationAvatar>
<NotificationItemIcon icon={icon} />
{!!avatar && <NotificationItemAvatar {...avatar} className="z-1" />}
</NotificationAvatar>
<NotificationText
<div className="relative flex h-full w-full flex-row items-start gap-3 rounded-16 p-3 pr-10">
<a
className="absolute inset-0 z-0 h-full w-full"
href={targetUrl}
onClick={onClick}
aria-label="Open notification"
/>
<div className="mt-0.5 flex w-10 shrink-0 items-center justify-start self-start">
<NotificationItemLead type={type} icon={icon} avatar={avatar} />
</div>
{/* A div (not a <p>): the sanitized title can itself be wrapped in a
<p>, and a <p> inside a <p> is invalid and gets auto-closed by the
browser, breaking the layout. */}
<div
className="mt-0.5 line-clamp-3 min-w-0 flex-1 break-words text-left font-normal text-text-primary typo-callout [&_b]:font-bold [&_p]:m-0 [&_p]:inline [&_strong]:font-bold"
dangerouslySetInnerHTML={{
__html: memoizedTitle,
}}
/>
</NotificationWrapper>
</div>
);
}
Loading
Loading