WIP PWA for Grain
0
fork

Configure Feed

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

docs: add photo-focused comments implementation plan

+509
+509
docs/plans/2025-12-28-photo-focused-comments.md
··· 1 + # Photo-Focused Comments Implementation Plan 2 + 3 + > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. 4 + 5 + **Goal:** Allow users to comment on a specific photo in a gallery by auto-selecting the currently visible photo as the comment focus. 6 + 7 + **Architecture:** The carousel exposes its current photo. When the comment icon is tapped, the parent reads the current photo and passes its URI/URL to the comment sheet. The input shows a thumbnail indicating the focus, with an "x" to clear. The mutation includes the focus URI when set. 8 + 9 + **Tech Stack:** Lit 3, Web Components, GraphQL (quickslice) 10 + 11 + --- 12 + 13 + ## Task 1: Expose current photo from carousel 14 + 15 + **Files:** 16 + - Modify: `src/components/organisms/grain-image-carousel.js` 17 + 18 + **Step 1: Add method to get current photo** 19 + 20 + Add after the `#shouldLoad` method: 21 + 22 + ```javascript 23 + getCurrentPhoto() { 24 + return this.photos[this._currentIndex] || null; 25 + } 26 + ``` 27 + 28 + **Step 2: Verify manually** 29 + 30 + Run: `npm run dev` 31 + Open browser console on a gallery detail page and run: 32 + ```javascript 33 + document.querySelector('grain-image-carousel').getCurrentPhoto() 34 + ``` 35 + Expected: Returns the current photo object with `uri`, `url`, `alt`, etc. 36 + 37 + **Step 3: Commit** 38 + 39 + ```bash 40 + git add src/components/organisms/grain-image-carousel.js 41 + git commit -m "feat: expose getCurrentPhoto method on carousel" 42 + ``` 43 + 44 + --- 45 + 46 + ## Task 2: Add focus photo display to comment input 47 + 48 + **Files:** 49 + - Modify: `src/components/molecules/grain-comment-input.js` 50 + 51 + **Step 1: Add focusPhotoUrl property** 52 + 53 + Add to `static properties`: 54 + 55 + ```javascript 56 + focusPhotoUrl: { type: String } 57 + ``` 58 + 59 + Add to constructor: 60 + 61 + ```javascript 62 + this.focusPhotoUrl = ''; 63 + ``` 64 + 65 + **Step 2: Add CSS for focus thumbnail** 66 + 67 + Add to `static styles` before the closing backtick: 68 + 69 + ```css 70 + .focus-photo { 71 + position: relative; 72 + flex-shrink: 0; 73 + } 74 + .focus-photo img { 75 + width: 32px; 76 + height: 32px; 77 + border-radius: 4px; 78 + object-fit: cover; 79 + } 80 + .focus-photo .clear-btn { 81 + position: absolute; 82 + top: -4px; 83 + right: -4px; 84 + width: 16px; 85 + height: 16px; 86 + border-radius: 50%; 87 + background: var(--color-bg-elevated); 88 + border: 1px solid var(--color-border); 89 + display: flex; 90 + align-items: center; 91 + justify-content: center; 92 + cursor: pointer; 93 + font-size: 10px; 94 + color: var(--color-text-secondary); 95 + padding: 0; 96 + } 97 + ``` 98 + 99 + **Step 3: Add clear focus handler** 100 + 101 + Add after `#handleSend`: 102 + 103 + ```javascript 104 + #handleClearFocus() { 105 + this.dispatchEvent(new CustomEvent('clear-focus')); 106 + } 107 + ``` 108 + 109 + **Step 4: Update render to show focus thumbnail** 110 + 111 + Replace the render method's return statement: 112 + 113 + ```javascript 114 + return html` 115 + <grain-avatar src=${this.avatarUrl} size="sm"></grain-avatar> 116 + ${this.focusPhotoUrl ? html` 117 + <div class="focus-photo"> 118 + <img src=${this.focusPhotoUrl} alt="Commenting on this photo" /> 119 + <button class="clear-btn" @click=${this.#handleClearFocus}>&times;</button> 120 + </div> 121 + ` : ''} 122 + <div class="input-wrapper"> 123 + <input 124 + type="text" 125 + .value=${this.value} 126 + placeholder=${this.placeholder} 127 + ?disabled=${this.disabled || this.loading} 128 + @input=${this.#handleInput} 129 + /> 130 + <button 131 + class="send-button" 132 + type="button" 133 + ?disabled=${!canSend} 134 + @click=${this.#handleSend} 135 + > 136 + ${this.loading ? html`<grain-spinner></grain-spinner>` : 'Post'} 137 + </button> 138 + </div> 139 + `; 140 + ``` 141 + 142 + **Step 5: Commit** 143 + 144 + ```bash 145 + git add src/components/molecules/grain-comment-input.js 146 + git commit -m "feat: add focus photo thumbnail to comment input" 147 + ``` 148 + 149 + --- 150 + 151 + ## Task 3: Update comment sheet to handle focus 152 + 153 + **Files:** 154 + - Modify: `src/components/organisms/grain-comment-sheet.js` 155 + 156 + **Step 1: Add focus properties** 157 + 158 + Add to `static properties`: 159 + 160 + ```javascript 161 + focusPhotoUri: { type: String }, 162 + focusPhotoUrl: { type: String } 163 + ``` 164 + 165 + **Step 2: Add internal focus state** 166 + 167 + Add to existing state properties: 168 + 169 + ```javascript 170 + _focusPhotoUri: { state: true }, 171 + _focusPhotoUrl: { state: true } 172 + ``` 173 + 174 + Add to constructor: 175 + 176 + ```javascript 177 + this._focusPhotoUri = null; 178 + this._focusPhotoUrl = null; 179 + ``` 180 + 181 + **Step 3: Sync props to state when sheet opens** 182 + 183 + Update the `updated` method: 184 + 185 + ```javascript 186 + updated(changedProps) { 187 + if (changedProps.has('open') && this.open && this.galleryUri) { 188 + this.#loadComments(); 189 + this._focusPhotoUri = this.focusPhotoUri || null; 190 + this._focusPhotoUrl = this.focusPhotoUrl || null; 191 + } 192 + } 193 + ``` 194 + 195 + **Step 4: Add clear focus handler** 196 + 197 + Add after `#handleReply`: 198 + 199 + ```javascript 200 + #handleClearFocus() { 201 + this._focusPhotoUri = null; 202 + this._focusPhotoUrl = null; 203 + } 204 + ``` 205 + 206 + **Step 5: Update mutation to include focus** 207 + 208 + In `#handleSend`, update the createComment call: 209 + 210 + ```javascript 211 + const commentUri = await mutations.createComment( 212 + this.galleryUri, 213 + text, 214 + this._replyToUri, 215 + this._focusPhotoUri 216 + ); 217 + ``` 218 + 219 + Also update the newComment object to include focus: 220 + 221 + ```javascript 222 + const newComment = { 223 + uri: commentUri, 224 + text, 225 + createdAt: new Date().toISOString(), 226 + handle: auth.user?.handle || '', 227 + displayName: auth.user?.displayName || '', 228 + avatarUrl: auth.user?.avatar?.url || '', 229 + replyToUri: this._replyToUri, 230 + isReply: !!this._replyToUri, 231 + focusImageUrl: this._focusPhotoUrl || null, 232 + focusImageAlt: '' 233 + }; 234 + ``` 235 + 236 + **Step 6: Clear focus after posting** 237 + 238 + In `#handleSend`, after clearing `_replyToUri`: 239 + 240 + ```javascript 241 + this._focusPhotoUri = null; 242 + this._focusPhotoUrl = null; 243 + ``` 244 + 245 + **Step 7: Update comment input to receive focus props** 246 + 247 + Update the `grain-comment-input` in render: 248 + 249 + ```javascript 250 + <grain-comment-input 251 + avatarUrl=${userAvatarUrl} 252 + .value=${this._inputValue} 253 + ?loading=${this._posting} 254 + focusPhotoUrl=${this._focusPhotoUrl || ''} 255 + @input-change=${this.#handleInputChange} 256 + @send=${this.#handleSend} 257 + @clear-focus=${this.#handleClearFocus} 258 + ></grain-comment-input> 259 + ``` 260 + 261 + **Step 8: Commit** 262 + 263 + ```bash 264 + git add src/components/organisms/grain-comment-sheet.js 265 + git commit -m "feat: add focus photo support to comment sheet" 266 + ``` 267 + 268 + --- 269 + 270 + ## Task 4: Update mutations to accept focus parameter 271 + 272 + **Files:** 273 + - Modify: `src/services/mutations.js` 274 + 275 + **Step 1: Update createComment signature** 276 + 277 + Change the method signature and add focus to input: 278 + 279 + ```javascript 280 + async createComment(galleryUri, text, replyToUri = null, focusUri = null) { 281 + const client = auth.getClient(); 282 + const input = { 283 + subject: galleryUri, 284 + text, 285 + createdAt: new Date().toISOString() 286 + }; 287 + 288 + if (replyToUri) { 289 + input.replyTo = replyToUri; 290 + } 291 + 292 + if (focusUri) { 293 + input.focus = focusUri; 294 + } 295 + 296 + const result = await client.mutate(` 297 + mutation CreateComment($input: SocialGrainCommentInput!) { 298 + createSocialGrainComment(input: $input) { uri } 299 + } 300 + `, { input }); 301 + 302 + return result.createSocialGrainComment.uri; 303 + } 304 + ``` 305 + 306 + **Step 2: Commit** 307 + 308 + ```bash 309 + git add src/services/mutations.js 310 + git commit -m "feat: add focus parameter to createComment mutation" 311 + ``` 312 + 313 + --- 314 + 315 + ## Task 5: Pass focus from gallery detail to comment sheet 316 + 317 + **Files:** 318 + - Modify: `src/components/pages/grain-gallery-detail.js` 319 + 320 + **Step 1: Add focus state properties** 321 + 322 + Add to `static properties`: 323 + 324 + ```javascript 325 + _focusPhotoUri: { state: true }, 326 + _focusPhotoUrl: { state: true } 327 + ``` 328 + 329 + Add to constructor: 330 + 331 + ```javascript 332 + this._focusPhotoUri = null; 333 + this._focusPhotoUrl = null; 334 + ``` 335 + 336 + **Step 2: Update comment click handler** 337 + 338 + Replace `#handleCommentClick`: 339 + 340 + ```javascript 341 + #handleCommentClick() { 342 + if (!auth.isAuthenticated) { 343 + this.dispatchEvent(new CustomEvent('show-login', { 344 + bubbles: true, 345 + composed: true 346 + })); 347 + return; 348 + } 349 + 350 + // Get current photo from carousel 351 + const carousel = this.shadowRoot.querySelector('grain-image-carousel'); 352 + const currentPhoto = carousel?.getCurrentPhoto(); 353 + 354 + this._focusPhotoUri = currentPhoto?.uri || null; 355 + this._focusPhotoUrl = currentPhoto?.url || null; 356 + this._commentSheetOpen = true; 357 + } 358 + ``` 359 + 360 + **Step 3: Clear focus when sheet closes** 361 + 362 + Update `#handleCommentSheetClose`: 363 + 364 + ```javascript 365 + #handleCommentSheetClose() { 366 + this._commentSheetOpen = false; 367 + this._focusPhotoUri = null; 368 + this._focusPhotoUrl = null; 369 + } 370 + ``` 371 + 372 + **Step 4: Pass focus to comment sheet** 373 + 374 + Update the `grain-comment-sheet` element in render: 375 + 376 + ```javascript 377 + <grain-comment-sheet 378 + ?open=${this._commentSheetOpen} 379 + galleryUri=${this._gallery?.uri || ''} 380 + focusPhotoUri=${this._focusPhotoUri || ''} 381 + focusPhotoUrl=${this._focusPhotoUrl || ''} 382 + @close=${this.#handleCommentSheetClose} 383 + ></grain-comment-sheet> 384 + ``` 385 + 386 + **Step 5: Commit** 387 + 388 + ```bash 389 + git add src/components/pages/grain-gallery-detail.js 390 + git commit -m "feat: pass current photo focus to comment sheet in gallery detail" 391 + ``` 392 + 393 + --- 394 + 395 + ## Task 6: Pass focus from timeline to comment sheet 396 + 397 + **Files:** 398 + - Modify: `src/components/pages/grain-timeline.js` 399 + 400 + **Step 1: Add focus state properties** 401 + 402 + Add to `static properties`: 403 + 404 + ```javascript 405 + _focusPhotoUri: { state: true }, 406 + _focusPhotoUrl: { state: true } 407 + ``` 408 + 409 + Add to constructor: 410 + 411 + ```javascript 412 + this._focusPhotoUri = null; 413 + this._focusPhotoUrl = null; 414 + ``` 415 + 416 + **Step 2: Update comment click handler** 417 + 418 + Replace `#handleCommentClick`: 419 + 420 + ```javascript 421 + #handleCommentClick(e) { 422 + const card = e.target.closest('grain-gallery-card'); 423 + const galleryUri = card?.gallery?.uri; 424 + if (!galleryUri) return; 425 + 426 + if (!auth.isAuthenticated) { 427 + this.dispatchEvent(new CustomEvent('show-login', { 428 + bubbles: true, 429 + composed: true 430 + })); 431 + return; 432 + } 433 + 434 + // Get current photo from carousel inside the card 435 + const carousel = card.shadowRoot?.querySelector('grain-image-carousel'); 436 + const currentPhoto = carousel?.getCurrentPhoto(); 437 + 438 + this._commentGalleryUri = galleryUri; 439 + this._focusPhotoUri = currentPhoto?.uri || null; 440 + this._focusPhotoUrl = currentPhoto?.url || null; 441 + this._commentSheetOpen = true; 442 + } 443 + ``` 444 + 445 + **Step 3: Clear focus when sheet closes** 446 + 447 + Update `#handleCommentSheetClose`: 448 + 449 + ```javascript 450 + #handleCommentSheetClose() { 451 + this._commentSheetOpen = false; 452 + this._commentGalleryUri = ''; 453 + this._focusPhotoUri = null; 454 + this._focusPhotoUrl = null; 455 + } 456 + ``` 457 + 458 + **Step 4: Pass focus to comment sheet** 459 + 460 + Update the `grain-comment-sheet` element in render: 461 + 462 + ```javascript 463 + <grain-comment-sheet 464 + ?open=${this._commentSheetOpen} 465 + galleryUri=${this._commentGalleryUri} 466 + focusPhotoUri=${this._focusPhotoUri || ''} 467 + focusPhotoUrl=${this._focusPhotoUrl || ''} 468 + @close=${this.#handleCommentSheetClose} 469 + ></grain-comment-sheet> 470 + ``` 471 + 472 + **Step 5: Commit** 473 + 474 + ```bash 475 + git add src/components/pages/grain-timeline.js 476 + git commit -m "feat: pass current photo focus to comment sheet in timeline" 477 + ``` 478 + 479 + --- 480 + 481 + ## Task 7: Manual testing 482 + 483 + **Steps:** 484 + 485 + 1. Run `npm run dev` 486 + 2. Navigate to a gallery with multiple photos 487 + 3. Swipe to photo 2 488 + 4. Tap the comment icon 489 + 5. Verify: Comment sheet opens with photo 2 thumbnail in input area 490 + 6. Tap the "x" on the thumbnail 491 + 7. Verify: Thumbnail disappears (commenting on gallery as a whole) 492 + 8. Swipe to photo 3, tap comment icon again 493 + 9. Type a comment and tap Post 494 + 10. Verify: Comment appears with photo 3 thumbnail 495 + 11. Test same flow from timeline 496 + 497 + --- 498 + 499 + ## Summary 500 + 501 + **New files:** None 502 + 503 + **Modified files:** 504 + - `src/components/organisms/grain-image-carousel.js` - added `getCurrentPhoto()` 505 + - `src/components/molecules/grain-comment-input.js` - added focus thumbnail with clear 506 + - `src/components/organisms/grain-comment-sheet.js` - accept and handle focus props 507 + - `src/services/mutations.js` - added `focusUri` parameter 508 + - `src/components/pages/grain-gallery-detail.js` - pass focus to sheet 509 + - `src/components/pages/grain-timeline.js` - pass focus to sheet