diff --git a/.vscode/settings.json b/.vscode/settings.json
index e2798e4..976ac74 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -3,5 +3,6 @@
"source.fixAll": "explicit",
"source.organizeImports": "explicit",
"source.sortMembers": "explicit"
- }
+ },
+ "editor.tabSize": 2
}
diff --git a/app/blog/[handle]/index.tsx b/app/blog/[handle]/index.tsx
deleted file mode 100644
index e732348..0000000
--- a/app/blog/[handle]/index.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import { Text } from "@/src/components/primitive/Text"
-import { ScrollView, View } from "react-native"
-
-export default function BlogPage() {
- return
-
- Hi!
-
-
-}
\ No newline at end of file
diff --git a/app/blog/[identifier]/index.tsx b/app/blog/[identifier]/index.tsx
new file mode 100644
index 0000000..72e3560
--- /dev/null
+++ b/app/blog/[identifier]/index.tsx
@@ -0,0 +1,42 @@
+import FeedPost from "@/src/components/FeedPost";
+import { AuthContext } from "@/src/context/AuthContext";
+import { Post } from "@/src/lib/post";
+import { fetchBlogPosts } from "@/src/lib/query/feed";
+import { resolveDid } from "@/src/util/handle";
+import { useLocalSearchParams } from "expo-router";
+import { useContext, useEffect, useState } from "react";
+import { ScrollView, View } from "react-native";
+
+export default function BlogPage() {
+ const [posts, setPosts] = useState();
+
+ const { identifier: blogId } = useLocalSearchParams<{ identifier?: string }>();
+ const authCtx = useContext(AuthContext)
+
+ useEffect(() => {
+ (async () => {
+ const blogDid = await resolveDid(blogId!)
+ const fetchedPosts = await fetchBlogPosts(blogDid, authCtx)
+ setPosts(fetchedPosts);
+ })();
+ }, [authCtx, blogId]);
+
+ return (
+
+
+ {posts?.map((post) => (
+
+ ))}
+
+
+ );
+}
diff --git a/lexicons/com/atproto/identity/resolveHandle.json b/lexicons/com/atproto/identity/resolveHandle.json
new file mode 100644
index 0000000..69751a5
--- /dev/null
+++ b/lexicons/com/atproto/identity/resolveHandle.json
@@ -0,0 +1,37 @@
+{
+ "lexicon": 1,
+ "id": "com.atproto.identity.resolveHandle",
+ "defs": {
+ "main": {
+ "type": "query",
+ "description": "Resolves an atproto handle (hostname) to a DID. Does not necessarily bi-directionally verify against the the DID document.",
+ "parameters": {
+ "type": "params",
+ "required": ["handle"],
+ "properties": {
+ "handle": {
+ "type": "string",
+ "format": "handle",
+ "description": "The handle to resolve."
+ }
+ }
+ },
+ "output": {
+ "encoding": "application/json",
+ "schema": {
+ "type": "object",
+ "required": ["did"],
+ "properties": {
+ "did": { "type": "string", "format": "did" }
+ }
+ }
+ },
+ "errors": [
+ {
+ "name": "HandleNotFound",
+ "description": "The resolution process confirmed that the handle does not resolve to any DID."
+ }
+ ]
+ }
+ }
+}
diff --git a/src/components/FeedPost.tsx b/src/components/FeedPost.tsx
new file mode 100644
index 0000000..086c25c
--- /dev/null
+++ b/src/components/FeedPost.tsx
@@ -0,0 +1,9 @@
+import { View } from "react-native";
+import { Post } from "../lib/post";
+import { Text } from "./primitive/Text";
+
+export default function FeedPost({ post }: { post: Post }) {
+ return
+ {post.text}
+ ;
+}
diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx
index aa71556..2968b55 100644
--- a/src/context/AuthContext.tsx
+++ b/src/context/AuthContext.tsx
@@ -3,7 +3,7 @@ import { createContext, useEffect, useState } from "react";
import { BrowserOAuthClient } from "@atproto/oauth-client-browser";
import { atprotoLoopbackClientMetadata } from "@atproto/oauth-types";
-type AuthState = {
+export type AuthState = {
agent: Agent | null;
login: (handle: string) => Promise;
diff --git a/src/lexicons/com.ts b/src/lexicons/com.ts
index 07cafde..f62e2ea 100644
--- a/src/lexicons/com.ts
+++ b/src/lexicons/com.ts
@@ -2,4 +2,5 @@
* THIS FILE WAS GENERATED BY "@atproto/lex". DO NOT EDIT.
*/
+export * as atproto from './com/atproto'
export * as 'bad-example' from './com/bad-example'
diff --git a/src/lexicons/com/atproto.ts b/src/lexicons/com/atproto.ts
new file mode 100644
index 0000000..8297ca3
--- /dev/null
+++ b/src/lexicons/com/atproto.ts
@@ -0,0 +1,5 @@
+/*
+ * THIS FILE WAS GENERATED BY "@atproto/lex". DO NOT EDIT.
+ */
+
+export * as identity from './atproto/identity'
diff --git a/src/lexicons/com/atproto/identity.ts b/src/lexicons/com/atproto/identity.ts
new file mode 100644
index 0000000..30fcf70
--- /dev/null
+++ b/src/lexicons/com/atproto/identity.ts
@@ -0,0 +1,5 @@
+/*
+ * THIS FILE WAS GENERATED BY "@atproto/lex". DO NOT EDIT.
+ */
+
+export * as resolveHandle from './identity/resolveHandle'
diff --git a/src/lexicons/com/atproto/identity/resolveHandle.defs.ts b/src/lexicons/com/atproto/identity/resolveHandle.defs.ts
new file mode 100644
index 0000000..35df513
--- /dev/null
+++ b/src/lexicons/com/atproto/identity/resolveHandle.defs.ts
@@ -0,0 +1,29 @@
+/*
+ * THIS FILE WAS GENERATED BY "@atproto/lex". DO NOT EDIT.
+ */
+
+import { l } from '@atproto/lex'
+
+const $nsid = 'com.atproto.identity.resolveHandle'
+
+export { $nsid }
+
+/** Resolves an atproto handle (hostname) to a DID. Does not necessarily bi-directionally verify against the the DID document. */
+const main = l.query(
+ $nsid,
+ l.params({ handle: l.string({ format: 'handle' }) }),
+ l.jsonPayload({ did: l.string({ format: 'did' }) }),
+ ['HandleNotFound'],
+)
+export { main }
+
+export type $Params = l.InferMethodParams
+export type $Output = l.InferMethodOutput
+export type $OutputBody = l.InferMethodOutputBody<
+ typeof main,
+ B
+>
+
+export const $lxm = main.nsid,
+ $params = main.parameters,
+ $output = main.output
diff --git a/src/lexicons/com/atproto/identity/resolveHandle.ts b/src/lexicons/com/atproto/identity/resolveHandle.ts
new file mode 100644
index 0000000..793247e
--- /dev/null
+++ b/src/lexicons/com/atproto/identity/resolveHandle.ts
@@ -0,0 +1,6 @@
+/*
+ * THIS FILE WAS GENERATED BY "@atproto/lex". DO NOT EDIT.
+ */
+
+export * from './resolveHandle.defs'
+export * as $defs from './resolveHandle.defs'
diff --git a/src/lib/post.ts b/src/lib/post.ts
new file mode 100644
index 0000000..7c39d58
--- /dev/null
+++ b/src/lib/post.ts
@@ -0,0 +1,12 @@
+import { dev } from "../lexicons";
+
+export type Post = {
+ author: {
+ did: string,
+ handle: string,
+ },
+ uri: string,
+ text: string,
+
+ record: dev.maelstrom.bramblr.post.Main,
+};
\ No newline at end of file
diff --git a/src/lib/query/feed.ts b/src/lib/query/feed.ts
new file mode 100644
index 0000000..097e702
--- /dev/null
+++ b/src/lib/query/feed.ts
@@ -0,0 +1,43 @@
+import { AuthState } from "@/src/context/AuthContext";
+import { dev } from "@/src/lexicons";
+import { Post } from "../post";
+import { resolveHandle } from "@/src/util/handle";
+import { AppView } from "@/src/util/slingshot";
+
+function parsePost(
+ author: { did: string, handle: string },
+ record: dev.maelstrom.bramblr.post.Main,
+ uri: string,
+ cid: string,
+): Post {
+ return {
+ author: author,
+ uri: uri,
+ text: record.text ?? '',
+ record: record,
+ };
+}
+
+// Fetch posts from a given blog
+export async function fetchBlogPosts(did: string, authCtx: AuthState) {
+ const author = {
+ did: did,
+ handle: await resolveHandle(did)
+ }
+
+ // Get records for user
+ // TODO: Get latest n post and cursor support
+ const result = await AppView.com.atproto.repo.listRecords({
+ repo: did,
+ collection: dev.maelstrom.bramblr.post.$nsid,
+ });
+
+ return result?.data.records.map((record) =>
+ parsePost(
+ author,
+ record.value as dev.maelstrom.bramblr.post.Main,
+ record.uri,
+ record.cid,
+ ),
+ );
+}
diff --git a/src/util/handle.ts b/src/util/handle.ts
index 78d4a39..7fd178e 100644
--- a/src/util/handle.ts
+++ b/src/util/handle.ts
@@ -3,6 +3,7 @@ import { Slingshot } from "./slingshot";
import { com } from "../lexicons";
const handleCache: { [did: string]: string } = {}
+const didCache: { [handle: string]: string } = {}
// Resolves handles either through the local cache or by requesting from Slingshot
export async function resolveHandle(did: string) {
@@ -10,9 +11,21 @@ export async function resolveHandle(did: string) {
if (handle === undefined) {
const result = await Slingshot.call(com["bad-example"].identity.resolveMiniDoc, {
identifier: did as AtIdentifierString
- });
+ })
handle = result.handle
handleCache[did] = handle
}
- return handle;
+ return handle
+}
+
+export async function resolveDid(handle: string) {
+ let did = didCache[handle]
+ if (did === undefined) {
+ const result = await Slingshot.call(com.atproto.identity.resolveHandle, {
+ handle: handle as any
+ })
+ did = result.did
+ didCache[handle] = did
+ }
+ return did
}
\ No newline at end of file
diff --git a/src/util/slingshot.ts b/src/util/slingshot.ts
index e7aa55a..547f13d 100644
--- a/src/util/slingshot.ts
+++ b/src/util/slingshot.ts
@@ -1,5 +1,12 @@
+import { Agent } from "@atproto/api";
import { Client } from "@atproto/lex";
export const Slingshot = new Client({
service: "https://slingshot.microcosm.blue/"
-});
\ No newline at end of file
+})
+
+// TEMPORARY!!
+// TODO: Make this use something else, or directly talk to the target user's PDS
+export const AppView = new Agent({
+ service: "https://api.bsky.app"
+})
\ No newline at end of file