diff --git a/bun.lockb b/bun.lockb index 8294f02..67d1787 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/index.html b/index.html index 362f0f8..f10fdc7 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,8 @@ - + + diff --git a/package.json b/package.json index 66d681a..b30b1e4 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,9 @@ "homepage": "https://hashmapsdata2value.github.io/edaga/", "dependencies": { "@agoralabs-sh/avm-web-provider": "^1.6.2", + "@bitjson/qr-code": "^1.0.2", "@blockshake/defly-connect": "^1.1.6", + "@perawallet/connect": "^1.3.4", "@radix-ui/react-alert-dialog": "^1.1.1", "@radix-ui/react-checkbox": "^1.1.1", "@radix-ui/react-dialog": "^1.1.1", @@ -24,10 +26,12 @@ "@radix-ui/react-toggle": "^1.1.0", "@radix-ui/react-toggle-group": "^1.1.0", "@radix-ui/react-tooltip": "^1.1.2", - "@txnlab/use-wallet-react": "^3.1.3", + "@txnlab/use-wallet-react": "^3.2.1", + "@walletconnect/core": "^2.15.1", "@walletconnect/modal": "^2.6.2", "@walletconnect/sign-client": "^2.15.0", - "algosdk": "^2.8.0", + "@walletconnect/web3wallet": "^1.14.1", + "algosdk": "^2.9.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "date-fns": "^3.6.0", @@ -39,6 +43,7 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^6.25.1", + "react-svg": "^16.1.34", "tailwind-merge": "^2.4.0", "tailwindcss-animate": "^1.0.7", "zustand": "^4.5.4" diff --git a/public/192.png b/public/192.png index 0f1e8b1..f4a2df3 100644 Binary files a/public/192.png and b/public/192.png differ diff --git a/public/512.png b/public/512.png index 791fba7..f6192d3 100644 Binary files a/public/512.png and b/public/512.png differ diff --git a/public/favicon.svg b/public/favicon.svg index e9114e9..6c45a51 100644 --- a/public/favicon.svg +++ b/public/favicon.svg @@ -1,4 +1,5 @@ - - - + + + + diff --git a/src/Routes.tsx b/src/Routes.tsx new file mode 100644 index 0000000..7d4758a --- /dev/null +++ b/src/Routes.tsx @@ -0,0 +1,61 @@ +import { createBrowserRouter } from "react-router-dom"; + +import { TransactionProvider } from "@/context/TransactionContext"; + +import All from "@/components/views/All"; +import Replies from "@/components/views/Replies"; +import Topics from "@/components/views/Topics"; +// import Topic from "@/components/views/Topic"; + +export const router = createBrowserRouter( + [ + { + path: "/", + element: ( + + + + ), + }, + { + path: "replies/:originalTxId", + element: ( + + + + ), + }, + { + path: "topics/", + element: ( + + + + ), + children: [ + { + path: "replies/:originalTxId", + element: ( + + + + ), + }, + ], + // TODO - Topics sub-view + // children: [ + // { + // path: ":topic", + // element: ( + // + // + // + // ), + // }, + // ], + }, + ], + { + basename: "", + } +); diff --git a/src/assets/data/quotes.ts b/src/assets/data/quotes.ts new file mode 100644 index 0000000..68c7247 --- /dev/null +++ b/src/assets/data/quotes.ts @@ -0,0 +1,14 @@ +export const quotes = [ + `“Ultimately, literature is nothing but carpentry. With both you are working with reality, a material just as hard as wood”, Gabriel García Márquez`, + `“I have to write things down to feel I fully comprehend them”, Haruki Murakami`, + `“Writing is nothing more than a guided dream”, Jorge Luis Borges`, + `“A book must be the axe for the frozen sea within us”, Franz Kafka`, + `“The storyteller who tells stories to help us better understand ourselves and our world”, Chinua Achebe`, + `“Writing is a way to talk without being interrupted”, Marguerite Yourcenar`, + `“Write what should not be forgotten”, Isabel Allende`, + `“You write in order to change the world, knowing perfectly well that you probably can’t, but also knowing that literature is indispensable to the world”, James Baldwin`, + `“Writing is a struggle against silence”, Nawal El Saadawi`, + `“Writing is an act of faith; I believe it’s also an act of hope, the hope that things can get better than they are”, Octavio Paz`, + `“The real voyage of discovery consists not in seeking new landscapes, but in having new eyes”, Marcel Proust`, + `“A word after a word after a word is power”, Margaret Atwood`, +]; diff --git a/src/components/app/Compose.tsx b/src/components/app/Compose.tsx new file mode 100644 index 0000000..95cfd02 --- /dev/null +++ b/src/components/app/Compose.tsx @@ -0,0 +1,269 @@ +import { useWallet } from "@txnlab/use-wallet-react"; +import algosdk from "algosdk"; +import { useState, useEffect, useMemo } from "react"; +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, +} from "@/components/ui/sheet"; +import { Textarea } from "@/components/ui/textarea"; +import { CornerDownLeft as IconCornerDownLeft } from "lucide-react"; +import { UpdateIcon } from "@radix-ui/react-icons"; +import { useApplicationState } from "@/store"; +import { useTransactionContext } from "@/context/TransactionContext"; +import { quotes } from "@/assets/data/quotes"; +import { Input } from "../ui/input"; + +interface ComposeProps { + open: boolean; + onOpenChange: (open: boolean) => void; + isTopic?: boolean; + isReply?: boolean; + replyToTxId?: string; + // currentPath: string; +} + +const Compose = ({ + open, + onOpenChange, + isTopic, + isReply, + replyToTxId, +}: ComposeProps) => { + const { + algodClient, + activeAddress, + // activeNetwork, + // setActiveNetwork, + transactionSigner, + } = useWallet(); + + const { broadcastChannel, handles } = useApplicationState(); + const { loadTransactions } = useTransactionContext(); + + const [message, setMessage] = useState(""); + const maxMessageLength = 800; + const [topicName, setTopicName] = useState(""); + const maxTopicLength = 60; + const [isSending, setIsSending] = useState(false); + + const activeHandle = activeAddress ? handles[activeAddress] || "" : ""; + + /** + * As the longest fixed part of a message is a reply: + * `ARC00-0;r;0000000000000000000000000000000000000000000000000000;;` (64 characters) + * + * ..and the longest NFD (including segment) is: + * `{27}.{27}.algo` (60 characters*) + * + * So, that's: + * ARC00-0;r;0000000000000000000000000000000000000000000000000000;aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa; (124 characters) + * + * ...leaving 900 for a message. + * + * I figure we'd want media attachment's at some point + * (common IPFS CID lengths are between 46 [v0] to 55 [v1, base58+sha256]), + * and to give the message format space for extensions, + * let's settle on 800 characters for a message. + * + * So that's: + * + * 60 characters max for a handle + * 800 characters max for a message + * + * *See [Fisherman's Discord post](https://discord.com/channels/925410112368156732/925410112951160879/1190400846547144754)) + */ + + const sendMessage = async () => { + if (!activeAddress) throw new Error("No active account"); + if ( + isTopic && + !isReply && + (topicName.length === 0 || topicName.length > maxTopicLength) + ) { + alert(`Please provide a topic name within ${maxTopicLength} characters.`); + return; + } + if (message.length === 0) { + alert("Type a message before posting"); + return; + } + if (message.length > maxMessageLength) { + alert( + `Your message exceeds the maximum length of ${maxMessageLength} characters.` + ); + return; + } + + setIsSending(true); + + try { + let prefix = ""; + if (isReply) { + prefix = `r;${replyToTxId}`; + } else if (isTopic && !isReply) { + prefix = `t;${topicName}`; + } else { + prefix = "a;"; + } + + const note = new Uint8Array( + Buffer.from(`ARC00-0;${prefix};${activeHandle};${message}`) + ); + + const transactionComposer = new algosdk.AtomicTransactionComposer(); + const suggestedParams = await algodClient.getTransactionParams().do(); + + const transaction = algosdk.makePaymentTxnWithSuggestedParamsFromObject({ + from: activeAddress, + to: broadcastChannel.address, + amount: 0, + note, + suggestedParams, + }); + + transactionComposer.addTransaction({ + txn: transaction, + signer: transactionSigner, + }); + + console.info("Sending message...", transaction); + + const result = await transactionComposer.execute(algodClient, 4); + + console.info("✅ Successfully sent transaction!", { + confirmedRound: result.confirmedRound, + txIDs: result.txIDs, + }); + + loadTransactions(); + + setMessage(""); + setTopicName(""); // Reset topic name + } catch (err) { + console.error("Failed to post message", err); + } finally { + setIsSending(false); + onOpenChange(false); + } + }; + + useEffect(() => { + if (open) { + document.body.classList.add("sheet-open"); + } else { + document.body.classList.remove("sheet-open"); + } + return () => document.body.classList.remove("sheet-open"); + }, [open]); + + const quote = useMemo(() => getDescriptionQuote(), []); + + return ( + + + + {`New ${ + isReply ? "Reply" : isTopic ? "Topic" : "Conversation" + }`} + + {quote} + + + + {isTopic && !isReply && ( +
+ { + const inputTopic = evt.target.value; + if (inputTopic.length <= maxTopicLength) { + setTopicName(inputTopic); + } else { + setTopicName(inputTopic.slice(0, maxTopicLength)); + } + }} + /> + {/* + {maxTopicLength - topicName.length}/{maxTopicLength} + */} +
+ )} + +
+
+ +