a proof of concept realtime collaborative text editor using atproto as a sync server jake.tngl.io/y-pds/
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

Initial commit

Jake Lazaroff 5c557554

+196
+25
callback.html
··· 1 + <!doctype html> 2 + <script type="importmap"> 3 + { 4 + "imports": { 5 + "@atcute/client": "https://esm.sh/@atcute/client", 6 + "@atcute/identity-resolver": "https://esm.sh/@atcute/identity-resolver", 7 + "@atcute/oauth-browser-client": "https://esm.sh/@atcute/oauth-browser-client@3.0.0" 8 + } 9 + } 10 + </script> 11 + <script type="module"> 12 + import { finalizeAuthorization } from "@atcute/oauth-browser-client"; 13 + 14 + import { configure } from "./oauth.js"; 15 + configure(); 16 + 17 + const params = new URLSearchParams(location.hash.slice(1)); 18 + 19 + finalizeAuthorization(params) 20 + .then(() => { 21 + console.log("/doc.html?id=" + crypto.randomUUID()); 22 + window.location.assign("/doc.html?id=" + crypto.randomUUID()); 23 + }) 24 + .catch((err) => console.error(err)); 25 + </script>
+12
client-metadata.json
··· 1 + { 2 + "client_id": "https://8a49-66-108-106-210.ngrok-free.app/client-metadata.json", 3 + "client_uri": "https://8a49-66-108-106-210.ngrok-free.app", 4 + "redirect_uris": ["https://8a49-66-108-106-210.ngrok-free.app/callback.html"], 5 + "application_type": "native", 6 + "client_name": "atrtc demo", 7 + "dpop_bound_access_tokens": true, 8 + "grant_types": ["authorization_code", "refresh_token"], 9 + "response_types": ["code"], 10 + "scope": "atproto repo?collection=com.jakelazaroff.ypds.doc&collection=com.jakelazaroff.ypds.update&collection=com.jakelazaroff.ypds.snapshot", 11 + "token_endpoint_auth_method": "none" 12 + }
+82
doc.html
··· 1 + <!doctype html> 2 + <html lang="en"> 3 + <head> 4 + <title>yjs via pds</title> 5 + <meta charset="utf8" /> 6 + <style> 7 + * { 8 + box-sizing: border-box; 9 + margin: 0; 10 + padding: 0; 11 + } 12 + html, 13 + body { 14 + height: 100%; 15 + } 16 + body { 17 + display: flex; 18 + flex-direction: column; 19 + } 20 + #editor { 21 + flex: 1; 22 + overflow-y: auto; 23 + padding: 2rem; 24 + font-family: Georgia, serif; 25 + font-size: 1rem; 26 + line-height: 1.6; 27 + } 28 + .ProseMirror { 29 + min-height: 100%; 30 + outline: none; 31 + } 32 + .ProseMirror p { 33 + margin-bottom: 1em; 34 + } 35 + </style> 36 + <script type="importmap"> 37 + { 38 + "imports": { 39 + "@atcute/client": "https://esm.sh/@atcute/client", 40 + "@atcute/identity-resolver": "https://esm.sh/@atcute/identity-resolver", 41 + "@atcute/oauth-browser-client": "https://esm.sh/@atcute/oauth-browser-client@3.0.0", 42 + "prosemirror-state": "https://esm.sh/prosemirror-state", 43 + "prosemirror-view": "https://esm.sh/prosemirror-view", 44 + "prosemirror-model": "https://esm.sh/prosemirror-model", 45 + "prosemirror-schema-basic": "https://esm.sh/prosemirror-schema-basic", 46 + "prosemirror-example-setup": "https://esm.sh/prosemirror-example-setup", 47 + "yjs": "https://esm.sh/yjs", 48 + "y-prosemirror": "https://esm.sh/y-prosemirror" 49 + } 50 + } 51 + </script> 52 + <link rel="stylesheet" href="https://esm.sh/prosemirror-view/style/prosemirror.css" /> 53 + <link rel="stylesheet" href="https://esm.sh/prosemirror-menu/style/menu.css" /> 54 + <link rel="stylesheet" href="https://esm.sh/prosemirror-example-setup/style/style.css" /> 55 + <link rel="stylesheet" href="https://esm.sh/prosemirror-gapcursor/style/gapcursor.css" /> 56 + <script type="module"> 57 + import { schema } from "prosemirror-schema-basic"; 58 + import { EditorState } from "prosemirror-state"; 59 + import { EditorView } from "prosemirror-view"; 60 + import { exampleSetup } from "prosemirror-example-setup"; 61 + import * as Y from "yjs"; 62 + import { ySyncPlugin, yUndoPlugin, undo, redo } from "y-prosemirror"; 63 + 64 + const ydoc = new Y.Doc(); 65 + const ytext = ydoc.getXmlFragment("prosemirror"); 66 + 67 + const state = EditorState.create({ 68 + schema, 69 + plugins: [ 70 + ...exampleSetup({ schema, history: false }), 71 + ySyncPlugin(ytext), 72 + yUndoPlugin(), 73 + ], 74 + }); 75 + 76 + const view = new EditorView(document.querySelector("#editor"), { state }); 77 + </script> 78 + </head> 79 + <body> 80 + <div id="editor"></div> 81 + </body> 82 + </html>
+46
index.html
··· 1 + <!doctype html> 2 + <html lang="en"> 3 + <head> 4 + <title>yjs via pds</title> 5 + <meta charset="utf8" /> 6 + <script type="importmap"> 7 + { 8 + "imports": { 9 + "@atcute/client": "https://esm.sh/@atcute/client", 10 + "@atcute/identity-resolver": "https://esm.sh/@atcute/identity-resolver", 11 + "@atcute/oauth-browser-client": "https://esm.sh/@atcute/oauth-browser-client@3.0.0" 12 + } 13 + } 14 + </script> 15 + </head> 16 + <body> 17 + <form> 18 + <input name="handle" /> 19 + <button>log in</button> 20 + </form> 21 + <script type="module"> 22 + import { createAuthorizationUrl } from "@atcute/oauth-browser-client"; 23 + 24 + import { configure, scope } from "./oauth.js"; 25 + 26 + configure(); 27 + 28 + document.querySelector("form").onsubmit = async (e) => { 29 + e.preventDefault(); 30 + const data = new FormData(e.currentTarget); 31 + const identifier = data.get("handle"); 32 + if (typeof identifier !== "string") throw new Error("invalid handle"); 33 + 34 + const authUrl = await createAuthorizationUrl({ 35 + target: { type: "account", identifier }, 36 + scope, 37 + }); 38 + 39 + // localStorage.setItem("did", identity.id); 40 + await new Promise((r) => setTimeout(r, 100)); 41 + 42 + window.location.assign(authUrl); 43 + }; 44 + </script> 45 + </body> 46 + </html>
+31
oauth.js
··· 1 + import metadata from "./client-metadata.json" with { type: "json" }; 2 + 3 + export const scope = metadata.scope; 4 + 5 + import { Client } from "@atcute/client"; 6 + import { configureOAuth, OAuthUserAgent } from "@atcute/oauth-browser-client"; 7 + import { 8 + CompositeDidDocumentResolver, 9 + LocalActorResolver, 10 + PlcDidDocumentResolver, 11 + WebDidDocumentResolver, 12 + XrpcHandleResolver, 13 + } from "@atcute/identity-resolver"; 14 + 15 + export function configure() { 16 + configureOAuth({ 17 + metadata: { client_id: metadata.client_id, redirect_uri: metadata.redirect_uris[0] }, 18 + identityResolver: new LocalActorResolver({ 19 + handleResolver: new XrpcHandleResolver({ serviceUrl: "https://public.api.bsky.app" }), 20 + didDocumentResolver: new CompositeDidDocumentResolver({ 21 + methods: { plc: new PlcDidDocumentResolver(), web: new WebDidDocumentResolver() }, 22 + }), 23 + }), 24 + }); 25 + } 26 + 27 + /** @param {import("@atcute/oauth-browser-client").Session} */ 28 + export function client(session) { 29 + const handler = new OAuthUserAgent(session); 30 + const client = new Client({ handler }); 31 + }