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