~ knotless Search/
↑↓ move↵ open⇧↵ all results

grain.social/grain

photography-focused photo sharing platform

grain.social
1 collaborator
0
2

web: Profiles, galleries, feeds, stories and search read from grain-serve

Merged chadtmiller.com asked to merge into feature/rust from grain.social/grain:web/reads

Only what this pull adds. It is built on grain runs on a Rust server modelled on knotless, without hatk.

test/resolveRouteActor.test.ts+3−3
@@ -11,9 +11,9 @@ const FOLLOW_URI = `at://did:plc:me/social.grain.graph.follow/1`;
let following: string | undefined;
let fetches = 0;
// Mocked by resolved path: the $hatk and $lib aliases resolve before vi.mock matches.
vi.mock("../hatk.generated.client.ts", () => ({
callXrpc: async (_nsid: string, _input: unknown) => {
// Mocked by resolved path: the $lib alias resolves before vi.mock matches.
vi.mock("../web/src/lib/xrpc/index.ts", () => ({
query: async (_nsid: string, _input: unknown) => {
fetches++;
return {
did: ALICE,
test/storyViews.test.ts+4−4
@@ -7,9 +7,9 @@ import { QueryClient } from "@tanstack/svelte-query";
const calls: { nsid: string; input: unknown }[] = [];
let fail = false;
// Mocked by resolved path: the $hatk and $lib aliases resolve before vi.mock matches.
vi.mock("../hatk.generated.client.ts", () => ({
callXrpc: async (nsid: string, input: unknown) => {
// Mocked by resolved path: the $lib alias resolves before vi.mock matches.
vi.mock("../web/src/lib/xrpc/index.ts", () => ({
call: async (nsid: string, input: unknown) => {
calls.push({ nsid, input });
if (fail) throw new Error("offline");
return {};
@@ -88,7 +88,7 @@ describe("markStoriesViewed", () => {
expect(calls).toEqual([
{
nsid: "social.grain.unspecced.markStoriesViewed",
nsid: "social.grain.story.markViewed",
input: { stories: [a3.uri] },
},
]);
web/src/lib/components/atoms/ExifInfo.svelte+1−1
@@ -1,5 +1,5 @@
<script lang="ts">
import type { ExifView } from '$hatk/client'
import type { ExifView } from '$lib/lexicons/social/grain/photo/defs.ts'
import { Camera, Aperture } from 'lucide-svelte'
import { cleanCameraName } from '$lib/utils/cameraName'
web/src/lib/components/atoms/LocationInput.svelte+1−1
@@ -2,7 +2,7 @@
import { MapPin, LoaderCircle } from 'lucide-svelte'
import { searchLocations, formatLocationName, formatGeoContext, formatStreet, extractAddress, type NominatimResult } from '$lib/utils/nominatim'
import { latLonToH3 } from '$lib/utils/h3'
import type { Gallery } from '$hatk/client'
import type { Main as Gallery } from '$lib/lexicons/social/grain/gallery.ts'
export type AddressData = NonNullable<Gallery['address']>
web/src/lib/components/molecules/Comment.svelte+6−4
@@ -1,5 +1,6 @@
<script lang="ts">
import type { CommentView } from '$hatk/client'
import type { CommentView } from '$lib/lexicons/social/grain/comment/defs.ts'
import type { PhotoView } from '$lib/lexicons/social/grain/photo/defs.ts'
import { callXrpc } from '$hatk/client'
import { createMutation, useQueryClient } from '@tanstack/svelte-query'
import Avatar from '../atoms/Avatar.svelte'
@@ -23,12 +24,13 @@
const isOwner = $derived($viewer?.did === comment.author?.did)
const timeStr = $derived(relativeTime(comment.createdAt || ''))
const isReply = $derived(!!comment.replyTo)
const focus = $derived(comment.focus as PhotoView | undefined)
const isMuted = $derived(!!comment.muted && !expanded)
// Comment favorite state
let favOverride: string | null | undefined = $state(undefined)
const viewerFav = $derived(comment.viewer?.fav ?? null)
const favUri = $derived(favOverride !== undefined ? favOverride : viewerFav)
const favUri: string | null = $derived(favOverride !== undefined ? favOverride : viewerFav)
const isFaved = $derived(!!favUri)
const originallyFaved = $derived(!!viewerFav)
const serverFavCount = $derived(comment.favCount ?? 0)
@@ -102,8 +104,8 @@
{/if}
</div>
</div>
{#if comment.focus?.thumb}
<img class="focus-thumb" src={comment.focus.thumb} alt={comment.focus?.alt ?? ''} />
{#if focus?.thumb}
<img class="focus-thumb" src={focus.thumb} alt={focus.alt ?? ''} />
{/if}
<button class="fav-btn" class:faved={isFaved} onclick={toggleFav} title={isFaved ? 'Unfavorite' : 'Favorite'}>
<Heart size={16} fill={isFaved ? 'currentColor' : 'none'} />
web/src/lib/components/molecules/GalleryCard.svelte+2−1
@@ -1,5 +1,6 @@
<script lang="ts">
import type { GalleryView, PhotoView, ExifView } from '$hatk/client'
import type { GalleryView } from '$lib/lexicons/social/grain/gallery/defs.ts'
import type { PhotoView, ExifView } from '$lib/lexicons/social/grain/photo/defs.ts'
import { isCaughtUp } from '$lib/stories'
import Avatar from '../atoms/Avatar.svelte'
import FavedByFollowing from './FavedByFollowing.svelte'
web/src/lib/components/molecules/GalleryMedia.svelte+1−1
@@ -6,7 +6,7 @@
*/
import { browser } from '$app/environment'
import { ChevronLeft, ChevronRight, Heart, Info } from 'lucide-svelte'
import type { PhotoView } from '$hatk/client'
import type { PhotoView } from '$lib/lexicons/social/grain/photo/defs.ts'
let {
photos,
web/src/lib/components/molecules/GalleryMenu.svelte+1−1
@@ -7,7 +7,7 @@
* since every item needs an account.
*/
import type { Snippet } from 'svelte'
import type { GalleryView } from '$hatk/client'
import type { GalleryView } from '$lib/lexicons/social/grain/gallery/defs.ts'
import { callXrpc } from '$hatk/client'
import { goto } from '$app/navigation'
import { useQueryClient } from '@tanstack/svelte-query'
web/src/lib/components/molecules/GallerySectionRow.svelte+2−1
@@ -2,7 +2,8 @@
import { profilePath, galleryPath } from '$lib/utils'
import { createQuery } from '@tanstack/svelte-query'
import { cameraFeedQuery, locationFeedQuery } from '$lib/queries'
import type { GalleryView, PhotoView } from '$hatk/client'
import type { GalleryView } from '$lib/lexicons/social/grain/gallery/defs.ts'
import type { PhotoView } from '$lib/lexicons/social/grain/photo/defs.ts'
import Avatar from '../atoms/Avatar.svelte'
import { ChevronRight } from 'lucide-svelte'
web/src/lib/components/molecules/ProfilePopover.svelte+2−1
@@ -1,7 +1,8 @@
<script lang="ts">
import { profilePath } from '$lib/utils'
import { createQuery } from '@tanstack/svelte-query'
import type { GrainActorDefsProfileViewDetailed, GrainUnspeccedGetKnownFollowersFollowerItem as GetKnownFollowersFollowerItem } from '$hatk/client'
import type { ProfileViewDetailed as GrainActorDefsProfileViewDetailed } from '$lib/lexicons/social/grain/actor/defs.ts'
import type { KnownFollowerItem as GetKnownFollowersFollowerItem } from '$lib/lexicons/social/grain/graph/defs.ts'
import { actorProfileQuery, knownFollowersQuery, storyAuthorsQuery } from '$lib/queries'
import { isCaughtUp } from '$lib/stories'
import { viewer } from '$lib/stores'
web/src/lib/components/organisms/CommentSheet.svelte+10−7
web/src/lib/components/organisms/FeedList.svelte+1−1
web/src/lib/components/organisms/GalleryGrid.svelte+2−1
web/src/lib/components/organisms/SearchFlyout.svelte+3−2
web/src/lib/components/organisms/SuggestedFollows.svelte+4−2
web/src/lib/actor.ts+1−1
web/src/lib/feed.ts+43−2
web/src/lib/labels.ts+82−8
web/src/lib/queries.ts+128−77
web/src/lib/seo.ts+4−2
web/src/lib/stories.ts+6−7
web/src/routes/feeds/groups/+page.svelte+3−2
web/src/routes/group/[did]/+page.svelte+1−1
web/src/routes/private/[authority]/[skey]/+page.svelte+7−4
web/src/routes/profile/[actor]/gallery/[rkey]/edit/+page.svelte+2−1
web/src/routes/profile/[actor]/gallery/[rkey]/+page.svelte+2−1
web/src/routes/profile/[actor]/story/[rkey]/+page.svelte+1−1
web/src/routes/sitemap/[kind]-[page].xml/+server.ts+8−6
web/src/routes/sitemap.xml/+server.ts+10−3