UNPKG

sveltekit-sync

Version:
138 lines (137 loc) 4.34 kB
/** * usePresence Hook * * Svelte 5 runes-compatible hook for simplified presence/awareness usage. * Provides reactive state, automatic tracking, and event handling. */ import { PresenceStore } from '../presence.svelte.js'; /** * Create a presence hook for a channel * * @param channel - The SyncChannel to use for presence * @param user - Current user information * @param options - Configuration options * @returns Presence API with reactive state and actions * * @example * ```typescript * const presence = usePresence(channel, currentUser, { * customState: { editing: 'doc-123' }, * trackCursor: true, * idleTimeout: 300000 // 5 minutes * }); * * // Access reactive state * const collaborators = presence.others; * const count = presence.onlineCount; * * // Update presence * presence.updateCursor({ x: 100, y: 200 }); * presence.updateCustom({ editing: 'section-2' }); * ``` */ export function usePresence(channel, user, options) { const opts = { idleTimeout: 300000, // 5 minutes trackCursor: false, trackSelection: false, ...options }; // Get or create presence store from channel const presence = channel.presence; if (!presence) { throw new Error('Channel must have presence enabled'); } // Setup automatic cursor tracking if enabled let cursorTrackingCleanup = null; if (opts.trackCursor && typeof document !== 'undefined') { const handleMouseMove = (e) => { presence.updateCursor({ x: e.clientX, y: e.clientY }); }; document.addEventListener('mousemove', handleMouseMove); cursorTrackingCleanup = () => { document.removeEventListener('mousemove', handleMouseMove); }; } // Setup automatic selection tracking if enabled let selectionTrackingCleanup = null; if (opts.trackSelection && typeof document !== 'undefined') { const handleSelectionChange = () => { const selection = document.getSelection(); if (selection && selection.rangeCount > 0) { const range = selection.getRangeAt(0); const rect = range.getBoundingClientRect(); presence.updateSelection({ start: { x: rect.left, y: rect.top }, end: { x: rect.right, y: rect.bottom }, text: selection.toString() }); } else { presence.updateSelection(null); } }; document.addEventListener('selectionchange', handleSelectionChange); selectionTrackingCleanup = () => { document.removeEventListener('selectionchange', handleSelectionChange); }; } return { // Reactive state get others() { return presence.others; }, get othersCount() { return presence.othersCount; }, get onlineCount() { return presence.onlineCount; }, get myPresence() { return presence.myPresence; }, // Actions updateCursor(position) { presence.updateCursor(position); }, updateSelection(selection) { presence.updateSelection(selection); }, updateEditing(editing) { presence.updateEditing(editing); }, updateCustom(custom) { presence.updatePresence({ custom }); }, setStatus(status) { presence.setStatus(status); }, // Queries getUser(userId) { return presence.getUser(userId); }, getUsersEditing(resourceId) { return presence.getUsersEditing(resourceId); }, // Follow mode follow(userId) { return presence.follow(userId); }, isFollowing(userId) { return presence.isFollowing(userId); }, // Events on(event, handler) { return presence.on(event, handler); }, // Cleanup destroy() { cursorTrackingCleanup?.(); selectionTrackingCleanup?.(); // Note: Don't destroy the underlying presence store as it's managed by the channel } }; }