WIP PWA for Grain

fix: prevent explore search from overwriting timeline cache

searchGalleries was calling transformTimelineResponse which caches results
under the 'timeline' key. This caused explore search results to overwrite
the actual timeline data, showing only search results when navigating to
timeline.

Created separate transformSearchResponse that caches individual gallery
records but does not update the timeline query cache.

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

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

+51 -1
+51 -1
src/services/grain-api.js
··· 176 176 `; 177 177 178 178 const response = await this.#execute(gqlQuery, { query, first, after }); 179 - return this.#transformTimelineResponse(response); 179 + return this.#transformSearchResponse(response); 180 + } 181 + 182 + #transformSearchResponse(response) { 183 + const connection = response.data?.socialGrainGallery; 184 + if (!connection) return { galleries: [], pageInfo: { hasNextPage: false } }; 185 + 186 + const galleries = connection.edges.map(edge => { 187 + const node = edge.node; 188 + const profile = node.socialGrainActorProfileByDid; 189 + const items = node.socialGrainGalleryItemViaGallery?.edges || []; 190 + 191 + const photos = items 192 + .map(i => { 193 + const photo = i.node.itemResolved; 194 + if (!photo) return null; 195 + return { 196 + url: photo.photo?.url || '', 197 + alt: photo.alt || '', 198 + aspectRatio: photo.aspectRatio 199 + ? photo.aspectRatio.width / photo.aspectRatio.height 200 + : 1 201 + }; 202 + }) 203 + .filter(Boolean); 204 + 205 + return { 206 + uri: node.uri, 207 + title: node.title, 208 + description: node.description, 209 + createdAt: node.createdAt, 210 + handle: node.actorHandle, 211 + displayName: profile?.displayName || '', 212 + avatarUrl: profile?.avatar?.url || '', 213 + photos, 214 + favoriteCount: node.socialGrainFavoriteViaSubject?.totalCount || 0, 215 + commentCount: node.socialGrainCommentViaSubject?.totalCount || 0, 216 + viewerHasFavorited: false, 217 + viewerFavoriteUri: null 218 + }; 219 + }).filter(gallery => gallery.photos.length > 0); 220 + 221 + // Cache each gallery record by URI (but don't update timeline query cache) 222 + galleries.forEach(gallery => { 223 + recordCache.set(gallery.uri, gallery); 224 + }); 225 + 226 + return { 227 + galleries, 228 + pageInfo: connection.pageInfo 229 + }; 180 230 } 181 231 182 232 async searchProfiles(query, { first = 20, after = null } = {}) {