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.

fix: align web notification styling with native app

- Restore gallery/story thumbnails (44px, right-aligned)
- Use 38px grouped avatars with -8px overlap to match native
- Stack avatar above text body for single notifications
- Inline timestamp with action text
- Vertically center icons with avatars

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

+76 -64
+76 -64
app/lib/components/atoms/NotificationItem.svelte
··· 33 33 const authorName = $derived(notif.author?.displayName || notif.author?.handle || authorDid.slice(0, 18)) 34 34 const authorHandle = $derived(notif.author?.handle ?? authorDid.slice(0, 18)) 35 35 const authorAvatar = $derived(notif.author?.avatar ?? null) 36 + const thumb = $derived(notif.galleryThumb ?? notif.storyThumb ?? null) 36 37 const contentHref = $derived( 37 38 notif.galleryUri 38 39 ? `/profile/${notif.galleryUri.split('/')[2]}/gallery/${notif.galleryUri.split('/').pop()}` ··· 103 104 <div class="grouped-avatars"> 104 105 {#each allAuthors.slice(0, 5) as author (author.did)} 105 106 <a href="/profile/{author.did}" class="grouped-avatar-link" onclick={(e) => e.stopPropagation()}> 106 - <Avatar did={author.did} src={author.avatar} name={author.name} size={34} /> 107 + <Avatar did={author.did} src={author.avatar} name={author.name} size={38} /> 107 108 </a> 108 109 {/each} 109 110 {#if group.authorCount > 5} ··· 115 116 </div> 116 117 {/if} 117 118 <a href={contentHref} class="notif-link"> 118 - <div class="notif-header"> 119 + <div class="notif-action-line"> 119 120 <span class="notif-text">{groupActionText}</span> 120 121 <span class="notif-time">{timeStr}</span> 121 122 </div> ··· 124 125 {/if} 125 126 </a> 126 127 </div> 128 + {#if thumb} 129 + <a href={contentHref} class="notif-thumb-link"> 130 + <img src={thumb} alt="" class="notif-thumb" loading="lazy" /> 131 + </a> 132 + {/if} 127 133 </div> 128 134 {:else} 129 135 <!-- Single notification --> ··· 136 142 {:else if isMention}<AtSign size={18} /> 137 143 {/if} 138 144 </div> 139 - <a class="notif-avatar" href={profileHref}> 140 - <Avatar did={authorDid} src={authorAvatar} name={authorName} size={34} /> 141 - </a> 142 - <a class="notif-body" href={contentHref}> 143 - <div class="notif-header"> 144 - <span class="notif-author">{authorName}</span> 145 - <span class="notif-handle">@{authorHandle}</span> 146 - <span class="notif-time">{timeStr}</span> 147 - </div> 148 - <div class="notif-action">{action}</div> 149 - {#if notif.reason === 'reply' && notif.replyToText} 150 - <div class="notif-quote">{notif.replyToText}</div> 151 - {/if} 152 - {#if notif.commentText} 153 - <div class="notif-comment">{notif.commentText}</div> 154 - {/if} 155 - {#if notif.galleryTitle && notif.reason !== 'follow'} 156 - <div class="notif-gallery-title">{notif.galleryTitle}</div> 157 - {/if} 158 - </a> 145 + <div class="notif-content"> 146 + <a class="notif-avatar" href={profileHref}> 147 + <Avatar did={authorDid} src={authorAvatar} name={authorName} size={34} /> 148 + </a> 149 + <a class="notif-body" href={contentHref}> 150 + <div class="notif-action-line"> 151 + <span><span class="notif-author">{authorName}</span> {action}</span> 152 + <span class="notif-time">{timeStr}</span> 153 + </div> 154 + {#if notif.commentText} 155 + <div class="notif-comment">{notif.commentText}</div> 156 + {/if} 157 + {#if notif.reason === 'reply' && notif.replyToText} 158 + <div class="notif-quote">{notif.replyToText}</div> 159 + {/if} 160 + {#if notif.galleryTitle && notif.reason !== 'follow'} 161 + <div class="notif-gallery-title">{notif.galleryTitle}</div> 162 + {/if} 163 + </a> 164 + </div> 165 + {#if thumb} 166 + <a href={contentHref} class="notif-thumb-link"> 167 + <img src={thumb} alt="" class="notif-thumb" loading="lazy" /> 168 + </a> 169 + {/if} 159 170 </div> 160 171 {/if} 161 172 162 173 <style> 163 174 .notif { 164 175 display: flex; 165 - gap: 12px; 176 + gap: 10px; 166 177 padding: 12px 16px; 167 178 border-bottom: 1px solid var(--border); 168 179 color: inherit; 169 180 transition: background 0.12s; 170 - align-items: center; 181 + align-items: flex-start; 171 182 } 172 183 .notif:hover { 173 184 background: var(--bg-hover); 174 185 } 175 186 .notif-icon { 176 187 flex-shrink: 0; 177 - width: 28px; 188 + width: 20px; 178 189 display: flex; 179 190 justify-content: center; 180 - padding-top: 2px; 191 + height: 34px; 192 + align-items: center; 193 + } 194 + .grouped .notif-icon { 195 + height: 38px; 181 196 } 182 197 .icon-grain { 183 198 color: var(--grain); 184 199 } 200 + .notif-content { 201 + flex: 1; 202 + min-width: 0; 203 + display: flex; 204 + flex-direction: column; 205 + gap: 6px; 206 + } 185 207 .notif-avatar { 186 208 flex-shrink: 0; 187 209 text-decoration: none; ··· 192 214 text-decoration: none; 193 215 color: inherit; 194 216 } 195 - .notif-header { 196 - display: flex; 197 - align-items: baseline; 217 + .notif-action-line { 198 218 font-size: 13px; 199 219 line-height: 1.4; 200 - min-width: 0; 220 + color: var(--text-primary); 201 221 } 202 222 .notif-author { 203 223 font-weight: 600; 204 - color: var(--text-primary); 205 - flex-shrink: 0; 206 224 } 207 - .notif-avatar:hover + .notif-body .notif-author { 225 + .notif-avatar:hover ~ .notif-body .notif-author { 208 226 text-decoration: underline; 209 227 } 210 - .notif-handle { 211 - color: var(--text-muted); 212 - margin-left: 6px; 213 - flex: 1; 214 - min-width: 0; 215 - overflow: hidden; 216 - text-overflow: ellipsis; 217 - white-space: nowrap; 218 - } 219 - .notif-action { 220 - color: var(--text-secondary); 221 - font-size: 13px; 222 - line-height: 1.4; 223 - margin-top: 1px; 224 - } 225 228 .notif-time { 226 229 color: var(--text-muted); 227 - margin-left: 6px; 228 230 font-size: 12px; 229 - flex-shrink: 0; 231 + margin-left: 4px; 232 + } 233 + .notif-comment { 234 + font-size: 13px; 235 + color: var(--text-secondary); 236 + margin-top: 2px; 237 + overflow: hidden; 238 + text-overflow: ellipsis; 239 + display: -webkit-box; 240 + -webkit-line-clamp: 3; 241 + -webkit-box-orient: vertical; 230 242 } 231 243 .notif-quote { 232 244 font-size: 12px; ··· 238 250 text-overflow: ellipsis; 239 251 white-space: nowrap; 240 252 } 241 - .notif-comment { 242 - font-size: 13px; 243 - color: var(--text-secondary); 244 - margin-top: 4px; 245 - overflow: hidden; 246 - text-overflow: ellipsis; 247 - white-space: nowrap; 248 - } 249 253 .notif-gallery-title { 250 254 font-size: 12px; 251 255 color: var(--text-muted); 252 256 margin-top: 2px; 253 257 } 258 + .notif-thumb-link { 259 + flex-shrink: 0; 260 + align-self: center; 261 + } 262 + .notif-thumb { 263 + width: 44px; 264 + height: 44px; 265 + object-fit: cover; 266 + border-radius: 6px; 267 + } 254 268 /* Grouped notification styles */ 255 269 .grouped-avatars { 256 270 display: flex; ··· 259 273 margin-bottom: 6px; 260 274 } 261 275 .grouped-avatar-link { 262 - margin-right: -4px; 276 + margin-right: -8px; 263 277 text-decoration: none; 264 278 position: relative; 265 279 } ··· 269 283 .more-count { 270 284 font-size: 12px; 271 285 color: var(--text-muted); 272 - margin-left: 8px; 286 + margin-left: 12px; 273 287 } 274 288 .notif-text { 275 - color: var(--text-secondary); 289 + color: var(--text-primary); 276 290 font-size: 13px; 277 291 line-height: 1.4; 278 - flex: 1; 279 - min-width: 0; 280 292 } 281 293 .notif-link { 282 294 text-decoration: none; ··· 305 317 color: var(--text-muted); 306 318 cursor: pointer; 307 319 padding: 4px 8px; 308 - margin-left: 8px; 320 + margin-left: 12px; 309 321 } 310 322 .expand-toggle-chevron:hover { 311 323 color: var(--text-secondary);