Blur conversation images by default

This commit is contained in:
jjbrunton committed 2026-08-09 07:55:51 +01:00
1 parent 5c01d7d9aa
commit e7c29f91f2
5 files changed
+60 -31

No files matched your search

+14 -5
View File
@@ -8,15 +8,24 @@ export interface OriginPost {
}
export function OriginPostCard({ post }: { post: OriginPost }) {
const [isImageRevealed, setIsImageRevealed] = useState(false);
return (
<a aria-label={post.title} className="origin-post-card" href={post.url} rel="noreferrer" target="_blank">
{post.imageUrl ? <img alt={`Source post: ${post.title}`} loading="lazy" src={post.imageUrl} /> : null}
<span>
<div className="origin-post-card">
{post.imageUrl ? (
<div className="origin-post-image">
<img alt={`Source post: ${post.title}`} className={isImageRevealed ? undefined : 'is-blurred'} loading="lazy" src={post.imageUrl} />
<button aria-pressed={isImageRevealed} onClick={() => setIsImageRevealed((current) => !current)} type="button">
{isImageRevealed ? 'Blur original image' : 'Reveal original image'}
</button>
</div>
) : null}
<a aria-label={post.title} className="origin-post-copy" href={post.url} rel="noreferrer" target="_blank">
<small>Started from {post.subreddit ? `r/${post.subreddit}` : 'Reddit'}</small>
<strong>{post.title}</strong>
<span>Open original post</span>
</span>
</a>
</a>
</div>
);
}
@@ -20,6 +20,8 @@ function mediaPath(localPath: string): string {
}
function MessageHistory({ conversation }: { conversation: ConversationDetailResponse }) {
const [revealedMedia, setRevealedMedia] = useState<Set<string>>(() => new Set());
if (!conversation.messages || conversation.messages.length === 0) {
return <div className="conversation-empty-detail">No messages have been loaded for this conversation.</div>;
}
@@ -38,21 +40,35 @@ function MessageHistory({ conversation }: { conversation: ConversationDetailResp
<p>{message.text}</p>
{message.media?.length ? (
<div className="conversation-message-images">
{message.media.map((media) => (
<a
aria-label={`View original image ${message.isSelf ? 'sent by you' : `received from ${redditUsernameLabel(conversation.username)}`}`}
href={mediaPath(media.localPath)}
key={`${message.timestamp}-${media.localPath}`}
rel="noreferrer"
target="_blank"
>
{message.media.map((media) => {
const mediaKey = `${message.timestamp}-${media.localPath}`;
const sender = message.isSelf ? 'sent by you' : `received from ${redditUsernameLabel(conversation.username)}`;
const isRevealed = revealedMedia.has(mediaKey);
return (
<div className="conversation-message-image" key={mediaKey}>
<img
alt={`Image ${message.isSelf ? 'sent by you' : `received from ${redditUsernameLabel(conversation.username)}`}`}
alt={`Image ${sender}`}
className={isRevealed ? undefined : 'is-blurred'}
loading="lazy"
src={mediaPath(media.localPath)}
/>
</a>
))}
<button
aria-pressed={isRevealed}
className="conversation-image-reveal"
onClick={() => setRevealedMedia((current) => {
const next = new Set(current);
if (next.has(mediaKey)) next.delete(mediaKey);
else next.add(mediaKey);
return next;
})}
type="button"
>
{isRevealed ? 'Blur conversation image' : 'Reveal conversation image'}
</button>
</div>
);
})}
</div>
) : null}
</li>
@@ -62,9 +62,11 @@
.conversation-messages time { color: var(--muted); font-size: .7rem; }
.conversation-messages p { margin: 0; line-height: 1.45; white-space: pre-wrap; }
.conversation-message-images { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(12rem, 100%), 1fr)); gap: .5rem; margin-top: .65rem; }
.conversation-message-images a { display: block; border-radius: .6rem; }
.conversation-message-images a:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.conversation-message-images img { display: block; width: 100%; max-width: 28rem; max-height: 32rem; object-fit: contain; background: #08100c; border: 1px solid var(--border); border-radius: .6rem; }
.conversation-message-image { position: relative; overflow: hidden; border-radius: .6rem; }
.conversation-message-images img { display: block; width: 100%; max-width: 28rem; max-height: 32rem; object-fit: contain; background: #08100c; border: 1px solid var(--border); border-radius: .6rem; transition: filter 160ms ease, transform 160ms ease; }
.conversation-message-images img.is-blurred { filter: blur(24px) brightness(.55); transform: scale(1.12); }
.conversation-image-reveal { position: absolute; bottom: .5rem; left: .5rem; min-height: 2rem !important; padding: .35rem .6rem !important; color: white !important; background: rgba(0, 0, 0, .76) !important; border-color: rgba(255, 255, 255, .45) !important; font-size: .75rem; font-weight: 800; }
.conversation-image-reveal:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.conversation-empty-detail { padding: 2rem; color: var(--muted); text-align: center; }
.conversation-composer { display: grid; gap: .65rem; padding: 1rem; border-top: 1px solid var(--border); }
.conversation-section-heading, .conversation-actions { display: flex; align-items: center; justify-content: space-between; gap: .75rem; }
+8 -4
View File
@@ -99,10 +99,14 @@ h2 { margin-bottom: 0; font-size: 1.1rem; }
.origin-post-dialog-copy h2 { margin: 0; font-size: 1.25rem; }
.origin-post-dialog-copy small { color: var(--muted); }
.origin-post-dialog-copy a { justify-self: start; text-decoration: none; }
.origin-post-card { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: .8rem; align-items: center; color: inherit; text-decoration: none; }
.origin-post-card > img { width: 5rem; height: 5rem; object-fit: cover; background: #050907; border-radius: .45rem; }
.origin-post-card > span { display: grid; gap: .2rem; min-width: 0; }
.origin-post-card small, .origin-post-card span span { color: var(--muted); font-size: .75rem; }
.origin-post-card { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: .8rem; align-items: center; }
.origin-post-image { position: relative; width: 5rem; height: 5rem; overflow: hidden; background: #050907; border-radius: .45rem; }
.origin-post-image img { width: 100%; height: 100%; object-fit: cover; transition: filter 160ms ease, transform 160ms ease; }
.origin-post-image img.is-blurred { filter: blur(18px) brightness(.55); transform: scale(1.12); }
.origin-post-image button { position: absolute; right: .2rem; bottom: .2rem; min-height: auto; padding: .2rem .3rem; border: 1px solid rgba(255, 255, 255, .45); border-radius: .3rem; color: white; background: rgba(0, 0, 0, .76); font-size: .58rem; font-weight: 800; }
.origin-post-image button:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.origin-post-copy { display: grid; gap: .2rem; min-width: 0; color: inherit; text-decoration: none; }
.origin-post-card small, .origin-post-card span { color: var(--muted); font-size: .75rem; }
.origin-post-card strong { overflow-wrap: anywhere; }
.activity-marker { flex: 0 0 auto; width: .55rem; height: .55rem; margin-top: .3rem; border-radius: 50%; background: var(--muted); }
.activity-marker.sent, .activity-marker.response { background: var(--accent); }
+6 -8
View File
@@ -142,6 +142,9 @@ describe('ConversationsPage', () => {
'src',
'https://i.redd.it/source-post.jpg',
);
expect(screen.getByRole('img', { name: 'Source post: Looking for a chat' })).toHaveClass('is-blurred');
await user.click(screen.getByRole('button', { name: 'Reveal original image' }));
expect(screen.getByRole('img', { name: 'Source post: Looking for a chat' })).not.toHaveClass('is-blurred');
});
it('orders conversations by latest message activity', async () => {
@@ -215,14 +218,9 @@ describe('ConversationsPage', () => {
'src',
'/media/received%20image.jpg',
);
expect(screen.getByRole('link', { name: 'View original image received from u/alice' })).toHaveAttribute(
'href',
'/media/received%20image.jpg',
);
expect(screen.getByRole('link', { name: 'View original image received from u/alice' })).toHaveAttribute(
'target',
'_blank',
);
expect(screen.getByRole('img', { name: 'Image received from u/alice' })).toHaveClass('is-blurred');
await user.click(screen.getByRole('button', { name: 'Reveal conversation image' }));
expect(screen.getByRole('img', { name: 'Image received from u/alice' })).not.toHaveClass('is-blurred');
});
it('turns a suggestion into an editable draft and confirms sending without losing failed input', async () => {