From 1ef970fef3466c8685c055aec5f98fb7ff2e0679 Mon Sep 17 00:00:00 2001 From: 1ynx Date: Sun, 10 May 2026 15:11:24 +0200 Subject: [PATCH] * revert changes --- src/app.ts | 114 +++++++------------------ src/components/DeadlineTimer.ts | 3 +- src/components/PollList.ts | 56 +++++------- src/components/PollOption.ts | 9 +- src/components/StatusBar.ts | 145 +++++--------------------------- src/crypto.ts | 119 -------------------------- src/state.ts | 109 ++++++++++++++++++++++-- src/styles.css | 8 -- src/userSync.ts | 70 --------------- src/yDocUtil.ts | 24 ------ 10 files changed, 183 insertions(+), 474 deletions(-) delete mode 100644 src/crypto.ts delete mode 100644 src/userSync.ts delete mode 100644 src/yDocUtil.ts diff --git a/src/app.ts b/src/app.ts index b396169..bf00075 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,21 +1,19 @@ +import { getUserId } from "./identity"; import { - User, addOption, toggleVote, + deleteOption, setDeadline, clearDeadline, - getDeadline, + createViewModel, } from "./state"; -import { v4 as uuidv4 } from 'uuid'; import { initSync } from "./sync"; -import { initUserSync } from "./userSync"; import { StatusBar } from "./components/StatusBar"; import { PollTitle } from "./components/PollTitle"; import { AddOption } from "./components/AddOption"; import { PollList } from "./components/PollList"; import { ShareSection } from "./components/ShareSection"; import { DeadlineTimer } from "./components/DeadlineTimer"; -import { generateUserKeyPair, exportPrivateKey, savePrivateKeyToFile, exportPublicKey, stringToCryptoKey } from "./crypto"; const ROOM_PARAM = "room"; @@ -41,9 +39,8 @@ function ensureRoomId(): string { export function initApp(container: HTMLElement): () => void { const roomId = ensureRoomId(); + const userId = getUserId(); const sync = initSync(roomId); - const userSync = initUserSync(); - let user : User | undefined = undefined; const shareUrl = window.location.href; @@ -51,12 +48,17 @@ export function initApp(container: HTMLElement): () => void { const actions = { addOption: (label: string) => { - if (!user || isVotingClosed()) return; - return addOption(sync.options, label, user.userid); + const vm = createViewModel(getViewModelParams()); + if (vm.votingClosed) return; + return addOption(sync.options, label, userId); }, toggleVote: (optionId: string) => { - if (!user || isVotingClosed()) return; - toggleVote(sync.votes, user.userid, optionId); + const vm = createViewModel(getViewModelParams()); + if (vm.votingClosed) return; + toggleVote(sync.votes, userId, optionId); + }, + deleteOption: (optionId: string) => { + deleteOption(sync.options, sync.votes, optionId); }, startDeadline: (durationMs: number) => { setDeadline(sync.deadlineMap, durationMs); @@ -64,79 +66,20 @@ export function initApp(container: HTMLElement): () => void { clearDeadline: () => { clearDeadline(sync.deadlineMap); }, - onLoginLogout: async (event: Event) => { - if(user){ - user = undefined - return false; - } else { - const target = event.target as HTMLInputElement; - const file = target.files?.[0]; - - if (file) { - try { - const content = await file.text(); - console.log("File loaded: "); - if (file.name && content) { - try { - const uuid = file.name.replace(".pem", ""); - // Standardize the string for the importer - const pkBase64 = content.replace(/-----BEGIN PRIVATE KEY-----|-----END PRIVATE KEY-----/g, "").replace(/\s+/g, ""); - - const key = await stringToCryptoKey(pkBase64, "private"); - - user = { - userid: uuid, - private_key: key, - public_key: undefined, // Note: You might need to import a pub key too! - }; - - console.log("Login successful for:", uuid); - return true; - } catch (err) { - console.error("Crypto Import Error:", err); - alert("The file content is not a valid Private Key."); - } - } - } catch (e) { - console.error("Failed to read file", e); - } - } - return false; - } - }, - onCreateUser: async (event: Event) => { - try { - const keypair = await generateUserKeyPair(); - - console.log('keypair:', keypair); - const uuid = uuidv4(); - user = { - userid: uuid, - private_key: keypair.privateKey, - public_key: keypair.publicKey, - }; - - const prvKeyString = await exportPrivateKey(keypair.privateKey); - - savePrivateKeyToFile(prvKeyString,uuid+".pem") - - - const pubKeyString = await exportPublicKey(keypair.publicKey); - - userSync.users.set(user.userid,pubKeyString) - return true; - } catch (err) { - user = undefined - console.error("Failed to create new User!", err); - } - return false; - } }; - function isVotingClosed() { - const deadline = getDeadline(sync.deadlineMap); - const votingClosed = deadline !== null && Date.now() >= deadline; - return votingClosed; + function getViewModelParams() { + return { + yTitle: sync.yTitle, + options: sync.options, + votes: sync.votes, + deadlineMap: sync.deadlineMap, + roomId, + shareUrl, + connectionStatus: sync.getConnectionStatus(), + peerCount: sync.getPeerCount(), + userId, + }; } // --- Build UI --- @@ -156,7 +99,7 @@ export function initApp(container: HTMLElement): () => void { Polly `; - const statusBar = StatusBar(sync.provider,userSync.provider,actions.onLoginLogout,actions.onCreateUser); + const statusBar = StatusBar(sync.provider); header.append(wordmark, statusBar); // Main card @@ -169,7 +112,10 @@ export function initApp(container: HTMLElement): () => void { if (result && !result.ok) return result.error; return null; }); - const pollList = PollList(sync.options, sync.votes, user, isVotingClosed, actions.toggleVote); + const pollList = PollList(sync.options, sync.votes, userId, () => { + const vm = createViewModel(getViewModelParams()); + return vm.votingClosed; + }, actions.toggleVote, actions.deleteOption); const deadlineTimer = DeadlineTimer( sync.deadlineMap, actions.startDeadline, diff --git a/src/components/DeadlineTimer.ts b/src/components/DeadlineTimer.ts index 72e1072..9ef1cb9 100644 --- a/src/components/DeadlineTimer.ts +++ b/src/components/DeadlineTimer.ts @@ -1,6 +1,5 @@ import * as Y from "yjs"; import { getDeadline } from "../state"; -import { enforceAppendOnly } from "../yDocUtil"; const DEADLINE_DURATION_MS = 2 * 60 * 1000; // 2 minutes @@ -80,7 +79,7 @@ export function DeadlineTimer( onClearDeadline(); }); - deadlineMap.observe(enforceAppendOnly(deadlineMap,render)); + deadlineMap.observe(() => render()); render(); return wrapper; diff --git a/src/components/PollList.ts b/src/components/PollList.ts index a4f909d..dfd4458 100644 --- a/src/components/PollList.ts +++ b/src/components/PollList.ts @@ -1,20 +1,15 @@ import * as Y from "yjs"; import type { OptionRecord } from "../state"; import { PollOption } from "./PollOption"; -import { enforceAppendOnly } from "../yDocUtil"; -import { User } from "../state"; export function PollList( yOptions: Y.Map, yVotes: Y.Map, - user: User | undefined, + userId: string, isVotingClosed: () => boolean, onVote: (optionId: string) => void, + onDelete: (optionId: string) => void, ): HTMLElement { - - var currentOptions : { [x: string]: any; } | undefined = undefined - var currentVotes : { [x: string]: any; } | undefined = undefined - const wrapper = document.createElement("div"); wrapper.className = "poll-list-wrapper"; @@ -46,32 +41,25 @@ export function PollList( votes: number; voted: boolean; }> = []; - if (currentOptions && currentVotes){ - // Tally votes per option - const tally = new Map(); - for (const optionId of Object.values(currentVotes)) { - tally.set(optionId, (tally.get(optionId) ?? 0) + 1); - } - - let myVote = null; - if (user) { - myVote = currentVotes[user.userid] - } - - Object.entries(currentOptions).forEach(([id,record]) => { - console.log(`${record}: ${id}`) - entries.push({ - id, - name: record.label, - votes: tally.get(id) ?? 0, - voted: myVote === id, - }); - }); - - entries.sort((a, b) => b.votes - a.votes || a.name.localeCompare(b.name)); + // Tally votes per option + const tally = new Map(); + for (const optionId of yVotes.values()) { + tally.set(optionId, (tally.get(optionId) ?? 0) + 1); } + const myVote = yVotes.get(userId) ?? null; + + yOptions.forEach((record, id) => { + entries.push({ + id, + name: record.label, + votes: tally.get(id) ?? 0, + voted: myVote === id, + }); + }); + + entries.sort((a, b) => b.votes - a.votes || a.name.localeCompare(b.name)); return entries; } @@ -115,6 +103,7 @@ export function PollList( totalVotes: total, votingClosed, onVote, + onDelete, }); const currentEl = list.children[i] as HTMLElement | undefined; @@ -129,10 +118,9 @@ export function PollList( } }); } - yOptions.observe(enforceAppendOnly(yOptions,(update : { [x: string]: any; }) => {currentOptions = update}, render)); - yVotes.observe(enforceAppendOnly(yVotes,(update : { [x: string]: any; }) => {currentVotes = update},render)); - currentOptions=yOptions.toJSON() - currentVotes=yVotes.toJSON() + + yOptions.observeDeep(() => render()); + yVotes.observe(() => render()); render(); return wrapper; diff --git a/src/components/PollOption.ts b/src/components/PollOption.ts index 70eff3b..c10ef22 100644 --- a/src/components/PollOption.ts +++ b/src/components/PollOption.ts @@ -8,10 +8,11 @@ export interface PollOptionProps { totalVotes: number; votingClosed: boolean; onVote: (id: string) => void; + onDelete: (id: string) => void; } export function PollOption(props: PollOptionProps): HTMLElement { - const { id, name, votes, voted, totalVotes, votingClosed, onVote } = props; + const { id, name, votes, voted, totalVotes, votingClosed, onVote, onDelete } = props; const row = document.createElement("div"); row.className = `poll-option${voted ? " poll-option--voted" : ""}`; @@ -29,11 +30,17 @@ export function PollOption(props: PollOptionProps): HTMLElement { + `; row.querySelector(".poll-option__vote-btn")!.addEventListener("click", () => onVote(id)); + row.querySelector(".poll-option__delete-btn")!.addEventListener("click", () => onDelete(id)); return row; } diff --git a/src/components/StatusBar.ts b/src/components/StatusBar.ts index 52d9950..d981c0a 100644 --- a/src/components/StatusBar.ts +++ b/src/components/StatusBar.ts @@ -1,110 +1,30 @@ import type { WebrtcProvider } from "y-webrtc"; -export function StatusBar(provider: WebrtcProvider, user_provider: WebrtcProvider, onLoginLogout: (event: Event) => Promise, onCreateUser: (event: Event) => Promise): HTMLElement { +export function StatusBar(provider: WebrtcProvider): HTMLElement { const el = document.createElement("div"); el.className = "status-bar"; - const statusPanel=document.createElement("div"); - statusPanel.className = "status-bar"; + const dot = document.createElement("span"); + dot.className = "status-dot connecting"; + + const statusText = document.createElement("span"); + statusText.className = "status-text"; + statusText.textContent = "Connecting"; const divider = document.createElement("span"); divider.className = "status-divider"; divider.textContent = "\u00b7"; - function getProviderStatus(){ + const peerText = document.createElement("span"); + peerText.className = "status-peers"; - const dot = document.createElement("span"); - dot.className = "status-dot connecting"; - - const statusText = document.createElement("span"); - statusText.className = "status-text"; - statusText.textContent = "Connecting"; - - const peerText = document.createElement("span"); - peerText.className = "status-peers"; - - return { dot: dot, statusText: statusText, peerText: peerText} - } - - const providerStatusPanel=document.createElement("div"); - providerStatusPanel.className = "provider-status-container"; - - - const pollProviderText = document.createElement("span"); - pollProviderText.className = "status-text"; - pollProviderText.textContent = "Polls: "; - const pollProviderElements = getProviderStatus() - const pollProviderStatusPanel=document.createElement("div"); - pollProviderStatusPanel.className = "status-bar"; - pollProviderStatusPanel.append(pollProviderText,pollProviderElements.dot, pollProviderElements.statusText, divider, pollProviderElements.peerText); - - - const userProviderText = document.createElement("span"); - userProviderText.className = "status-text"; - userProviderText.textContent = "Users: "; - const userProviderElements = getProviderStatus() - const userProviderStatusPanel=document.createElement("div"); - userProviderStatusPanel.className = "status-bar"; - userProviderStatusPanel.append(userProviderText,userProviderElements.dot, userProviderElements.statusText, divider, userProviderElements.peerText); - - providerStatusPanel.append(userProviderStatusPanel,pollProviderStatusPanel) - - const userButtons = document.createElement("div"); - userButtons.className = "status-bar"; - - const loginLabel = document.createElement("label"); - loginLabel.setAttribute("title", "Select Key File"); - const loginSpan = document.createElement("span"); - loginSpan.className = "add-option-btn" - loginSpan.textContent = "Login"; - const loginInput = document.createElement("input"); - loginInput.type = "file" - loginInput.accept = ".pem" - loginInput.hidden = true - - - loginLabel.append(loginSpan,loginInput) - - - const logoutButton = document.createElement("button"); - logoutButton.className = "add-option-btn"; - logoutButton.setAttribute("aria-label", "Logout"); - logoutButton.innerHTML="Logout" - logoutButton.style.display = "none"; - - const createUserButton = document.createElement("button"); - createUserButton.className = "add-option-btn"; - createUserButton.setAttribute("aria-label", "Create User"); - createUserButton.innerHTML="Create User" - - async function onLoginLogoutResult(event: Event, loginLogout: (event: Event) => Promise){ - if(await loginLogout(event)){ - console.log('created / logged in') - loginLabel.style.display = "none"; - logoutButton.style.display = "block"; - createUserButton.style.display = "none"; - } else { - console.log('logged out') - loginLabel.style.display = "block"; - logoutButton.style.display = "none"; - createUserButton.hidden = false; - createUserButton.style.display = "block"; - } - } - - loginLabel.addEventListener("change", (e) => onLoginLogoutResult(e,onLoginLogout)); - logoutButton.addEventListener("click", (e) => onLoginLogoutResult(e,onLoginLogout)); - createUserButton.addEventListener("click", (e) => onLoginLogoutResult(e,onCreateUser)); - - userButtons.append(loginLabel,logoutButton,createUserButton) - - el.append(providerStatusPanel, divider, userButtons); + el.append(dot, statusText, divider, peerText); // --- Connection state --- let syncTimeout: ReturnType | undefined = setTimeout(() => { - pollProviderElements.statusText.textContent = "Ready"; - pollProviderElements.dot.className = "status-dot ready"; + statusText.textContent = "Ready"; + dot.className = "status-dot ready"; }, 3000); provider.on("synced", ({ synced }: { synced: boolean }) => { @@ -112,37 +32,18 @@ export function StatusBar(provider: WebrtcProvider, user_provider: WebrtcProvide clearTimeout(syncTimeout); syncTimeout = undefined; } - pollProviderElements.dot.className = `status-dot ${synced ? "connected" : "connecting"}`; - pollProviderElements.statusText.textContent = synced ? "Connected" : "Connecting"; - }); - - - let syncTimeout2: ReturnType | undefined = setTimeout(() => { - userProviderElements.statusText.textContent = "Ready"; - userProviderElements.dot.className = "status-dot ready"; - }, 3000); - - user_provider.on("synced", ({ synced }: { synced: boolean }) => { - if (syncTimeout2) { - clearTimeout(syncTimeout2); - syncTimeout2 = undefined; - } - userProviderElements.dot.className = `status-dot ${synced ? "connected" : "connecting"}`; - userProviderElements.statusText.textContent = synced ? "Connected" : "Connecting"; + dot.className = `status-dot ${synced ? "connected" : "connecting"}`; + statusText.textContent = synced ? "Connected" : "Connecting"; }); // Online/offline awareness const handleOffline = () => { - pollProviderElements.dot.className = "status-dot connecting"; - pollProviderElements.statusText.textContent = "Offline"; - userProviderElements.dot.className = "status-dot connecting"; - userProviderElements.statusText.textContent = "Offline"; + dot.className = "status-dot connecting"; + statusText.textContent = "Offline"; }; const handleOnline = () => { - pollProviderElements.dot.className = "status-dot connecting"; - pollProviderElements.statusText.textContent = "Reconnecting"; - userProviderElements.dot.className = "status-dot connecting"; - userProviderElements.statusText.textContent = "Reconnecting"; + dot.className = "status-dot connecting"; + statusText.textContent = "Reconnecting"; }; window.addEventListener("offline", handleOffline); @@ -153,18 +54,10 @@ export function StatusBar(provider: WebrtcProvider, user_provider: WebrtcProvide function updatePeerCount() { const total = provider.awareness.getStates().size; const others = total - 1; - pollProviderElements.peerText.textContent = + peerText.textContent = others === 0 ? "Only you" : `${others} other${others !== 1 ? "s" : ""}`; - - - const total2 = user_provider.awareness.getStates().size; - const others2 = total2 - 1; - userProviderElements.peerText.textContent = - others2 === 0 - ? "Only you" - : `${others2} other${others2 !== 1 ? "s" : ""}`; } provider.awareness.on("change", updatePeerCount); diff --git a/src/crypto.ts b/src/crypto.ts deleted file mode 100644 index 91d9b0f..0000000 --- a/src/crypto.ts +++ /dev/null @@ -1,119 +0,0 @@ -export const generateUserKeyPair = async () => { - return await window.crypto.subtle.generateKey( - { - name: "RSASSA-PKCS1-v1_5", - modulusLength: 2048, - publicExponent: new Uint8Array([1, 0, 1]), // 65537 - hash: "SHA-256", - }, - true, // extractable - ["sign", "verify"] - ); -}; - -export const signData = async (data: any, privateKey: CryptoKey) => { - const encoder = new TextEncoder(); - const encodedData = encoder.encode(JSON.stringify(data)); - - const signature = await window.crypto.subtle.sign( - "RSASSA-PKCS1-v1_5", - privateKey, - encodedData - ); - - // Convert to Base64 or Hex to store in Yjs easily - return btoa(String.fromCharCode(...new Uint8Array(signature))); -}; - - -// Helper to convert ArrayBuffer to Base64 string -const bufferToBase64 = (buf: ArrayBuffer) => - window.btoa(String.fromCharCode(...new Uint8Array(buf))); - -export const exportPublicKey = async (key: CryptoKey) => { - // Export Public Key - const exportedPublic = await window.crypto.subtle.exportKey("spki", key); - const publicKeyString = bufferToBase64(exportedPublic); - - return publicKeyString; -}; -export const exportPrivateKey = async (key: CryptoKey) => { - // Export Private Key - const exportedPrivate = await window.crypto.subtle.exportKey("pkcs8", key); - const privateKeyString = bufferToBase64(exportedPrivate); - - return privateKeyString; -}; - -/** - * Converts a Base64 string back into a usable CryptoKey object - * @param keyStr The Base64 string (without PEM headers) - * @param type 'public' or 'private' - */ -export const stringToCryptoKey = async (keyStr: string, type: 'public' | 'private'): Promise => { - // 1. Convert Base64 string to a Uint8Array (binary) - const binaryString = window.atob(keyStr); - const bytes = new Uint8Array(binaryString.length); - for (let i = 0; i < binaryString.length; i++) { - bytes[i] = binaryString.charCodeAt(i); - } - - // 2. Identify the format based on the key type - // Public keys usually use 'spki', Private keys use 'pkcs8' - const format = type === 'public' ? 'spki' : 'pkcs8'; - const usages: KeyUsage[] = type === 'public' ? ['verify'] : ['sign']; - - // 3. Import the key - return await window.crypto.subtle.importKey( - format, - bytes.buffer, - { - name: "RSASSA-PKCS1-v1_5", - hash: "SHA-256", - }, - true, // extractable (set to false if you want to lock it in memory) - usages - ); -}; - -export const savePrivateKeyToFile = (privateKeyStr: string, filename: string) => { - // Optional: Wrap in PEM headers for standard formatting - const pemHeader = "-----BEGIN PRIVATE KEY-----\n"; - const pemFooter = "\n-----END PRIVATE KEY-----"; - const fileContent = pemHeader + privateKeyStr + pemFooter; - - const blob = new Blob([fileContent], { type: "text/plain" }); - const url = URL.createObjectURL(blob); - - const link = document.createElement("a"); - link.href = url; - link.download = filename; - - document.body.appendChild(link); - link.click(); - - // Cleanup - document.body.removeChild(link); - URL.revokeObjectURL(url); -}; - -export const loadPrivateKeyFromFile = async (file: File): Promise => { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - - reader.onload = (e) => { - const content = e.target?.result as string; - - // Clean up the string by removing PEM headers and newlines - const cleanKey = content - .replace("-----BEGIN PRIVATE KEY-----", "") - .replace("-----END PRIVATE KEY-----", "") - .replace(/\s+/g, ""); // Removes all whitespace/newlines - - resolve(cleanKey); - }; - - reader.onerror = () => reject("Error reading file"); - reader.readAsText(file); - }); -}; \ No newline at end of file diff --git a/src/state.ts b/src/state.ts index fc20f17..65fd560 100644 --- a/src/state.ts +++ b/src/state.ts @@ -4,12 +4,6 @@ import * as Y from "yjs"; export type ConnectionStatus = "connecting" | "connected" | "offline"; -export interface User{ - userid: string, - private_key: CryptoKey, - public_key: CryptoKey | undefined, -} - export interface OptionRecord { id: string; label: string; @@ -17,6 +11,25 @@ export interface OptionRecord { createdBy: string; } +export interface PollOptionViewModel extends OptionRecord { + voteCount: number; + isVotedByMe: boolean; + percentage: number; +} + +export interface PollViewModel { + title: string; + roomId: string; + shareUrl: string; + connectionStatus: ConnectionStatus; + peerCount: number; + options: PollOptionViewModel[]; + totalVotes: number; + myVoteOptionId: string | null; + deadline: number | null; + votingClosed: boolean; +} + // --- Helpers --- export function createOptionId(): string { @@ -91,6 +104,20 @@ export function toggleVote( } } +export function deleteOption( + options: Y.Map, + votes: Y.Map, + optionId: string, +): void { + options.delete(optionId); + // Clean up votes pointing to this option + for (const [userId, votedOptionId] of votes.entries()) { + if (votedOptionId === optionId) { + votes.delete(userId); + } + } +} + // --- Deadline --- export function setDeadline( @@ -108,3 +135,73 @@ export function getDeadline(deadlineMap: Y.Map): number | null { const val = deadlineMap.get("deadline"); return typeof val === "number" ? val : null; } + +// --- ViewModel --- + +export function createViewModel(params: { + yTitle: Y.Text; + options: Y.Map; + votes: Y.Map; + deadlineMap: Y.Map; + roomId: string; + shareUrl: string; + connectionStatus: ConnectionStatus; + peerCount: number; + userId: string; +}): PollViewModel { + const { + yTitle, + options, + votes, + deadlineMap, + roomId, + shareUrl, + connectionStatus, + peerCount, + userId, + } = params; + + // Tally votes per option + const tally = new Map(); + for (const optionId of votes.values()) { + tally.set(optionId, (tally.get(optionId) ?? 0) + 1); + } + + let totalVotes = 0; + for (const count of tally.values()) { + totalVotes += count; + } + + const myVoteOptionId = votes.get(userId) ?? null; + const deadline = getDeadline(deadlineMap); + const votingClosed = deadline !== null && Date.now() >= deadline; + + // Sort by votes desc, then alphabetically + const sortedOptions = Array.from(options.values()).sort((a, b) => { + const aVotes = tally.get(a.id) ?? 0; + const bVotes = tally.get(b.id) ?? 0; + if (bVotes !== aVotes) return bVotes - aVotes; + return a.label.localeCompare(b.label); + }); + + return { + title: getPollTitle(yTitle), + roomId, + shareUrl, + connectionStatus, + peerCount, + myVoteOptionId, + totalVotes, + deadline, + votingClosed, + options: sortedOptions.map((option) => ({ + ...option, + voteCount: tally.get(option.id) ?? 0, + isVotedByMe: myVoteOptionId === option.id, + percentage: + totalVotes > 0 + ? Math.round(((tally.get(option.id) ?? 0) / totalVotes) * 100) + : 0, + })), + }; +} diff --git a/src/styles.css b/src/styles.css index e213662..965045c 100644 --- a/src/styles.css +++ b/src/styles.css @@ -86,14 +86,6 @@ button { cursor: pointer; } font-size: 0.8rem; color: var(--text-secondary); } -.provider-status-container { - display: flex; - flex-direction: column; - align-items: center; - gap: 0.4rem; - font-size: 0.8rem; - color: var(--text-secondary); -} .status-dot { width: 6px; diff --git a/src/userSync.ts b/src/userSync.ts deleted file mode 100644 index 5ba0f51..0000000 --- a/src/userSync.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { IndexeddbPersistence } from "y-indexeddb"; -import { WebrtcProvider } from "y-webrtc"; -import * as Y from "yjs"; - -import type { ConnectionStatus, OptionRecord } from "./state"; - -export interface UserSync { - doc: Y.Doc; - users: Y.Map; - provider: WebrtcProvider; - persistence: IndexeddbPersistence; - getConnectionStatus: () => ConnectionStatus; - getPeerCount: () => number; - destroy: () => void; -} - -export function initUserSync(): UserSync { - const doc = new Y.Doc(); - const users = doc.getMap("users"); - - let connectionStatus: ConnectionStatus = navigator.onLine - ? "connecting" - : "offline"; - - const provider = new WebrtcProvider("users", doc,{ - signaling: ["ws://localhost:4444", "ws://lynxpi.ddns.net:4444"] - }); - const persistence = new IndexeddbPersistence("users", doc); - - const syncConnectionStatus = (status: ConnectionStatus) => { - connectionStatus = navigator.onLine ? status : "offline"; - }; - - const handleOnline = () => { - syncConnectionStatus(provider.connected ? "connected" : "connecting"); - }; - const handleOffline = () => { - connectionStatus = "offline"; - }; - - provider.on("status", (event: { connected: boolean }) => { - syncConnectionStatus(event.connected ? "connected" : "connecting"); - }); - - provider.on("synced", ({ synced }: { synced: boolean }) => { - if (synced) syncConnectionStatus("connected"); - }); - - window.addEventListener("online", handleOnline); - window.addEventListener("offline", handleOffline); - - return { - doc, - users, - provider, - persistence, - getConnectionStatus: () => connectionStatus, - getPeerCount: () => { - const total = provider.awareness.getStates().size; - return Math.max(0, total - 1); - }, - destroy: () => { - window.removeEventListener("online", handleOnline); - window.removeEventListener("offline", handleOffline); - persistence.destroy(); - provider.destroy(); - doc.destroy(); - }, - }; -} diff --git a/src/yDocUtil.ts b/src/yDocUtil.ts deleted file mode 100644 index 66a687d..0000000 --- a/src/yDocUtil.ts +++ /dev/null @@ -1,24 +0,0 @@ -import * as Y from "yjs"; - -/** - * Enforces append-only logic on a Y.Map. - * Reverts any 'update' or 'delete' actions detected in the observer. - */ -export function enforceAppendOnly(yMap: Y.Map,update: (update : { [x: string]: any; }) => void, render: () => void) { - return (event: Y.YMapEvent, transaction: Y.Transaction) => { - var isOperationIllegal = false - event.keys.forEach((change, key) => { - const { action, oldValue } = change; - - if (action === 'update' || action === 'delete') { - isOperationIllegal = true - console.log("Illegal Operation: "+action) - } - }); - if(!isOperationIllegal) { - console.log("Updating Map!") - update(yMap.toJSON()) - render(); - } - }; -} \ No newline at end of file