Real-time collaboration (Yjs + WebSocket)

- Go WebSocket hub: rooms per document, broadcast updates, persist state
- Yjs integration: connect/disconnect, sync document state
- Collab toggle button in toolbar (Solo/Live)
- When Live: edits broadcast to all connected users in real-time
- Yjs state persisted to SQLite (survives server restart)
- gorilla/websocket dependency added
This commit is contained in:
2026-05-22 23:49:12 +02:00
parent 1a77d068a7
commit ed4d0b261f
6 changed files with 226 additions and 1 deletions
+22
View File
@@ -59,6 +59,9 @@
<div class="toolbar-right">
<span class="file-name">{{ currentFile || 'No file open' }}</span>
<span class="file-meta" v-if="fileMeta">{{ fileMeta }}</span>
<button v-if="currentFile" class="toolbar-btn" @click="toggleCollab" :title="collabActive ? 'Disconnect collab' : 'Enable collab'">
{{ collabActive ? '👥 Live' : '👤 Solo' }}
</button>
<button v-if="currentFile" class="toolbar-btn" @click="showHistory = !showHistory" title="History">📜</button>
<button v-if="currentFile" class="toolbar-btn" @click="showShareDialog = !showShareDialog" title="Share">🤝</button>
<button v-if="currentFile" class="toolbar-btn" @click="aiVerify" title="Verify with AI">🤖 Verify</button>
@@ -248,6 +251,7 @@ import MilkdownEditor from './components/MilkdownEditor.vue'
import { api, setToken } from './lib/api.js'
import { renderMarkdown } from './lib/markdown.js'
import { cacheFile, getCachedFile, addPendingChange, getPendingChanges, clearAllPending } from './lib/offline.js'
import { connectCollab, disconnectCollab, setCollabContent } from './lib/collab.js'
const authenticated = ref(false)
const email = ref('')
@@ -277,6 +281,7 @@ const shareMsg = ref('')
const gitDirty = ref(0)
const aiResult = ref('')
const trashItems = ref([])
const collabActive = ref(false)
// Preferences
const prefs = ref({ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, defaultMode: 'split', theme: 'dark' })
@@ -678,6 +683,23 @@ async function aiVerify() {
}
}
// ─── Collab ──────────────────────────────────────────────────────────────────
function toggleCollab() {
if (collabActive.value) {
disconnectCollab()
collabActive.value = false
} else {
if (!currentFile.value) return
const fileId = currentFile.value.replace(/[^a-zA-Z0-9]/g, '-')
connectCollab(fileId, (newContent) => {
content.value = newContent
})
setCollabContent(content.value)
collabActive.value = true
}
}
// ─── Formatting ──────────────────────────────────────────────────────────────
function insertFormat(before, after) {