merge with master

This commit is contained in:
Sasha 2024-11-13 13:30:18 +07:00
commit db65143aca
No known key found for this signature in database
14 changed files with 341 additions and 20277 deletions

View File

@ -23,14 +23,6 @@
browser: true browser: true
}; };
</script> </script>
<script type="text/javascript">
(function() {
const path = window.location.pathname;
if (path && !window.location.search.includes('?/')) {
window.location.replace('/?/' + path + window.location.search + window.location.hash);
}
})();
</script>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

File diff suppressed because it is too large Load Diff

View File

@ -48,6 +48,7 @@
"@types/node": "^22.7.6", "@types/node": "^22.7.6",
"@types/react": "^18.3.10", "@types/react": "^18.3.10",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.0",
"@types/uuid": "^10.0.0",
"@vitejs/plugin-react": "^4.3.2", "@vitejs/plugin-react": "^4.3.2",
"@waku/interfaces": "^0.0.29-5674b0e.0", "@waku/interfaces": "^0.0.29-5674b0e.0",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",

View File

@ -1,21 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>BuddyBook</title>
<script type="text/javascript">
var pathSegmentsToKeep = 0;
var l = window.location;
l.replace(
l.protocol + '//' + l.hostname + (l.port ? ':' + l.port : '') +
'/?/' +
l.pathname.slice(1).split('/').slice(pathSegmentsToKeep).join('/').replace(/&/g, '~and~') +
(l.search ? '&' + l.search.slice(1).replace(/&/g, '~and~') : '') +
l.hash
);
</script>
</head>
<body>
</body>
</html>

View File

@ -12,9 +12,12 @@ import { BlockPayload, getMessagesFromStore, subscribeToFilter } from './lib/wak
import TelemetryOptIn from './components/TelemetryOptIn'; import TelemetryOptIn from './components/TelemetryOptIn';
import TelemetryPage from './components/TelemetryPage'; import TelemetryPage from './components/TelemetryPage';
import SignSharedChain from './components/Chain/SignSharedChain' import SignSharedChain from './components/Chain/SignSharedChain'
import ConnectionStatus from '@/components/ConnectionStatus';
type Status = 'success' | 'in-progress' | 'error'; type Status = 'success' | 'in-progress' | 'error';
interface WakuStatus { interface WakuStatus {
filter: Status; filter: Status;
store: Status; store: Status;
@ -29,6 +32,9 @@ function App() {
filter: 'in-progress', filter: 'in-progress',
store: 'in-progress', store: 'in-progress',
}); });
(global.window as any).waku = node;
const [telemetryOptIn, setTelemetryOptIn] = useState<boolean | null>(null); const [telemetryOptIn, setTelemetryOptIn] = useState<boolean | null>(null);
const [isLoadingChains, setIsLoadingChains] = useState(true); const [isLoadingChains, setIsLoadingChains] = useState(true);
@ -40,9 +46,22 @@ function App() {
}, []); }, []);
useEffect(() => { useEffect(() => {
if (isWakuLoading || !node || node.libp2p.getConnections().length === 0 || chainsData.length > 0 || isListening) return; if (isWakuLoading || !node || node.libp2p.getConnections().length === 0 || chainsData.length > 0 || isListening) {
console.log("not starting message listening");
console.log({
isWakuLoading,
node,
connections: node?.libp2p.getConnections().length,
chainsData,
isListening
})
return;
}
setIsListening(true); setIsListening(true);
startMessageListening(); console.log("connections", node.libp2p.getConnections().length)
setTimeout(() => {
startMessageListening();
}, 2000);
}, [node, isWakuLoading, wakuStatus]) }, [node, isWakuLoading, wakuStatus])
const handleTelemetryOptIn = (optIn: boolean) => { const handleTelemetryOptIn = (optIn: boolean) => {
@ -66,25 +85,38 @@ function App() {
const startMessageListening = async () => { const startMessageListening = async () => {
console.log("Starting message listening") console.log("Starting message listening")
console.log("connections", node.libp2p.getConnections().length)
// Add timeout for store query
const STORE_TIMEOUT = 30000; // 30 seconds
const storeTimeout = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Store query timeout')), STORE_TIMEOUT);
});
try { try {
setWakuStatus(prev => ({ ...prev, store: 'in-progress' })); setWakuStatus(prev => ({ ...prev, store: 'in-progress' }));
setIsLoadingChains(true); setIsLoadingChains(true);
const messageGenerator = getMessagesFromStore(node as LightNode); const messageGenerator = getMessagesFromStore(node as LightNode);
try { try {
for await (const message of messageGenerator) { // Race between store query and timeout
setChainsData(prevChains => { await Promise.race([
const blockExists = prevChains.some(block => block.blockUUID === message.blockUUID); (async () => {
if (blockExists) return prevChains; for await (const message of messageGenerator) {
return [...prevChains, message]; setChainsData(prevChains => {
}); const blockExists = prevChains.some(block => block.blockUUID === message.blockUUID);
} if (blockExists) return prevChains;
return [...prevChains, message];
});
}
})(),
storeTimeout
]);
setWakuStatus(prev => ({ ...prev, store: 'success' }));
} catch (error) { } catch (error) {
console.error("Error processing message:", error); console.error("Error processing message:", error);
// Continue processing other messages setWakuStatus(prev => ({ ...prev, store: 'error' }));
} }
setWakuStatus(prev => ({ ...prev, store: 'success' }));
} catch (error) { } catch (error) {
console.error("Error fetching messages from store:", error); console.error("Error fetching messages from store:", error);
setWakuStatus(prev => ({ ...prev, store: 'error' })); setWakuStatus(prev => ({ ...prev, store: 'error' }));
@ -92,11 +124,21 @@ function App() {
setIsLoadingChains(false); setIsLoadingChains(false);
} }
// Add timeout for filter subscription
const FILTER_TIMEOUT = 15000; // 15 seconds
try { try {
setWakuStatus(prev => ({ ...prev, filter: 'in-progress' })); setWakuStatus(prev => ({ ...prev, filter: 'in-progress' }));
await subscribeToFilter(node as LightNode, (message) => { const filterPromise = subscribeToFilter(node as LightNode, (message) => {
handleChainUpdate(message); // Use the same function for both updates handleChainUpdate(message);
}) });
await Promise.race([
filterPromise,
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Filter subscription timeout')), FILTER_TIMEOUT)
)
]);
setWakuStatus(prev => ({ ...prev, filter: 'success' })); setWakuStatus(prev => ({ ...prev, filter: 'success' }));
} catch (error) { } catch (error) {
console.error("Error subscribing to filter:", error); console.error("Error subscribing to filter:", error);
@ -132,6 +174,9 @@ function App() {
return ( return (
<div className="min-h-screen bg-background text-foreground"> <div className="min-h-screen bg-background text-foreground">
<Header wakuStatus={wakuStatus} /> <Header wakuStatus={wakuStatus} />
<div className="md:hidden">
<ConnectionStatus filter={wakuStatus.filter} store={wakuStatus.store} />
</div>
<main className="container mx-auto px-4 py-4 md:py-8 max-w-7xl"> <main className="container mx-auto px-4 py-4 md:py-8 max-w-7xl">
<Routes> <Routes>
<Route path="" element={<Home />} /> <Route path="" element={<Home />} />

View File

@ -7,13 +7,13 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, Di
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Loader2 } from "lucide-react"; import { Loader2 } from "lucide-react";
import QRCode from '@/components/QRCode'; import QRCode from '@/components/QRCode';
import { v4 as uuidv4 } from 'uuid';
import { useWalletPrompt } from '@/hooks/useWalletPrompt'; import { useWalletPrompt } from '@/hooks/useWalletPrompt';
import { v4 as uuidv4 } from 'uuid';
import { fromLightPush, Telemetry } from '@/lib/telemetry'; import { fromLightPush, Telemetry } from '@/lib/telemetry';
interface SignChainProps { interface SignChainProps {
block: BlockPayload; block: BlockPayload;
chainsData: BlockPayload[]; // Add this prop chainsData: BlockPayload[];
onSuccess: (newBlock: BlockPayload) => void; onSuccess: (newBlock: BlockPayload) => void;
} }
@ -22,42 +22,49 @@ const SignChain: React.FC<SignChainProps> = ({ block, chainsData, onSuccess }) =
const [isSigning, setIsSigning] = useState(false); const [isSigning, setIsSigning] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [alreadySigned, setAlreadySigned] = useState(false); const [alreadySigned, setAlreadySigned] = useState(false);
const [isWalletPrompt, setIsWalletPrompt] = useState(false);
const { address } = useAccount(); const { address } = useAccount();
const { data: ensName } = useEnsName({ address }); const { data: ensName } = useEnsName({ address });
const { node } = useWaku<LightNode>(); const { node } = useWaku<LightNode>();
const { ensureWalletConnected } = useWalletPrompt(); const { ensureWalletConnected } = useWalletPrompt();
const checkSignatures = (blockToCheck: BlockPayload, visitedBlocks: Set<string>): boolean => {
if (visitedBlocks.has(blockToCheck.blockUUID)) return false;
visitedBlocks.add(blockToCheck.blockUUID);
// Check current block signatures
if (blockToCheck.signatures?.some(sig => sig?.address?.toLowerCase() === address?.toLowerCase())) {
return true;
}
// Check parent block
const parentBlock = chainsData.find(b => b.blockUUID === blockToCheck.parentBlockUUID);
if (parentBlock && checkSignatures(parentBlock, visitedBlocks)) {
return true;
}
// Check immediate child blocks
return chainsData
.filter(b => b.parentBlockUUID === blockToCheck.blockUUID)
.some(childBlock => checkSignatures(childBlock, visitedBlocks));
};
useEffect(() => { useEffect(() => {
if (address) { if (!address) return;
// Check if the address has signed this block or any blocks in the chain
const checkSignatures = (blockToCheck: BlockPayload): boolean => {
// Check current block's signatures
if (blockToCheck.signatures.some(
sig => sig.address.toLowerCase() === address.toLowerCase()
)) {
return true;
}
// Check parent blocks try {
const parentBlock = chainsData.find(b => b.blockUUID === blockToCheck.parentBlockUUID); const visitedBlocks = new Set<string>();
if (parentBlock && checkSignatures(parentBlock)) { setAlreadySigned(checkSignatures(block, visitedBlocks));
return true; } catch (error) {
} console.error('Error in signature check:', error);
setAlreadySigned(false);
// Check child blocks
const childBlocks = chainsData.filter(b => b.parentBlockUUID === blockToCheck.blockUUID);
return childBlocks.some(childBlock => checkSignatures(childBlock));
};
const hasAlreadySigned = checkSignatures(block);
setAlreadySigned(hasAlreadySigned);
} }
}, [address, block, chainsData]); }, [address, block, chainsData]);
const { signMessage } = useSignMessage({ const { signMessage } = useSignMessage({
mutation: { mutation: {
onMutate() { onMutate() {
// Reset any previous errors when starting a new signing attempt
setError(null); setError(null);
setIsSigning(true); setIsSigning(true);
}, },
@ -65,7 +72,6 @@ const SignChain: React.FC<SignChainProps> = ({ block, chainsData, onSuccess }) =
if (!address || !node) return; if (!address || !node) return;
try { try {
// Double check signature before proceeding
if (block.signatures.some(sig => sig.address.toLowerCase() === address.toLowerCase())) { if (block.signatures.some(sig => sig.address.toLowerCase() === address.toLowerCase())) {
setError('You have already signed this chain.'); setError('You have already signed this chain.');
return; return;
@ -121,36 +127,57 @@ const SignChain: React.FC<SignChainProps> = ({ block, chainsData, onSuccess }) =
const handleSign = async () => { const handleSign = async () => {
try { try {
if (!address) { if (!address) {
// If not connected, try to connect first setIsWalletPrompt(true);
const connected = await ensureWalletConnected(); const connected = await ensureWalletConnected();
if (!connected) return; if (!connected) {
setError('Please ensure your wallet is connected and the app is open.');
return;
}
} }
// Check if already signed
if (alreadySigned) { if (alreadySigned) {
setError('You have already signed this chain.'); setError('You have already signed this chain.');
return; return;
} }
// Prepare the message const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
const message = `Sign Block: if (isMobile && typeof window.ethereum === 'undefined') {
Chain UUID: ${block.chainUUID} setError('Please ensure your wallet app is installed and open before signing.');
Block UUID: ${block.blockUUID} window.location.href = 'metamask:///';
Title: ${block.title} return;
Description: ${block.description} }
Timestamp: ${new Date().getTime()}
Parent Block UUID: ${block.parentBlockUUID} const message = [
Signed by: ${ensName || address}`; 'Sign Block:',
`Chain UUID: ${block.chainUUID}`,
`Block UUID: ${block.blockUUID}`,
`Title: ${block.title}`,
`Description: ${block.description}`,
`Timestamp: ${new Date().getTime()}`,
`Parent Block UUID: ${block.parentBlockUUID}`,
`Signed by: ${ensName || address}`
].join('\n');
// Trigger signing
signMessage({ message }); signMessage({ message });
} catch (error) { } catch (error) {
console.error('Error in sign flow:', error); console.error('Error in sign flow:', error);
setError('Failed to initiate signing. Please try again.'); setError('Failed to initiate signing. Please ensure your wallet app is open and try again.');
setIsSigning(false); setIsSigning(false);
} finally {
setIsWalletPrompt(false);
} }
}; };
const getButtonText = () => {
if (isSigning) return 'Signing...';
if (isWalletPrompt) return 'Connecting...';
if (alreadySigned) return 'Already Signed';
if (!address) return 'Connect Wallet';
return 'Sign';
};
const showLoadingSpinner = isSigning || isWalletPrompt;
return ( return (
<> <>
<Button onClick={() => setIsOpen(true)} disabled={alreadySigned}> <Button onClick={() => setIsOpen(true)} disabled={alreadySigned}>
@ -174,22 +201,27 @@ Signed by: ${ensName || address}`;
</div> </div>
<QRCode text={`${window.location.origin}/sign/${block.chainUUID}/${block.blockUUID}`} /> <QRCode text={`${window.location.origin}/sign/${block.chainUUID}/${block.blockUUID}`} />
</div> </div>
{error && <p className="text-sm text-destructive">{error}</p>} {(error || isWalletPrompt) && (
<div className="space-y-2">
{error && <p className="text-sm text-destructive">{error}</p>}
{isWalletPrompt && (
<div className="rounded-md bg-blue-50 p-4">
<p className="text-sm text-blue-700">Attempting to connect to your wallet...</p>
<p className="text-xs text-blue-600 mt-1">
If your wallet doesn't open automatically, please open it manually to approve the connection.
</p>
</div>
)}
</div>
)}
<DialogFooter> <DialogFooter>
<Button variant="secondary" onClick={() => setIsOpen(false)}>Cancel</Button> <Button variant="secondary" onClick={() => setIsOpen(false)}>Cancel</Button>
<Button onClick={handleSign} disabled={isSigning || alreadySigned}> <Button
{isSigning ? ( onClick={handleSign}
<> disabled={isSigning || alreadySigned || isWalletPrompt}
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> >
Signing... {showLoadingSpinner && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
</> {getButtonText()}
) : alreadySigned ? (
'Already Signed'
) : !address ? (
'Connect Wallet'
) : (
'Sign'
)}
</Button> </Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>

View File

@ -4,7 +4,7 @@ import { type BlockPayload } from '@/lib/waku';
import SignChain from '@/components/Chain/SignChain'; import SignChain from '@/components/Chain/SignChain';
import { useEnsName } from 'wagmi'; import { useEnsName } from 'wagmi';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, DialogDescription } from "@/components/ui/dialog"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, DialogDescription, DialogFooter } from "@/components/ui/dialog";
import QRCode from '@/components/QRCode'; import QRCode from '@/components/QRCode';
import { Loader2 } from "lucide-react"; import { Loader2 } from "lucide-react";
@ -64,22 +64,23 @@ const ChainList: React.FC<ChainListProps> = ({ chainsData, onChainUpdate, isLoad
<DialogTrigger asChild> <DialogTrigger asChild>
<Button variant="outline">Share</Button> <Button variant="outline">Share</Button>
</DialogTrigger> </DialogTrigger>
<DialogContent className="sm:max-w-md"> <DialogContent className="flex flex-col gap-4">
<DialogHeader> <DialogHeader>
<DialogTitle>Share this Book</DialogTitle> <DialogTitle>Share this Book</DialogTitle>
<DialogDescription> <DialogDescription>
Share this book with others to collect their signatures. Share this book with others to collect their signatures.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="flex flex-col items-center space-y-4">
<QRCode text={shareUrl} width={200} height={200} /> <div className="flex flex-col items-center gap-4">
<p className="text-sm text-center break-all">{shareUrl}</p> <QRCode
<Button text={shareUrl}
onClick={() => navigator.clipboard.writeText(shareUrl)} width={180}
variant="outline" height={180}
> showCopyButton="text"
Copy Link title={block.title}
</Button> description={`Sign this chain: ${block.title}`}
/>
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>

View File

@ -0,0 +1,54 @@
import React from 'react';
import { Card } from "@/components/ui/card";
import { Loader2, CheckCircle2, XCircle } from "lucide-react";
type Status = 'success' | 'error' | 'in-progress';
interface ConnectionStatusProps {
filter: Status;
store: Status;
}
const StatusIndicator: React.FC<{ status: Status; label: string }> = ({ status, label }) => {
const getStatusIcon = () => {
switch (status) {
case 'success':
return <CheckCircle2 className="h-4 w-4 text-green-500" />;
case 'error':
return <XCircle className="h-4 w-4 text-red-500" />;
case 'in-progress':
return <Loader2 className="h-4 w-4 animate-spin text-yellow-500" />;
}
};
const getStatusColor = () => {
switch (status) {
case 'success':
return 'bg-green-500/10 text-green-500';
case 'error':
return 'bg-red-500/10 text-red-500';
case 'in-progress':
return 'bg-yellow-500/10 text-yellow-500';
}
};
return (
<div className={`flex items-center gap-2 px-2 py-1 rounded-full ${getStatusColor()}`}>
{getStatusIcon()}
<span className="text-xs font-medium">{label}</span>
</div>
);
};
const ConnectionStatus: React.FC<ConnectionStatusProps> = ({ filter, store }) => {
return (
<Card className="fixed bottom-4 left-4 right-4 md:static md:bottom-auto md:left-auto p-2 bg-background/80 backdrop-blur-sm border shadow-lg z-50 md:z-auto">
<div className="flex flex-row justify-around md:justify-start md:gap-4">
<StatusIndicator status={filter} label="Filter" />
<StatusIndicator status={store} label="Store" />
</div>
</Card>
);
};
export default ConnectionStatus;

View File

@ -82,30 +82,32 @@ const Header: React.FC<HeaderProps> = ({ wakuStatus }) => {
</nav> </nav>
<div className="flex items-center gap-2 md:gap-4"> <div className="flex items-center gap-2 md:gap-4">
<div className="hidden md:flex items-center gap-2"> <div className="flex items-center gap-2">
{!isWakuLoading && !wakuError && ( {!isWakuLoading && !wakuError && (
<> <>
<div className="flex items-center space-x-1"> <div className="flex items-center space-x-1">
<span className="text-muted-foreground">Filter:</span> <span className="hidden md:inline text-muted-foreground">Filter:</span>
<div className={`w-2 h-2 md:w-3 md:h-3 rounded-full ${getStatusColor(wakuStatus.filter)}`}></div> <div className={`w-2 h-2 md:w-3 md:h-3 rounded-full ${getStatusColor(wakuStatus.filter)}`}></div>
</div> </div>
<div className="flex items-center space-x-1"> <div className="flex items-center space-x-1">
<span className="text-muted-foreground">Store:</span> <span className="hidden md:inline text-muted-foreground">Store:</span>
<div className={`w-2 h-2 md:w-3 md:h-3 rounded-full ${getStatusColor(wakuStatus.store)}`}></div> <div className={`w-2 h-2 md:w-3 md:h-3 rounded-full ${getStatusColor(wakuStatus.store)}`}></div>
</div> </div>
<div className="flex items-center space-x-1">
<span className="hidden md:inline text-muted-foreground">Peers:</span>
{isWakuLoading ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : wakuError ? (
<div className={`w-2 h-2 md:w-3 md:h-3 rounded-full bg-red-500`} />
) : (
<div className={`w-2 h-2 md:w-3 md:h-3 rounded-full ${connections > 0 ? "bg-green-500" : "bg-yellow-500"}`} />
)}
</div>
</> </>
)} )}
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{isWakuLoading ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : wakuError ? (
<span className="text-xs text-red-500">Error</span>
) : (
<div className={`w-2 h-2 rounded-full ${connections > 0 ? "bg-green-500" : "bg-yellow-500"}`} />
)}
{isConnected ? ( {isConnected ? (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-xs md:text-sm text-muted-foreground truncate max-w-[80px] md:max-w-[120px]"> <span className="text-xs md:text-sm text-muted-foreground truncate max-w-[80px] md:max-w-[120px]">

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { QRCodeSVG } from 'qrcode.react'; import { QRCodeSVG } from 'qrcode.react';
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Check, Copy } from "lucide-react"; import { Check, Copy } from "lucide-react";
@ -7,11 +7,37 @@ interface QRCodeProps {
text: string; text: string;
width?: number; width?: number;
height?: number; height?: number;
showCopyButton?: 'icon' | 'text' | 'both';
title?: string;
description?: string;
} }
const QRCode: React.FC<QRCodeProps> = ({ text, width = 256, height = 256 }) => { const QRCode: React.FC<QRCodeProps> = ({
text,
width = 256,
height = 256,
showCopyButton = 'both',
title,
description
}) => {
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const isMobile = window.innerWidth < 640; const [isMobile, setIsMobile] = useState(false);
const [qrSize, setQrSize] = useState(Math.min(width, height));
useEffect(() => {
const checkMobile = () => {
setIsMobile(window.innerWidth < 640);
setQrSize(
window.innerWidth < 640
? Math.min(window.innerWidth - 80, 200)
: Math.min(width, height)
);
};
checkMobile();
window.addEventListener('resize', checkMobile);
return () => window.removeEventListener('resize', checkMobile);
}, [width, height]);
const handleCopy = async () => { const handleCopy = async () => {
await navigator.clipboard.writeText(text); await navigator.clipboard.writeText(text);
@ -19,28 +45,66 @@ const QRCode: React.FC<QRCodeProps> = ({ text, width = 256, height = 256 }) => {
setTimeout(() => setCopied(false), 2000); setTimeout(() => setCopied(false), 2000);
}; };
const handleShare = async () => {
if (navigator.share) {
try {
await navigator.share({
title: title || 'Share Chain',
text: description || 'Sign this chain',
url: text
});
} catch (error) {
if (error instanceof Error && error.name !== 'AbortError') {
console.error('Error sharing:', error);
}
}
} else {
handleCopy();
}
};
return ( return (
<div className="flex flex-col items-center space-y-4"> <div className="flex flex-col items-center w-full space-y-4">
<QRCodeSVG <div className="flex justify-center w-full">
value={text} <QRCodeSVG
size={isMobile ? Math.min(width * 0.8, window.innerWidth - 64) : Math.min(width, height)}
/>
<div className="flex items-center space-x-2 w-full max-w-[300px]">
<input
type="text"
value={text} value={text}
readOnly size={qrSize}
className="flex-1 px-3 py-2 text-xs sm:text-sm border rounded-md bg-muted truncate" className="max-w-full"
/> />
<Button
variant="outline"
size="icon"
onClick={handleCopy}
className="shrink-0"
>
{copied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
</Button>
</div> </div>
{showCopyButton !== 'text' && (
<div className="flex items-center space-x-2 w-full">
<input
type="text"
value={text}
readOnly
className="flex-1 px-3 py-2 text-xs sm:text-sm border rounded-md bg-muted truncate"
/>
<Button
variant="outline"
size="icon"
onClick={handleCopy}
className="shrink-0"
>
{copied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
</Button>
</div>
)}
{showCopyButton === 'text' && (
<Button
onClick={isMobile && 'share' in navigator ? handleShare : handleCopy}
variant="secondary"
className="w-full sm:w-auto"
>
{isMobile && 'share' in navigator
? 'Share'
: copied
? 'Copied!'
: 'Copy Link'}
</Button>
)}
</div> </div>
); );
}; };

View File

@ -1,6 +1,5 @@
import * as React from "react" import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog" import * as DialogPrimitive from "@radix-ui/react-dialog"
import { Cross2Icon } from "@radix-ui/react-icons"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
@ -36,9 +35,12 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content <DialogPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200", "fixed z-50 border bg-background shadow-lg duration-200",
"rounded-lg", "p-4 md:p-6",
"max-h-[85vh] overflow-y-auto", "w-full md:w-[calc(100%-2rem)] md:max-w-lg",
"focus:outline-none focus-visible:ring-2 focus-visible:ring-accent",
"bottom-0 rounded-t-lg md:rounded-lg",
"md:left-[50%] md:top-[50%] md:translate-x-[-50%] md:translate-y-[-50%]",
className className
)} )}
{...props} {...props}

View File

@ -7,16 +7,33 @@ export function useWalletPrompt() {
const ensureWalletConnected = async () => { const ensureWalletConnected = async () => {
if (!isConnected) { if (!isConnected) {
try { try {
// Find the first available connector (usually injected/metamask)
const connector = connectors[0] const connector = connectors[0]
if (connector) { if (connector) {
// Check if we're on iOS
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent)
// If on iOS, try to open MetaMask first
if (isIOS) {
// Attempt to open MetaMask
window.location.href = 'metamask:///'
// Give a small delay before attempting connection
await new Promise(resolve => setTimeout(resolve, 1000))
}
await connect({ connector }) await connect({ connector })
// Wait a brief moment for the connection to be established
await new Promise(resolve => setTimeout(resolve, 1000))
return true
} }
// Wait a brief moment for the connection to be established
await new Promise(resolve => setTimeout(resolve, 1000));
return true
} catch (error) { } catch (error) {
console.error('Error connecting wallet:', error) console.error('Error connecting wallet:', error)
// If connection fails, try to open MetaMask directly
if (typeof window.ethereum === 'undefined') {
// Redirect to MetaMask download page if not installed
window.open('https://metamask.io/download/', '_blank')
}
return false return false
} }
} }

View File

@ -18,7 +18,7 @@ export type BlockPayload = {
parentBlockUUID: string | null; parentBlockUUID: string | null;
} }
export const contentTopic = "/buddybook-dogfood/1/chain/proto"; const contentTopic = "/buddybook-devcon/1/chain/proto";
export const encoder = createEncoder({ export const encoder = createEncoder({
contentTopic: contentTopic, contentTopic: contentTopic,
@ -113,7 +113,7 @@ export async function subscribeToFilter(node: LightNode, callback: (message: Blo
console.log("Error subscribing to filter", error) console.log("Error subscribing to filter", error)
} }
if (!subscription || error || results.successes.length === 0 ||results.failures.length >0) { if (!subscription || error || results.successes.length === 0 || results.failures.length > 0) {
throw new Error("Failed to subscribe to filter") throw new Error("Failed to subscribe to filter")
} }
} }

View File

@ -9,7 +9,7 @@ import App from './App.tsx'
import './index.css' import './index.css'
import { LightNodeProvider } from "@waku/react"; import { LightNodeProvider } from "@waku/react";
import { config } from './lib/walletConnect.ts' import { config } from './lib/walletConnect.ts'
import { WAKU_NODE_OPTIONS } from './lib/waku-config.ts' import { WAKU_NODE_OPTIONS } from './lib/waku.ts'
// Polyfills // Polyfills
if (typeof global === 'undefined') { if (typeof global === 'undefined') {