grain.social is a photo sharing platform built on atproto. grain.social
atproto photography appview
57
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: expandable gallery descriptions and compact relative time

Add a "more" button to gallery card descriptions that are truncated
beyond 2 lines, matching grain-native behavior. Move timestamp to
the card header inline with the handle, and unify relativeTime to
the compact format (now, 2m, 3h, 4d, 1w) used in grain-native.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+46 -9
+39 -2
app/lib/components/molecules/GalleryCard.svelte
··· 84 84 let currentIndex = $state(0) 85 85 let carouselEl: HTMLDivElement | undefined = $state(undefined) 86 86 let activeAltIndex: number | null = $state(null) 87 + let descriptionExpanded = $state(false) 88 + let descriptionClamped = $state(false) 89 + let descriptionEl: HTMLParagraphElement | undefined = $state(undefined) 90 + 91 + $effect(() => { 92 + if (descriptionEl && !descriptionExpanded) { 93 + descriptionClamped = descriptionEl.scrollHeight > descriptionEl.clientHeight + 1 94 + } 95 + }) 87 96 const currentExif = $derived(photos[currentIndex]?.exif as ExifView | undefined) 88 97 89 98 function onScroll() { ··· 150 159 <span class="author-name-row"> 151 160 <span class="author-handle">{displayName}</span> 152 161 {#if handle}<span class="author-subtext">{handle}</span>{/if} 162 + <span class="header-time">· {timeStr}</span> 153 163 </span> 154 164 {#if gallery.location} 155 165 <!-- svelte-ignore node_invalid_placement_ssr --> ··· 255 265 <p class="title">{gallery.title}</p> 256 266 </a> 257 267 {#if gallery.description} 258 - <p class="description"><RichText text={gallery.description} /></p> 268 + <p class="description" class:expanded={descriptionExpanded} bind:this={descriptionEl}><RichText text={gallery.description} /></p> 269 + {#if descriptionClamped && !descriptionExpanded} 270 + <button class="more-btn" type="button" onclick={() => (descriptionExpanded = true)}>more</button> 271 + {/if} 259 272 {/if} 260 273 {#if labelResult.action === 'badge'} 261 274 <span class="label-badge"><AlertTriangle size={12} /> {labelResult.name}</span> 262 275 {/if} 263 - <time class="timestamp">{timeStr}</time> 264 276 </div> 265 277 </div> 266 278 </article> ··· 306 318 text-overflow: ellipsis; 307 319 flex-shrink: 1; 308 320 } 321 + .header-time { 322 + font-size: 12px; 323 + color: var(--text-muted); 324 + white-space: nowrap; 325 + flex-shrink: 0; 326 + } 309 327 310 328 .card-header :global(.overflow-menu) { 311 329 margin-left: auto; ··· 517 535 line-clamp: 2; 518 536 -webkit-box-orient: vertical; 519 537 overflow: hidden; 538 + } 539 + .description.expanded { 540 + display: block; 541 + -webkit-line-clamp: unset; 542 + line-clamp: unset; 543 + overflow: visible; 544 + } 545 + .more-btn { 546 + background: none; 547 + border: none; 548 + padding: 0; 549 + margin: 0 0 4px; 550 + font-size: 13px; 551 + font-family: inherit; 552 + color: var(--text-muted); 553 + cursor: pointer; 554 + } 555 + .more-btn:hover { 556 + color: var(--text-secondary); 520 557 } 521 558 .location-link { 522 559 font-size: 12px;
+7 -7
app/lib/utils.ts
··· 25 25 return did.slice(0, 12) + "\u2026" + did.slice(-6); 26 26 } 27 27 28 - /** Format a date like grain-next: Today, Yesterday, N days ago, then Mon DD. */ 28 + /** Compact relative time matching grain-native: now, 2m, 3h, 4d, 1w, then Mon DD. */ 29 29 export function relativeTime(iso: string): string { 30 30 const date = new Date(iso); 31 - const now = new Date(); 32 - const diffMs = now.getTime() - date.getTime(); 33 - const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); 31 + const diff = (Date.now() - date.getTime()) / 1000; 34 32 35 - if (diffDays === 0) return "Today"; 36 - if (diffDays === 1) return "Yesterday"; 37 - if (diffDays < 7) return `${diffDays} days ago`; 33 + if (diff < 60) return "now"; 34 + if (diff < 3600) return `${Math.floor(diff / 60)}m`; 35 + if (diff < 86400) return `${Math.floor(diff / 3600)}h`; 36 + if (diff < 604800) return `${Math.floor(diff / 86400)}d`; 37 + if (diff < 2592000) return `${Math.floor(diff / 604800)}w`; 38 38 39 39 return date.toLocaleDateString("en-US", { month: "short", day: "numeric" }); 40 40 }