diff --git a/bun.lockb b/bun.lockb index 11f4fe2..688f053 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index b859a6f..c510971 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@radix-ui/react-toggle": "^1.1.0", "@radix-ui/react-toggle-group": "^1.1.0", "@radix-ui/react-tooltip": "^1.1.2", + "@tabler/icons-react": "^3.33.0", "@txnlab/use-wallet-react": "^3.5.0", "@walletconnect/core": "^2.16.2", "@walletconnect/modal": "^2.6.2", diff --git a/src/assets/icons/currency.algorand.svg.tsx b/src/assets/icons/currency.algorand.svg.tsx new file mode 100644 index 0000000..71fafe1 --- /dev/null +++ b/src/assets/icons/currency.algorand.svg.tsx @@ -0,0 +1,35 @@ +import { FC } from "react"; + +interface SVGIconProps { + width?: number | string; + height?: number | string; + fill?: string; + className?: string; +} + +const Algorand: FC = ({ + width = 240, + height = 240, + fill = "currentColor", + className = "", + ...props +}) => { + return ( + + + + ); +}; + +export default Algorand; diff --git a/src/components/app/Compose.tsx b/src/components/app/Compose.tsx index cc5a551..c162206 100644 --- a/src/components/app/Compose.tsx +++ b/src/components/app/Compose.tsx @@ -1,6 +1,7 @@ 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 { @@ -18,6 +19,16 @@ import { useTransactionContext } from "@/context/TransactionContext"; import { quotes } from "@/assets/data/quotes"; import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, +} from "@/components/ui/select"; +import Algorand from "@/assets/icons/currency.algorand.svg"; + interface ComposeProps { open: boolean; onOpenChange: (open: boolean) => void; @@ -27,6 +38,19 @@ interface ComposeProps { // currentPath: string; } +interface TransactionFeeOption { + label: string; + value: number; +} +const TXN_FEE: TransactionFeeOption[] = [ + { label: "Default", value: 0.001 }, + { label: "Medium", value: 0.01 }, + { label: "High", value: 0.1 }, + { label: "Priority", value: 1.0 }, +]; + +const FEE_CAP = 5; // Maximum fee in ALGO for custom fees + const Compose = ({ open, onOpenChange, @@ -34,13 +58,7 @@ const Compose = ({ isReply, replyToTxId, }: ComposeProps) => { - const { - algodClient, - activeAddress, - // activeNetwork, - // setActiveNetwork, - transactionSigner, - } = useWallet(); + const { algodClient, activeAddress, transactionSigner } = useWallet(); const { broadcastChannel, handles } = useApplicationState(); const { loadTransactions, loadReplies } = useTransactionContext(); @@ -49,35 +67,11 @@ const Compose = ({ const maxMessageLength = 800; const [topicName, setTopicName] = useState(""); const maxTopicLength = 60; + const [fee, setFee] = useState(0.001); 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 ( @@ -118,6 +112,9 @@ const Compose = ({ const transactionComposer = new algosdk.AtomicTransactionComposer(); const suggestedParams = await algodClient.getTransactionParams().do(); + suggestedParams.flatFee = true; + suggestedParams.fee = fee * 1_000_000; + const transaction = algosdk.makePaymentTxnWithSuggestedParamsFromObject({ from: activeAddress, to: broadcastChannel.address, @@ -147,7 +144,7 @@ const Compose = ({ } setMessage(""); - setTopicName(""); // Reset topic name + setTopicName(""); } catch (err) { console.error("Failed to post message", err); } finally { @@ -167,6 +164,35 @@ const Compose = ({ const quote = useMemo(() => getDescriptionQuote(), []); + const [fees, setFees] = useState(TXN_FEE); + const [customFee, setCustomFee] = useState(""); + + const selectedOption = fees.find((f) => f.value === fee) ?? { + value: fee, + label: "Custom", + }; + + const { addCustomFee } = useApplicationState(); + + const handleCustomFee = () => { + const parsed = parseFloat(customFee); + + if ( + !isNaN(parsed) && + parsed >= 0.001 && + parsed <= FEE_CAP && + !fees.some((f) => f.value === parsed) + ) { + const newFees = [...fees, { value: parsed, label: "Custom" }]; + + setFees(newFees); + setFee(parsed); + setCustomFee(""); + + addCustomFee(parsed); + } + }; + return ( @@ -196,9 +222,6 @@ const Compose = ({ } }} /> - {/* - {maxTopicLength - topicName.length}/{maxTopicLength} - */} )} @@ -213,7 +236,7 @@ const Compose = ({