feat: add optimistic UI for favoriting

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

Changed files
+23 -5
src
components
+23 -5
src/components/organisms/grain-engagement-bar.js
··· 73 73 if (!auth.isAuthenticated || this._loading || !this.galleryUri) return; 74 74 75 75 this._loading = true; 76 + 77 + // Store previous state for rollback 78 + const previousState = { 79 + viewerHasFavorited: this.viewerHasFavorited, 80 + viewerFavoriteUri: this.viewerFavoriteUri, 81 + favoriteCount: this.favoriteCount 82 + }; 83 + 84 + // Optimistic update - apply immediately 85 + this.viewerHasFavorited = !this.viewerHasFavorited; 86 + this.favoriteCount += this.viewerHasFavorited ? 1 : -1; 87 + if (!this.viewerHasFavorited) { 88 + this.viewerFavoriteUri = null; 89 + } 90 + 76 91 try { 77 92 const update = await mutations.toggleFavorite( 78 93 this.galleryUri, 79 - this.viewerHasFavorited, 80 - this.viewerFavoriteUri, 81 - this.favoriteCount 94 + previousState.viewerHasFavorited, 95 + previousState.viewerFavoriteUri, 96 + previousState.favoriteCount 82 97 ); 83 - this.viewerHasFavorited = update.viewerHasFavorited; 98 + // Update with real URI from server (needed for future deletes) 84 99 this.viewerFavoriteUri = update.viewerFavoriteUri; 85 - this.favoriteCount = update.favoriteCount; 86 100 } catch (err) { 101 + // Rollback on failure 87 102 console.error('Failed to toggle favorite:', err); 103 + this.viewerHasFavorited = previousState.viewerHasFavorited; 104 + this.viewerFavoriteUri = previousState.viewerFavoriteUri; 105 + this.favoriteCount = previousState.favoriteCount; 88 106 this.shadowRoot.querySelector('grain-toast').show('Failed to update'); 89 107 } finally { 90 108 this._loading = false;