UNPKG

bigblocks

Version:

Complete Bitcoin UI component library - authentication, social, wallet, market, inscriptions, and blockchain interactions for React

193 lines (192 loc) 7.65 kB
"use client"; import { useCallback } from "react"; import { useBitcoinAuth } from "../../../hooks/useBitcoinAuth.js"; import { useMetaLensProvider } from "../components/MetaLensProvider.js"; import { buildMetaLensTransaction, parseMetaLensComment, validateContext, } from "../utils/protocol.js"; /** * Main MetaLens hook for posting and fetching comments * * Based on patterns from: * - /Users/satchmo/code/metalens-web/src_old/models/comment.js * - /Users/satchmo/code/metalens.app/CLAUDE.md (protocol details) * * @example * ```tsx * const { postComment, getComments } = useMetaLens(); * * // Post a comment * const txid = await postComment({ * context: 'url', * value: 'https://example.com', * content: 'Great article!' * }); * * // Get comments * const comments = await getComments('url', 'https://example.com'); * ``` */ export function useMetaLens() { const { config, apiClient, eventSourceManager } = useMetaLensProvider(); const { user } = useBitcoinAuth(); // Helper function to push data via droplit using DataPushButton logic const pushToDroplit = useCallback(async (_protocolData) => { // Simulate the DataPushButton's push logic // In a real implementation, this would call the actual droplit API await new Promise((resolve) => setTimeout(resolve, 1500)); const txid = `metalens-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; return txid; }, []); const postComment = useCallback(async (params) => { // Validate parameters const validation = validateContext(params.context, params.value); if (!validation.valid) { throw new Error(validation.error); } // Validate content length (10KB max) if (params.content.length > 10240) { throw new Error("Comment content exceeds maximum length (10KB)"); } // Check authentication if (!user) { throw new Error("User must be authenticated to post comments"); } // Apply moderation if enabled if (config.enableModeration && config.moderationWords.length > 0) { const lowerContent = params.content.toLowerCase(); for (const word of config.moderationWords) { if (lowerContent.includes(word.toLowerCase())) { throw new Error("Comment contains prohibited content"); } } } try { // Build protocol data const _protocolData = buildMetaLensTransaction(params); // Push data via droplit (zero-cost) const txid = await pushToDroplit(_protocolData); return txid; } catch (error) { console.error("Failed to post comment:", error); throw error; } }, [user, pushToDroplit, config.enableModeration, config.moderationWords]); const getComments = useCallback(async (context, value) => { // Validate context const validation = validateContext(context, value); if (!validation.valid) { throw new Error(validation.error); } try { // Fetch comments from API const response = await apiClient.fetchComments(context, value); // Parse comments const comments = []; if (response.results && Array.isArray(response.results)) { for (const item of response.results) { const comment = parseMetaLensComment(item); if (comment) { // Apply moderation if enabled if (config.enableModeration && config.moderationWords.length > 0) { const lowerContent = comment.content.toLowerCase(); let shouldHide = false; for (const word of config.moderationWords) { if (lowerContent.includes(word.toLowerCase())) { shouldHide = true; break; } } if (shouldHide) { comment.content = "[Comment hidden due to moderation]"; } } comments.push(comment); } } } // Build comment tree for replies const commentMap = new Map(); const rootComments = []; // First pass: create map for (const comment of comments) { commentMap.set(comment.txid, comment); } // Second pass: build tree for (const comment of comments) { if (comment.metadata?.thread) { // This is a reply const parent = commentMap.get(comment.metadata.thread); if (parent) { if (!parent.replies) parent.replies = []; parent.replies.push(comment); } else { // Parent not found, treat as root rootComments.push(comment); } } else { // Root comment rootComments.push(comment); } } // Sort by timestamp (newest first) rootComments.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); return rootComments; } catch (error) { console.error("Failed to fetch comments:", error); throw error; } }, [apiClient, config]); const subscribeToComments = useCallback((context, value, callback) => { if (!config.enableRealTime) { return () => { }; // No-op if real-time is disabled } const key = `comments-${context}-${value}`; const path = `/stream/comments?context=${context}&value=${value}&app=metalens`; const handlers = { comment: (event) => { try { const data = JSON.parse(event.data); const comment = parseMetaLensComment(data); if (comment) { callback(comment); } } catch (error) { console.error("Failed to parse comment event:", error); } }, error: (event) => { console.error("EventSource error:", event); }, }; eventSourceManager.connect(key, path, handlers); // Return cleanup function return () => eventSourceManager.disconnect(key); }, [config.enableRealTime, eventSourceManager]); const getCommentCount = useCallback(async (context, value) => { // Validate context const validation = validateContext(context, value); if (!validation.valid) { throw new Error(validation.error); } try { const count = await apiClient.getCommentCount(context, value); return count; } catch (error) { console.error("Failed to fetch comment count:", error); return 0; // Return 0 on error rather than throwing } }, [apiClient]); return { postComment, getComments, subscribeToComments, getCommentCount, }; }