mirror of
https://github.com/logos-messaging/examples.waku.org.git
synced 2026-01-02 12:53:08 +00:00
Merge 3c7795e121a0d305040401d5b4e6fdd7231648b5 into fca33640692a0c1cae431585782aa799042751a7
This commit is contained in:
commit
eac385859f
35
examples/flush-notes/.gitignore
vendored
Normal file
35
examples/flush-notes/.gitignore
vendored
Normal file
@ -0,0 +1,35 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
16
examples/flush-notes/README.md
Normal file
16
examples/flush-notes/README.md
Normal file
@ -0,0 +1,16 @@
|
||||
## Waku dependencies
|
||||
- @waku/interfaces
|
||||
- @waku/message-encryption
|
||||
- @waku/sdk
|
||||
- @waku/utils
|
||||
|
||||
## Description
|
||||
Exchange encrypted or plain notes by link.
|
||||
This example shows how symmetric encryption can be used to encrypt only part of Waku message.
|
||||
|
||||
## How to run
|
||||
```bash
|
||||
npm start
|
||||
# or
|
||||
yarn start
|
||||
```
|
||||
14
examples/flush-notes/next.config.js
Normal file
14
examples/flush-notes/next.config.js
Normal file
@ -0,0 +1,14 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
output: "export",
|
||||
async rewrites() {
|
||||
return [
|
||||
{
|
||||
source: "/view/:path*",
|
||||
destination: "/view",
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
||||
6902
examples/flush-notes/package-lock.json
generated
Normal file
6902
examples/flush-notes/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
30
examples/flush-notes/package.json
Normal file
30
examples/flush-notes/package.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "flush-notes",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "next build",
|
||||
"start": "next dev",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@waku/interfaces": "0.0.21-7eb3375.0",
|
||||
"@waku/message-encryption": "0.0.24-7eb3375.0",
|
||||
"@waku/sdk": "0.0.22-7eb3375.0",
|
||||
"@waku/utils": "0.0.14-7eb3375.0",
|
||||
"next": "14.0.2",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"react-markdown": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"autoprefixer": "^10.0.1",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.0.2",
|
||||
"postcss": "^8",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
5
examples/flush-notes/postcss.config.js
Normal file
5
examples/flush-notes/postcss.config.js
Normal file
@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
BIN
examples/flush-notes/src/app/favicon.ico
Normal file
BIN
examples/flush-notes/src/app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
39
examples/flush-notes/src/app/globals.css
Normal file
39
examples/flush-notes/src/app/globals.css
Normal file
@ -0,0 +1,39 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.loading-block {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.note-info {
|
||||
font-size: 0.8rem;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.create-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.to-encrypt {
|
||||
font-size: 1rem;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.save-note {
|
||||
width: 100px;
|
||||
height: 50px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.note-value {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
min-height: 500px;
|
||||
}
|
||||
23
examples/flush-notes/src/app/layout.tsx
Normal file
23
examples/flush-notes/src/app/layout.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { Inter } from "next/font/google";
|
||||
import { WakuProvider } from "@/components/WakuProvider";
|
||||
import "./globals.css";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<title>Share notes</title>
|
||||
<body className={inter.className}>
|
||||
<WakuProvider>{children}</WakuProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
57
examples/flush-notes/src/app/page.tsx
Normal file
57
examples/flush-notes/src/app/page.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { notes } from "@/services/notes";
|
||||
|
||||
export default function Create() {
|
||||
const router = useRouter();
|
||||
const { note, onNoteChange } = useEditNote();
|
||||
|
||||
const onSave = async () => {
|
||||
if (!note) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { id, key } = await notes.createNote(note);
|
||||
const passwordParam = `?key=${key}`;
|
||||
|
||||
router.push(`/view/${id}${passwordParam}`);
|
||||
} catch (error) {
|
||||
console.error("Failed to create a note:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p className="note-info">
|
||||
Your record will be stored for couple of days. Markdown is supported.
|
||||
</p>
|
||||
<div className="create-header">
|
||||
<div></div>
|
||||
<button onClick={onSave} className="save-note">
|
||||
Save note
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
className="note-value"
|
||||
value={note}
|
||||
onChange={onNoteChange}
|
||||
></textarea>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const useEditNote = () => {
|
||||
const [state, setState] = React.useState<string>("");
|
||||
|
||||
const onNoteChange = (event: React.FormEvent<HTMLTextAreaElement>) => {
|
||||
setState(event?.currentTarget?.value);
|
||||
};
|
||||
|
||||
return {
|
||||
note: state,
|
||||
onNoteChange,
|
||||
};
|
||||
};
|
||||
31
examples/flush-notes/src/app/view/page.tsx
Normal file
31
examples/flush-notes/src/app/view/page.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import Markdown from "react-markdown";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useNoteURL } from "@/hooks/useNoteURL";
|
||||
import { notes } from "@/services/notes";
|
||||
import { Loading } from "@/components/Loading";
|
||||
|
||||
const View = () => {
|
||||
const router = useRouter();
|
||||
const { id, key } = useNoteURL();
|
||||
const [note, setNote] = React.useState<string>("");
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!id) {
|
||||
router.replace("/404");
|
||||
return;
|
||||
}
|
||||
|
||||
notes.readNote(id, key).then((note) => setNote(note || ""));
|
||||
}, [id, key, setNote]);
|
||||
|
||||
if (!note) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return <Markdown>{note}</Markdown>;
|
||||
};
|
||||
|
||||
export default View;
|
||||
7
examples/flush-notes/src/components/Loading.tsx
Normal file
7
examples/flush-notes/src/components/Loading.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
export const Loading = () => {
|
||||
return (
|
||||
<div className="loading-block">
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
65
examples/flush-notes/src/components/WakuProvider.tsx
Normal file
65
examples/flush-notes/src/components/WakuProvider.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { waku, Waku, WakuEvents } from "@/services/waku";
|
||||
import { WakuStatus } from "@/const";
|
||||
import { Loading } from "@/components/Loading";
|
||||
|
||||
type WakuContextProps = {
|
||||
status: WakuStatus;
|
||||
waku?: Waku;
|
||||
};
|
||||
|
||||
const WakuContext = React.createContext<WakuContextProps>({
|
||||
status: WakuStatus.Initializing,
|
||||
waku: undefined,
|
||||
});
|
||||
|
||||
type WakuProviderProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const useWaku = () => {
|
||||
const { status, waku } = React.useContext(WakuContext);
|
||||
|
||||
return { status, waku };
|
||||
};
|
||||
|
||||
export const WakuProvider = (props: WakuProviderProps) => {
|
||||
const wakuRef = React.useRef<Waku>();
|
||||
const [status, setStatus] = React.useState<WakuStatus>(
|
||||
WakuStatus.Initializing
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (wakuRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const statusListener = (event: CustomEvent) => {
|
||||
setStatus(event.detail);
|
||||
};
|
||||
waku.addEventListener(WakuEvents.Status, statusListener);
|
||||
|
||||
waku.init().then(() => {
|
||||
wakuRef.current = waku;
|
||||
});
|
||||
return () => {
|
||||
waku.removeEventListener(WakuEvents.Status, statusListener);
|
||||
};
|
||||
}, [wakuRef, setStatus]);
|
||||
|
||||
if (status === WakuStatus.Failed) {
|
||||
return <>{status}</>;
|
||||
}
|
||||
|
||||
if (status !== WakuStatus.Connected) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<WakuContext.Provider value={{ status, waku: wakuRef.current }}>
|
||||
{props.children}
|
||||
</WakuContext.Provider>
|
||||
);
|
||||
};
|
||||
8
examples/flush-notes/src/const.ts
Normal file
8
examples/flush-notes/src/const.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export const CONTENT_TOPIC = "/flush-notes/1/note/proto";
|
||||
|
||||
export enum WakuStatus {
|
||||
Initializing = "Initializing...",
|
||||
WaitingForPeers = "Waiting for peers...",
|
||||
Connected = "Connected",
|
||||
Failed = "Failed to initialize(see logs)",
|
||||
}
|
||||
15
examples/flush-notes/src/hooks/useNoteURL.ts
Normal file
15
examples/flush-notes/src/hooks/useNoteURL.ts
Normal file
@ -0,0 +1,15 @@
|
||||
"use client";
|
||||
import { usePathname, useSearchParams } from "next/navigation";
|
||||
|
||||
export const useNoteURL = (): { id: string; key: string } => {
|
||||
const pathname = usePathname();
|
||||
const params = useSearchParams();
|
||||
|
||||
const segments = pathname.split("/");
|
||||
const viewIndex = segments.indexOf("view");
|
||||
const key = params.get("key");
|
||||
|
||||
const id = segments[viewIndex + 1];
|
||||
|
||||
return { key, id };
|
||||
};
|
||||
91
examples/flush-notes/src/services/notes.ts
Normal file
91
examples/flush-notes/src/services/notes.ts
Normal file
@ -0,0 +1,91 @@
|
||||
"use client";
|
||||
|
||||
import { waku } from "@/services/waku";
|
||||
import { CONTENT_TOPIC } from "@/const";
|
||||
import {
|
||||
createEncoder,
|
||||
createDecoder,
|
||||
DecodedMessage,
|
||||
generateSymmetricKey,
|
||||
} from "@waku/message-encryption/symmetric";
|
||||
import { Unsubscribe, utf8ToBytes, bytesToUtf8 } from "@waku/sdk";
|
||||
import { bytesToHex, hexToBytes } from "@waku/utils/bytes";
|
||||
|
||||
const UUID_V4_STR_LEN = 8 + 1 + 4 + 1 + 4 + 1 + 4 + 1 + 12; // 8-4-4-4-12 format
|
||||
|
||||
type Note = {
|
||||
id: string;
|
||||
content: string;
|
||||
};
|
||||
|
||||
type NoteResult = {
|
||||
id: string;
|
||||
key: string;
|
||||
};
|
||||
|
||||
export class Notes {
|
||||
private messages: DecodedMessage[] = [];
|
||||
private subscription: undefined | Unsubscribe;
|
||||
|
||||
constructor() {}
|
||||
|
||||
public async createNote(content: string): Promise<NoteResult> {
|
||||
const symKey = generateSymmetricKey();
|
||||
|
||||
const encoder = createEncoder({ contentTopic: CONTENT_TOPIC, symKey });
|
||||
const id = self.crypto.randomUUID();
|
||||
|
||||
if (id.length !== UUID_V4_STR_LEN) {
|
||||
throw "Unexpected uuid length";
|
||||
}
|
||||
|
||||
await waku.send(encoder, {
|
||||
payload: utf8ToBytes(id + content),
|
||||
});
|
||||
|
||||
return {
|
||||
id,
|
||||
key: bytesToHex(symKey),
|
||||
};
|
||||
}
|
||||
|
||||
public async readNote(id: string, key: string): Promise<string | undefined> {
|
||||
await this.initMessages(hexToBytes(key));
|
||||
|
||||
const message = this.messages
|
||||
.map((m) => {
|
||||
try {
|
||||
const str = bytesToUtf8(m.payload);
|
||||
|
||||
const id = str.substring(0, UUID_V4_STR_LEN);
|
||||
const content = str.substring(UUID_V4_STR_LEN);
|
||||
|
||||
return { id, content } as Note;
|
||||
} catch (error) {
|
||||
console.log("Failed to read message:", error);
|
||||
}
|
||||
})
|
||||
.find((v) => {
|
||||
if (v?.id === id) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
return message?.content;
|
||||
}
|
||||
|
||||
private async initMessages(key: Uint8Array) {
|
||||
if (this.subscription) {
|
||||
return;
|
||||
}
|
||||
|
||||
const decoder = createDecoder(CONTENT_TOPIC, key);
|
||||
|
||||
this.messages = await waku.getHistory(decoder);
|
||||
this.subscription = await waku.subscribe(decoder, (message) => {
|
||||
this.messages.push(message);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const notes = new Notes();
|
||||
105
examples/flush-notes/src/services/waku.ts
Normal file
105
examples/flush-notes/src/services/waku.ts
Normal file
@ -0,0 +1,105 @@
|
||||
"use client";
|
||||
|
||||
import { WakuStatus } from "@/const";
|
||||
import {
|
||||
IDecoder,
|
||||
IEncoder,
|
||||
IMessage,
|
||||
LightNode,
|
||||
createLightNode,
|
||||
waitForRemotePeer,
|
||||
IDecodedMessage,
|
||||
} from "@waku/sdk";
|
||||
|
||||
type EventListener = (event: CustomEvent) => void;
|
||||
|
||||
export enum WakuEvents {
|
||||
Status = "status",
|
||||
}
|
||||
|
||||
export class Waku {
|
||||
private node: undefined | LightNode;
|
||||
|
||||
private emitter = new EventTarget();
|
||||
private initialized: boolean = false;
|
||||
private initializing: boolean = false;
|
||||
|
||||
constructor() {}
|
||||
|
||||
public async init(): Promise<void> {
|
||||
if (this.initialized || this.initializing) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.initializing = true;
|
||||
try {
|
||||
this.emitStatusEvent(WakuStatus.Initializing);
|
||||
const node = await createLightNode({ defaultBootstrap: true });
|
||||
await node.start();
|
||||
this.emitStatusEvent(WakuStatus.WaitingForPeers);
|
||||
await waitForRemotePeer(node);
|
||||
this.node = node;
|
||||
this.initialized = true;
|
||||
this.emitStatusEvent(WakuStatus.Connected);
|
||||
} catch (error) {
|
||||
console.error("Failed to initialize Waku node:", error);
|
||||
this.emitStatusEvent(WakuStatus.Failed);
|
||||
}
|
||||
this.initializing = false;
|
||||
}
|
||||
|
||||
public addEventListener(event: WakuEvents, fn: EventListener) {
|
||||
return this.emitter.addEventListener(event, fn as any);
|
||||
}
|
||||
|
||||
public removeEventListener(event: WakuEvents, fn: EventListener) {
|
||||
return this.emitter.removeEventListener(event, fn as any);
|
||||
}
|
||||
|
||||
public send(encoder: IEncoder, message: IMessage) {
|
||||
this.ensureWakuInitialized();
|
||||
return this.node?.lightPush.send(encoder, message);
|
||||
}
|
||||
|
||||
public async getHistory(
|
||||
decoder: IDecoder<IDecodedMessage>
|
||||
): Promise<IDecodedMessage[]> {
|
||||
this.ensureWakuInitialized();
|
||||
|
||||
let messages: IDecodedMessage[] = [];
|
||||
for await (const promises of this.node!.store.queryGenerator([decoder])) {
|
||||
const messagesRaw = await Promise.all(promises);
|
||||
const filteredMessages = messagesRaw.filter(
|
||||
(v): v is IDecodedMessage => !!v
|
||||
);
|
||||
|
||||
messages = [...messages, ...filteredMessages];
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
public async subscribe(
|
||||
decoder: IDecoder<IDecodedMessage>,
|
||||
fn: (m: IDecodedMessage) => void
|
||||
) {
|
||||
this.ensureWakuInitialized();
|
||||
return this.node!.filter.subscribe(decoder, fn);
|
||||
}
|
||||
|
||||
private emitStatusEvent(payload: string) {
|
||||
this.emitter.dispatchEvent(
|
||||
new CustomEvent(WakuEvents.Status, { detail: payload })
|
||||
);
|
||||
}
|
||||
|
||||
private ensureWakuInitialized() {
|
||||
if (!waku.initialized) {
|
||||
const message = "Waku is not initialized.";
|
||||
console.log(message);
|
||||
throw Error(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const waku = new Waku();
|
||||
27
examples/flush-notes/tsconfig.json
Normal file
27
examples/flush-notes/tsconfig.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user