feat: add optimistic UI for following

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

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

Changed files
+29 -6
src
components
+29 -6
src/components/organisms/grain-profile-header.js
··· 238 238 if (!this._user || this._followLoading || !this.profile) return; 239 239 240 240 this._followLoading = true; 241 + 242 + // Store previous state for rollback 243 + const previousState = { 244 + viewerIsFollowing: this.profile.viewerIsFollowing, 245 + viewerFollowUri: this.profile.viewerFollowUri, 246 + followerCount: this.profile.followerCount || 0 247 + }; 248 + 249 + // Optimistic update - apply immediately 250 + const newIsFollowing = !previousState.viewerIsFollowing; 251 + this.profile = { 252 + ...this.profile, 253 + viewerIsFollowing: newIsFollowing, 254 + viewerFollowUri: newIsFollowing ? this.profile.viewerFollowUri : null, 255 + followerCount: previousState.followerCount + (newIsFollowing ? 1 : -1) 256 + }; 257 + 241 258 try { 242 259 const update = await mutations.toggleFollow( 243 260 this.profile.handle, 244 261 this.profile.did, 245 - this.profile.viewerIsFollowing, 246 - this.profile.viewerFollowUri, 247 - this.profile.followerCount || 0 262 + previousState.viewerIsFollowing, 263 + previousState.viewerFollowUri, 264 + previousState.followerCount 248 265 ); 266 + // Update with real URI from server (needed for future unfollows) 249 267 this.profile = { 250 268 ...this.profile, 251 - viewerIsFollowing: update.viewerIsFollowing, 252 - viewerFollowUri: update.viewerFollowUri, 253 - followerCount: update.followerCount 269 + viewerFollowUri: update.viewerFollowUri 254 270 }; 255 271 } catch (err) { 272 + // Rollback on failure 256 273 console.error('Failed to toggle follow:', err); 274 + this.profile = { 275 + ...this.profile, 276 + viewerIsFollowing: previousState.viewerIsFollowing, 277 + viewerFollowUri: previousState.viewerFollowUri, 278 + followerCount: previousState.followerCount 279 + }; 257 280 this.shadowRoot.querySelector('grain-toast')?.show('Failed to update'); 258 281 } finally { 259 282 this._followLoading = false;