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:
@@ -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) {
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import * as Y from 'yjs'
|
||||
import { WebsocketProvider } from 'y-websocket'
|
||||
|
||||
let ydoc = null
|
||||
let provider = null
|
||||
let ytext = null
|
||||
|
||||
/**
|
||||
* Connect to the collab WebSocket for a given file.
|
||||
* Returns the Yjs Text type that can be bound to an editor.
|
||||
*/
|
||||
export function connectCollab(fileId, onUpdate) {
|
||||
disconnectCollab()
|
||||
|
||||
ydoc = new Y.Doc()
|
||||
ytext = ydoc.getText('content')
|
||||
|
||||
const wsUrl = `${location.protocol === 'https:' ? 'wss:' : 'ws:'}//${location.host}/ws/collab/${fileId}`
|
||||
provider = new WebsocketProvider(wsUrl, fileId, ydoc, { connect: true })
|
||||
|
||||
ytext.observe((event) => {
|
||||
if (onUpdate) {
|
||||
onUpdate(ytext.toString())
|
||||
}
|
||||
})
|
||||
|
||||
provider.on('status', (event) => {
|
||||
console.log('[collab]', event.status)
|
||||
})
|
||||
|
||||
return { ydoc, ytext, provider }
|
||||
}
|
||||
|
||||
export function disconnectCollab() {
|
||||
if (provider) {
|
||||
provider.destroy()
|
||||
provider = null
|
||||
}
|
||||
if (ydoc) {
|
||||
ydoc.destroy()
|
||||
ydoc = null
|
||||
}
|
||||
ytext = null
|
||||
}
|
||||
|
||||
export function setCollabContent(text) {
|
||||
if (!ytext) return
|
||||
ydoc.transact(() => {
|
||||
ytext.delete(0, ytext.length)
|
||||
ytext.insert(0, text)
|
||||
})
|
||||
}
|
||||
|
||||
export function getCollabContent() {
|
||||
return ytext ? ytext.toString() : ''
|
||||
}
|
||||
|
||||
export function isConnected() {
|
||||
return provider && provider.wsconnected
|
||||
}
|
||||
Reference in New Issue
Block a user