Diffdown is a real-time collaborative Markdown editor/previewer built on the AT Protocol diffdown.com

Namespace fix, copy changes

Corrected comment namespace from app.diffdown.comment to com.diffdown.comment

Skip steps already applied via WS while the POST was inflight.

Copy updates to landing and about pages

+67 -27
+7 -1
.claude/settings.local.json
··· 30 30 "Bash(node:*)", 31 31 "Bash(magick:*)", 32 32 "mcp__codebase-memory-mcp__index_repository", 33 - "mcp__codebase-memory-mcp__manage_adr" 33 + "mcp__codebase-memory-mcp__manage_adr", 34 + "Bash(go test:*)", 35 + "WebFetch(domain:tiptap.dev)", 36 + "WebFetch(domain:developers.google.com)", 37 + "WebFetch(domain:milkdown.dev)", 38 + "Bash(find /Users/johnluther/projects/diffdown -name \"Dockerfile*\" -o -name \"docker-compose*\" | xargs cat)", 39 + "Bash(go vet:*)" 34 40 ] 35 41 } 36 42 }
-1
.gitignore
··· 2 2 diffdown.db-shm 3 3 diffdown.db-wal 4 4 node_modules/ 5 - .claude/ 6 5 /node_modules 7 6 package-lock.json 8 7 .worktrees/
+1 -1
internal/handler/handler.go
··· 799 799 Resolved: false, 800 800 } 801 801 802 - // Create as separate record in app.diffdown.comment collection 802 + // Create as separate record in com.diffdown.comment collection 803 803 uri, _, err := ownerClient.CreateRecord(model.CollectionComment, comment) 804 804 if err != nil { 805 805 log.Printf("CommentCreate: CreateRecord: %v", err)
+1 -1
internal/model/models.go
··· 94 94 95 95 const ( 96 96 CollectionDocument = "com.diffdown.document" 97 - CollectionComment = "app.diffdown.comment" 97 + CollectionComment = "com.diffdown.comment" 98 98 )
+17 -7
static/collab-client.js
··· 21 21 } 22 22 23 23 // Rich mode: register the sendable-steps provider and confirmation callback. 24 - // getSendable: () => { steps, version } | null (calls sendableSteps(pmState)) 25 - // confirm: (steps, clientIDs) => void (calls receiveTransaction on own steps) 26 - setPMHandlers(getSendable, confirm) { 24 + // getSendable: () => { steps, version } | null (calls sendableSteps(pmState)) 25 + // confirm: (steps, clientIDs) => void (calls receiveTransaction on own steps) 26 + // getVersion: () => number (calls getVersion(pmState)) 27 + setPMHandlers(getSendable, confirm, getVersion) { 27 28 this._pmGetSendable = getSendable; 28 29 this._pmConfirm = confirm; 30 + this._pmGetVersion = getVersion || null; 29 31 this._queue = []; 30 32 } 31 33 ··· 78 80 } 79 81 } else if (resp.status === 409) { 80 82 const {steps: missedJSON} = await resp.json(); 81 - const missed = missedJSON.map(s => JSON.parse(s)); 82 - // Apply missed steps — for rich mode this calls receiveTransaction which 83 - // rebases unconfirmed steps and advances the collab plugin version. 84 - this.applyRemoteSteps(missed); 83 + let missed = missedJSON.map(s => JSON.parse(s)); 84 + // Skip steps already applied via WS while the POST was inflight. 85 + // Without this, the same steps are applied twice, corrupting the 86 + // document state and producing TransformError / position-out-of-range. 87 + if (isRichMode && this._pmGetVersion) { 88 + const currentVersion = this._pmGetVersion(); 89 + const alreadyApplied = Math.max(0, currentVersion - clientVersion); 90 + if (alreadyApplied > 0) missed = missed.slice(alreadyApplied); 91 + } 92 + if (missed.length > 0) { 93 + this.applyRemoteSteps(missed); 94 + } 85 95 setTimeout(() => this._flush(), 30); 86 96 return; // skip finally retry 87 97 } else {
+1 -1
static/css/style.css
··· 9 9 :root { 10 10 --bg: #fafafa; 11 11 --bg-card: #fff; 12 - --text: #1a1a2e; 12 + --text: #090909; 13 13 --text-muted: #6b7280; 14 14 --border: #e5e7eb; 15 15 --primary: #2563eb;
+10 -7
templates/about.html
··· 7 7 <section class="about-content"> 8 8 <div class="about-col"> 9 9 <h2>What is This?</h2> 10 - <p>Diffdown is a real-time collaborative <a href="https://www.markdownguide.org/basic-syntax/">Markdown</a> editor/previewer built on <a href="https://atproto.brussels/about-the-atmosphere">AT Protocol</a> (the tech that powers <a href="https://bsky.app">Bluesky</a> and <a href="https://atproto.brussels/atproto-apps">many other cool apps</a>). 11 - <p>Diffdown is decentralized; it stores documents as <a href="https://atproto.wiki/en/wiki/reference/data/records">records</a> on the document creator's <a href="https://atproto.wiki/en/wiki/reference/core-architecture/pds">PDS</a>, not on the Diffdown server or a cloud provider. Your data is yours, literally.</p> 10 + <p>Diffdown is a real-time collaborative <a href="https://www.markdownguide.org/basic-syntax/">Markdown</a> editor/previewer built on <a href="https://atproto.brussels/about-the-atmosphere">AT Protocol</a> (the tech that powers <a href="https://bsky.app">Bluesky</a> and <a href="https://blueskydirectory.com">many other cool apps</a>). 11 + <p>Diffdown is decentralized; it stores documents as <a href="https://atproto.wiki/en/wiki/reference/data/records">records</a> on the document creator's <a href="https://atproto.wiki/en/wiki/reference/core-architecture/pds">Personal Data Server (PDS)</a>, not on the Diffdown server or a cloud provider. Your data is yours, literally.</p> 12 12 <h3>About Me</h3> 13 13 <p>I'm a tech tinkerer, co-founder of <a href="https://limeleaf.coop">Limeleaf Worker Collective</a>, and an advisor to a few startups. Read about my journey building Diffdown <a href="https://leaflet.jluther.net">on my Leaflet</a>.</p> 14 14 <h2>Contact</h2> 15 - <p>Feedback is welcome! Create an issue in the <a href="https://tangled.org/diffdown.com/diffdown-app/issues">Diffdown Tangled repository</a>.</p> 15 + <p>Feedback is welcome! Create an issue in the <a href="https://tangled.org/diffdown.com/diffdown-app/issues">Diffdown repository on Tangled</a>.</p> 16 16 </div> 17 17 <div class="about-col"> 18 18 <h2>Status</h2> 19 - <p>This app is alpha quality. Use at your own risk. Expect bugs, breaking changes, and limited features. However, any documents you create will be stored in your AT Proto account, so even if Diffdown goes away, you will still have your documents.</p> 20 - <p><strong class="warning">Important:</strong> Because AT Proto does not support private records (<a href="https://atproto.wiki/en/working-groups/private-data">yet</a>), any documents you create will be visible to anyone with the URL to the record (<a href="at://did:plc:za4vlvbizdstoym7lpymc5q5/com.diffdown.document/3mgncllbr7424">see this example</a>).</p> 19 + <p>This app is alpha quality. Use at your own risk. Expect bugs, breaking changes, and limited features. However, any documents you create will be stored on your AT Proto PDS, so even if Diffdown goes away, you will still have your documents and comments.</p> 20 + <p><strong class="warning">Important:</strong> Because AT Proto does not support private records (<a href="https://atproto.wiki/en/working-groups/private-data">yet</a>), any documents you create will be visible to the world (not on diffdown.com, but with a PDS record viewer, <a href="https://atproto.at/viewer?uri=did:plc:za4vlvbizdstoym7lpymc5q5/com.diffdown.document/3mhg62vlznz24">see this example</a>).</p> 21 21 <p>The app wasn't designed for mobile, so it is likely to be a very bad UX on small screens.</p> 22 + <p>Also, it is running on a free <a href="https://fly.io">fly.io</a> instance, so it may be slow or unavailable at times.</p> 22 23 <h3>Roadmap</h3> 23 24 <ul> 24 - <li>Document versioning</li> 25 25 <li>Export to .md, HTML, PDF</li> 26 + <li>Publish to the ATmosphere (for example, <a href="https://leaflet.pub">Leaflet</a>)</li> 27 + <li>Document versioning</li> 28 + <li>Other ideas? <a href="https://tangled.org/diffdown.com/diffdown-app/issues">Create an issue</a>.</li> 26 29 </ul> 27 30 <h3>Technology</h3> 28 31 <ul> 29 - <li><strong>Backend:</strong> Go, SQLite</li> 32 + <li><strong>Backend:</strong> Go, SQLite (only for session management; no user data is stored)</li> 30 33 <li><strong>Frontend:</strong> Plain 'ole HTML, CSS, JavaScript</li> 31 34 <li><strong>Editor:</strong> <a href="https://prosemirror.net/">ProseMirror</a> with <a href="https://github.com/ProseMirror/prosemirror-collab">prosemirror-collab</a> for real-time collaboration; <a href="https://milkdown.dev/">Milkdown</a> for Markdown parsing and rendering</li> 32 35 <li><strong>Authentication:</strong> <a href="https://atproto.com/guides/auth">ATProto OAuth</a> (any PDS)</li>
+3 -3
templates/base.html
··· 9 9 <link rel="apple-touch-icon" href="/favicon/apple-touch-icon.png"> 10 10 <link rel="manifest" href="/favicon/site.webmanifest"> 11 11 <title>{{.Title}} — Diffdown</title> 12 - <meta name="description" content="{{if .Description}}{{.Description}}{{else}}Diffdown — collaborative markdown editing and publishing.{{end}}"> 12 + <meta name="description" content="{{if .Description}}{{.Description}}{{else}}Diffdown — collaborative markdown editing.{{end}}"> 13 13 <!-- Open Graph --> 14 14 <meta property="og:type" content="website"> 15 15 <meta property="og:site_name" content="Diffdown"> 16 16 <meta property="og:title" content="{{.Title}} — Diffdown"> 17 - <meta property="og:description" content="{{if .Description}}{{.Description}}{{else}}Diffdown — collaborative markdown editing and publishing.{{end}}"> 17 + <meta property="og:description" content="{{if .Description}}{{.Description}}{{else}}Diffdown — collaborative markdown editing.{{end}}"> 18 18 <meta property="og:image" content="{{if .OGImage}}{{.OGImage}}{{else}}/favicon/android-chrome-512x512.png{{end}}"> 19 19 <!-- Twitter Card --> 20 20 <meta name="twitter:card" content="summary"> 21 21 <meta name="twitter:title" content="{{.Title}} — Diffdown"> 22 - <meta name="twitter:description" content="{{if .Description}}{{.Description}}{{else}}Diffdown — collaborative markdown editing and publishing.{{end}}"> 22 + <meta name="twitter:description" content="{{if .Description}}{{.Description}}{{else}}Diffdown — collaborative markdown editing.{{end}}"> 23 23 <meta name="twitter:image" content="{{if .OGImage}}{{.OGImage}}{{else}}/favicon/android-chrome-512x512.png{{end}}"> 24 24 <link rel="stylesheet" href="/static/css/style.css"> 25 25 {{block "head" .}}{{end}}
+24 -1
templates/document_edit.html
··· 409 409 } finally { 410 410 applyingRemote = false; 411 411 } 412 + }, 413 + () => { 414 + const pmView = milkdownEditor.action(c => c.get(editorViewCtx)); 415 + return getVersion(pmView.state); 412 416 } 413 417 ); 414 418 ··· 688 692 wsReconnectDelay = 1000; 689 693 wsMissedPings = 0; 690 694 startHeartbeat(); 695 + // Fetch any steps missed while the WS was disconnected. 696 + catchUpSteps(); 691 697 }; 692 698 693 699 ws.onmessage = (event) => { ··· 791 797 stopHeartbeat(); 792 798 } 793 799 800 + // Fetch steps missed during a WS disconnect and apply them to the PM state. 801 + async function catchUpSteps() { 802 + if (currentMode !== 'rich' || !milkdownEditor) return; 803 + try { 804 + const pmView = milkdownEditor.action(c => c.get(editorViewCtx)); 805 + const since = getVersion(pmView.state); 806 + const resp = await fetch(`/api/docs/${rkey}/steps?since=${since}`); 807 + if (!resp.ok) return; 808 + const {steps: stepsJSON} = await resp.json(); 809 + if (!stepsJSON || stepsJSON.length === 0) return; 810 + const missed = stepsJSON.map(s => JSON.parse(s)); 811 + collabClient.applyRemoteSteps(missed); 812 + } catch(e) { 813 + console.warn('catchUpSteps:', e); 814 + } 815 + } 816 + 794 817 // ── Presence ────────────────────────────────────────────────────────────── 795 818 796 819 function updatePresence(users) { ··· 1017 1040 async function replyToComment(comment) { 1018 1041 if (!commentForm || !commentTextEl) return; 1019 1042 pendingCommentRange = { from: 0, to: 0, quotedText: '' }; 1020 - pendingReplyTo = `at://${comment.docOwnerDid}/app.diffdown.comment/${comment.id}`; 1043 + pendingReplyTo = `at://${comment.docOwnerDid}/com.diffdown.comment/${comment.id}`; 1021 1044 pendingThreadId = comment.threadId; 1022 1045 commentTextEl.placeholder = `Replying to ${comment.authorHandle || comment.author}...`; 1023 1046 commentTextEl.value = '';
+3 -4
templates/landing.html
··· 3 3 <div class="landing"> 4 4 <section class="landing-header"> 5 5 <h1>Collaborative Markdown Editing</h1> 6 - <p>Write, review, and collaborate on Markdown documents with your team in the <a href="https://www.bskyinfo.com/glossary/atmosphere/">ATmosphere</a>. 7 - <p>Diffdown uses your Bluesky or <a href="https://atproto.com/">AT Protocol</a> account, so no need to create an account, just log in.</p> 8 - </p> 6 + <p>Write, review, and collaborate on Markdown documents in the <a href="https://www.bskyinfo.com/glossary/atmosphere/">ATmosphere</a>. 7 + <p>Diffdown uses your <a href="https://bsky.app">Bluesky</a> (or any <a href="https://atproto.com/">AT Protocol</a> account) so no need to create an account here, just log in.</p> 9 8 </section> 10 9 <div class="landing-actions"> 11 10 <a href="/auth/atproto" class="btn btn-lg">Log In</a> 12 11 </div> 13 12 <hr class="landing-hr"> 14 13 <section> 15 - <p>This is barely Alpha-quality software. Don't use it for anything important. <a href="/about">Learn more about Diffdown</a>.</p> 14 + <p><a href="/about">Diffdown is alpha-quality software</a>.</p> 16 15 </section> 17 16 18 17 </div>