diff --git a/src/lib/atproto/server/feed.remote.ts b/src/lib/atproto/server/feed.remote.ts index f9cbea6..e8baa6b 100644 --- a/src/lib/atproto/server/feed.remote.ts +++ b/src/lib/atproto/server/feed.remote.ts @@ -234,7 +234,10 @@ export const getAuthorFeed = command( export const searchPosts = command( v.object({ q: v.string(), - cursor: v.optional(v.string()) + cursor: v.optional(v.string()), + author: v.optional(v.string()), + sort: v.optional(v.picklist(['top', 'latest'])), + since: v.optional(v.string()) }), async (input) => { const { locals } = getRequestEvent(); @@ -247,7 +250,10 @@ export const searchPosts = command( params: { q: input.q, limit: 25, - ...(input.cursor ? { cursor: input.cursor } : {}) + ...(input.cursor ? { cursor: input.cursor } : {}), + ...(input.author ? { author: input.author as `did:${string}:${string}` } : {}), + ...(input.sort ? { sort: input.sort } : {}), + ...(input.since ? { since: input.since } : {}) } }); if (!res.ok) error(res.status, 'Failed to search posts'); diff --git a/src/lib/components/embed/special/AppEmbed.svelte b/src/lib/components/embed/special/AppEmbed.svelte index 608c1c6..bb5ab75 100644 --- a/src/lib/components/embed/special/AppEmbed.svelte +++ b/src/lib/components/embed/special/AppEmbed.svelte @@ -7,5 +7,5 @@ {#if config} - + {/if} diff --git a/src/lib/components/embed/special/IframeEmbed.svelte b/src/lib/components/embed/special/IframeEmbed.svelte index 4ce8754..c50c700 100644 --- a/src/lib/components/embed/special/IframeEmbed.svelte +++ b/src/lib/components/embed/special/IframeEmbed.svelte @@ -1,26 +1,26 @@
- + {#if activated} + + {:else} + + {/if}
diff --git a/src/lib/components/embed/special/embed-registry.ts b/src/lib/components/embed/special/embed-registry.ts index 5e32e27..f6d723c 100644 --- a/src/lib/components/embed/special/embed-registry.ts +++ b/src/lib/components/embed/special/embed-registry.ts @@ -19,6 +19,10 @@ export interface EmbedAppConfig { allowedCollections: string[]; /** Aspect ratio for the embed to prevent layout shift */ aspectRatio: { width: number; height: number }; + /** If true, show a click-to-load overlay before loading the iframe */ + requireClick?: boolean; + /** Label for the click-to-load overlay */ + label?: string; } export const embedApps: EmbedAppConfig[] = [ @@ -36,6 +40,21 @@ export const embedApps: EmbedAppConfig[] = [ 'community.lexicon.calendar.rsvp' ], aspectRatio: { width: 2, height: 1 } + }, + { + domain: 'stream.place', + match: (href) => /^https?:\/\/(www\.)?stream\.place\/[^/]+\/?$/.test(href), + embedUrl: (href) => { + const url = new URL(href); + const actor = url.pathname.replace(/^\//, '').replace(/\/$/, ''); + url.pathname = `/embed/${actor}`; + url.search = ''; + return url.toString(); + }, + allowedCollections: [], + aspectRatio: { width: 16, height: 9 }, + requireClick: true, + label: 'Load stream' } ]; diff --git a/src/routes/profile/[handle]/+page.svelte b/src/routes/profile/[handle]/+page.svelte index fac8a0d..4ab110c 100644 --- a/src/routes/profile/[handle]/+page.svelte +++ b/src/routes/profile/[handle]/+page.svelte @@ -11,6 +11,7 @@ import { getAuthorFeed, followUser, unfollowUser, getProfile } from '$lib/atproto/server/feed.remote'; import type { FeedItem } from '$lib/cache.svelte'; import ScrollablePostList, { getCachedList, setCachedList } from '$lib/components/ScrollablePostList.svelte'; + import PostList from '$lib/components/PostList.svelte'; import { UserPlus, UserCheck } from '@lucide/svelte'; @@ -32,6 +33,10 @@ let postsLoading = $state(true); let loadingMore = $state(false); + // Top posts state + let topPostUris = $state([]); + let topPostsLoading = $state(false); + async function toggleFollow() { if (!profile?.did || followLoading) return; followLoading = true; @@ -62,6 +67,8 @@ error = null; followUri = null; followsMe = false; + topPostUris = []; + topPostsLoading = false; const key = `profile-${actor}`; @@ -110,21 +117,52 @@ loading = false; } - // Fetch fresh posts - try { - const result = await getAuthorFeed({ actor }); - const freshItems = ingestFeedPosts(result.posts); - for (const item of freshItems) prefetchThread(item.uri); - if (!cachedList) { - feedItems = freshItems; - postsCursor = result.cursor; + // Fetch fresh posts and top posts in parallel + const feedPromise = (async () => { + try { + const result = await getAuthorFeed({ actor }); + const freshItems = ingestFeedPosts(result.posts); + for (const item of freshItems) prefetchThread(item.uri); + if (!cachedList) { + feedItems = freshItems; + postsCursor = result.cursor; + } + setCachedList(key, freshItems, result.cursor); + } catch (e) { + console.error('Failed to load author feed:', e); + } finally { + postsLoading = false; } - setCachedList(key, freshItems, result.cursor); - } catch (e) { - console.error('Failed to load author feed:', e); - } finally { - postsLoading = false; - } + })(); + + const topPostsPromise = (async () => { + if (!user.did) return; + topPostsLoading = true; + try { + // Fetch multiple pages to find top posts by engagement + let allPosts: any[] = []; // eslint-disable-line @typescript-eslint/no-explicit-any + let pageCursor: string | undefined; + for (let i = 0; i < 17; i++) { + const result = await getAuthorFeed({ actor, ...(pageCursor ? { cursor: pageCursor } : {}) }); + const items = ingestFeedPosts(result.posts); + allPosts.push(...result.posts); + pageCursor = result.cursor ?? undefined; + if (!pageCursor) break; + } + // Sort by like count descending, take top 5 + const sorted = allPosts + .filter((p: any) => p.post?.likeCount != null) // eslint-disable-line @typescript-eslint/no-explicit-any + .sort((a: any, b: any) => (b.post.likeCount ?? 0) - (a.post.likeCount ?? 0)) // eslint-disable-line @typescript-eslint/no-explicit-any + .slice(0, 5); + topPostUris = sorted.map((p: any) => p.post.uri); // eslint-disable-line @typescript-eslint/no-explicit-any + } catch (e) { + console.error('Failed to load top posts:', e); + } finally { + topPostsLoading = false; + } + })(); + + await Promise.all([feedPromise, topPostsPromise]); } $effect(() => { @@ -218,6 +256,14 @@ + {#if user.did && topPostUris.length > 0} +
+

Top Posts

+
+ +
+ {/if} +