Rework tweet card click targets: frame->tweet, photo->new tab, video->play

The whole card previously linked to our own /article/[id] page. Per
request, clicking the card frame now opens the original tweet (in a
new tab) instead — there's no separate "full article" view for a
tweet anyway. Clicking a photo opens that image by itself in a new
tab (still resolved through the configured media mode, so proxy mode
doesn't leak the browser's IP to Twitter when viewing the full image
either). Clicking a video's native controls still just plays/pauses
it rather than navigating anywhere.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014c1L8ghNBFjfiH64UMViP8
This commit is contained in:
Claude
2026-07-23 00:39:30 +00:00
parent 058f6bbca8
commit 7c44552ae0
+30 -8
View File
@@ -9,16 +9,26 @@
// article.tweet.media is undefined for tweets published before this field existed —
// older rows in the DB weren't backfilled, so this can't assume it's always an array.
const media = $derived(article.tweet?.media?.slice(0, 4) ?? []);
// The card's own href — clicking the frame goes to the original tweet, not our
// internal article page (there's no separate "full article" for a tweet anyway).
const tweetUrl = $derived(article.sources[0]?.link ?? `/article/${article.id}`);
// Native <video controls> needs its clicks not to fall through to the card's own
// <a> navigation (play/pause/scrub would otherwise just open the article instead).
// Plain images keep navigating as before — only the video element itself is exempted.
function handleMediaClick(e: MouseEvent) {
if ((e.target as HTMLElement).tagName === 'VIDEO') e.preventDefault();
// A <video controls> click must not fall through to the card's own <a> navigation
// (play/pause/scrub would otherwise just open the tweet in a new tab instead).
function stopVideoNav(e: MouseEvent) {
e.preventDefault();
}
// A photo click opens the image itself in a new tab, rather than the tweet link the
// rest of the card points to — also resolved through the configured media mode
// (proxy/self-host/direct) so opening the full image doesn't bypass proxy anonymity.
function openImage(e: MouseEvent, url: string) {
e.preventDefault();
window.open(resolveMediaUrl(url), '_blank', 'noopener,noreferrer');
}
</script>
<a class="tweet-card" href={`/article/${article.id}`}>
<a class="tweet-card" href={tweetUrl} target="_blank" rel="noreferrer">
<div class="meta">
<span>{article.category[0] ?? ''}</span>
<span>&middot;</span>
@@ -37,7 +47,7 @@
</div>
<div class="text">{article.body}</div>
{#if media.length > 0}
<div class="media-grid" data-count={media.length} onclick={handleMediaClick} role="presentation">
<div class="media-grid" data-count={media.length}>
{#each media as item, i (item.url)}
<div class="media-cell" class:span-2={media.length === 3 && i === 0}>
{#if item.type === 'video' || item.type === 'gif'}
@@ -49,11 +59,14 @@
loop={item.type === 'gif'}
muted={item.type === 'gif'}
poster={item.thumbnailUrl ? resolveMediaUrl(item.thumbnailUrl) : undefined}
onclick={stopVideoNav}
>
<source src={resolveMediaUrl(item.url)} type="video/mp4" />
</video>
{:else}
<img class="media-el" src={resolveMediaUrl(item.url)} alt="" loading="lazy" />
<button type="button" class="media-btn" onclick={(e) => openImage(e, item.url)}>
<img class="media-el" src={resolveMediaUrl(item.url)} alt="" loading="lazy" />
</button>
{/if}
</div>
{/each}
@@ -153,6 +166,15 @@
.media-cell.span-2 {
grid-row: 1 / 3;
}
.media-btn {
display: block;
width: 100%;
height: 100%;
padding: 0;
border: none;
background: none;
cursor: pointer;
}
.media-el {
width: 100%;
height: 100%;