Previously the file bubble wrapped JS middle-ellipsis in CSS truncate,
so narrow containers silently clipped the tail+extension, leaving
e.g. "25cb264a-e06…." instead of "25cb264a-e06…811a.jpg".
Split the displayed name into a shrinking head (with CSS truncate)
and a non-shrinking tail (last 4 base chars + extension). The browser
now decides how much head to clip while the suffix is always visible.
Route resource card clicks to /browse?post=<id> instead of relying on hash
anchors, then manually calculate the sticky filter offset when scrolling to
the target bubble. Start from the top and smooth-scroll to the card for a
clear transition, with delayed auto realignments after media above the target
settles.
Add optional post-level and localized title fields, and use a new
postTitleText helper for Resource.title so card/list surfaces prefer short
backend-provided titles instead of full body text. When title is missing,
fallback to the first non-empty body line, then filename, then post id.
Document the backend handoff in docs/posts-title-api.md alongside the other
backend task docs.
Use object-fill for compact preview thumbnails so rank-list covers and
file previews fill their fixed boxes without cropping or black bars. This
keeps the list layout stable while matching the desired compressed-ratio
thumbnail treatment.
Clamp goPrev / goNext instead of wrapping the index, and hide the
left chevron on the first image and the right chevron on the last
one. Arrow keys, swipe gestures, and the on-screen buttons all
behave like a linear gallery now, so users get a clear cue that
they have reached the end instead of unexpectedly looping back.
Split clampRatio into a sanity-check helper (safeRatio: reject
zero / NaN / negatives) and the actual aspect clamp. Two-image
albums now keep each image's real ratio so the cells match the
images exactly with no object-cover cropping. Three-plus image
layouts still clamp ratios to a 0.55-2 band so a single extreme
image cannot warp the mosaic.
- MessageInlineVideo (new): custom-controlled inline video that disables
the iOS Safari / Chromium native overlays entirely and reimplements
the essentials: tap-to-play, centered play affordance while paused,
bottom bar with play/pause + current time + drag-to-scrub progress
bar + remaining time + fullscreen. Pointer events with pointer
capture cover both mouse and touch scrubbing, including dragging
past the bar's bounds. The element listens to 'seeked' as well as
'timeupdate' so external currentTime writes paint the bar even when
the video is paused, and the goFullscreen callback synchronously
syncs React state on close so the inline progress reflects the user's
fullscreen playhead with no perceptible delay.
- VideoBubble: replace the inline <video controls> with
MessageInlineVideo and thread postId through openVideo so the
fullscreen overlay can attach the download pill to the right post.
- VideoPlayer overlay: replace its <video controls> with
MessageInlineVideo size='lg', removing the iOS native arrows / PiP /
mute / overflow controls. The overlay supplies its own large
download pill and a beefier close button.
- AttachmentDownloadPill: new 'size' prop ('sm' default 30 px, 'lg'
44 px with 22 px icon and text-[14px]) for overlay surfaces where
the affordance can breathe and should feel touch-friendly.
- ImageLightbox: drop the inline LightboxDownloadButton and use the
shared AttachmentDownloadPill size='lg' instead, with a matching
larger close button. Unused imports cleaned up.
Adds the front-end side of the link-preview feature so the back-end
team has a fixed contract to implement against.
- docs/link-preview.md: full spec for the `/api/link-preview` proxy
and the preferred inline-on-Post integration. Covers caching, SSRF
guards, metadata-extraction precedence, provider quirks, and the
front-end rendering rules. Scope is the first URL only.
- types/post.ts: new `LinkPreview` type and optional `linkPreview`
field on `Post`.
- LinkPreviewCard: clickable card with a themeColor accent bar,
siteName / title / description (line-clamped), and an optional
1.91:1 thumbnail. Whole card is an `<a target="_blank">` to the
canonical URL.
- MessageBubble: render the card between the bubble body and the
timestamp, with padding that matches visual vs. text-only bubbles.
- mockPosts: example `linkPreview` payloads on p-005 and p-010 so
the visual works when running with VITE_USE_MOCK_POSTS=true,
and so the back-end has concrete reference values.
- AttachmentDownloadPill: bring back the optional adaptive sizing
branch (default off). When the host sets containerType: inline-size,
the pill scales 22-30px with the tile width using a steeper
18cqw curve, so two-image tiles already reach 30px while tiny
thumbs in mixed layouts stay at 22px.
- AlbumBubble: opt every tile into adaptive sizing and add the query
container so the cqw units resolve against each tile's width.
- Single-image and video bubbles continue to render the pill at a
fixed 30px (no host opt-in needed).
Successful downloads no longer pop a 'downloadOk' toast across all
the download sites (PopularRankList, RecommendedCard, FileDocBubble,
ImageLightbox). The browser's native download UI already confirms
the action, and the toast was redundant noise. Failure toasts and
the spinner state remain so users still see errors.
- VideoBubble: add a Maximize2 fullscreen button at the top-right of
the inline player (mirrors the download pill on the left), so the
user explicitly opts into fullscreen instead of any tile click being
promoted to one. Removed the outer onClick that opened fullscreen
on any tap.
- AttachmentDownloadPill: drop the adaptive (cqw-based) sizing branch
and the corresponding 'adaptive' prop. The pill is now a uniform
30px tall in every host (album tile, single image, video tile),
matching the design without needing query containers on parents.
- AlbumBubble: remove the now-unused 'adaptive' prop and the
containerType: inline-size hook that supported it.
- VideoBubble: stopPropagation on the inline <video> element so
clicks on the native controls (play/pause/seek) don't bubble up
to the outer tile, which previously promoted any control click
into a fullscreen overlay.
- VideoPlayer: replace overflow:hidden body lock with the iOS-safe
position-fixed + scroll-restore pattern. Closing the fullscreen
player now returns the page to the exact scroll offset it had,
instead of snapping back to the top on iOS Safari.
Bump the previous/next chevron buttons from translucent white tint
to a solid-ish black with a subtle ring + shadow, so the controls
stay legible on light or busy image backgrounds.
- Home: lock category carousel height to the tallest page so the
Official Recommendations section below does not jump up when
swiping to a page with fewer categories.
- CollapsibleText: raise default threshold to 25 lines and tighten
the spacing between the expand-all button and the timestamp
(drop the fixed h-8 and use mt-1 instead of mt-1.5).
- formatTime: always render dates as yyyy/m/d HH:mm regardless of
locale, matching the requested timestamp format.
Combine collapsible long-text (main) with adaptive image frame
(this branch). Resolved conflict in ImageWithTextBubble.tsx by
keeping both SingleImageFrame and CollapsibleText imports.
Image bubbles previously used the raw filename as alt text, so a failed
asset load exposed the file name in the broken-image box. Add a reusable
BubbleImage that renders an empty alt and falls back to a neutral
placeholder (ImageOff icon) on error; use it in the album, image, and
image-with-text bubbles, and drop the filename from their aria-labels.
Also add a global ScrollToTop that resets the window on route change so
desktop navigation matches mobile (e.g. clicking a category card no
longer lands at the bottom of the new page). Hash navigations are skipped
so #post-<id> deep-link scrolling still works.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Render the 52x52 preview with an empty alt and an onError handler, so a
broken thumbnail no longer shows the browser's broken-image box (which
leaked the raw filename) — it falls back to the file-type icon instead.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Use thumbnailUrl/posterUrl (and the image url itself for image-type
attachments) inside the 52x52 box of the file/document bubble, falling
back to the file-type icon when no preview is available.
Also tune the deep-link scroll-mt offset (82px / 98px) so a targeted
bubble lands just below the sticky header.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Recommended cards already routed to /browse#post-<id>, but the stream had
no logic to scroll to the target bubble — and the post might not be paged
in yet. MessageStream now resolves the #post-<id> hash, auto-loads more
pages until the bubble renders, scrolls to it, and gives it a brief gold
highlight. Bubbles get scroll-mt so they clear the sticky header.
Also adds a global floating back-to-top button (BackToTop) mounted in
PublicLayout, shown after scrolling past 400px.
Bundles related staging UI work already present in the working tree.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>