From 8aed2c55509ecdd8935f24a3ccf602ba53415b98 Mon Sep 17 00:00:00 2001 From: Kieran Date: Mon, 16 Jan 2023 17:48:25 +0000 Subject: [PATCH] More TSX --- src/Util.ts | 4 +- src/element/{Copy.js => Copy.tsx} | 6 +- src/element/DM.tsx | 2 +- .../{FollowButton.js => FollowButton.tsx} | 21 ++- .../{FollowListBase.js => FollowListBase.tsx} | 7 +- .../{FollowersList.js => FollowersList.tsx} | 9 +- .../{FollowsList.js => FollowsList.tsx} | 9 +- src/element/{Hashtag.js => Hashtag.tsx} | 2 +- src/element/{Invoice.js => Invoice.tsx} | 14 +- src/element/{LNURLTip.js => LNURLTip.tsx} | 123 ++++++++++------ src/element/{LazyImage.js => LazyImage.tsx} | 2 +- src/element/{Modal.js => Modal.tsx} | 10 +- src/element/Nip05.js | 83 ----------- src/element/Nip05.tsx | 90 ++++++++++++ src/element/{Note.js => Note.tsx} | 45 ++++-- src/element/NoteCreator.js | 91 ------------ src/element/NoteCreator.tsx | 101 +++++++++++++ src/element/{NoteFooter.js => NoteFooter.tsx} | 30 ++-- src/element/{NoteGhost.js => NoteGhost.tsx} | 2 +- .../{NoteReaction.js => NoteReaction.tsx} | 20 ++- src/element/{NoteTime.js => NoteTime.tsx} | 14 +- .../{ProfileImage.js => ProfileImage.tsx} | 36 +++-- .../{ProfilePreview.js => ProfilePreview.tsx} | 13 +- src/element/{QrCode.js => QrCode.tsx} | 18 ++- src/element/{Relay.js => Relay.tsx} | 20 ++- src/element/{Text.js => Text.tsx} | 54 ++++--- src/element/Textarea.tsx | 15 +- src/element/{Timeline.js => Timeline.tsx} | 14 +- src/feed/EventPublisher.ts | 2 +- src/feed/ProfileFeed.ts | 13 +- src/feed/RelayState.ts | 8 +- src/feed/VoidUpload.js | 34 ----- src/feed/VoidUpload.ts | 55 +++++++ src/nostr/Event.ts | 2 +- src/pages/ChatPage.tsx | 3 - src/pages/{ProfilePage.js => ProfilePage.tsx} | 76 +++++----- .../{SettingsPage.js => SettingsPage.tsx} | 138 +++++++++--------- src/state/Login.ts | 4 +- src/{useCopy.js => useCopy.ts} | 2 +- 39 files changed, 706 insertions(+), 486 deletions(-) rename src/element/{Copy.js => Copy.tsx} (85%) rename src/element/{FollowButton.js => FollowButton.tsx} (54%) rename src/element/{FollowListBase.js => FollowListBase.tsx} (74%) rename src/element/{FollowersList.js => FollowersList.tsx} (74%) rename src/element/{FollowsList.js => FollowsList.tsx} (75%) rename src/element/{Hashtag.js => Hashtag.tsx} (75%) rename src/element/{Invoice.js => Invoice.tsx} (80%) rename src/element/{LNURLTip.js => LNURLTip.tsx} (65%) rename src/element/{LazyImage.js => LazyImage.tsx} (90%) rename src/element/{Modal.js => Modal.tsx} (66%) delete mode 100644 src/element/Nip05.js create mode 100644 src/element/Nip05.tsx rename src/element/{Note.js => Note.tsx} (66%) delete mode 100644 src/element/NoteCreator.js create mode 100644 src/element/NoteCreator.tsx rename src/element/{NoteFooter.js => NoteFooter.tsx} (78%) rename src/element/{NoteGhost.js => NoteGhost.tsx} (88%) rename src/element/{NoteReaction.js => NoteReaction.tsx} (78%) rename src/element/{NoteTime.js => NoteTime.tsx} (79%) rename src/element/{ProfileImage.js => ProfileImage.tsx} (50%) rename src/element/{ProfilePreview.js => ProfilePreview.tsx} (65%) rename src/element/{QrCode.js => QrCode.tsx} (74%) rename src/element/{Relay.js => Relay.tsx} (84%) rename src/element/{Text.js => Text.tsx} (74%) rename src/element/{Timeline.js => Timeline.tsx} (71%) delete mode 100644 src/feed/VoidUpload.js create mode 100644 src/feed/VoidUpload.ts rename src/pages/{ProfilePage.js => ProfilePage.tsx} (66%) rename src/pages/{SettingsPage.js => SettingsPage.tsx} (60%) rename src/{useCopy.js => useCopy.ts} (91%) diff --git a/src/Util.ts b/src/Util.ts index 30441a8..b6f5c9b 100644 --- a/src/Util.ts +++ b/src/Util.ts @@ -2,7 +2,7 @@ import * as secp from "@noble/secp256k1"; import { bech32 } from "bech32"; import { HexKey, u256 } from "./nostr"; -export async function openFile() { +export async function openFile(): Promise { return new Promise((resolve, reject) => { let elm = document.createElement("input"); elm.type = "file"; @@ -10,6 +10,8 @@ export async function openFile() { let elm = e.target as HTMLInputElement; if (elm.files) { resolve(elm.files[0]); + } else { + resolve(undefined); } }; elm.click(); diff --git a/src/element/Copy.js b/src/element/Copy.tsx similarity index 85% rename from src/element/Copy.js rename to src/element/Copy.tsx index 4bc922b..8a4fddb 100644 --- a/src/element/Copy.js +++ b/src/element/Copy.tsx @@ -3,7 +3,11 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faCopy, faCheck } from "@fortawesome/free-solid-svg-icons"; import { useCopy } from "../useCopy"; -export default function Copy({ text, maxSize = 32 }) { +export interface CopyProps { + text: string, + maxSize?: number +} +export default function Copy({ text, maxSize = 32 }: CopyProps) { const { copy, copied, error } = useCopy(); const sliceLength = maxSize / 2 const trimmed = text.length > maxSize ? `${text.slice(0, sliceLength)}:${text.slice(-sliceLength)}` : text diff --git a/src/element/DM.tsx b/src/element/DM.tsx index 0bea6f9..c7766f1 100644 --- a/src/element/DM.tsx +++ b/src/element/DM.tsx @@ -40,7 +40,7 @@ export default function DM(props: DMProps) {
- +
) diff --git a/src/element/FollowButton.js b/src/element/FollowButton.tsx similarity index 54% rename from src/element/FollowButton.js rename to src/element/FollowButton.tsx index 47b2036..b0d75ac 100644 --- a/src/element/FollowButton.js +++ b/src/element/FollowButton.tsx @@ -2,28 +2,33 @@ import { useSelector } from "react-redux"; import useEventPublisher from "../feed/EventPublisher"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faUserMinus, faUserPlus } from "@fortawesome/free-solid-svg-icons"; +import { HexKey } from "../nostr"; +import { RootState } from "../state/Store"; -export default function FollowButton(props) { +export interface FollowButtonProps { + pubkey: HexKey, + className?: string, +} +export default function FollowButton(props: FollowButtonProps) { const pubkey = props.pubkey; const publiser = useEventPublisher(); - const follows = useSelector(s => s.login.follows); - let isFollowing = follows?.includes(pubkey) ?? false; - const baseClassName = isFollowing ? `btn btn-warn follow-button` : `btn btn-success follow-button` + const isFollowing = useSelector(s => s.login.follows?.includes(pubkey) ?? false); + const baseClassName = isFollowing ? `btn btn-warn follow-button` : `btn btn-success follow-button` const className = props.className ? `${baseClassName} ${props.className}` : `${baseClassName}`; - - async function follow(pubkey) { + + async function follow(pubkey: HexKey) { let ev = await publiser.addFollow(pubkey); publiser.broadcast(ev); } - async function unfollow(pubkey) { + async function unfollow(pubkey: HexKey) { let ev = await publiser.removeFollow(pubkey); publiser.broadcast(ev); } return (
isFollowing ? unfollow(pubkey) : follow(pubkey)}> - +
) } \ No newline at end of file diff --git a/src/element/FollowListBase.js b/src/element/FollowListBase.tsx similarity index 74% rename from src/element/FollowListBase.js rename to src/element/FollowListBase.tsx index bae3b52..99dfe88 100644 --- a/src/element/FollowListBase.js +++ b/src/element/FollowListBase.tsx @@ -1,7 +1,12 @@ import useEventPublisher from "../feed/EventPublisher"; +import { HexKey } from "../nostr"; import ProfilePreview from "./ProfilePreview"; -export default function FollowListBase({ pubkeys, title}) { +export interface FollowListBaseProps { + pubkeys: HexKey[], + title?: string +} +export default function FollowListBase({ pubkeys, title }: FollowListBaseProps) { const publisher = useEventPublisher(); async function followAll() { diff --git a/src/element/FollowersList.js b/src/element/FollowersList.tsx similarity index 74% rename from src/element/FollowersList.js rename to src/element/FollowersList.tsx index 5220045..a8b3c9b 100644 --- a/src/element/FollowersList.js +++ b/src/element/FollowersList.tsx @@ -1,9 +1,14 @@ import { useMemo } from "react"; import useFollowersFeed from "../feed/FollowersFeed"; +import { HexKey } from "../nostr"; import EventKind from "../nostr/EventKind"; import FollowListBase from "./FollowListBase"; -export default function FollowersList({ pubkey }) { +export interface FollowersListProps { + pubkey: HexKey +} + +export default function FollowersList({ pubkey }: FollowersListProps) { const feed = useFollowersFeed(pubkey); const pubkeys = useMemo(() => { @@ -11,5 +16,5 @@ export default function FollowersList({ pubkey }) { return [...new Set(contactLists?.map(a => a.pubkey))]; }, [feed]); - return + return } \ No newline at end of file diff --git a/src/element/FollowsList.js b/src/element/FollowsList.tsx similarity index 75% rename from src/element/FollowsList.js rename to src/element/FollowsList.tsx index c9922e3..d2bb2af 100644 --- a/src/element/FollowsList.js +++ b/src/element/FollowsList.tsx @@ -1,9 +1,14 @@ import { useMemo } from "react"; import useFollowsFeed from "../feed/FollowsFeed"; +import { HexKey } from "../nostr"; import EventKind from "../nostr/EventKind"; import FollowListBase from "./FollowListBase"; -export default function FollowsList({ pubkey }) { +export interface FollowsListProps { + pubkey: HexKey +} + +export default function FollowsList({ pubkey }: FollowsListProps) { const feed = useFollowsFeed(pubkey); const pubkeys = useMemo(() => { @@ -12,5 +17,5 @@ export default function FollowsList({ pubkey }) { return [...new Set(pTags?.flat())]; }, [feed]); - return + return } \ No newline at end of file diff --git a/src/element/Hashtag.js b/src/element/Hashtag.tsx similarity index 75% rename from src/element/Hashtag.js rename to src/element/Hashtag.tsx index a79c8d0..8a6b7b3 100644 --- a/src/element/Hashtag.js +++ b/src/element/Hashtag.tsx @@ -1,6 +1,6 @@ import './Hashtag.css' -const Hashtag = ({ children }) => { +const Hashtag = ({ children }: any) => { return ( {children} diff --git a/src/element/Invoice.js b/src/element/Invoice.tsx similarity index 80% rename from src/element/Invoice.js rename to src/element/Invoice.tsx index 8a23e8d..84d8478 100644 --- a/src/element/Invoice.js +++ b/src/element/Invoice.tsx @@ -1,11 +1,15 @@ import "./Invoice.css"; import { useState } from "react"; +// @ts-expect-error import { decode as invoiceDecode } from "light-bolt11-decoder"; import { useMemo } from "react"; import NoteTime from "./NoteTime"; import LNURLTip from "./LNURLTip"; -export default function Invoice(props) { +export interface InvoiceProps { + invoice: string +} +export default function Invoice(props: InvoiceProps) { const invoice = props.invoice; const [showInvoice, setShowInvoice] = useState(false); @@ -13,10 +17,10 @@ export default function Invoice(props) { try { let parsed = invoiceDecode(invoice); - let amount = parseInt(parsed.sections.find(a => a.name === "amount")?.value); - let timestamp = parseInt(parsed.sections.find(a => a.name === "timestamp")?.value); - let expire = parseInt(parsed.sections.find(a => a.name === "expiry")?.value); - let description = parsed.sections.find(a => a.name === "description")?.value; + let amount = parseInt(parsed.sections.find((a: any) => a.name === "amount")?.value); + let timestamp = parseInt(parsed.sections.find((a: any) => a.name === "timestamp")?.value); + let expire = parseInt(parsed.sections.find((a: any) => a.name === "expiry")?.value); + let description = parsed.sections.find((a: any) => a.name === "description")?.value; let ret = { amount: !isNaN(amount) ? (amount / 1000) : 0, expire: !isNaN(timestamp) && !isNaN(expire) ? timestamp + expire : null, diff --git a/src/element/LNURLTip.js b/src/element/LNURLTip.tsx similarity index 65% rename from src/element/LNURLTip.js rename to src/element/LNURLTip.tsx index 08b10b8..0d947de 100644 --- a/src/element/LNURLTip.js +++ b/src/element/LNURLTip.tsx @@ -5,31 +5,67 @@ import Modal from "./Modal"; import QrCode from "./QrCode"; import Copy from "./Copy"; -export default function LNURLTip(props) { +declare global { + interface Window { + webln?: { + enabled: boolean, + enable: () => Promise, + sendPayment: (pr: string) => Promise + } + } +} + +interface LNURLService { + minSendable?: number, + maxSendable?: number, + metadata: string, + callback: string, + commentAllowed?: number +} + +interface LNURLInvoice { + pr: string, + successAction?: LNURLSuccessAction +} + +interface LNURLSuccessAction { + description?: string, + url?: string +} + +export interface LNURLTipProps { + onClose?: () => void, + svc?: string, + show?: boolean, + invoice?: string, // shortcut to invoice qr tab + title?: string +} + +export default function LNURLTip(props: LNURLTipProps) { const onClose = props.onClose || (() => { }); const service = props.svc; const show = props.show || false; - const amounts = [50, 100, 500, 1_000, 5_000, 10_000]; - const [payService, setPayService] = useState(""); - const [amount, setAmount] = useState(0); - const [customAmount, setCustomAmount] = useState(0); - const [invoice, setInvoice] = useState(null); - const [comment, setComment] = useState(""); - const [error, setError] = useState(""); - const [success, setSuccess] = useState(null); + const amounts = [50, 100, 500, 1_000, 5_000, 10_000, 50_000]; + const [payService, setPayService] = useState(); + const [amount, setAmount] = useState(); + const [customAmount, setCustomAmount] = useState(); + const [invoice, setInvoice] = useState(); + const [comment, setComment] = useState(); + const [error, setError] = useState(); + const [success, setSuccess] = useState(); useEffect(() => { if (show && !props.invoice) { loadService() - .then(a => setPayService(a)) + .then(a => setPayService(a!)) .catch(() => setError("Failed to load LNURL service")); } else { - setPayService(""); - setError(""); - setInvoice(props.invoice ? { pr: props.invoice } : null); - setAmount(0); - setComment(""); - setSuccess(null); + setPayService(undefined); + setError(undefined); + setInvoice(props.invoice ? { pr: props.invoice } : undefined); + setAmount(undefined); + setComment(undefined); + setSuccess(undefined); } }, [show, service]); @@ -44,7 +80,7 @@ export default function LNURLTip(props) { const metadata = useMemo(() => { if (payService) { - let meta = JSON.parse(payService.metadata); + let meta: string[][] = JSON.parse(payService.metadata); let desc = meta.find(a => a[0] === "text/plain"); let image = meta.find(a => a[0] === "image/png;base64"); return { @@ -55,37 +91,40 @@ export default function LNURLTip(props) { return null; }, [payService]); - const selectAmount = (a) => { - setError(""); - setInvoice(null); + const selectAmount = (a: number) => { + setError(undefined); + setInvoice(undefined); setAmount(a); }; - async function fetchJson(url) { + async function fetchJson(url: string) { let rsp = await fetch(url); if (rsp.ok) { - let data = await rsp.json(); + let data: T = await rsp.json(); console.log(data); - setError(""); + setError(undefined); return data; } return null; } - async function loadService() { - let isServiceUrl = service.toLowerCase().startsWith("lnurl"); - if (isServiceUrl) { - let serviceUrl = bech32ToText(service); - return await fetchJson(serviceUrl); - } else { - let ns = service.split("@"); - return await fetchJson(`https://${ns[1]}/.well-known/lnurlp/${ns[0]}`); + async function loadService(): Promise { + if (service) { + let isServiceUrl = service.toLowerCase().startsWith("lnurl"); + if (isServiceUrl) { + let serviceUrl = bech32ToText(service); + return await fetchJson(serviceUrl); + } else { + let ns = service.split("@"); + return await fetchJson(`https://${ns[1]}/.well-known/lnurlp/${ns[0]}`); + } } + return null; } async function loadInvoice() { - if (amount === 0) return null; - const url = `${payService.callback}?amount=${parseInt(amount * 1000)}&comment=${encodeURIComponent(comment)}`; + if (!amount || !payService) return null; + const url = `${payService.callback}?amount=${Math.floor(amount * 1000)}${comment ? `&comment=${encodeURIComponent(comment)}` : ""}`; try { let rsp = await fetch(url); if (rsp.ok) { @@ -110,21 +149,21 @@ export default function LNURLTip(props) { let max = (payService?.maxSendable ?? 21_000_000_000) / 1000; return (
- setCustomAmount(e.target.value)} /> -
selectAmount(customAmount)}>Confirm
+ setCustomAmount(parseInt(e.target.value))} /> +
selectAmount(customAmount!)}>Confirm
); } async function payWebLN() { try { - if (!window.webln.enabled) { - await window.webln.enable(); + if (!window.webln!.enabled) { + await window.webln!.enable(); } - let res = await window.webln.sendPayment(invoice.pr); + let res = await window.webln!.sendPayment(invoice!.pr); console.log(res); - setSuccess(invoice.successAction || {}); - } catch (e) { + setSuccess(invoice!.successAction || {}); + } catch (e: any) { setError(e.toString()); console.warn(e); } @@ -145,7 +184,7 @@ export default function LNURLTip(props) { <>
{metadata?.description ?? service}
- {payService?.commentAllowed > 0 ? + {(payService?.commentAllowed ?? 0) > 0 ? setComment(e.target.value)} /> : null}
@@ -158,7 +197,7 @@ export default function LNURLTip(props) { : null}
{amount === -1 ? custom() : null} - {amount > 0 ?
loadInvoice()}>Get Invoice
: null} + {(amount ?? 0) > 0 ?
loadInvoice()}>Get Invoice
: null} ) } diff --git a/src/element/LazyImage.js b/src/element/LazyImage.tsx similarity index 90% rename from src/element/LazyImage.js rename to src/element/LazyImage.tsx index ff40bd3..5e7590b 100644 --- a/src/element/LazyImage.js +++ b/src/element/LazyImage.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react'; import { useInView } from 'react-intersection-observer'; -export default function LazyImage(props) { +export default function LazyImage(props: any) { const { ref, inView, entry } = useInView(); const [shown, setShown] = useState(false); diff --git a/src/element/Modal.js b/src/element/Modal.tsx similarity index 66% rename from src/element/Modal.js rename to src/element/Modal.tsx index 7a3baf1..d91ad50 100644 --- a/src/element/Modal.js +++ b/src/element/Modal.tsx @@ -1,7 +1,13 @@ import "./Modal.css"; import { useEffect } from "react" +import * as React from "react"; -export default function Modal(props) { +export interface ModalProps { + onClose?: () => void, + children: React.ReactNode +} + +export default function Modal(props: ModalProps) { const onClose = props.onClose || (() => { }); useEffect(() => { @@ -10,7 +16,7 @@ export default function Modal(props) { }, []); return ( -
{ e.stopPropagation(); onClose(e); }}> +
{ e.stopPropagation(); onClose(); }}> {props.children}
) diff --git a/src/element/Nip05.js b/src/element/Nip05.js deleted file mode 100644 index c273d6b..0000000 --- a/src/element/Nip05.js +++ /dev/null @@ -1,83 +0,0 @@ -import { useQuery } from "react-query"; - -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faCheck, faSpinner, faTriangleExclamation } from "@fortawesome/free-solid-svg-icons"; - -import './Nip05.css' - -function fetchNip05Pubkey(name, domain) { - if (!name || !domain) { - return Promise.resolve(null) - } - return fetch(`https://${domain}/.well-known/nostr.json?name=${encodeURIComponent(name)}`) - .then((res) => res.json()) - .then(({ names }) => { - const match = Object.keys(names).find(n => { - return n.toLowerCase() === name.toLowerCase() - }) - return names[match] - }) -} - -const VERIFICATION_CACHE_TIME = 24 * 60 * 60 * 1000 -const VERIFICATION_STALE_TIMEOUT = 10 * 60 * 1000 - -export function useIsVerified(nip05, pubkey) { - const [name, domain] = nip05 ? nip05.split('@') : [] - const address = domain && `${name}@${domain.toLowerCase()}` - const { isLoading, isError, isSuccess, isIdle, data } = useQuery( - ['nip05', nip05], - () => fetchNip05Pubkey(name, domain), - { - retry: false, - retryOnMount: false, - cacheTime: VERIFICATION_CACHE_TIME, - staleTime: VERIFICATION_STALE_TIMEOUT - }, - ) - const isVerified = isSuccess && data === pubkey - const cantVerify = isSuccess && data !== pubkey - return { isVerified, couldNotVerify: isError || cantVerify } -} - -const Nip05 = ({ nip05, pubkey }) => { - const [name, domain] = nip05 ? nip05.split('@') : [] - const isDefaultUser = name === '_' - const { isVerified, couldNotVerify } = useIsVerified(nip05, pubkey) - - return ( -
ev.stopPropagation()}> -
- {!isDefaultUser && name} -
-
- {domain} -
- - {!isVerified && !couldNotVerify && ( - - )} - {isVerified && ( - - )} - {couldNotVerify && ( - - )} - -
- ) -} - -export default Nip05 diff --git a/src/element/Nip05.tsx b/src/element/Nip05.tsx new file mode 100644 index 0000000..2adb34a --- /dev/null +++ b/src/element/Nip05.tsx @@ -0,0 +1,90 @@ +import { useQuery } from "react-query"; + +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faCheck, faSpinner, faTriangleExclamation } from "@fortawesome/free-solid-svg-icons"; + +import './Nip05.css' +import { HexKey } from "../nostr"; + +interface NostrJson { + names: Record +} + +async function fetchNip05Pubkey(name: string, domain: string) { + if (!name || !domain) { + return undefined; + } + const res = await fetch(`https://${domain}/.well-known/nostr.json?name=${encodeURIComponent(name)}`); + const data: NostrJson = await res.json(); + const match = Object.keys(data.names).find(n => { + return n.toLowerCase() === name.toLowerCase(); + }); + return match ? data.names[match] : undefined; +} + +const VERIFICATION_CACHE_TIME = 24 * 60 * 60 * 1000 +const VERIFICATION_STALE_TIMEOUT = 10 * 60 * 1000 + +export function useIsVerified(pubkey: HexKey, nip05?: string) { + const [name, domain] = nip05 ? nip05.split('@') : [] + const { isError, isSuccess, data } = useQuery( + ['nip05', nip05], + () => fetchNip05Pubkey(name, domain), + { + retry: false, + retryOnMount: false, + cacheTime: VERIFICATION_CACHE_TIME, + staleTime: VERIFICATION_STALE_TIMEOUT, + }, + ) + const isVerified = isSuccess && data === pubkey + const cantVerify = isSuccess && data !== pubkey + return { isVerified, couldNotVerify: isError || cantVerify } +} + +export interface Nip05Params { + nip05?: string, + pubkey: HexKey +} + +const Nip05 = (props: Nip05Params) => { + const [name, domain] = props.nip05 ? props.nip05.split('@') : [] + const isDefaultUser = name === '_' + const { isVerified, couldNotVerify } = useIsVerified(props.pubkey, props.nip05) + + return ( +
ev.stopPropagation()}> +
+ {!isDefaultUser && name} +
+
+ {domain} +
+ + {!isVerified && !couldNotVerify && ( + + )} + {isVerified && ( + + )} + {couldNotVerify && ( + + )} + +
+ ) +} + +export default Nip05 diff --git a/src/element/Note.js b/src/element/Note.tsx similarity index 66% rename from src/element/Note.js rename to src/element/Note.tsx index c9f5425..d6a95b4 100644 --- a/src/element/Note.js +++ b/src/element/Note.tsx @@ -2,7 +2,7 @@ import "./Note.css"; import { useCallback, useMemo } from "react"; import { useNavigate } from "react-router-dom"; -import Event from "../nostr/Event"; +import { default as NEvent } from "../nostr/Event"; import ProfileImage from "./ProfileImage"; import Text from "./Text"; import { eventLink, hexToBech32 } from "../Util"; @@ -10,11 +10,26 @@ import NoteFooter from "./NoteFooter"; import NoteTime from "./NoteTime"; import EventKind from "../nostr/EventKind"; import useProfile from "../feed/ProfileFeed"; +import { TaggedRawEvent, u256 } from "../nostr"; -export default function Note(props) { +export interface NoteProps { + data?: TaggedRawEvent, + isThread?: boolean, + reactions: TaggedRawEvent[], + deletion: TaggedRawEvent[], + highlight?: boolean, + options?: { + showHeader?: boolean, + showTime?: boolean, + showFooter?: boolean + }, + ["data-ev"]?: NEvent +} + +export default function Note(props: NoteProps) { const navigate = useNavigate(); - const { data, isThread, reactions, deletion, hightlight, options: opt, ["data-ev"]: parsedEvent } = props - const ev = useMemo(() => parsedEvent ?? new Event(data), [data]); + const { data, isThread, reactions, deletion, highlight, options: opt, ["data-ev"]: parsedEvent } = props + const ev = useMemo(() => parsedEvent ?? new NEvent(data), [data]); const pubKeys = useMemo(() => ev.Thread?.PubKeys || [], [ev]); const users = useProfile(pubKeys); @@ -31,10 +46,10 @@ export default function Note(props) { if (deletion?.length > 0) { return (Deleted); } - return ; + return ; }, [props]); - function goToEvent(e, id) { + function goToEvent(e: any, id: u256) { if (!window.location.pathname.startsWith("/e/")) { e.stopPropagation(); navigate(eventLink(id)); @@ -48,13 +63,21 @@ export default function Note(props) { const maxMentions = 2; let replyId = ev.Thread?.ReplyTo?.Event ?? ev.Thread?.Root?.Event; - let mentions = ev.Thread?.PubKeys?.map(a => [a, users ? users[a] : null])?.map(a => (a[1]?.name?.length ?? 0) > 0 ? a[1].name : hexToBech32("npub", a[0]).substring(0, 12)) - .sort((a, b) => a.startsWith("npub") ? 1 : -1); + let mentions: string[] = []; + for (let pk of ev.Thread?.PubKeys) { + let u = users?.get(pk); + if (u) { + mentions.push(u.name ?? hexToBech32("npub", pk).substring(0, 12)); + } else { + mentions.push(hexToBech32("npub", pk).substring(0, 12)); + } + } + mentions.sort((a, b) => a.startsWith("npub") ? 1 : -1); let othersLength = mentions.length - maxMentions let pubMentions = mentions.length > maxMentions ? `${mentions?.slice(0, maxMentions).join(", ")} & ${othersLength} other${othersLength > 1 ? 's' : ''}` : mentions?.join(", "); return (
- ➡️ {(pubMentions?.length ?? 0) > 0 ? pubMentions : hexToBech32("note", replyId)?.substring(0, 12)} + ➡️ {(pubMentions?.length ?? 0) > 0 ? pubMentions : replyId ? hexToBech32("note", replyId)?.substring(0, 12) : ""}
) } @@ -71,10 +94,10 @@ export default function Note(props) { } return ( -
+
{options.showHeader ?
- + {options.showTime ?
diff --git a/src/element/NoteCreator.js b/src/element/NoteCreator.js deleted file mode 100644 index 01a6d60..0000000 --- a/src/element/NoteCreator.js +++ /dev/null @@ -1,91 +0,0 @@ -import { useState } from "react"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faPaperclip } from "@fortawesome/free-solid-svg-icons"; - -import "./NoteCreator.css"; - -import useEventPublisher from "../feed/EventPublisher"; -import { openFile } from "../Util"; -import VoidUpload from "../feed/VoidUpload"; -import { FileExtensionRegex } from "../Const"; -import Textarea from "../element/Textarea"; - -export function NoteCreator(props) { - const replyTo = props.replyTo; - const onSend = props.onSend; - const show = props.show || false; - const autoFocus = props.autoFocus || false; - const publisher = useEventPublisher(); - const [note, setNote] = useState(""); - const [error, setError] = useState(""); - const [active, setActive] = useState(false); - - async function sendNote() { - let ev = replyTo ? await publisher.reply(replyTo, note) : await publisher.note(note); - console.debug("Sending note: ", ev); - publisher.broadcast(ev); - setNote(""); - if (typeof onSend === "function") { - onSend(); - } - setActive(false); - } - - async function attachFile() { - try { - let file = await openFile(); - let rsp = await VoidUpload(file); - let ext = file.name.match(FileExtensionRegex)[1]; - - // extension tricks note parser to embed the content - let url = rsp.metadata.url ?? `https://void.cat/d/${rsp.id}.${ext}`; - - setNote(n => `${n}\n${url}`); - } catch (error) { - setError(error?.message) - } - } - - function onChange(ev) { - const { value } = ev.target - setNote(value) - if (value) { - setActive(true) - } else { - setActive(false) - } - } - - function onSubmit(ev) { - ev.stopPropagation(); - sendNote() - } - - if (!show) return false; - return ( - <> -
-
-