mirror of
https://github.com/waku-org/waku-lab.git
synced 2025-01-11 16:34:29 +00:00
feat: buddychain (#101)
This commit is contained in:
parent
49f3a436d3
commit
e823496abf
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
@ -21,7 +21,8 @@ jobs:
|
|||||||
experimental/rln-identity,
|
experimental/rln-identity,
|
||||||
dogfooding,
|
dogfooding,
|
||||||
message-monitor,
|
message-monitor,
|
||||||
flush-notes
|
flush-notes,
|
||||||
|
buddybook
|
||||||
]
|
]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
@ -40,7 +41,12 @@ jobs:
|
|||||||
working-directory: "examples/${{ matrix.example }}"
|
working-directory: "examples/${{ matrix.example }}"
|
||||||
|
|
||||||
- name: build
|
- name: build
|
||||||
run: npm run build
|
run: |
|
||||||
|
if [ "${{ matrix.example }}" == "buddybook" ]; then
|
||||||
|
npm run build:ci
|
||||||
|
else
|
||||||
|
npm run build
|
||||||
|
fi
|
||||||
working-directory: "examples/${{ matrix.example }}"
|
working-directory: "examples/${{ matrix.example }}"
|
||||||
|
|
||||||
- name: test
|
- name: test
|
||||||
|
@ -57,6 +57,11 @@ This example uses Waku Relay to send and receive simple text messages.
|
|||||||
- [website](https://lab.waku.org/experimental/relay-direct-rtc)
|
- [website](https://lab.waku.org/experimental/relay-direct-rtc)
|
||||||
- Demonstrates: Relay over WebRTC.
|
- Demonstrates: Relay over WebRTC.
|
||||||
|
|
||||||
|
### Buddychain App
|
||||||
|
|
||||||
|
- [code](examples/buddychain)
|
||||||
|
- [website](https://lab.waku.org/buddychain)
|
||||||
|
- Demonstrates: [Add a brief description of what the Buddychain app demonstrates]
|
||||||
|
|
||||||
# Continuous Integration
|
# Continuous Integration
|
||||||
|
|
||||||
|
10
ci/Jenkinsfile
vendored
10
ci/Jenkinsfile
vendored
@ -44,6 +44,16 @@ pipeline {
|
|||||||
stage('dogfooding') { steps { script { buildExample() } } }
|
stage('dogfooding') { steps { script { buildExample() } } }
|
||||||
stage('message-monitor') { steps { script { buildExample() } } }
|
stage('message-monitor') { steps { script { buildExample() } } }
|
||||||
stage('flush-notes') { steps { script { buildNextJSExample() } } }
|
stage('flush-notes') { steps { script { buildNextJSExample() } } }
|
||||||
|
stage('buddybook') {
|
||||||
|
steps {
|
||||||
|
script {
|
||||||
|
dir('examples/buddybook') {
|
||||||
|
sh 'npm install'
|
||||||
|
sh 'npm run build:ci'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
17
examples/buddybook/.gitignore
vendored
Normal file
17
examples/buddybook/.gitignore
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
instructions
|
||||||
|
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
.env
|
50
examples/buddybook/README.md
Normal file
50
examples/buddybook/README.md
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
# React + TypeScript + Vite
|
||||||
|
|
||||||
|
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||||
|
|
||||||
|
Currently, two official plugins are available:
|
||||||
|
|
||||||
|
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||||
|
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||||
|
|
||||||
|
## Expanding the ESLint configuration
|
||||||
|
|
||||||
|
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
|
||||||
|
|
||||||
|
- Configure the top-level `parserOptions` property like this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
export default tseslint.config({
|
||||||
|
languageOptions: {
|
||||||
|
// other options...
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
|
||||||
|
- Optionally add `...tseslint.configs.stylisticTypeChecked`
|
||||||
|
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// eslint.config.js
|
||||||
|
import react from 'eslint-plugin-react'
|
||||||
|
|
||||||
|
export default tseslint.config({
|
||||||
|
// Set the react version
|
||||||
|
settings: { react: { version: '18.3' } },
|
||||||
|
plugins: {
|
||||||
|
// Add the react plugin
|
||||||
|
react,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
// other rules...
|
||||||
|
// Enable its recommended rules
|
||||||
|
...react.configs.recommended.rules,
|
||||||
|
...react.configs['jsx-runtime'].rules,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
20
examples/buddybook/components.json
Normal file
20
examples/buddybook/components.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "new-york",
|
||||||
|
"rsc": false,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "tailwind.config.js",
|
||||||
|
"css": "src/index.css",
|
||||||
|
"baseColor": "neutral",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"lib": "@/lib",
|
||||||
|
"hooks": "@/hooks"
|
||||||
|
}
|
||||||
|
}
|
28
examples/buddybook/eslint.config.js
Normal file
28
examples/buddybook/eslint.config.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import js from '@eslint/js'
|
||||||
|
import globals from 'globals'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
import tseslint from 'typescript-eslint'
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{ ignores: ['dist'] },
|
||||||
|
{
|
||||||
|
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
'react-hooks': reactHooks,
|
||||||
|
'react-refresh': reactRefresh,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
13
examples/buddybook/index.html
Normal file
13
examples/buddybook/index.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Buddychain Dogfood</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
20578
examples/buddybook/package-lock.json
generated
Normal file
20578
examples/buddybook/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
60
examples/buddybook/package.json
Normal file
60
examples/buddybook/package.json
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
{
|
||||||
|
"name": "buddybook",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"build:ci": "vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-dialog": "^1.1.2",
|
||||||
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
|
"@radix-ui/react-label": "^2.1.0",
|
||||||
|
"@radix-ui/react-scroll-area": "^1.2.0",
|
||||||
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
|
"@t3-oss/env-core": "^0.9.2",
|
||||||
|
"@t3-oss/env-nextjs": "^0.9.2",
|
||||||
|
"@tailwindcss/typography": "^0.5.15",
|
||||||
|
"@tanstack/react-query": "^5.59.15",
|
||||||
|
"@waku/react": "0.0.7-0adebab",
|
||||||
|
"class-variance-authority": "^0.7.0",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"connectkit": "^1.8.2",
|
||||||
|
"lucide-react": "^0.453.0",
|
||||||
|
"protobufjs": "^7.4.0",
|
||||||
|
"qrcode.react": "^4.0.1",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"react-markdown": "^9.0.1",
|
||||||
|
"react-router-dom": "^6.27.0",
|
||||||
|
"tailwind-merge": "^2.5.4",
|
||||||
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"uuid": "^10.0.0",
|
||||||
|
"viem": "^2.21.28",
|
||||||
|
"wagmi": "^2.12.19",
|
||||||
|
"zod": "^3.22.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.7.6",
|
||||||
|
"@types/react": "^18.3.10",
|
||||||
|
"@types/react-dom": "^18.3.0",
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
|
"copy-webpack-plugin": "^11.0.0",
|
||||||
|
"eslint": "^8.56.0",
|
||||||
|
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.12",
|
||||||
|
"postcss": "^8.4.47",
|
||||||
|
"tailwindcss": "^3.4.14",
|
||||||
|
"ts-loader": "^9.5.1",
|
||||||
|
"typescript": "^5.5.3",
|
||||||
|
"webpack": "^5.74.0",
|
||||||
|
"webpack-cli": "^4.10.0",
|
||||||
|
"webpack-dev-server": "^4.11.1",
|
||||||
|
"@vitejs/plugin-react": "^4.3.2",
|
||||||
|
"vite": "^5.4.8"
|
||||||
|
}
|
||||||
|
}
|
6
examples/buddybook/postcss.config.js
Normal file
6
examples/buddybook/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
1
examples/buddybook/public/vite.svg
Normal file
1
examples/buddybook/public/vite.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
42
examples/buddybook/src/App.css
Normal file
42
examples/buddybook/src/App.css
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
#root {
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
height: 6em;
|
||||||
|
padding: 1.5em;
|
||||||
|
will-change: filter;
|
||||||
|
transition: filter 300ms;
|
||||||
|
}
|
||||||
|
.logo:hover {
|
||||||
|
filter: drop-shadow(0 0 2em #646cffaa);
|
||||||
|
}
|
||||||
|
.logo.react:hover {
|
||||||
|
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes logo-spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
a:nth-of-type(2) .logo {
|
||||||
|
animation: logo-spin infinite 20s linear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
padding: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.read-the-docs {
|
||||||
|
color: #888;
|
||||||
|
}
|
149
examples/buddybook/src/App.tsx
Normal file
149
examples/buddybook/src/App.tsx
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import './App.css'
|
||||||
|
import Header from './components/Header'
|
||||||
|
import ChainCreationForm from './components/Chain/Create/CreationPreview'
|
||||||
|
import ChainList from './components/Chain/View/ChainList'
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { type LightNode } from "@waku/sdk"
|
||||||
|
import { useWaku } from "@waku/react"
|
||||||
|
import { Loader2 } from "lucide-react"
|
||||||
|
import { Routes, Route, Navigate, Link } from 'react-router-dom'
|
||||||
|
import { BlockPayload, getMessagesFromStore, subscribeToFilter } from './lib/waku'
|
||||||
|
import TelemetryOptIn from './components/TelemetryOptIn';
|
||||||
|
import TelemetryPage from './components/TelemetryPage';
|
||||||
|
|
||||||
|
type Status = 'success' | 'in-progress' | 'error';
|
||||||
|
|
||||||
|
interface WakuStatus {
|
||||||
|
filter: Status;
|
||||||
|
store: Status;
|
||||||
|
}
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [isListening, setIsListening] = useState(false);
|
||||||
|
const [chainsData, setChainsData] = useState<BlockPayload[]>([])
|
||||||
|
const { isLoading: isWakuLoading, error: wakuError, node } = useWaku();
|
||||||
|
// Add this new state
|
||||||
|
const [wakuStatus, setWakuStatus] = useState<WakuStatus>({
|
||||||
|
filter: 'in-progress',
|
||||||
|
store: 'in-progress',
|
||||||
|
});
|
||||||
|
const [telemetryOptIn, setTelemetryOptIn] = useState<boolean | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const storedOptIn = localStorage.getItem('telemetryOptIn');
|
||||||
|
if (storedOptIn !== null) {
|
||||||
|
setTelemetryOptIn(storedOptIn === 'true');
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isWakuLoading || !node || node.libp2p.getConnections().length === 0 || chainsData.length > 0 || isListening) return;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsListening(true);
|
||||||
|
startMessageListening();
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
|
||||||
|
}, [node, isWakuLoading, wakuStatus])
|
||||||
|
|
||||||
|
const handleTelemetryOptIn = (optIn: boolean) => {
|
||||||
|
setTelemetryOptIn(optIn);
|
||||||
|
localStorage.setItem('telemetryOptIn', optIn.toString());
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isWakuLoading) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background text-foreground flex justify-center items-center">
|
||||||
|
<Loader2 className="h-8 w-8 animate-spin" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const startMessageListening = async () => {
|
||||||
|
console.log("Starting message listening")
|
||||||
|
try {
|
||||||
|
setWakuStatus(prev => ({ ...prev, store: 'in-progress' }));
|
||||||
|
const storeMessages = await getMessagesFromStore(node as LightNode)
|
||||||
|
setChainsData(storeMessages)
|
||||||
|
setWakuStatus(prev => ({ ...prev, store: 'success' }));
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching messages from store:", error);
|
||||||
|
setWakuStatus(prev => ({ ...prev, store: 'error' }));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setWakuStatus(prev => ({ ...prev, filter: 'in-progress' }));
|
||||||
|
await subscribeToFilter(node as LightNode, (message) => {
|
||||||
|
handleChainUpdate(message); // Use the same function for both updates
|
||||||
|
})
|
||||||
|
setWakuStatus(prev => ({ ...prev, filter: 'success' }));
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error subscribing to filter:", error);
|
||||||
|
setWakuStatus(prev => ({ ...prev, filter: 'error' }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wakuError) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background text-foreground flex flex-col justify-center items-center">
|
||||||
|
<p className="text-red-500">Error connecting to Waku network</p>
|
||||||
|
<p className="text-sm text-muted-foreground">{wakuError.toString()}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleChainUpdate = (newBlock: BlockPayload) => {
|
||||||
|
setChainsData(prevChains => {
|
||||||
|
// Check if the block already exists
|
||||||
|
const blockExists = prevChains.some(block => block.blockUUID === newBlock.blockUUID);
|
||||||
|
if (blockExists) {
|
||||||
|
return prevChains; // Don't add duplicate blocks
|
||||||
|
}
|
||||||
|
return [...prevChains, newBlock];
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (telemetryOptIn === null) {
|
||||||
|
return <TelemetryOptIn onOptIn={handleTelemetryOptIn} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background text-foreground">
|
||||||
|
<Header wakuStatus={wakuStatus} />
|
||||||
|
<main className="container mx-auto px-4 py-8">
|
||||||
|
<Routes>
|
||||||
|
<Route path="/create" element={<ChainCreationForm />} />
|
||||||
|
<Route path="/view" element={<ChainList chainsData={chainsData} onChainUpdate={handleChainUpdate} />} />
|
||||||
|
<Route path="/" element={<Home />} />
|
||||||
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
|
<Route path="/telemetry" element={<TelemetryPage />} />
|
||||||
|
</Routes>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Home: React.FC = () => (
|
||||||
|
<div className="space-y-6 text-center">
|
||||||
|
<h1 className="text-4xl font-bold">BuddyChain</h1>
|
||||||
|
<div className="max-w-md mx-auto p-6 bg-card rounded-lg shadow-md">
|
||||||
|
<Link to="/create">
|
||||||
|
<Button
|
||||||
|
className="w-full mb-4"
|
||||||
|
>
|
||||||
|
Create New Chain
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Click the button above to start creating a new chain.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Welcome to BuddyChain - Create and share your chains!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default App
|
1
examples/buddybook/src/assets/react.svg
Normal file
1
examples/buddybook/src/assets/react.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
After Width: | Height: | Size: 4.0 KiB |
@ -0,0 +1,208 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
import { Textarea } from "@/components/ui/textarea"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Label } from "@/components/ui/label"
|
||||||
|
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"
|
||||||
|
import { useAccount, useSignMessage } from 'wagmi'
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"
|
||||||
|
import { Loader2 } from "lucide-react"
|
||||||
|
import QRCode from '@/components/QRCode';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import { useWaku } from '@waku/react';
|
||||||
|
import { LightNode } from '@waku/sdk';
|
||||||
|
import { createMessage, encoder } from '@/lib/waku';
|
||||||
|
|
||||||
|
interface FormData {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
uuid: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const DEFAULT_FORM_DATA: FormData = {
|
||||||
|
title: 'Devcon24 DeFi Dynamo',
|
||||||
|
description: 'A revolutionary blockchain for Devcon 24, focusing on scalable DeFi solutions and cross-chain interoperability.',
|
||||||
|
uuid: uuidv4(),
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChainCreationForm: React.FC = () => {
|
||||||
|
const [formData, setFormData] = useState<FormData>(DEFAULT_FORM_DATA);
|
||||||
|
const [errors, setErrors] = useState<Partial<FormData>>({});
|
||||||
|
const [showModal, setShowModal] = useState<boolean>(false);
|
||||||
|
const [isSigning, setIsSigning] = useState<boolean>(false);
|
||||||
|
const [isSuccess, setIsSuccess] = useState<boolean>(false);
|
||||||
|
const [sendError, setSendError] = useState<string | null>(null);
|
||||||
|
const [signedMessage, setSignedMessage] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const { node } = useWaku<LightNode>();
|
||||||
|
|
||||||
|
const { address } = useAccount();
|
||||||
|
const { signMessage } = useSignMessage({
|
||||||
|
mutation: {
|
||||||
|
async onSuccess(signature: string) {
|
||||||
|
if (!address || !node) return;
|
||||||
|
|
||||||
|
setSignedMessage(signature);
|
||||||
|
const message = createMessage({
|
||||||
|
chainUUID: formData.uuid,
|
||||||
|
blockUUID: uuidv4(),
|
||||||
|
title: formData.title,
|
||||||
|
description: formData.description,
|
||||||
|
signedMessage: signature,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
signatures: [{address, signature}],
|
||||||
|
parentBlockUUID: null
|
||||||
|
});
|
||||||
|
|
||||||
|
await node?.lightPush.send(encoder, message)
|
||||||
|
setIsSuccess(true);
|
||||||
|
setIsSigning(false);
|
||||||
|
},
|
||||||
|
onError(error: Error) {
|
||||||
|
console.error('Error signing message:', error);
|
||||||
|
setIsSigning(false);
|
||||||
|
setSendError('Error signing message. Please try again.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setFormData(prevData => ({
|
||||||
|
...prevData,
|
||||||
|
[name]: value,
|
||||||
|
}));
|
||||||
|
if (errors[name as keyof FormData]) {
|
||||||
|
setErrors(prevErrors => ({
|
||||||
|
...prevErrors,
|
||||||
|
[name]: undefined,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateForm = (): boolean => {
|
||||||
|
const newErrors: Partial<FormData> = {};
|
||||||
|
if (!formData.title.trim()) {
|
||||||
|
newErrors.title = 'Title is required';
|
||||||
|
}
|
||||||
|
if (!formData.description.trim()) {
|
||||||
|
newErrors.description = 'Description is required';
|
||||||
|
}
|
||||||
|
setErrors(newErrors);
|
||||||
|
return Object.keys(newErrors).length === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateChain = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (validateForm()) {
|
||||||
|
setShowModal(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
setIsSigning(true);
|
||||||
|
setSendError(null);
|
||||||
|
const message = `Create Chain:
|
||||||
|
Chain UUID: ${formData.uuid}
|
||||||
|
Title: ${formData.title}
|
||||||
|
Description: ${formData.description}
|
||||||
|
Timestamp: ${new Date().getTime()}
|
||||||
|
Signed by: ${address}`;
|
||||||
|
signMessage({ message });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseModal = () => {
|
||||||
|
setShowModal(false);
|
||||||
|
setIsSuccess(false);
|
||||||
|
setIsSigning(false);
|
||||||
|
setSendError(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="w-full max-w-2xl mx-auto">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Create a New Chain</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<form onSubmit={handleCreateChain} className="space-y-6">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="title">Chain Title</Label>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
id="title"
|
||||||
|
name="title"
|
||||||
|
value={formData.title}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
maxLength={50}
|
||||||
|
/>
|
||||||
|
{errors.title && <p className="text-sm text-destructive">{errors.title}</p>}
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="description">Chain Description</Label>
|
||||||
|
<Textarea
|
||||||
|
id="description"
|
||||||
|
name="description"
|
||||||
|
value={formData.description}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
maxLength={500}
|
||||||
|
/>
|
||||||
|
{errors.description && <p className="text-sm text-destructive">{errors.description}</p>}
|
||||||
|
</div>
|
||||||
|
<Button type="submit" className="w-full">Create Chain</Button>
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
<Dialog open={showModal} onOpenChange={handleCloseModal}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{isSuccess ? "Chain Created" : "Chain Preview"}</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
{!isSuccess ? (
|
||||||
|
<>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h4 className="text-xl font-semibold">{formData.title}</h4>
|
||||||
|
<p className="text-muted-foreground">{formData.description}</p>
|
||||||
|
{sendError && <p className="text-sm text-destructive">{sendError}</p>}
|
||||||
|
</div>
|
||||||
|
<DialogFooter className="sm:justify-start">
|
||||||
|
<Button type="button" variant="secondary" onClick={handleCloseModal}>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
<Button type="button" onClick={handleSubmit} disabled={isSigning}>
|
||||||
|
{isSigning ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
Signing...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'Sign'
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{signedMessage && (
|
||||||
|
<QRCode
|
||||||
|
data={{
|
||||||
|
chainUUID: formData.uuid,
|
||||||
|
blockUUID: uuidv4(),
|
||||||
|
title: formData.title,
|
||||||
|
description: formData.description,
|
||||||
|
signedMessage: signedMessage,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
signatures: [{address: address!, signature: signedMessage}],
|
||||||
|
parentBlockUUID: null
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChainCreationForm;
|
111
examples/buddybook/src/components/Chain/SignChain.tsx
Normal file
111
examples/buddybook/src/components/Chain/SignChain.tsx
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useAccount, useSignMessage, useEnsName } from 'wagmi';
|
||||||
|
import type { LightNode } from '@waku/interfaces';
|
||||||
|
import { useWaku } from '@waku/react';
|
||||||
|
import { createMessage, encoder, BlockPayload } from '@/lib/waku';
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Loader2 } from "lucide-react";
|
||||||
|
import QRCode from '@/components/QRCode';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
interface SignChainProps {
|
||||||
|
block: BlockPayload;
|
||||||
|
onSuccess: (newBlock: BlockPayload) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SignChain: React.FC<SignChainProps> = ({ block, onSuccess }) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [isSigning, setIsSigning] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const { address } = useAccount();
|
||||||
|
const { data: ensName } = useEnsName({ address });
|
||||||
|
const { node } = useWaku<LightNode>();
|
||||||
|
const { signMessage } = useSignMessage({
|
||||||
|
mutation: {
|
||||||
|
async onSuccess(signature) {
|
||||||
|
if (!address || !node) return;
|
||||||
|
|
||||||
|
const newBlock: BlockPayload = {
|
||||||
|
chainUUID: block.chainUUID,
|
||||||
|
blockUUID: uuidv4(),
|
||||||
|
title: block.title,
|
||||||
|
description: block.description,
|
||||||
|
signedMessage: signature,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
signatures: [{ address, signature }],
|
||||||
|
parentBlockUUID: block.blockUUID
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const wakuMessage = createMessage(newBlock);
|
||||||
|
const { failures, successes } = await node.lightPush.send(encoder, wakuMessage);
|
||||||
|
|
||||||
|
if (failures.length > 0 || successes.length === 0) {
|
||||||
|
throw new Error('Failed to send message to Waku network');
|
||||||
|
}
|
||||||
|
|
||||||
|
onSuccess(newBlock);
|
||||||
|
setIsOpen(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating new block:', error);
|
||||||
|
setError('Failed to create new block. Please try again.');
|
||||||
|
} finally {
|
||||||
|
setIsSigning(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError(error) {
|
||||||
|
console.error('Error signing message:', error);
|
||||||
|
setError('Error signing message. Please try again.');
|
||||||
|
setIsSigning(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSign = () => {
|
||||||
|
setIsSigning(true);
|
||||||
|
setError(null);
|
||||||
|
const message = `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}`;
|
||||||
|
signMessage({ message });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button onClick={() => setIsOpen(true)}>Sign Chain</Button>
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Sign Chain</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Review the block details and sign to add your signature to the chain.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<QRCode data={block} />
|
||||||
|
{error && <p className="text-sm text-destructive">{error}</p>}
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="secondary" onClick={() => setIsOpen(false)}>Cancel</Button>
|
||||||
|
<Button onClick={handleSign} disabled={isSigning}>
|
||||||
|
{isSigning ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
Signing...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'Sign'
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SignChain;
|
95
examples/buddybook/src/components/Chain/View/ChainList.tsx
Normal file
95
examples/buddybook/src/components/Chain/View/ChainList.tsx
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
||||||
|
import { type BlockPayload } from '@/lib/waku';
|
||||||
|
import SignChain from '@/components/Chain/SignChain';
|
||||||
|
import { useEnsName } from 'wagmi';
|
||||||
|
|
||||||
|
interface ChainListProps {
|
||||||
|
chainsData: BlockPayload[];
|
||||||
|
onChainUpdate: (newBlock: BlockPayload) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChainList: React.FC<ChainListProps> = ({ chainsData, onChainUpdate }) => {
|
||||||
|
const handleChainUpdate = (newBlock: BlockPayload) => {
|
||||||
|
onChainUpdate(newBlock);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderBlock = (block: BlockPayload, depth: number = 0) => {
|
||||||
|
const childBlocks = chainsData.filter(b => b.parentBlockUUID === block.blockUUID);
|
||||||
|
const totalSignatures = block.signatures.length + childBlocks.reduce((acc, child) => acc + child.signatures.length, 0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li key={`${block.blockUUID}-${depth}`} className="mb-4">
|
||||||
|
<div className="flex items-start">
|
||||||
|
<div className="mr-4 mt-2">
|
||||||
|
{depth > 0 && (
|
||||||
|
<div className="w-6 h-6 border-l-2 border-b-2 border-gray-300"></div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{depth === 0 ? (
|
||||||
|
<Card className="flex-grow">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>{block.title}</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p>{block.description}</p>
|
||||||
|
<p className="text-sm text-muted-foreground mt-2">
|
||||||
|
Created at: {new Date(block.timestamp).toLocaleString()}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Total Signatures: {totalSignatures}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Block UUID: {block.blockUUID}
|
||||||
|
</p>
|
||||||
|
<div className="mt-2">
|
||||||
|
<SignChain block={block} onSuccess={handleChainUpdate} />
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<div className="flex-grow">
|
||||||
|
<SignerName address={block.signatures[0].address} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{childBlocks.length > 0 && (
|
||||||
|
<ul className="ml-8 mt-2">
|
||||||
|
{childBlocks.map((childBlock) => renderBlock(childBlock, depth + 1))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const rootBlocks = chainsData.filter(block => !block.parentBlockUUID);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="w-full max-w-4xl mx-auto">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Existing Chains</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{rootBlocks.length === 0 ? (
|
||||||
|
<p>No chains found.</p>
|
||||||
|
) : (
|
||||||
|
<ul className="space-y-4">
|
||||||
|
{rootBlocks.map((block) => renderBlock(block, 0))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const SignerName: React.FC<{ address: `0x${string}` }> = ({ address }) => {
|
||||||
|
const { data: ensName } = useEnsName({ address })
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p className="text-sm">
|
||||||
|
Signed by: {ensName || `${address.slice(0, 6)}...${address.slice(-4)}`}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChainList;
|
128
examples/buddybook/src/components/Header.tsx
Normal file
128
examples/buddybook/src/components/Header.tsx
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useAccount, useDisconnect, useEnsName } from 'wagmi';
|
||||||
|
import { ConnectKitButton } from 'connectkit';
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { useWaku } from "@waku/react";
|
||||||
|
import { Loader2 } from "lucide-react";
|
||||||
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
|
// Add these new types
|
||||||
|
type Status = 'success' | 'in-progress' | 'error';
|
||||||
|
|
||||||
|
interface WakuStatus {
|
||||||
|
filter: Status;
|
||||||
|
store: Status;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HeaderProps {
|
||||||
|
wakuStatus: WakuStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Header: React.FC<HeaderProps> = ({ wakuStatus }) => {
|
||||||
|
const { address, isConnected } = useAccount();
|
||||||
|
const { disconnect } = useDisconnect();
|
||||||
|
const { isLoading: isWakuLoading, error: wakuError, node: waku } = useWaku();
|
||||||
|
const [connections, setConnections] = useState(0);
|
||||||
|
const location = useLocation();
|
||||||
|
const { data: ensName } = useEnsName({ address });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (waku) {
|
||||||
|
const updateConnections = () => {
|
||||||
|
setConnections(waku.libp2p.getConnections().length);
|
||||||
|
};
|
||||||
|
|
||||||
|
updateConnections();
|
||||||
|
|
||||||
|
waku.libp2p.addEventListener("peer:connect", updateConnections);
|
||||||
|
waku.libp2p.addEventListener("peer:disconnect", updateConnections);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
waku.libp2p.removeEventListener("peer:connect", updateConnections);
|
||||||
|
waku.libp2p.removeEventListener("peer:disconnect", updateConnections);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [waku]);
|
||||||
|
|
||||||
|
const getStatusColor = (status: Status) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'success':
|
||||||
|
return 'bg-green-500';
|
||||||
|
case 'in-progress':
|
||||||
|
return 'bg-yellow-500';
|
||||||
|
case 'error':
|
||||||
|
return 'bg-red-500';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header className="bg-background border-b border-border">
|
||||||
|
<div className="container mx-auto px-4 py-4 flex justify-between items-center">
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<h1 className="text-2xl font-bold">BuddyBook</h1>
|
||||||
|
<nav>
|
||||||
|
<ul className="flex space-x-4">
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
to="/create"
|
||||||
|
className={`text-sm ${location.pathname === '/create' ? 'text-primary font-semibold' : 'text-muted-foreground'}`}
|
||||||
|
>
|
||||||
|
Create Chain
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
to="/view"
|
||||||
|
className={`text-sm ${location.pathname === '/view' ? 'text-primary font-semibold' : 'text-muted-foreground'}`}
|
||||||
|
>
|
||||||
|
View Existing Chains
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
to="/telemetry"
|
||||||
|
className={`text-sm ${location.pathname === '/telemetry' ? 'text-primary font-semibold' : 'text-muted-foreground'}`}
|
||||||
|
>
|
||||||
|
Telemetry
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="flex items-center space-x-1">
|
||||||
|
<span className="text-sm text-muted-foreground">Filter:</span>
|
||||||
|
<div className={`w-3 h-3 rounded-full ${getStatusColor(wakuStatus.filter)}`}></div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-1">
|
||||||
|
<span className="text-sm text-muted-foreground">Store:</span>
|
||||||
|
<div className={`w-3 h-3 rounded-full ${getStatusColor(wakuStatus.store)}`}></div>
|
||||||
|
</div>
|
||||||
|
{isWakuLoading ? (
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
) : wakuError ? (
|
||||||
|
<span className="text-sm text-red-500">Waku Error</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
Waku Connections: {connections}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{isConnected ? (
|
||||||
|
<>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{ensName || (address ? `${address.slice(0, 6)}...${address.slice(-4)}` : '')}
|
||||||
|
</span>
|
||||||
|
<Button variant="outline" size="sm" onClick={() => disconnect()}>
|
||||||
|
Logout
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<ConnectKitButton />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Header;
|
53
examples/buddybook/src/components/QRCode.tsx
Normal file
53
examples/buddybook/src/components/QRCode.tsx
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { QRCodeSVG } from 'qrcode.react';
|
||||||
|
import { BlockPayload } from '@/lib/waku';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { useEnsName } from 'wagmi';
|
||||||
|
|
||||||
|
interface QRCodeProps {
|
||||||
|
data: BlockPayload;
|
||||||
|
size?: number;
|
||||||
|
onSign?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QRCode: React.FC<QRCodeProps> = ({ data, size = 256, onSign }) => {
|
||||||
|
const shareableLink = `${window.location.origin}/view/${data.chainUUID}/${data.blockUUID}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center space-y-4">
|
||||||
|
<QRCodeSVG value={shareableLink} size={size} />
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
<p><strong>Title:</strong> {data.title}</p>
|
||||||
|
<p><strong>Description:</strong> {data.description}</p>
|
||||||
|
<p><strong>Timestamp:</strong> {new Date(data.timestamp).toLocaleString()}</p>
|
||||||
|
<p><strong>Signed Message:</strong> {`0x${data.signedMessage.slice(2, 6)}....${data.signedMessage.slice(-6)}`}</p>
|
||||||
|
<p><strong>Parent Block:</strong> {data.parentBlockUUID || 'Root'}</p>
|
||||||
|
<p><strong>Signatures:</strong></p>
|
||||||
|
<ul>
|
||||||
|
{data.signatures.map((sig, index) => (
|
||||||
|
<SignatureItem key={index} address={sig.address} />
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={shareableLink}
|
||||||
|
readOnly
|
||||||
|
className="w-full p-2 border rounded"
|
||||||
|
/>
|
||||||
|
{onSign && <Button onClick={onSign}>Sign This Block</Button>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const SignatureItem: React.FC<{ address: `0x${string}` }> = ({ address }) => {
|
||||||
|
const { data: ensName } = useEnsName({ address });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li>
|
||||||
|
{ensName || `${address.slice(0, 6)}...${address.slice(-4)}`}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default QRCode;
|
54
examples/buddybook/src/components/TelemetryOptIn.tsx
Normal file
54
examples/buddybook/src/components/TelemetryOptIn.tsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardHeader, CardTitle, CardContent, CardFooter } from "@/components/ui/card";
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog";
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
import { privacyPolicy } from '@/lib/privacyPolicy';
|
||||||
|
import ReactMarkdown from 'react-markdown';
|
||||||
|
|
||||||
|
interface TelemetryOptInProps {
|
||||||
|
onOptIn: (optIn: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TelemetryOptIn: React.FC<TelemetryOptInProps> = ({ onOptIn }) => {
|
||||||
|
const [showFullPolicy, setShowFullPolicy] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-background">
|
||||||
|
<Card className="w-full max-w-md">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Telemetry Data Collection</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
|
We collect telemetry data to improve our services. This data is anonymous and helps us understand how our application is used. You can opt-in or opt-out of this data collection.
|
||||||
|
</p>
|
||||||
|
<Button variant="link" onClick={() => setShowFullPolicy(true)}>
|
||||||
|
View Full Privacy Policy
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter className="flex justify-between">
|
||||||
|
<Button variant="outline" onClick={() => onOptIn(false)}>Opt Out</Button>
|
||||||
|
<Button onClick={() => onOptIn(true)}>Opt In</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Dialog open={showFullPolicy} onOpenChange={setShowFullPolicy}>
|
||||||
|
<DialogContent className="max-w-4xl max-h-[80vh]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Privacy Policy</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<ScrollArea className="mt-4 h-[60vh]">
|
||||||
|
<DialogDescription className="space-y-4">
|
||||||
|
<ReactMarkdown className="prose dark:prose-invert max-w-none">
|
||||||
|
{privacyPolicy}
|
||||||
|
</ReactMarkdown>
|
||||||
|
</DialogDescription>
|
||||||
|
</ScrollArea>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TelemetryOptIn;
|
57
examples/buddybook/src/components/TelemetryPage.tsx
Normal file
57
examples/buddybook/src/components/TelemetryPage.tsx
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
import { privacyPolicy } from '@/lib/privacyPolicy';
|
||||||
|
import ReactMarkdown from 'react-markdown';
|
||||||
|
|
||||||
|
|
||||||
|
const TelemetryPage: React.FC = () => {
|
||||||
|
const [telemetryOptIn, setTelemetryOptIn] = useState<boolean>(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const storedOptIn = localStorage.getItem('telemetryOptIn');
|
||||||
|
if (storedOptIn !== null) {
|
||||||
|
setTelemetryOptIn(storedOptIn === 'true');
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleToggleTelemetry = () => {
|
||||||
|
const newOptIn = !telemetryOptIn;
|
||||||
|
setTelemetryOptIn(newOptIn);
|
||||||
|
localStorage.setItem('telemetryOptIn', newOptIn.toString());
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="w-full max-w-4xl mx-auto">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Telemetry Settings</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-muted-foreground mb-2">
|
||||||
|
We collect telemetry data to improve our services. This data is anonymous and helps us understand how our application is used.
|
||||||
|
</p>
|
||||||
|
<p className="font-semibold mb-2">
|
||||||
|
Current status: {telemetryOptIn ? 'Opted In' : 'Opted Out'}
|
||||||
|
</p>
|
||||||
|
<Button onClick={handleToggleTelemetry}>
|
||||||
|
{telemetryOptIn ? 'Opt Out' : 'Opt In'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold mb-4">Privacy Policy</h3>
|
||||||
|
<ScrollArea className="h-[60vh] border rounded-md p-4">
|
||||||
|
<ReactMarkdown className="prose dark:prose-invert max-w-none">
|
||||||
|
{privacyPolicy}
|
||||||
|
</ReactMarkdown>
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TelemetryPage;
|
57
examples/buddybook/src/components/ui/button.tsx
Normal file
57
examples/buddybook/src/components/ui/button.tsx
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default:
|
||||||
|
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
||||||
|
outline:
|
||||||
|
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
||||||
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-9 px-4 py-2",
|
||||||
|
sm: "h-8 rounded-md px-3 text-xs",
|
||||||
|
lg: "h-10 rounded-md px-8",
|
||||||
|
icon: "h-9 w-9",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export interface ButtonProps
|
||||||
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
|
VariantProps<typeof buttonVariants> {
|
||||||
|
asChild?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||||
|
const Comp = asChild ? Slot : "button"
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Button.displayName = "Button"
|
||||||
|
|
||||||
|
export { Button, buttonVariants }
|
76
examples/buddybook/src/components/ui/card.tsx
Normal file
76
examples/buddybook/src/components/ui/card.tsx
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Card = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"rounded-xl border bg-card text-card-foreground shadow",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
Card.displayName = "Card"
|
||||||
|
|
||||||
|
const CardHeader = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CardHeader.displayName = "CardHeader"
|
||||||
|
|
||||||
|
const CardTitle = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLHeadingElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<h3
|
||||||
|
ref={ref}
|
||||||
|
className={cn("font-semibold leading-none tracking-tight", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CardTitle.displayName = "CardTitle"
|
||||||
|
|
||||||
|
const CardDescription = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLParagraphElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<p
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CardDescription.displayName = "CardDescription"
|
||||||
|
|
||||||
|
const CardContent = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||||
|
))
|
||||||
|
CardContent.displayName = "CardContent"
|
||||||
|
|
||||||
|
const CardFooter = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn("flex items-center p-6 pt-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CardFooter.displayName = "CardFooter"
|
||||||
|
|
||||||
|
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
120
examples/buddybook/src/components/ui/dialog.tsx
Normal file
120
examples/buddybook/src/components/ui/dialog.tsx
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||||
|
import { Cross2Icon } from "@radix-ui/react-icons"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Dialog = DialogPrimitive.Root
|
||||||
|
|
||||||
|
const DialogTrigger = DialogPrimitive.Trigger
|
||||||
|
|
||||||
|
const DialogPortal = DialogPrimitive.Portal
|
||||||
|
|
||||||
|
const DialogClose = DialogPrimitive.Close
|
||||||
|
|
||||||
|
const DialogOverlay = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DialogPrimitive.Overlay
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||||
|
|
||||||
|
const DialogContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<DialogPortal>
|
||||||
|
<DialogOverlay />
|
||||||
|
<DialogPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||||
|
<Cross2Icon className="h-4 w-4" />
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</DialogPrimitive.Close>
|
||||||
|
</DialogPrimitive.Content>
|
||||||
|
</DialogPortal>
|
||||||
|
))
|
||||||
|
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||||
|
|
||||||
|
const DialogHeader = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
DialogHeader.displayName = "DialogHeader"
|
||||||
|
|
||||||
|
const DialogFooter = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
DialogFooter.displayName = "DialogFooter"
|
||||||
|
|
||||||
|
const DialogTitle = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DialogPrimitive.Title
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"text-lg font-semibold leading-none tracking-tight",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||||
|
|
||||||
|
const DialogDescription = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DialogPrimitive.Description
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||||
|
|
||||||
|
export {
|
||||||
|
Dialog,
|
||||||
|
DialogPortal,
|
||||||
|
DialogOverlay,
|
||||||
|
DialogTrigger,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogFooter,
|
||||||
|
DialogTitle,
|
||||||
|
DialogDescription,
|
||||||
|
}
|
25
examples/buddybook/src/components/ui/input.tsx
Normal file
25
examples/buddybook/src/components/ui/input.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
export interface InputProps
|
||||||
|
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||||
|
|
||||||
|
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
|
({ className, type, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
className={cn(
|
||||||
|
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Input.displayName = "Input"
|
||||||
|
|
||||||
|
export { Input }
|
24
examples/buddybook/src/components/ui/label.tsx
Normal file
24
examples/buddybook/src/components/ui/label.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const labelVariants = cva(
|
||||||
|
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||||
|
)
|
||||||
|
|
||||||
|
const Label = React.forwardRef<
|
||||||
|
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||||
|
VariantProps<typeof labelVariants>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<LabelPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(labelVariants(), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
Label.displayName = LabelPrimitive.Root.displayName
|
||||||
|
|
||||||
|
export { Label }
|
46
examples/buddybook/src/components/ui/scroll-area.tsx
Normal file
46
examples/buddybook/src/components/ui/scroll-area.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const ScrollArea = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<ScrollAreaPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn("relative overflow-hidden", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
||||||
|
{children}
|
||||||
|
</ScrollAreaPrimitive.Viewport>
|
||||||
|
<ScrollBar />
|
||||||
|
<ScrollAreaPrimitive.Corner />
|
||||||
|
</ScrollAreaPrimitive.Root>
|
||||||
|
))
|
||||||
|
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
|
||||||
|
|
||||||
|
const ScrollBar = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||||
|
>(({ className, orientation = "vertical", ...props }, ref) => (
|
||||||
|
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||||
|
ref={ref}
|
||||||
|
orientation={orientation}
|
||||||
|
className={cn(
|
||||||
|
"flex touch-none select-none transition-colors",
|
||||||
|
orientation === "vertical" &&
|
||||||
|
"h-full w-2.5 border-l border-l-transparent p-[1px]",
|
||||||
|
orientation === "horizontal" &&
|
||||||
|
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
||||||
|
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||||
|
))
|
||||||
|
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
|
||||||
|
|
||||||
|
export { ScrollArea, ScrollBar }
|
24
examples/buddybook/src/components/ui/textarea.tsx
Normal file
24
examples/buddybook/src/components/ui/textarea.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
export interface TextareaProps
|
||||||
|
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
||||||
|
|
||||||
|
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||||
|
({ className, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
className={cn(
|
||||||
|
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Textarea.displayName = "Textarea"
|
||||||
|
|
||||||
|
export { Textarea }
|
10
examples/buddybook/src/env.ts
Normal file
10
examples/buddybook/src/env.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { createEnv } from "@t3-oss/env-core";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const env = createEnv({
|
||||||
|
clientPrefix: "VITE_",
|
||||||
|
client: {
|
||||||
|
VITE_WALLETCONNECT_PROJECT_ID: z.string().min(1),
|
||||||
|
},
|
||||||
|
runtimeEnv: import.meta.env,
|
||||||
|
});
|
66
examples/buddybook/src/index.css
Normal file
66
examples/buddybook/src/index.css
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
@layer base {
|
||||||
|
:root {
|
||||||
|
--background: 0 0% 100%;
|
||||||
|
--foreground: 0 0% 3.9%;
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 0 0% 3.9%;
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 0 0% 3.9%;
|
||||||
|
--primary: 0 0% 9%;
|
||||||
|
--primary-foreground: 0 0% 98%;
|
||||||
|
--secondary: 0 0% 96.1%;
|
||||||
|
--secondary-foreground: 0 0% 9%;
|
||||||
|
--muted: 0 0% 96.1%;
|
||||||
|
--muted-foreground: 0 0% 45.1%;
|
||||||
|
--accent: 0 0% 96.1%;
|
||||||
|
--accent-foreground: 0 0% 9%;
|
||||||
|
--destructive: 0 84.2% 60.2%;
|
||||||
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
--border: 0 0% 89.8%;
|
||||||
|
--input: 0 0% 89.8%;
|
||||||
|
--ring: 0 0% 3.9%;
|
||||||
|
--chart-1: 12 76% 61%;
|
||||||
|
--chart-2: 173 58% 39%;
|
||||||
|
--chart-3: 197 37% 24%;
|
||||||
|
--chart-4: 43 74% 66%;
|
||||||
|
--chart-5: 27 87% 67%;
|
||||||
|
--radius: 0.5rem
|
||||||
|
}
|
||||||
|
.dark {
|
||||||
|
--background: 0 0% 3.9%;
|
||||||
|
--foreground: 0 0% 98%;
|
||||||
|
--card: 0 0% 3.9%;
|
||||||
|
--card-foreground: 0 0% 98%;
|
||||||
|
--popover: 0 0% 3.9%;
|
||||||
|
--popover-foreground: 0 0% 98%;
|
||||||
|
--primary: 0 0% 98%;
|
||||||
|
--primary-foreground: 0 0% 9%;
|
||||||
|
--secondary: 0 0% 14.9%;
|
||||||
|
--secondary-foreground: 0 0% 98%;
|
||||||
|
--muted: 0 0% 14.9%;
|
||||||
|
--muted-foreground: 0 0% 63.9%;
|
||||||
|
--accent: 0 0% 14.9%;
|
||||||
|
--accent-foreground: 0 0% 98%;
|
||||||
|
--destructive: 0 62.8% 30.6%;
|
||||||
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
--border: 0 0% 14.9%;
|
||||||
|
--input: 0 0% 14.9%;
|
||||||
|
--ring: 0 0% 83.1%;
|
||||||
|
--chart-1: 220 70% 50%;
|
||||||
|
--chart-2: 160 60% 45%;
|
||||||
|
--chart-3: 30 80% 55%;
|
||||||
|
--chart-4: 280 65% 60%;
|
||||||
|
--chart-5: 340 75% 55%
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
84
examples/buddybook/src/lib/privacyPolicy.ts
Normal file
84
examples/buddybook/src/lib/privacyPolicy.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
export const privacyPolicy = `
|
||||||
|
# Waku - Dogfooding Website
|
||||||
|
|
||||||
|
*Last updated: 22 August 2024*
|
||||||
|
|
||||||
|
This Privacy Policy is intended to inform users of our approach to privacy in respect of this website ("Website"). In this regard, if you are visiting or interacting with our Website, this Privacy Policy applies to you.
|
||||||
|
|
||||||
|
This Website has been set-up by us for the purposes of gathering telemetry data from users of the Website about the reliability protocols utilised by Waku (such activity and related activity, being referred to as "Dogfooding").
|
||||||
|
|
||||||
|
Waku is a family of robust, censorship-resistant communication protocols designed to enable privacy-focused messaging for web3 apps. You can read more about Waku here: https://waku.org/.
|
||||||
|
|
||||||
|
## Who we are
|
||||||
|
|
||||||
|
For the purposes of this Privacy Policy and the collection and processing of personal data as a controller, the relevant entity is the Logos Collective Association, which has its registered office in Zug and its legal domicile address at
|
||||||
|
|
||||||
|
Logos Collective Association
|
||||||
|
c/o PST Consulting GmbH
|
||||||
|
Baarerstrasse 10
|
||||||
|
6300 Zug
|
||||||
|
Switzerland
|
||||||
|
|
||||||
|
Whenever we refer to "Logos", "we" or other similar references, we are referring to the Logos Collective Association.
|
||||||
|
|
||||||
|
## We limit the collection and processing of personal data from your use of the Website and for "Dogfooding" purposes
|
||||||
|
|
||||||
|
We aim to limit the collection and processing of personal data from users of the Website and your participation in Dogfooding. We only collect and process certain personal data for specific purposes and where we have the legal basis to do so under applicable privacy legislation. We will not collect or process any personal data that we don't need and where we do store any personal data, we will only store it for the least amount of time needed for the indicated purpose and in any event no longer than thirty (30) days.
|
||||||
|
|
||||||
|
In this regard, we collect and process the following personal data from your use of the Website and your participation in Dogfooding:
|
||||||
|
|
||||||
|
**IP address**: As part of such use of the Website, we briefly process your IP address. We however have a legitimate interest in processing such IP addresses to ensure the technical functionality and enhance the security measures of the Website. This IP address is not stored by us over time.
|
||||||
|
|
||||||
|
**Telemetry data**: We collect certain technical information (also referred to as telemetry data) to primarily determine the reliability (including, the performance and usage patterns) of the Waku reliability protocols for light clients. This information is only collected once you have accepted the prompt on the Website to participate in Dogfooding. Once you've accepted this prompt, a Waku node will start operating on your browser and then the technical information will start being collected by us.
|
||||||
|
|
||||||
|
The information collected includes:
|
||||||
|
|
||||||
|
- timestamps of receiving and sending messages;
|
||||||
|
- size of message packets;
|
||||||
|
- content topics/pubsub topics of the messages;
|
||||||
|
- code logs of errors and warnings; and
|
||||||
|
- a randomly generated temporary peer ID that represents your Waku node.
|
||||||
|
|
||||||
|
The temporary peer ID is a seeded identifier used for the duration of your participation in Dogfooding and allows us to correlate the obtained technical information with a particular session and with the additional information collected, could be potentially considered personal data. We process such data based on our legitimate interest to improve our software and the user's experience in respect of Waku.
|
||||||
|
|
||||||
|
If you do not agree to this data collection and processing, please do not interact with the Website or proceed with your participation in Dogfooding.
|
||||||
|
|
||||||
|
## Personal data sharing with third party service providers
|
||||||
|
|
||||||
|
We may share personal data with third party service providers in order for us to fulfil the above purposes. Such third party service providers act as data processors on our behalf and are only permitted to process personal data in accordance with our instructions and for the purposes specified above.
|
||||||
|
|
||||||
|
## Security measures we take in respect of the Website
|
||||||
|
|
||||||
|
As a general approach, we take data security seriously and we have implemented a variety of security measures on the Website that are reasonably designed to maintain the safety of your personal data when you submit such information to us.
|
||||||
|
|
||||||
|
In addition to the security measures of the Website, personal data is also protected by Waku (protocol) itself. You can read more about the security features implemented by the Waku protocol here.
|
||||||
|
|
||||||
|
## Exporting data outside the European Union and Switzerland
|
||||||
|
|
||||||
|
We are obliged to protect the privacy of personal data that you may have submitted in the unlikely event that we export your personal data to places outside the European Union or Switzerland. This means that personal data will only be processed in countries or by parties that provide an adequate level of protection as deemed by Switzerland or the European Commission. Otherwise, we will use other forms of protections, such as specific forms of contractual clauses to ensure such personal data is provided the same protection as required in Switzerland or Europe. In any event, the transmission of personal data outside the European Union and Switzerland will always occur in conformity with applicable privacy legislation.
|
||||||
|
|
||||||
|
## Your choices and rights
|
||||||
|
|
||||||
|
As explained in this Privacy Policy, we limit our collection and processing of your personal data wherever possible. Nonetheless, you still have certain choices and rights in respect of the personal data which we do collect and process. As laid out in relevant privacy legislation, you have the right to:
|
||||||
|
|
||||||
|
- Ask us to correct or update your personal data (where reasonably possible);
|
||||||
|
- Ask us to remove your personal data from our systems;
|
||||||
|
- Ask us for a copy of your personal data, which may also be transferred to another data controller at your request;
|
||||||
|
- Withdraw your consent to process your personal data (only if consent was asked for a processing activity), which only affects processing activities that are based on your consent and doesn't affect the validity of such processing activities before you have withdrawn your consent;
|
||||||
|
- Object to the processing of your personal data; and
|
||||||
|
- File a complaint with the Federal Data Protection and Information Commissioner (FDPIC), if you believe that your personal data has been processed unlawfully.
|
||||||
|
|
||||||
|
## Third party links
|
||||||
|
|
||||||
|
On this Website, you may come across links to third party websites. These third party sites have separate and independent privacy policies. We therefore have no responsibility or liability for the content and activities of these third party websites.
|
||||||
|
|
||||||
|
## This Privacy Policy might change
|
||||||
|
|
||||||
|
We may modify or replace any part of this Privacy Policy at any time and without notice. Please check the Website periodically for any changes. The new Privacy Policy will be effective immediately upon its posting on our Website.
|
||||||
|
|
||||||
|
## Contact information
|
||||||
|
|
||||||
|
To the extent that you have any questions about the Privacy Policy, please contact us at legal@free.technology.
|
||||||
|
|
||||||
|
This document is licensed under CC-BY-SA.
|
||||||
|
`;
|
6
examples/buddybook/src/lib/utils.ts
Normal file
6
examples/buddybook/src/lib/utils.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { clsx, type ClassValue } from "clsx"
|
||||||
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
101
examples/buddybook/src/lib/waku.ts
Normal file
101
examples/buddybook/src/lib/waku.ts
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import { createEncoder, createDecoder, type LightNode, type CreateWakuNodeOptions } from "@waku/sdk";
|
||||||
|
import protobuf from 'protobufjs';
|
||||||
|
|
||||||
|
export const WAKU_NODE_OPTIONS: CreateWakuNodeOptions = { defaultBootstrap: true, nodeToUse: {
|
||||||
|
store: "/dns4/store-02.ac-cn-hongkong-c.status.staging.status.im/tcp/443/wss/p2p/16Uiu2HAmU7xtcwytXpGpeDrfyhJkiFvTkQbLB9upL5MXPLGceG9K"
|
||||||
|
} };
|
||||||
|
|
||||||
|
|
||||||
|
export type Signature = {
|
||||||
|
address: `0x${string}`;
|
||||||
|
signature: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BlockPayload = {
|
||||||
|
chainUUID: string;
|
||||||
|
blockUUID: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
signedMessage: string;
|
||||||
|
timestamp: number;
|
||||||
|
signatures: Signature[];
|
||||||
|
parentBlockUUID: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentTopic = "/buddychain-dogfood/1/chain/proto";
|
||||||
|
|
||||||
|
export const encoder = createEncoder({
|
||||||
|
contentTopic: contentTopic,
|
||||||
|
ephemeral: false
|
||||||
|
});
|
||||||
|
|
||||||
|
export const decoder = createDecoder(contentTopic);
|
||||||
|
|
||||||
|
export const block = new protobuf.Type("block")
|
||||||
|
.add(new protobuf.Field("chainUUID", 1, "string"))
|
||||||
|
.add(new protobuf.Field("blockUUID", 2, "string"))
|
||||||
|
.add(new protobuf.Field("title", 3, "string"))
|
||||||
|
.add(new protobuf.Field("description", 4, "string"))
|
||||||
|
.add(new protobuf.Field("signedMessage", 5, "string"))
|
||||||
|
.add(new protobuf.Field("timestamp", 6, "uint64"))
|
||||||
|
.add(new protobuf.Field("signatures", 7, "string", "repeated"))
|
||||||
|
.add(new protobuf.Field("parentBlockUUID", 8, "string"));
|
||||||
|
|
||||||
|
export function createMessage({
|
||||||
|
chainUUID,
|
||||||
|
blockUUID,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
signedMessage,
|
||||||
|
timestamp,
|
||||||
|
signatures,
|
||||||
|
parentBlockUUID
|
||||||
|
}: BlockPayload) {
|
||||||
|
const protoMessage = block.create({
|
||||||
|
chainUUID,
|
||||||
|
blockUUID,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
signedMessage,
|
||||||
|
timestamp,
|
||||||
|
signatures: signatures.map(s => JSON.stringify(s)),
|
||||||
|
parentBlockUUID
|
||||||
|
});
|
||||||
|
const payload = block.encode(protoMessage).finish();
|
||||||
|
return { payload: payload };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getMessagesFromStore(node: LightNode) {
|
||||||
|
console.time("getMessagesFromStore")
|
||||||
|
const messages: BlockPayload[] = [];
|
||||||
|
await node.store.queryWithOrderedCallback([decoder], async (message) => {
|
||||||
|
console.log(message)
|
||||||
|
if (!message.payload) return;
|
||||||
|
const blockPayload = block.decode(message.payload) as unknown as BlockPayload;
|
||||||
|
blockPayload.signatures = blockPayload.signatures.map(s => JSON.parse(s as unknown as string) as Signature);
|
||||||
|
messages.push(blockPayload);
|
||||||
|
})
|
||||||
|
console.timeEnd("getMessagesFromStore")
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function subscribeToFilter(node: LightNode, callback: (message: BlockPayload) => void) {
|
||||||
|
const {error, subscription, results} = await node.filter.subscribe([decoder], (message) => {
|
||||||
|
console.log('message received from filter', message)
|
||||||
|
if (message.payload) {
|
||||||
|
const blockPayload = block.decode(message.payload) as unknown as BlockPayload;
|
||||||
|
blockPayload.signatures = blockPayload.signatures.map(s => JSON.parse(s as unknown as string) as Signature);
|
||||||
|
callback(blockPayload);
|
||||||
|
}
|
||||||
|
}, {forceUseAllPeers: true});
|
||||||
|
|
||||||
|
console.log("results", results)
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.log("Error subscribing to filter", error)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!subscription || error || results.successes.length === 0 ||results.failures.length >0) {
|
||||||
|
throw new Error("Failed to subscribe to filter")
|
||||||
|
}
|
||||||
|
}
|
15
examples/buddybook/src/lib/walletConnect.ts
Normal file
15
examples/buddybook/src/lib/walletConnect.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { mainnet } from 'wagmi/chains'
|
||||||
|
import { createConfig, http } from 'wagmi'
|
||||||
|
import { getDefaultConfig } from 'connectkit'
|
||||||
|
import { env } from '@/env'
|
||||||
|
|
||||||
|
export const config = createConfig(
|
||||||
|
getDefaultConfig({
|
||||||
|
appName: 'BuddyBook',
|
||||||
|
walletConnectProjectId: env.VITE_WALLETCONNECT_PROJECT_ID,
|
||||||
|
chains: [mainnet],
|
||||||
|
transports: {
|
||||||
|
[mainnet.id]: http(),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
30
examples/buddybook/src/main.tsx
Normal file
30
examples/buddybook/src/main.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { StrictMode } from 'react'
|
||||||
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import { WagmiProvider } from 'wagmi'
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||||
|
import { ConnectKitProvider} from 'connectkit'
|
||||||
|
import { BrowserRouter as Router } from 'react-router-dom'
|
||||||
|
import App from './App.tsx'
|
||||||
|
import './index.css'
|
||||||
|
import { LightNodeProvider } from "@waku/react";
|
||||||
|
import { config } from './lib/walletConnect.ts'
|
||||||
|
import { WAKU_NODE_OPTIONS } from './lib/waku.ts'
|
||||||
|
|
||||||
|
|
||||||
|
const queryClient = new QueryClient()
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<WagmiProvider config={config}>
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<ConnectKitProvider>
|
||||||
|
<LightNodeProvider options={WAKU_NODE_OPTIONS}>
|
||||||
|
<Router>
|
||||||
|
<App />
|
||||||
|
</Router>
|
||||||
|
</LightNodeProvider>
|
||||||
|
</ConnectKitProvider>
|
||||||
|
</QueryClientProvider>
|
||||||
|
</WagmiProvider>
|
||||||
|
</StrictMode>,
|
||||||
|
)
|
1
examples/buddybook/src/vite-env.d.ts
vendored
Normal file
1
examples/buddybook/src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
63
examples/buddybook/tailwind.config.js
Normal file
63
examples/buddybook/tailwind.config.js
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
darkMode: ["class"],
|
||||||
|
content: [
|
||||||
|
"./index.html",
|
||||||
|
"./src/**/*.{js,ts,jsx,tsx}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
borderRadius: {
|
||||||
|
lg: 'var(--radius)',
|
||||||
|
md: 'calc(var(--radius) - 2px)',
|
||||||
|
sm: 'calc(var(--radius) - 4px)'
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
background: 'hsl(var(--background))',
|
||||||
|
foreground: 'hsl(var(--foreground))',
|
||||||
|
card: {
|
||||||
|
DEFAULT: 'hsl(var(--card))',
|
||||||
|
foreground: 'hsl(var(--card-foreground))'
|
||||||
|
},
|
||||||
|
popover: {
|
||||||
|
DEFAULT: 'hsl(var(--popover))',
|
||||||
|
foreground: 'hsl(var(--popover-foreground))'
|
||||||
|
},
|
||||||
|
primary: {
|
||||||
|
DEFAULT: 'hsl(var(--primary))',
|
||||||
|
foreground: 'hsl(var(--primary-foreground))'
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
DEFAULT: 'hsl(var(--secondary))',
|
||||||
|
foreground: 'hsl(var(--secondary-foreground))'
|
||||||
|
},
|
||||||
|
muted: {
|
||||||
|
DEFAULT: 'hsl(var(--muted))',
|
||||||
|
foreground: 'hsl(var(--muted-foreground))'
|
||||||
|
},
|
||||||
|
accent: {
|
||||||
|
DEFAULT: 'hsl(var(--accent))',
|
||||||
|
foreground: 'hsl(var(--accent-foreground))'
|
||||||
|
},
|
||||||
|
destructive: {
|
||||||
|
DEFAULT: 'hsl(var(--destructive))',
|
||||||
|
foreground: 'hsl(var(--destructive-foreground))'
|
||||||
|
},
|
||||||
|
border: 'hsl(var(--border))',
|
||||||
|
input: 'hsl(var(--input))',
|
||||||
|
ring: 'hsl(var(--ring))',
|
||||||
|
chart: {
|
||||||
|
'1': 'hsl(var(--chart-1))',
|
||||||
|
'2': 'hsl(var(--chart-2))',
|
||||||
|
'3': 'hsl(var(--chart-3))',
|
||||||
|
'4': 'hsl(var(--chart-4))',
|
||||||
|
'5': 'hsl(var(--chart-5))'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
require('@tailwindcss/typography'),
|
||||||
|
require("tailwindcss-animate")
|
||||||
|
],
|
||||||
|
}
|
8
examples/buddybook/tasks.md
Normal file
8
examples/buddybook/tasks.md
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
- [ ] waku connections on header should have green/yellow/red color indicator
|
||||||
|
- [ ] clicking on the indicator should show a list of peers
|
||||||
|
- [ ] chains can't be signed twice by an address
|
||||||
|
- [ ] generate waku peer id using the wallet address
|
||||||
|
- [ ] telemetry
|
||||||
|
- [ x ] disclaimer
|
||||||
|
- [ ] functionality
|
||||||
|
- [ ] landing page
|
33
examples/buddybook/tsconfig.app.json
Normal file
33
examples/buddybook/tsconfig.app.json
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
|
||||||
|
/* Path aliases */
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"./src/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
1
examples/buddybook/tsconfig.app.tsbuildinfo
Normal file
1
examples/buddybook/tsconfig.app.tsbuildinfo
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"root":["./src/app.tsx","./src/env.ts","./src/main.tsx","./src/vite-env.d.ts","./src/components/header.tsx","./src/components/chain/creation/chaincreationform.tsx","./src/components/chain/preview/chainpreview.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/textarea.tsx","./src/lib/utils.ts"],"errors":true,"version":"5.6.3"}
|
13
examples/buddybook/tsconfig.json
Normal file
13
examples/buddybook/tsconfig.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
],
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
22
examples/buddybook/tsconfig.node.json
Normal file
22
examples/buddybook/tsconfig.node.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
1
examples/buddybook/tsconfig.node.tsbuildinfo
Normal file
1
examples/buddybook/tsconfig.node.tsbuildinfo
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"root":["./vite.config.ts"],"version":"5.6.3"}
|
12
examples/buddybook/vite.config.ts
Normal file
12
examples/buddybook/vite.config.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import path from "path"
|
||||||
|
import react from "@vitejs/plugin-react"
|
||||||
|
import { defineConfig } from "vite"
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@": path.resolve(__dirname, "./src"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
34
examples/buddybook/webpack.config.js
Normal file
34
examples/buddybook/webpack.config.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
entry: "./src/main.tsx",
|
||||||
|
output: {
|
||||||
|
path: path.resolve(__dirname, "build"),
|
||||||
|
filename: "index.js",
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.ts', '.tsx', '.js', '.jsx'],
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.tsx?$/,
|
||||||
|
use: 'ts-loader',
|
||||||
|
exclude: /node_modules/,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
experiments: {
|
||||||
|
asyncWebAssembly: true,
|
||||||
|
},
|
||||||
|
mode: "development",
|
||||||
|
plugins: [
|
||||||
|
new CopyWebpackPlugin({
|
||||||
|
patterns: [
|
||||||
|
{ from: "index.html" },
|
||||||
|
{ from: "public", to: "public" }
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
@ -10,7 +10,7 @@
|
|||||||
"@types/node": "^16.18.111",
|
"@types/node": "^16.18.111",
|
||||||
"@types/react": "^18.3.9",
|
"@types/react": "^18.3.9",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
"@waku/sdk": "^0.0.29-338250a.0",
|
"@waku/sdk": "0.0.29-de10ff4.0",
|
||||||
"protobufjs": "^7.4.0",
|
"protobufjs": "^7.4.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
|
@ -99,7 +99,7 @@ function App() {
|
|||||||
<h1 className="text-4xl font-bold text-gray-800 mb-8">Waku Message Monitor</h1>
|
<h1 className="text-4xl font-bold text-gray-800 mb-8">Waku Message Monitor</h1>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<MessageList title="Sent Messages" messages={sentMessages} showSequence={true} />
|
<MessageList title="Sent Messages" messages={sentMessages.reverse()} showSequence={true} />
|
||||||
<MessageList title="Received Messages" messages={receivedMessages} showSequence={false} />
|
<MessageList title="Received Messages" messages={receivedMessages} showSequence={false} />
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
|
@ -30,10 +30,14 @@ const MessageList: React.FC<MessageListProps> = ({ title, messages, showSequence
|
|||||||
if (!acc[message.sequenceId]) {
|
if (!acc[message.sequenceId]) {
|
||||||
acc[message.sequenceId] = [];
|
acc[message.sequenceId] = [];
|
||||||
}
|
}
|
||||||
acc[message.sequenceId].unshift(message); // Use unshift instead of push
|
acc[message.sequenceId].push(message);
|
||||||
return acc;
|
return acc;
|
||||||
}, {} as Record<number, Message[]>)
|
}, {} as Record<number, Message[]>)
|
||||||
: { 0: messages.slice().reverse() }; // Reverse the messages array
|
: { 0: messages };
|
||||||
|
|
||||||
|
// Sort sequences in descending order
|
||||||
|
const sortedSequences = Object.entries(groupedMessages)
|
||||||
|
.sort((a, b) => Number(b[0]) - Number(a[0]));
|
||||||
|
|
||||||
if (messages.length === 0) {
|
if (messages.length === 0) {
|
||||||
return (
|
return (
|
||||||
@ -50,14 +54,16 @@ const MessageList: React.FC<MessageListProps> = ({ title, messages, showSequence
|
|||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<h2 className="text-xl font-semibold mb-4 text-gray-700">{title}</h2>
|
<h2 className="text-xl font-semibold mb-4 text-gray-700">{title}</h2>
|
||||||
<div className="bg-gray-50 rounded-lg p-4 h-96 overflow-y-auto border border-gray-200">
|
<div className="bg-gray-50 rounded-lg p-4 h-96 overflow-y-auto border border-gray-200">
|
||||||
{Object.entries(groupedMessages).map(([sequenceId, sequenceMessages]) => (
|
{sortedSequences.map(([sequenceId, sequenceMessages]) => (
|
||||||
<div key={sequenceId} className={`mb-4 p-2 rounded-lg ${showSequence ? getSequenceColor(Number(sequenceId)) : ''}`}>
|
<div key={sequenceId} className={`mb-4 p-2 rounded-lg ${showSequence ? getSequenceColor(Number(sequenceId)) : ''}`}>
|
||||||
{showSequence && <div className="text-xs font-semibold mb-2">Sequence {sequenceId}</div>}
|
{showSequence && <div className="text-xs font-semibold mb-2">Sequence {sequenceId}</div>}
|
||||||
{sequenceMessages.map((message: Message, index: number) => (
|
{sequenceMessages
|
||||||
|
.sort((a, b) => b.timestamp - a.timestamp)
|
||||||
|
.map((message: Message, index: number) => (
|
||||||
<div key={index} className="text-sm mb-2 font-mono bg-white p-2 rounded shadow-sm">
|
<div key={index} className="text-sm mb-2 font-mono bg-white p-2 rounded shadow-sm">
|
||||||
{shortenMessage(message.content)}
|
{shortenMessage(message.content)}
|
||||||
</div>
|
</div>
|
||||||
)).reverse()}
|
))}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
@ -92,11 +92,13 @@ export async function startLightPushSequence(
|
|||||||
if (success) {
|
if (success) {
|
||||||
const messageText = JSON.stringify({ content: `${reportingHash} - ${sequenceIndex + 1} of ${sequenceTotal}`, success: true });
|
const messageText = JSON.stringify({ content: `${reportingHash} - ${sequenceIndex + 1} of ${sequenceTotal}`, success: true });
|
||||||
onMessageSent(messageText, true);
|
onMessageSent(messageText, true);
|
||||||
sequenceIndex++;
|
|
||||||
} else {
|
} else {
|
||||||
onMessageSent(JSON.stringify({ content: `Failed to send message ${sequenceIndex + 1} of ${sequenceTotal}`, success: false }), false);
|
const messageText = JSON.stringify({ content: `Failed to send message ${sequenceIndex + 1} of ${sequenceTotal}`, success: false });
|
||||||
|
onMessageSent(messageText, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sequenceIndex++;
|
||||||
|
|
||||||
if (sequenceIndex < sequenceTotal) {
|
if (sequenceIndex < sequenceTotal) {
|
||||||
let countdown = period / 1000;
|
let countdown = period / 1000;
|
||||||
const countdownInterval = setInterval(() => {
|
const countdownInterval = setInterval(() => {
|
||||||
@ -113,7 +115,7 @@ export async function startLightPushSequence(
|
|||||||
}, period);
|
}, period);
|
||||||
} else {
|
} else {
|
||||||
console.info("Lightpush sequence completed");
|
console.info("Lightpush sequence completed");
|
||||||
updateSequenceId(sequenceIndex + 1);
|
updateSequenceId(sequenceIndex);
|
||||||
updateCountdown(null);
|
updateCountdown(null);
|
||||||
isRunning.current = false;
|
isRunning.current = false;
|
||||||
// Start a new sequence after a delay
|
// Start a new sequence after a delay
|
||||||
@ -121,11 +123,21 @@ export async function startLightPushSequence(
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error sending message", error);
|
console.error("Error sending message", error);
|
||||||
onMessageSent(JSON.stringify({ content: `Error sending message ${sequenceIndex + 1} of ${sequenceTotal}`, success: false }), false);
|
const messageText = JSON.stringify({ content: `Error sending message ${sequenceIndex + 1} of ${sequenceTotal}`, success: false });
|
||||||
|
onMessageSent(messageText, false);
|
||||||
|
sequenceIndex++;
|
||||||
|
|
||||||
|
if (sequenceIndex < sequenceTotal) {
|
||||||
|
setTimeout(() => sendMessage(), period);
|
||||||
|
} else {
|
||||||
|
console.info("Lightpush sequence completed with errors");
|
||||||
|
updateSequenceId(sequenceIndex);
|
||||||
|
updateCountdown(null);
|
||||||
isRunning.current = false;
|
isRunning.current = false;
|
||||||
// Retry starting a new sequence after a delay
|
// Start a new sequence after a delay
|
||||||
setTimeout(() => startLightPushSequence(waku, telemetryClient, onMessageSent, updateSequenceId, updateCountdown, isRunning, numMessages, period), period);
|
setTimeout(() => startLightPushSequence(waku, telemetryClient, onMessageSent, updateSequenceId, updateCountdown, isRunning, numMessages, period), period);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
sendMessage();
|
sendMessage();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user