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}
+