diff --git a/app/discover/PubListing.tsx b/app/discover/PubListing.tsx
index 14387eb1..78699686 100644
--- a/app/discover/PubListing.tsx
+++ b/app/discover/PubListing.tsx
@@ -65,11 +65,8 @@ export const PubListing = (props: {
Updated{" "}
{timeAgo(
- props.documents_in_publications.sort((a, b) => {
- let dateA = new Date(a.documents?.indexed_at || 0);
- let dateB = new Date(b.documents?.indexed_at || 0);
- return dateB.getTime() - dateA.getTime();
- })[0].documents?.indexed_at || "",
+ props.documents_in_publications?.[0]?.documents?.indexed_at ||
+ "",
)}
diff --git a/app/reader/ReaderContent.tsx b/app/reader/ReaderContent.tsx
index e62b1665..db42c558 100644
--- a/app/reader/ReaderContent.tsx
+++ b/app/reader/ReaderContent.tsx
@@ -1,6 +1,6 @@
"use client";
import { AtUri } from "@atproto/api";
-import { Interactions } from "app/lish/[did]/[publication]/[rkey]/Interactions/Interactions";
+import { getPublicationURL } from "app/lish/createPub/getPublicationURL";
import { PubIcon } from "components/ActionBar/Publications";
import { ButtonPrimary } from "components/Buttons";
import { CommentTiny } from "components/Icons/CommentTiny";
@@ -14,31 +14,11 @@ import { useSmoker } from "components/Toast";
import { PubLeafletDocument, PubLeafletPublication } from "lexicons/api";
import { blobRefToSrc } from "src/utils/blobRefToSrc";
import { Json } from "supabase/database.types";
+import type { Post } from "./getReaderFeed";
export const ReaderContent = (props: {
root_entity: string;
- posts: {
- publication: {
- href: string;
- pubRecord: Json;
- uri: string;
- };
- documents: {
- data: Json;
- uri: string;
- indexed_at: string;
- comments_on_documents:
- | {
- count: number;
- }[]
- | undefined;
- document_mentions_in_bsky:
- | {
- count: number;
- }[]
- | undefined;
- };
- }[];
+ posts: Post[];
}) => {
if (props.posts.length === 0) return ;
return (
@@ -48,28 +28,7 @@ export const ReaderContent = (props: {
);
};
-const Post = (props: {
- publication: {
- pubRecord: Json;
- uri: string;
- href: string;
- };
- documents: {
- data: Json;
- uri: string;
- indexed_at: string;
- comments_on_documents:
- | {
- count: number;
- }[]
- | undefined;
- document_mentions_in_bsky:
- | {
- count: number;
- }[]
- | undefined;
- };
-}) => {
+const Post = (props: Post) => {
let pubRecord = props.publication.pubRecord as PubLeafletPublication.Record;
let postRecord = props.documents.data as PubLeafletDocument.Record;
@@ -133,7 +92,7 @@ const Post = (props: {
/>
@@ -173,7 +132,7 @@ const PostInfo = (props: {
}) => {
return (
- NAME HERE
+ {props.author}
{props.publishedAt && (
<>
diff --git a/app/reader/getReaderFeed.ts b/app/reader/getReaderFeed.ts
new file mode 100644
index 00000000..75d18dbe
--- /dev/null
+++ b/app/reader/getReaderFeed.ts
@@ -0,0 +1,161 @@
+"use server";
+
+import { getIdentityData } from "actions/getIdentityData";
+import { getPublicationURL } from "app/lish/createPub/getPublicationURL";
+import { supabaseServerClient } from "supabase/serverClient";
+import { IdResolver } from "@atproto/identity";
+import type { DidCache, CacheResult, DidDocument } from "@atproto/identity";
+import Client from "ioredis";
+import { AtUri } from "@atproto/api";
+import { Json } from "supabase/database.types";
+
+// Create Redis client for DID caching
+let redisClient: Client | null = null;
+if (process.env.REDIS_URL) {
+ redisClient = new Client(process.env.REDIS_URL);
+}
+
+// Redis-based DID cache implementation
+class RedisDidCache implements DidCache {
+ private staleTTL: number;
+ private maxTTL: number;
+
+ constructor(
+ private client: Client,
+ staleTTL = 60 * 60, // 1 hour
+ maxTTL = 60 * 60 * 24, // 24 hours
+ ) {
+ this.staleTTL = staleTTL;
+ this.maxTTL = maxTTL;
+ }
+
+ async cacheDid(did: string, doc: DidDocument): Promise
{
+ const cacheVal = {
+ doc,
+ updatedAt: Date.now(),
+ };
+ await this.client.setex(
+ `did:${did}`,
+ this.maxTTL,
+ JSON.stringify(cacheVal),
+ );
+ }
+
+ async checkCache(did: string): Promise {
+ const cached = await this.client.get(`did:${did}`);
+ if (!cached) return null;
+
+ const { doc, updatedAt } = JSON.parse(cached);
+ const now = Date.now();
+ const age = now - updatedAt;
+
+ return {
+ did,
+ doc,
+ updatedAt,
+ stale: age > this.staleTTL * 1000,
+ expired: age > this.maxTTL * 1000,
+ };
+ }
+
+ async refreshCache(
+ did: string,
+ getDoc: () => Promise,
+ ): Promise {
+ const doc = await getDoc();
+ if (doc) {
+ await this.cacheDid(did, doc);
+ }
+ }
+
+ async clearEntry(did: string): Promise {
+ await this.client.del(`did:${did}`);
+ }
+
+ async clear(): Promise {
+ const keys = await this.client.keys("did:*");
+ if (keys.length > 0) {
+ await this.client.del(...keys);
+ }
+ }
+}
+
+// Create IdResolver with Redis-based DID cache
+const idResolver = new IdResolver({
+ didCache: redisClient ? new RedisDidCache(redisClient) : undefined,
+});
+
+export async function getReaderFeed(
+ cursor?: string,
+): Promise<{ posts: Post[]; nextCursor: string | null }> {
+ let auth_res = await getIdentityData();
+ if (!auth_res?.atp_did) return { posts: [], nextCursor: null };
+ let query = supabaseServerClient
+ .from("documents")
+ .select(
+ `*,
+ comments_on_documents(count),
+ document_mentions_in_bsky(count),
+ documents_in_publications!inner(publications!inner(*, publication_subscriptions!inner(*)))`,
+ )
+ .eq(
+ "documents_in_publications.publications.publication_subscriptions.identity",
+ auth_res.atp_did,
+ )
+ .order("indexed_at", { ascending: false })
+ .limit(25);
+ if (cursor) query.lt("indexed_at", cursor);
+ let { data: feed, error } = await query;
+
+ let posts = await Promise.all(
+ feed?.map(async (post) => {
+ let pub = post.documents_in_publications[0].publications!;
+ let uri = new AtUri(post.uri);
+ let handle = await idResolver.did.resolve(uri.host);
+ let p: Post = {
+ publication: {
+ href: getPublicationURL(pub),
+ pubRecord: pub?.record || null,
+ uri: pub?.uri || "",
+ },
+ author: handle,
+ documents: {
+ comments_on_documents: post.comments_on_documents,
+ document_mentions_in_bsky: post.document_mentions_in_bsky,
+ data: post.data,
+ uri: post.uri,
+ indexed_at: post.indexed_at,
+ },
+ };
+ return p;
+ }) || [],
+ );
+ return {
+ posts,
+ nextCursor: posts[posts.length - 1]?.documents.indexed_at || null,
+ };
+}
+
+export type Post = {
+ author: DidDocument | null;
+ publication: {
+ href: string;
+ pubRecord: Json;
+ uri: string;
+ };
+ documents: {
+ data: Json;
+ uri: string;
+ indexed_at: string;
+ comments_on_documents:
+ | {
+ count: number;
+ }[]
+ | undefined;
+ document_mentions_in_bsky:
+ | {
+ count: number;
+ }[]
+ | undefined;
+ };
+};
diff --git a/app/reader/page.tsx b/app/reader/page.tsx
index f6974341..c757f4c9 100644
--- a/app/reader/page.tsx
+++ b/app/reader/page.tsx
@@ -1,65 +1,25 @@
import { cookies } from "next/headers";
-import { Fact, ReplicacheProvider, useEntity } from "src/replicache";
+import { Fact, ReplicacheProvider } from "src/replicache";
import type { Attribute } from "src/replicache/attributes";
import {
ThemeBackgroundProvider,
ThemeProvider,
} from "components/ThemeManager/ThemeProvider";
import { EntitySetProvider } from "components/EntitySetProvider";
-import { createIdentity } from "actions/createIdentity";
-import { drizzle } from "drizzle-orm/node-postgres";
-import { IdentitySetter } from "app/home/IdentitySetter";
import { getIdentityData } from "actions/getIdentityData";
-import { getFactsFromHomeLeaflets } from "app/api/rpc/[command]/getFactsFromHomeLeaflets";
import { supabaseServerClient } from "supabase/serverClient";
-import { pool } from "supabase/pool";
import { NotFoundLayout } from "components/PageLayouts/NotFoundLayout";
import { DashboardLayout } from "components/PageLayouts/DashboardLayout";
import { ReaderContent } from "./ReaderContent";
import { SubscriptionsContent } from "./SubscriptionsContent";
-import { Json } from "supabase/database.types";
-import { getPublicationURL } from "app/lish/createPub/getPublicationURL";
-import { PubLeafletDocument } from "lexicons/api";
+import { getReaderFeed } from "./getReaderFeed";
export default async function Reader(props: {}) {
let cookieStore = await cookies();
let auth_res = await getIdentityData();
let identity: string | undefined;
- if (auth_res) identity = auth_res.id;
- else identity = cookieStore.get("identity")?.value;
- let needstosetcookie = false;
- if (!identity) {
- const client = await pool.connect();
- const db = drizzle(client);
- let newIdentity = await createIdentity(db);
- client.release();
- identity = newIdentity.id;
- needstosetcookie = true;
- }
-
- async function setCookie() {
- "use server";
-
- (await cookies()).set("identity", identity as string, {
- sameSite: "strict",
- });
- }
-
let permission_token = auth_res?.home_leaflet;
- if (!permission_token) {
- let res = await supabaseServerClient
- .from("identities")
- .select(
- `*,
- permission_tokens!identities_home_page_fkey(*, permission_token_rights(*))
- `,
- )
- .eq("id", identity)
- .single();
- permission_token = res.data?.permission_tokens;
- }
-
if (!permission_token)
return (
@@ -70,83 +30,31 @@ export default async function Reader(props: {}) {
);
- let [homeLeafletFacts, allLeafletFacts] = await Promise.all([
+ let [homeLeafletFacts] = await Promise.all([
supabaseServerClient.rpc("get_facts", {
root: permission_token.root_entity,
}),
- auth_res
- ? getFactsFromHomeLeaflets.handler(
- {
- tokens: auth_res.permission_token_on_homepage.map(
- (r) => r.permission_tokens.root_entity,
- ),
- },
- { supabase: supabaseServerClient },
- )
- : undefined,
]);
let initialFacts =
(homeLeafletFacts.data as unknown as Fact[]) || [];
let root_entity = permission_token.root_entity;
if (!auth_res?.atp_did) return;
- let { data: publications } = await supabaseServerClient
+ let posts = await getReaderFeed();
+ let { data: pubs, error } = await supabaseServerClient
.from("publication_subscriptions")
- .select(
- `publications(*, documents_in_publications(documents(
- *,
- comments_on_documents(count),
- document_mentions_in_bsky(count)
- )))`,
- )
- .eq("identity", auth_res?.atp_did);
-
- // get publications to fit PublicationList type
- let subbedPublications =
- publications
+ .select(`publications(*, documents_in_publications(*, documents(*)))`)
+ .order(`created_at`, { ascending: false })
+ .order("indexed_at", {
+ referencedTable: "publications.documents_in_publications",
+ })
+ .limit(1, { referencedTable: "publications.documents_in_publications" })
+ .eq("identity", auth_res.atp_did);
+ console.log(error);
+ let publications =
+ pubs
?.map((subscription) => subscription.publications)
.filter((pub) => pub !== null) || [];
-
- // Flatten all posts from all publications into a single array
- let posts =
- subbedPublications?.flatMap((pub) => {
- const postsInPub = pub.documents_in_publications.filter(
- (d) => !!d?.documents,
- );
-
- if (!postsInPub || postsInPub.length === 0) return [];
-
- return postsInPub
- .filter(
- (postInPub) =>
- postInPub.documents?.data &&
- postInPub.documents?.uri &&
- postInPub.documents?.indexed_at,
- )
- .map((postInPub) => ({
- publication: {
- href: getPublicationURL(pub!),
- pubRecord: pub?.record || null,
- uri: pub?.uri || "",
- },
- documents: {
- data: postInPub.documents!.data,
- uri: postInPub.documents!.uri,
- indexed_at: postInPub.documents!.indexed_at,
- comments_on_documents: postInPub.documents?.comments_on_documents,
- document_mentions_in_bsky:
- postInPub.documents?.document_mentions_in_bsky,
- },
- }));
- }) || [];
-
- let sortedPosts = posts.sort((a, b) => {
- let recordA = a.documents.data as PubLeafletDocument.Record;
- let recordB = b.documents.data as PubLeafletDocument.Record;
- const dateA = new Date(recordA.publishedAt || 0);
- const dateB = new Date(recordB.publishedAt || 0);
- return dateB.getTime() - dateA.getTime();
- });
return (
-
@@ -172,15 +79,13 @@ export default async function Reader(props: {}) {
content: (
),
},
Subscriptions: {
controls: null,
- content: (
-
- ),
+ content: ,
},
}}
/>
diff --git a/components/Blocks/TextBlock/RenderYJSFragment.tsx b/components/Blocks/TextBlock/RenderYJSFragment.tsx
index 6cbfe1f5..132e72a7 100644
--- a/components/Blocks/TextBlock/RenderYJSFragment.tsx
+++ b/components/Blocks/TextBlock/RenderYJSFragment.tsx
@@ -32,7 +32,7 @@ export function RenderYJSFragment({
node.toArray().map((node, index) => {
if (node.constructor === XmlText) {
let deltas = node.toDelta() as Delta[];
- if (deltas.length === 0) return
;
+ if (deltas.length === 0) return
;
return (
{deltas.map((d, index) => {
diff --git a/lexicons/src/blocks.ts b/lexicons/src/blocks.ts
index 16bb6d4a..b416e32b 100644
--- a/lexicons/src/blocks.ts
+++ b/lexicons/src/blocks.ts
@@ -177,6 +177,36 @@ export const PubLeafletBlocksImage: LexiconDoc = {
},
};
+export const PubLeafletBlocksOrderedList: LexiconDoc = {
+ lexicon: 1,
+ id: "pub.leaflet.blocks.orderedList",
+ defs: {
+ main: {
+ type: "object",
+ required: ["children"],
+ properties: {
+ startIndex: { type: "integer" },
+ children: { type: "array", items: { type: "ref", ref: "#listItem" } },
+ },
+ },
+ listItem: {
+ type: "object",
+ required: ["content"],
+ properties: {
+ content: {
+ type: "union",
+ refs: [
+ PubLeafletBlocksText,
+ PubLeafletBlocksHeader,
+ PubLeafletBlocksImage,
+ ].map((l) => l.id),
+ },
+ children: { type: "array", items: { type: "ref", ref: "#listItem" } },
+ },
+ },
+ },
+};
+
export const PubLeafletBlocksUnorderedList: LexiconDoc = {
lexicon: 1,
id: "pub.leaflet.blocks.unorderedList",