bigblocks
Version:
Complete Bitcoin UI component library - authentication, social, wallet, market, inscriptions, and blockchain interactions for React
193 lines (192 loc) • 7.65 kB
JavaScript
"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,
};
}