fix eslint warnings
This commit is contained in:
parent
61e6876c6d
commit
441983b8ae
13
.eslintrc.cjs
Normal file
13
.eslintrc.cjs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
module.exports = {
|
||||||
|
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
|
||||||
|
parser: "@typescript-eslint/parser",
|
||||||
|
plugins: ["@typescript-eslint"],
|
||||||
|
root: true,
|
||||||
|
ignorePatterns: ["build/"],
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
worker: true,
|
||||||
|
commonjs: true,
|
||||||
|
node: true,
|
||||||
|
},
|
||||||
|
};
|
6
d.ts
6
d.ts
@ -1,14 +1,14 @@
|
|||||||
declare module "*.jpg" {
|
declare module "*.jpg" {
|
||||||
const value: any;
|
const value: unknown;
|
||||||
export default value;
|
export default value;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module "*.svg" {
|
declare module "*.svg" {
|
||||||
const value: any;
|
const value: unknown;
|
||||||
export default value;
|
export default value;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module "*.webp" {
|
declare module "*.webp" {
|
||||||
const value: any;
|
const value: string;
|
||||||
export default value;
|
export default value;
|
||||||
}
|
}
|
||||||
|
@ -83,17 +83,20 @@ export const RecommendedFollows = [
|
|||||||
* Regex to match email address
|
* Regex to match email address
|
||||||
*/
|
*/
|
||||||
export const EmailRegex =
|
export const EmailRegex =
|
||||||
|
// eslint-disable-next-line no-useless-escape
|
||||||
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generic URL regex
|
* Generic URL regex
|
||||||
*/
|
*/
|
||||||
export const UrlRegex =
|
export const UrlRegex =
|
||||||
|
// eslint-disable-next-line no-useless-escape
|
||||||
/((?:http|ftp|https):\/\/(?:[\w+?\.\w+])+(?:[a-zA-Z0-9\~\!\@\#\$\%\^\&\*\(\)_\-\=\+\\\/\?\.\:\;\'\,]*)?)/i;
|
/((?:http|ftp|https):\/\/(?:[\w+?\.\w+])+(?:[a-zA-Z0-9\~\!\@\#\$\%\^\&\*\(\)_\-\=\+\\\/\?\.\:\;\'\,]*)?)/i;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract file extensions regex
|
* Extract file extensions regex
|
||||||
*/
|
*/
|
||||||
|
// eslint-disable-next-line no-useless-escape
|
||||||
export const FileExtensionRegex = /\.([\w]+)$/i;
|
export const FileExtensionRegex = /\.([\w]+)$/i;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -121,6 +124,7 @@ export const TweetUrlRegex =
|
|||||||
/**
|
/**
|
||||||
* Hashtag regex
|
* Hashtag regex
|
||||||
*/
|
*/
|
||||||
|
// eslint-disable-next-line no-useless-escape
|
||||||
export const HashtagRegex = /(#[^\s!@#$%^&*()=+.\/,\[{\]};:'"?><]+)/;
|
export const HashtagRegex = /(#[^\s!@#$%^&*()=+.\/,\[{\]};:'"?><]+)/;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,14 +1,20 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
export default function AsyncButton(props: any) {
|
interface AsyncButtonProps
|
||||||
|
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
|
onClick(e: React.MouseEvent): Promise<void> | void;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AsyncButton(props: AsyncButtonProps) {
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
async function handle(e: any) {
|
async function handle(e: React.MouseEvent) {
|
||||||
if (loading) return;
|
if (loading) return;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
if (typeof props.onClick === "function") {
|
if (typeof props.onClick === "function") {
|
||||||
let f = props.onClick(e);
|
const f = props.onClick(e);
|
||||||
if (f instanceof Promise) {
|
if (f instanceof Promise) {
|
||||||
await f;
|
await f;
|
||||||
}
|
}
|
||||||
@ -19,12 +25,7 @@ export default function AsyncButton(props: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button type="button" disabled={loading} {...props} onClick={handle}>
|
||||||
type="button"
|
|
||||||
disabled={loading}
|
|
||||||
{...props}
|
|
||||||
onClick={(e) => handle(e)}
|
|
||||||
>
|
|
||||||
{props.children}
|
{props.children}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
@ -1,13 +1,7 @@
|
|||||||
import { useMemo } from "react";
|
|
||||||
import { useSelector } from "react-redux";
|
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
import { HexKey } from "Nostr";
|
|
||||||
import type { RootState } from "State/Store";
|
|
||||||
import MuteButton from "Element/MuteButton";
|
import MuteButton from "Element/MuteButton";
|
||||||
import BlockButton from "Element/BlockButton";
|
import BlockButton from "Element/BlockButton";
|
||||||
import ProfilePreview from "Element/ProfilePreview";
|
import ProfilePreview from "Element/ProfilePreview";
|
||||||
import useMutedFeed, { getMuted } from "Feed/MuteList";
|
|
||||||
import useModeration from "Hooks/useModeration";
|
import useModeration from "Hooks/useModeration";
|
||||||
|
|
||||||
import messages from "./messages";
|
import messages from "./messages";
|
||||||
@ -17,7 +11,6 @@ interface BlockListProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function BlockList({ variant }: BlockListProps) {
|
export default function BlockList({ variant }: BlockListProps) {
|
||||||
const { publicKey } = useSelector((s: RootState) => s.login);
|
|
||||||
const { blocked, muted } = useModeration();
|
const { blocked, muted } = useModeration();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useState, ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
import ShowMore from "Element/ShowMore";
|
import ShowMore from "Element/ShowMore";
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ export interface CopyProps {
|
|||||||
maxSize?: number;
|
maxSize?: number;
|
||||||
}
|
}
|
||||||
export default function Copy({ text, maxSize = 32 }: CopyProps) {
|
export default function Copy({ text, maxSize = 32 }: CopyProps) {
|
||||||
const { copy, copied, error } = useCopy();
|
const { copy, copied } = useCopy();
|
||||||
const sliceLength = maxSize / 2;
|
const sliceLength = maxSize / 2;
|
||||||
const trimmed =
|
const trimmed =
|
||||||
text.length > maxSize
|
text.length > maxSize
|
||||||
|
@ -12,6 +12,7 @@ import { setLastReadDm } from "Pages/MessagesPage";
|
|||||||
import { RootState } from "State/Store";
|
import { RootState } from "State/Store";
|
||||||
import { HexKey, TaggedRawEvent } from "Nostr";
|
import { HexKey, TaggedRawEvent } from "Nostr";
|
||||||
import { incDmInteraction } from "State/Login";
|
import { incDmInteraction } from "State/Login";
|
||||||
|
import { unwrap } from "Util";
|
||||||
|
|
||||||
import messages from "./messages";
|
import messages from "./messages";
|
||||||
|
|
||||||
@ -32,11 +33,11 @@ export default function DM(props: DMProps) {
|
|||||||
const isMe = props.data.pubkey === pubKey;
|
const isMe = props.data.pubkey === pubKey;
|
||||||
const otherPubkey = isMe
|
const otherPubkey = isMe
|
||||||
? pubKey
|
? pubKey
|
||||||
: props.data.tags.find((a) => a[0] === "p")![1];
|
: unwrap(props.data.tags.find((a) => a[0] === "p")?.[1]);
|
||||||
|
|
||||||
async function decrypt() {
|
async function decrypt() {
|
||||||
let e = new Event(props.data);
|
const e = new Event(props.data);
|
||||||
let decrypted = await publisher.decryptDm(e);
|
const decrypted = await publisher.decryptDm(e);
|
||||||
setContent(decrypted || "<ERROR>");
|
setContent(decrypted || "<ERROR>");
|
||||||
if (!isMe) {
|
if (!isMe) {
|
||||||
setLastReadDm(e.PubKey);
|
setLastReadDm(e.PubKey);
|
||||||
|
@ -21,12 +21,12 @@ export default function FollowButton(props: FollowButtonProps) {
|
|||||||
const baseClassname = `${props.className} follow-button`;
|
const baseClassname = `${props.className} follow-button`;
|
||||||
|
|
||||||
async function follow(pubkey: HexKey) {
|
async function follow(pubkey: HexKey) {
|
||||||
let ev = await publiser.addFollow(pubkey);
|
const ev = await publiser.addFollow(pubkey);
|
||||||
publiser.broadcast(ev);
|
publiser.broadcast(ev);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function unfollow(pubkey: HexKey) {
|
async function unfollow(pubkey: HexKey) {
|
||||||
let ev = await publiser.removeFollow(pubkey);
|
const ev = await publiser.removeFollow(pubkey);
|
||||||
publiser.broadcast(ev);
|
publiser.broadcast(ev);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ export default function FollowListBase({
|
|||||||
const publisher = useEventPublisher();
|
const publisher = useEventPublisher();
|
||||||
|
|
||||||
async function followAll() {
|
async function followAll() {
|
||||||
let ev = await publisher.addFollow(pubkeys);
|
const ev = await publisher.addFollow(pubkeys);
|
||||||
publisher.broadcast(ev);
|
publisher.broadcast(ev);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ export default function FollowersList({ pubkey }: FollowersListProps) {
|
|||||||
const feed = useFollowersFeed(pubkey);
|
const feed = useFollowersFeed(pubkey);
|
||||||
|
|
||||||
const pubkeys = useMemo(() => {
|
const pubkeys = useMemo(() => {
|
||||||
let contactLists = feed?.store.notes.filter(
|
const contactLists = feed?.store.notes.filter(
|
||||||
(a) =>
|
(a) =>
|
||||||
a.kind === EventKind.ContactList &&
|
a.kind === EventKind.ContactList &&
|
||||||
a.tags.some((b) => b[0] === "p" && b[1] === pubkey)
|
a.tags.some((b) => b[0] === "p" && b[1] === pubkey)
|
||||||
|
@ -25,7 +25,7 @@ export default function FollowsYou({ pubkey }: FollowsYouProps) {
|
|||||||
return getFollowers(feed.store, pubkey);
|
return getFollowers(feed.store, pubkey);
|
||||||
}, [feed, pubkey]);
|
}, [feed, pubkey]);
|
||||||
|
|
||||||
const followsMe = pubkeys.includes(loginPubKey!) ?? false;
|
const followsMe = loginPubKey ? pubkeys.includes(loginPubKey) : false;
|
||||||
|
|
||||||
return followsMe ? (
|
return followsMe ? (
|
||||||
<span className="follows-you">{formatMessage(messages.FollowsYou)}</span>
|
<span className="follows-you">{formatMessage(messages.FollowsYou)}</span>
|
||||||
|
@ -135,7 +135,9 @@ export default function HyperText({
|
|||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {}
|
} catch (error) {
|
||||||
|
// Ignore the error.
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
href={a}
|
href={a}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import "./Invoice.css";
|
import "./Invoice.css";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useIntl, FormattedMessage } from "react-intl";
|
import { useIntl, FormattedMessage } from "react-intl";
|
||||||
// @ts-expect-error
|
// @ts-expect-error No types available
|
||||||
import { decode as invoiceDecode } from "light-bolt11-decoder";
|
import { decode as invoiceDecode } from "light-bolt11-decoder";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import SendSats from "Element/SendSats";
|
import SendSats from "Element/SendSats";
|
||||||
@ -13,6 +13,11 @@ import messages from "./messages";
|
|||||||
export interface InvoiceProps {
|
export interface InvoiceProps {
|
||||||
invoice: string;
|
invoice: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Section {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default function Invoice(props: InvoiceProps) {
|
export default function Invoice(props: InvoiceProps) {
|
||||||
const invoice = props.invoice;
|
const invoice = props.invoice;
|
||||||
const webln = useWebln();
|
const webln = useWebln();
|
||||||
@ -21,21 +26,21 @@ export default function Invoice(props: InvoiceProps) {
|
|||||||
|
|
||||||
const info = useMemo(() => {
|
const info = useMemo(() => {
|
||||||
try {
|
try {
|
||||||
let parsed = invoiceDecode(invoice);
|
const parsed = invoiceDecode(invoice);
|
||||||
|
|
||||||
let amount = parseInt(
|
const amount = parseInt(
|
||||||
parsed.sections.find((a: any) => a.name === "amount")?.value
|
parsed.sections.find((a: Section) => a.name === "amount")?.value
|
||||||
);
|
);
|
||||||
let timestamp = parseInt(
|
const timestamp = parseInt(
|
||||||
parsed.sections.find((a: any) => a.name === "timestamp")?.value
|
parsed.sections.find((a: Section) => a.name === "timestamp")?.value
|
||||||
);
|
);
|
||||||
let expire = parseInt(
|
const expire = parseInt(
|
||||||
parsed.sections.find((a: any) => a.name === "expiry")?.value
|
parsed.sections.find((a: Section) => a.name === "expiry")?.value
|
||||||
);
|
);
|
||||||
let description = parsed.sections.find(
|
const description = parsed.sections.find(
|
||||||
(a: any) => a.name === "description"
|
(a: Section) => a.name === "description"
|
||||||
)?.value;
|
)?.value;
|
||||||
let ret = {
|
const ret = {
|
||||||
amount: !isNaN(amount) ? amount / 1000 : 0,
|
amount: !isNaN(amount) ? amount / 1000 : 0,
|
||||||
expire: !isNaN(timestamp) && !isNaN(expire) ? timestamp + expire : null,
|
expire: !isNaN(timestamp) && !isNaN(expire) ? timestamp + expire : null,
|
||||||
description,
|
description,
|
||||||
@ -72,7 +77,7 @@ export default function Invoice(props: InvoiceProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function payInvoice(e: any) {
|
async function payInvoice(e: React.MouseEvent<HTMLButtonElement>) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (webln?.enabled) {
|
if (webln?.enabled) {
|
||||||
try {
|
try {
|
||||||
|
59
src/Element/LNURLTip.css
Normal file
59
src/Element/LNURLTip.css
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
.lnurl-tip {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lnurl-tip .btn {
|
||||||
|
background-color: inherit;
|
||||||
|
width: 210px;
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lnurl-tip .btn:hover {
|
||||||
|
background-color: var(--gray);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sat-amount {
|
||||||
|
display: inline-block;
|
||||||
|
background-color: var(--gray-secondary);
|
||||||
|
color: var(--font-color);
|
||||||
|
padding: 2px 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
user-select: none;
|
||||||
|
margin: 2px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sat-amount:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sat-amount.active {
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--note-bg);
|
||||||
|
background-color: var(--font-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lnurl-tip .invoice {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lnurl-tip .invoice .actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lnurl-tip .invoice .actions .copy-action {
|
||||||
|
margin: 10px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lnurl-tip .invoice .actions .pay-actions {
|
||||||
|
margin: 10px auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
301
src/Element/LNURLTip.tsx
Normal file
301
src/Element/LNURLTip.tsx
Normal file
@ -0,0 +1,301 @@
|
|||||||
|
import "./LNURLTip.css";
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { bech32ToText, unwrap } from "Util";
|
||||||
|
import { HexKey } from "Nostr";
|
||||||
|
import useEventPublisher from "Feed/EventPublisher";
|
||||||
|
import Modal from "Element/Modal";
|
||||||
|
import QrCode from "Element/QrCode";
|
||||||
|
import Copy from "Element/Copy";
|
||||||
|
import useWebln from "Hooks/useWebln";
|
||||||
|
|
||||||
|
interface LNURLService {
|
||||||
|
nostrPubkey?: HexKey;
|
||||||
|
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;
|
||||||
|
notice?: string;
|
||||||
|
note?: HexKey;
|
||||||
|
author?: HexKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LNURLTip(props: LNURLTipProps) {
|
||||||
|
const onClose = props.onClose || (() => undefined);
|
||||||
|
const service = props.svc;
|
||||||
|
const show = props.show || false;
|
||||||
|
const { note, author } = props;
|
||||||
|
const amounts = [50, 100, 500, 1_000, 5_000, 10_000, 50_000];
|
||||||
|
const [payService, setPayService] = useState<LNURLService>();
|
||||||
|
const [amount, setAmount] = useState<number>();
|
||||||
|
const [customAmount, setCustomAmount] = useState<number>(0);
|
||||||
|
const [invoice, setInvoice] = useState<LNURLInvoice>();
|
||||||
|
const [comment, setComment] = useState<string>();
|
||||||
|
const [error, setError] = useState<string>();
|
||||||
|
const [success, setSuccess] = useState<LNURLSuccessAction>();
|
||||||
|
const webln = useWebln(show);
|
||||||
|
const publisher = useEventPublisher();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (show && !props.invoice) {
|
||||||
|
loadService()
|
||||||
|
.then((a) => setPayService(unwrap(a)))
|
||||||
|
.catch(() => setError("Failed to load LNURL service"));
|
||||||
|
} else {
|
||||||
|
setPayService(undefined);
|
||||||
|
setError(undefined);
|
||||||
|
setInvoice(props.invoice ? { pr: props.invoice } : undefined);
|
||||||
|
setAmount(undefined);
|
||||||
|
setComment(undefined);
|
||||||
|
setSuccess(undefined);
|
||||||
|
}
|
||||||
|
}, [show, service]);
|
||||||
|
|
||||||
|
const serviceAmounts = useMemo(() => {
|
||||||
|
if (payService) {
|
||||||
|
const min = (payService.minSendable ?? 0) / 1000;
|
||||||
|
const max = (payService.maxSendable ?? 0) / 1000;
|
||||||
|
return amounts.filter((a) => a >= min && a <= max);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}, [payService]);
|
||||||
|
|
||||||
|
const metadata = useMemo(() => {
|
||||||
|
if (payService) {
|
||||||
|
const meta: string[][] = JSON.parse(payService.metadata);
|
||||||
|
const desc = meta.find((a) => a[0] === "text/plain");
|
||||||
|
const image = meta.find((a) => a[0] === "image/png;base64");
|
||||||
|
return {
|
||||||
|
description: desc ? desc[1] : null,
|
||||||
|
image: image ? image[1] : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [payService]);
|
||||||
|
|
||||||
|
const selectAmount = (a: number) => {
|
||||||
|
setError(undefined);
|
||||||
|
setInvoice(undefined);
|
||||||
|
setAmount(a);
|
||||||
|
};
|
||||||
|
|
||||||
|
async function fetchJson<T>(url: string) {
|
||||||
|
const rsp = await fetch(url);
|
||||||
|
if (rsp.ok) {
|
||||||
|
const data: T = await rsp.json();
|
||||||
|
console.log(data);
|
||||||
|
setError(undefined);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadService(): Promise<LNURLService | null> {
|
||||||
|
if (service) {
|
||||||
|
const isServiceUrl = service.toLowerCase().startsWith("lnurl");
|
||||||
|
if (isServiceUrl) {
|
||||||
|
const serviceUrl = bech32ToText(service);
|
||||||
|
return await fetchJson(serviceUrl);
|
||||||
|
} else {
|
||||||
|
const ns = service.split("@");
|
||||||
|
return await fetchJson(`https://${ns[1]}/.well-known/lnurlp/${ns[0]}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadInvoice() {
|
||||||
|
if (!amount || !payService) return null;
|
||||||
|
let url = "";
|
||||||
|
const amountParam = `amount=${Math.floor(amount * 1000)}`;
|
||||||
|
const commentParam = comment
|
||||||
|
? `&comment=${encodeURIComponent(comment)}`
|
||||||
|
: "";
|
||||||
|
if (payService.nostrPubkey && author) {
|
||||||
|
const ev = await publisher.zap(author, note, comment);
|
||||||
|
const nostrParam =
|
||||||
|
ev && `&nostr=${encodeURIComponent(JSON.stringify(ev.ToObject()))}`;
|
||||||
|
url = `${payService.callback}?${amountParam}${commentParam}${nostrParam}`;
|
||||||
|
} else {
|
||||||
|
url = `${payService.callback}?${amountParam}${commentParam}`;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const rsp = await fetch(url);
|
||||||
|
if (rsp.ok) {
|
||||||
|
const data = await rsp.json();
|
||||||
|
console.log(data);
|
||||||
|
if (data.status === "ERROR") {
|
||||||
|
setError(data.reason);
|
||||||
|
} else {
|
||||||
|
setInvoice(data);
|
||||||
|
setError("");
|
||||||
|
payWebLNIfEnabled(data);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setError("Failed to load invoice");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setError("Failed to load invoice");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function custom() {
|
||||||
|
const min = (payService?.minSendable ?? 0) / 1000;
|
||||||
|
const max = (payService?.maxSendable ?? 21_000_000_000) / 1000;
|
||||||
|
return (
|
||||||
|
<div className="flex mb10">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={min}
|
||||||
|
max={max}
|
||||||
|
className="f-grow mr10"
|
||||||
|
value={customAmount}
|
||||||
|
onChange={(e) => setCustomAmount(parseInt(e.target.value))}
|
||||||
|
/>
|
||||||
|
<div className="btn" onClick={() => selectAmount(customAmount)}>
|
||||||
|
Confirm
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function payWebLNIfEnabled(invoice: LNURLInvoice) {
|
||||||
|
try {
|
||||||
|
if (webln?.enabled) {
|
||||||
|
const res = await webln.sendPayment(invoice.pr);
|
||||||
|
console.log(res);
|
||||||
|
setSuccess(invoice.successAction || {});
|
||||||
|
}
|
||||||
|
} catch (e: unknown) {
|
||||||
|
console.warn(e);
|
||||||
|
if (e instanceof Error) {
|
||||||
|
setError(e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function invoiceForm() {
|
||||||
|
if (invoice) return null;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="f-ellipsis mb10">
|
||||||
|
{metadata?.description ?? service}
|
||||||
|
</div>
|
||||||
|
<div className="flex">
|
||||||
|
{(payService?.commentAllowed ?? 0) > 0 ? (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Comment"
|
||||||
|
className="mb10 f-grow"
|
||||||
|
maxLength={payService?.commentAllowed}
|
||||||
|
onChange={(e) => setComment(e.target.value)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div className="mb10">
|
||||||
|
{serviceAmounts.map((a) => (
|
||||||
|
<span
|
||||||
|
className={`sat-amount ${amount === a ? "active" : ""}`}
|
||||||
|
key={a}
|
||||||
|
onClick={() => selectAmount(a)}
|
||||||
|
>
|
||||||
|
{a.toLocaleString()}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
{payService ? (
|
||||||
|
<span
|
||||||
|
className={`sat-amount ${amount === -1 ? "active" : ""}`}
|
||||||
|
onClick={() => selectAmount(-1)}
|
||||||
|
>
|
||||||
|
Custom
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
{amount === -1 ? custom() : null}
|
||||||
|
{(amount ?? 0) > 0 && (
|
||||||
|
<button type="button" className="mb10" onClick={() => loadInvoice()}>
|
||||||
|
Get Invoice
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function payInvoice() {
|
||||||
|
if (success) return null;
|
||||||
|
const pr = invoice?.pr;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="invoice">
|
||||||
|
{props.notice && <b className="error">{props.notice}</b>}
|
||||||
|
<QrCode data={pr} link={`lightning:${pr}`} />
|
||||||
|
<div className="actions">
|
||||||
|
{pr && (
|
||||||
|
<>
|
||||||
|
<div className="copy-action">
|
||||||
|
<Copy text={pr} maxSize={26} />
|
||||||
|
</div>
|
||||||
|
<div className="pay-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => window.open(`lightning:${pr}`)}
|
||||||
|
>
|
||||||
|
Open Wallet
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function successAction() {
|
||||||
|
if (!success) return null;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p>{success?.description ?? "Paid!"}</p>
|
||||||
|
{success.url ? (
|
||||||
|
<a href={success.url} rel="noreferrer" target="_blank">
|
||||||
|
{success.url}
|
||||||
|
</a>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultTitle = payService?.nostrPubkey
|
||||||
|
? "⚡️ Send Zap!"
|
||||||
|
: "⚡️ Send sats";
|
||||||
|
if (!show) return null;
|
||||||
|
return (
|
||||||
|
<Modal onClose={onClose}>
|
||||||
|
<div className="lnurl-tip" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<h2>{props.title || defaultTitle}</h2>
|
||||||
|
{invoiceForm()}
|
||||||
|
{error ? <p className="error">{error}</p> : null}
|
||||||
|
{payInvoice()}
|
||||||
|
{successAction()}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
@ -23,7 +23,7 @@ export default function LoadMore({
|
|||||||
}, [inView, shouldLoadMore, tick]);
|
}, [inView, shouldLoadMore, tick]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let t = setInterval(() => {
|
const t = setInterval(() => {
|
||||||
setTick((x) => (x += 1));
|
setTick((x) => (x += 1));
|
||||||
}, 500);
|
}, 500);
|
||||||
return () => clearInterval(t);
|
return () => clearInterval(t);
|
||||||
|
@ -9,10 +9,10 @@ export default function Mention({ pubkey }: { pubkey: HexKey }) {
|
|||||||
|
|
||||||
const name = useMemo(() => {
|
const name = useMemo(() => {
|
||||||
let name = hexToBech32("npub", pubkey).substring(0, 12);
|
let name = hexToBech32("npub", pubkey).substring(0, 12);
|
||||||
if ((user?.display_name?.length ?? 0) > 0) {
|
if (user?.display_name !== undefined && user.display_name.length > 0) {
|
||||||
name = user!.display_name!;
|
name = user.display_name;
|
||||||
} else if ((user?.name?.length ?? 0) > 0) {
|
} else if (user?.name !== undefined && user.name.length > 0) {
|
||||||
name = user!.name!;
|
name = user.name;
|
||||||
}
|
}
|
||||||
return name;
|
return name;
|
||||||
}, [user, pubkey]);
|
}, [user, pubkey]);
|
||||||
|
@ -8,10 +8,13 @@ export interface ModalProps {
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
function useOnClickOutside(ref: any, onClickOutside: () => void) {
|
function useOnClickOutside(
|
||||||
|
ref: React.MutableRefObject<Element | null>,
|
||||||
|
onClickOutside: () => void
|
||||||
|
) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleClickOutside(ev: any) {
|
function handleClickOutside(ev: MouseEvent) {
|
||||||
if (ref && ref.current && !ref.current.contains(ev.target)) {
|
if (ref && ref.current && !ref.current.contains(ev.target as Node)) {
|
||||||
onClickOutside();
|
onClickOutside();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -24,7 +27,7 @@ function useOnClickOutside(ref: any, onClickOutside: () => void) {
|
|||||||
|
|
||||||
export default function Modal(props: ModalProps) {
|
export default function Modal(props: ModalProps) {
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
const onClose = props.onClose || (() => {});
|
const onClose = props.onClose || (() => undefined);
|
||||||
const className = props.className || "";
|
const className = props.className || "";
|
||||||
useOnClickOutside(ref, onClose);
|
useOnClickOutside(ref, onClose);
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
import { HexKey } from "Nostr";
|
import { HexKey } from "Nostr";
|
||||||
import MuteButton from "Element/MuteButton";
|
import MuteButton from "Element/MuteButton";
|
||||||
import ProfilePreview from "Element/ProfilePreview";
|
import ProfilePreview from "Element/ProfilePreview";
|
||||||
|
@ -29,7 +29,9 @@ type Nip05ServiceProps = {
|
|||||||
supportLink: string;
|
supportLink: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ReduxStore = any;
|
interface ReduxStore {
|
||||||
|
login: { publicKey: string };
|
||||||
|
}
|
||||||
|
|
||||||
export default function Nip5Service(props: Nip05ServiceProps) {
|
export default function Nip5Service(props: Nip05ServiceProps) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -64,9 +66,9 @@ export default function Nip5Service(props: Nip05ServiceProps) {
|
|||||||
if ("error" in a) {
|
if ("error" in a) {
|
||||||
setError(a as ServiceError);
|
setError(a as ServiceError);
|
||||||
} else {
|
} else {
|
||||||
let svc = a as ServiceConfig;
|
const svc = a as ServiceConfig;
|
||||||
setServiceConfig(svc);
|
setServiceConfig(svc);
|
||||||
let defaultDomain =
|
const defaultDomain =
|
||||||
svc.domains.find((a) => a.default)?.name || svc.domains[0].name;
|
svc.domains.find((a) => a.default)?.name || svc.domains[0].name;
|
||||||
setDomain(defaultDomain);
|
setDomain(defaultDomain);
|
||||||
}
|
}
|
||||||
@ -86,7 +88,7 @@ export default function Nip5Service(props: Nip05ServiceProps) {
|
|||||||
setAvailabilityResponse({ available: false, why: "TOO_LONG" });
|
setAvailabilityResponse({ available: false, why: "TOO_LONG" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let rx = new RegExp(
|
const rx = new RegExp(
|
||||||
domainConfig?.regex[0] ?? "",
|
domainConfig?.regex[0] ?? "",
|
||||||
domainConfig?.regex[1] ?? ""
|
domainConfig?.regex[1] ?? ""
|
||||||
);
|
);
|
||||||
@ -111,14 +113,14 @@ export default function Nip5Service(props: Nip05ServiceProps) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (registerResponse && showInvoice) {
|
if (registerResponse && showInvoice) {
|
||||||
let t = setInterval(async () => {
|
const t = setInterval(async () => {
|
||||||
let status = await svc.CheckRegistration(registerResponse.token);
|
const status = await svc.CheckRegistration(registerResponse.token);
|
||||||
if ("error" in status) {
|
if ("error" in status) {
|
||||||
setError(status);
|
setError(status);
|
||||||
setRegisterResponse(undefined);
|
setRegisterResponse(undefined);
|
||||||
setShowInvoice(false);
|
setShowInvoice(false);
|
||||||
} else {
|
} else {
|
||||||
let result: CheckRegisterResponse = status;
|
const result: CheckRegisterResponse = status;
|
||||||
if (result.available && result.paid) {
|
if (result.available && result.paid) {
|
||||||
setShowInvoice(false);
|
setShowInvoice(false);
|
||||||
setRegisterStatus(status);
|
setRegisterStatus(status);
|
||||||
@ -131,8 +133,14 @@ export default function Nip5Service(props: Nip05ServiceProps) {
|
|||||||
}
|
}
|
||||||
}, [registerResponse, showInvoice, svc]);
|
}, [registerResponse, showInvoice, svc]);
|
||||||
|
|
||||||
function mapError(e: ServiceErrorCode, t: string | null): string | undefined {
|
function mapError(
|
||||||
let whyMap = new Map([
|
e: ServiceErrorCode | undefined,
|
||||||
|
t: string | null
|
||||||
|
): string | undefined {
|
||||||
|
if (e === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const whyMap = new Map([
|
||||||
["TOO_SHORT", formatMessage(messages.TooShort)],
|
["TOO_SHORT", formatMessage(messages.TooShort)],
|
||||||
["TOO_LONG", formatMessage(messages.TooLong)],
|
["TOO_LONG", formatMessage(messages.TooLong)],
|
||||||
["REGEX", formatMessage(messages.Regex)],
|
["REGEX", formatMessage(messages.Regex)],
|
||||||
@ -149,7 +157,7 @@ export default function Nip5Service(props: Nip05ServiceProps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let rsp = await svc.RegisterHandle(handle, domain, pubkey);
|
const rsp = await svc.RegisterHandle(handle, domain, pubkey);
|
||||||
if ("error" in rsp) {
|
if ("error" in rsp) {
|
||||||
setError(rsp);
|
setError(rsp);
|
||||||
} else {
|
} else {
|
||||||
@ -160,11 +168,11 @@ export default function Nip5Service(props: Nip05ServiceProps) {
|
|||||||
|
|
||||||
async function updateProfile(handle: string, domain: string) {
|
async function updateProfile(handle: string, domain: string) {
|
||||||
if (user) {
|
if (user) {
|
||||||
let newProfile = {
|
const newProfile = {
|
||||||
...user,
|
...user,
|
||||||
nip05: `${handle}@${domain}`,
|
nip05: `${handle}@${domain}`,
|
||||||
} as UserMetadata;
|
} as UserMetadata;
|
||||||
let ev = await publisher.metadata(newProfile);
|
const ev = await publisher.metadata(newProfile);
|
||||||
publisher.broadcast(ev);
|
publisher.broadcast(ev);
|
||||||
navigate("/settings");
|
navigate("/settings");
|
||||||
}
|
}
|
||||||
@ -231,7 +239,7 @@ export default function Nip5Service(props: Nip05ServiceProps) {
|
|||||||
<b className="error">
|
<b className="error">
|
||||||
<FormattedMessage {...messages.NotAvailable} />{" "}
|
<FormattedMessage {...messages.NotAvailable} />{" "}
|
||||||
{mapError(
|
{mapError(
|
||||||
availabilityResponse.why!,
|
availabilityResponse.why,
|
||||||
availabilityResponse.reasonTag || null
|
availabilityResponse.reasonTag || null
|
||||||
)}
|
)}
|
||||||
</b>
|
</b>
|
||||||
|
@ -17,7 +17,6 @@ import Text from "Element/Text";
|
|||||||
import { eventLink, getReactions, hexToBech32 } from "Util";
|
import { eventLink, getReactions, hexToBech32 } from "Util";
|
||||||
import NoteFooter, { Translation } from "Element/NoteFooter";
|
import NoteFooter, { Translation } from "Element/NoteFooter";
|
||||||
import NoteTime from "Element/NoteTime";
|
import NoteTime from "Element/NoteTime";
|
||||||
import ShowMore from "Element/ShowMore";
|
|
||||||
import EventKind from "Nostr/EventKind";
|
import EventKind from "Nostr/EventKind";
|
||||||
import { useUserProfiles } from "Feed/ProfileFeed";
|
import { useUserProfiles } from "Feed/ProfileFeed";
|
||||||
import { TaggedRawEvent, u256 } from "Nostr";
|
import { TaggedRawEvent, u256 } from "Nostr";
|
||||||
@ -39,10 +38,10 @@ export interface NoteProps {
|
|||||||
["data-ev"]?: NEvent;
|
["data-ev"]?: NEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
const HiddenNote = ({ children }: any) => {
|
const HiddenNote = ({ children }: { children: React.ReactNode }) => {
|
||||||
const [show, setShow] = useState(false);
|
const [show, setShow] = useState(false);
|
||||||
return show ? (
|
return show ? (
|
||||||
children
|
<>{children}</>
|
||||||
) : (
|
) : (
|
||||||
<div className="card note hidden-note">
|
<div className="card note hidden-note">
|
||||||
<div className="header">
|
<div className="header">
|
||||||
@ -61,7 +60,6 @@ export default function Note(props: NoteProps) {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
className,
|
|
||||||
related,
|
related,
|
||||||
highlight,
|
highlight,
|
||||||
options: opt,
|
options: opt,
|
||||||
@ -80,9 +78,9 @@ export default function Note(props: NoteProps) {
|
|||||||
const { ref, inView, entry } = useInView({ triggerOnce: true });
|
const { ref, inView, entry } = useInView({ triggerOnce: true });
|
||||||
const [extendable, setExtendable] = useState<boolean>(false);
|
const [extendable, setExtendable] = useState<boolean>(false);
|
||||||
const [showMore, setShowMore] = useState<boolean>(false);
|
const [showMore, setShowMore] = useState<boolean>(false);
|
||||||
const baseClassname = `note card ${props.className ? props.className : ""}`;
|
const baseClassName = `note card ${props.className ? props.className : ""}`;
|
||||||
const [translated, setTranslated] = useState<Translation>();
|
const [translated, setTranslated] = useState<Translation>();
|
||||||
const replyId = ev.Thread?.ReplyTo?.Event ?? ev.Thread?.Root?.Event;
|
// TODO Why was this unused? Was this a mistake?
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
@ -93,7 +91,7 @@ export default function Note(props: NoteProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const transformBody = useCallback(() => {
|
const transformBody = useCallback(() => {
|
||||||
let body = ev?.Content ?? "";
|
const body = ev?.Content ?? "";
|
||||||
if (deletions?.length > 0) {
|
if (deletions?.length > 0) {
|
||||||
return (
|
return (
|
||||||
<b className="error">
|
<b className="error">
|
||||||
@ -113,14 +111,14 @@ export default function Note(props: NoteProps) {
|
|||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (entry && inView && extendable === false) {
|
if (entry && inView && extendable === false) {
|
||||||
let h = entry?.target.clientHeight ?? 0;
|
const h = entry?.target.clientHeight ?? 0;
|
||||||
if (h > 650) {
|
if (h > 650) {
|
||||||
setExtendable(true);
|
setExtendable(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [inView, entry, extendable]);
|
}, [inView, entry, extendable]);
|
||||||
|
|
||||||
function goToEvent(e: any, id: u256) {
|
function goToEvent(e: React.MouseEvent, id: u256) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
navigate(eventLink(id));
|
navigate(eventLink(id));
|
||||||
}
|
}
|
||||||
@ -131,9 +129,9 @@ export default function Note(props: NoteProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const maxMentions = 2;
|
const maxMentions = 2;
|
||||||
let replyId = ev.Thread?.ReplyTo?.Event ?? ev.Thread?.Root?.Event;
|
const replyId = ev.Thread?.ReplyTo?.Event ?? ev.Thread?.Root?.Event;
|
||||||
let mentions: { pk: string; name: string; link: ReactNode }[] = [];
|
const mentions: { pk: string; name: string; link: ReactNode }[] = [];
|
||||||
for (let pk of ev.Thread?.PubKeys) {
|
for (const pk of ev.Thread?.PubKeys ?? []) {
|
||||||
const u = users?.get(pk);
|
const u = users?.get(pk);
|
||||||
const npub = hexToBech32("npub", pk);
|
const npub = hexToBech32("npub", pk);
|
||||||
const shortNpub = npub.substring(0, 12);
|
const shortNpub = npub.substring(0, 12);
|
||||||
@ -153,9 +151,9 @@ export default function Note(props: NoteProps) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mentions.sort((a, b) => (a.name.startsWith("npub") ? 1 : -1));
|
mentions.sort((a) => (a.name.startsWith("npub") ? 1 : -1));
|
||||||
let othersLength = mentions.length - maxMentions;
|
const othersLength = mentions.length - maxMentions;
|
||||||
const renderMention = (m: any, idx: number) => {
|
const renderMention = (m: { link: React.ReactNode }, idx: number) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{idx > 0 && ", "}
|
{idx > 0 && ", "}
|
||||||
@ -268,7 +266,7 @@ export default function Note(props: NoteProps) {
|
|||||||
|
|
||||||
const note = (
|
const note = (
|
||||||
<div
|
<div
|
||||||
className={`${baseClassname}${highlight ? " active " : " "}${
|
className={`${baseClassName}${highlight ? " active " : " "}${
|
||||||
extendable && !showMore ? " note-expand" : ""
|
extendable && !showMore ? " note-expand" : ""
|
||||||
}`}
|
}`}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
@ -33,14 +33,14 @@ export interface NoteCreatorProps {
|
|||||||
show: boolean;
|
show: boolean;
|
||||||
setShow: (s: boolean) => void;
|
setShow: (s: boolean) => void;
|
||||||
replyTo?: NEvent;
|
replyTo?: NEvent;
|
||||||
onSend?: Function;
|
onSend?: () => void;
|
||||||
autoFocus: boolean;
|
autoFocus: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NoteCreator(props: NoteCreatorProps) {
|
export function NoteCreator(props: NoteCreatorProps) {
|
||||||
const { show, setShow, replyTo, onSend, autoFocus } = props;
|
const { show, setShow, replyTo, onSend, autoFocus } = props;
|
||||||
const publisher = useEventPublisher();
|
const publisher = useEventPublisher();
|
||||||
const [note, setNote] = useState<string>();
|
const [note, setNote] = useState<string>("");
|
||||||
const [error, setError] = useState<string>();
|
const [error, setError] = useState<string>();
|
||||||
const [active, setActive] = useState<boolean>(false);
|
const [active, setActive] = useState<boolean>(false);
|
||||||
const uploader = useFileUpload();
|
const uploader = useFileUpload();
|
||||||
@ -48,7 +48,7 @@ export function NoteCreator(props: NoteCreatorProps) {
|
|||||||
|
|
||||||
async function sendNote() {
|
async function sendNote() {
|
||||||
if (note) {
|
if (note) {
|
||||||
let ev = replyTo
|
const ev = replyTo
|
||||||
? await publisher.reply(replyTo, note)
|
? await publisher.reply(replyTo, note)
|
||||||
: await publisher.note(note);
|
: await publisher.note(note);
|
||||||
console.debug("Sending note: ", ev);
|
console.debug("Sending note: ", ev);
|
||||||
@ -64,21 +64,23 @@ export function NoteCreator(props: NoteCreatorProps) {
|
|||||||
|
|
||||||
async function attachFile() {
|
async function attachFile() {
|
||||||
try {
|
try {
|
||||||
let file = await openFile();
|
const file = await openFile();
|
||||||
if (file) {
|
if (file) {
|
||||||
let rx = await uploader.upload(file, file.name);
|
const rx = await uploader.upload(file, file.name);
|
||||||
if (rx.url) {
|
if (rx.url) {
|
||||||
setNote((n) => `${n ? `${n}\n` : ""}${rx.url}`);
|
setNote((n) => `${n ? `${n}\n` : ""}${rx.url}`);
|
||||||
} else if (rx?.error) {
|
} else if (rx?.error) {
|
||||||
setError(rx.error);
|
setError(rx.error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof Error) {
|
||||||
setError(error?.message);
|
setError(error?.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function onChange(ev: any) {
|
function onChange(ev: React.ChangeEvent<HTMLTextAreaElement>) {
|
||||||
const { value } = ev.target;
|
const { value } = ev.target;
|
||||||
setNote(value);
|
setNote(value);
|
||||||
if (value) {
|
if (value) {
|
||||||
@ -88,7 +90,7 @@ export function NoteCreator(props: NoteCreatorProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancel(ev: any) {
|
function cancel() {
|
||||||
setShow(false);
|
setShow(false);
|
||||||
setNote("");
|
setNote("");
|
||||||
}
|
}
|
||||||
@ -112,11 +114,7 @@ export function NoteCreator(props: NoteCreatorProps) {
|
|||||||
value={note}
|
value={note}
|
||||||
onFocus={() => setActive(true)}
|
onFocus={() => setActive(true)}
|
||||||
/>
|
/>
|
||||||
<button
|
<button type="button" className="attachment" onClick={attachFile}>
|
||||||
type="button"
|
|
||||||
className="attachment"
|
|
||||||
onClick={(e) => attachFile()}
|
|
||||||
>
|
|
||||||
<Attachment />
|
<Attachment />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -95,7 +95,7 @@ export default function NoteFooter(props: NoteFooterProps) {
|
|||||||
const groupReactions = useMemo(() => {
|
const groupReactions = useMemo(() => {
|
||||||
const result = reactions?.reduce(
|
const result = reactions?.reduce(
|
||||||
(acc, reaction) => {
|
(acc, reaction) => {
|
||||||
let kind = normalizeReaction(reaction.content);
|
const kind = normalizeReaction(reaction.content);
|
||||||
const rs = acc[kind] || [];
|
const rs = acc[kind] || [];
|
||||||
if (rs.map((e) => e.pubkey).includes(reaction.pubkey)) {
|
if (rs.map((e) => e.pubkey).includes(reaction.pubkey)) {
|
||||||
return acc;
|
return acc;
|
||||||
@ -128,7 +128,7 @@ export default function NoteFooter(props: NoteFooterProps) {
|
|||||||
|
|
||||||
async function react(content: string) {
|
async function react(content: string) {
|
||||||
if (!hasReacted(content)) {
|
if (!hasReacted(content)) {
|
||||||
let evLike = await publisher.react(ev, content);
|
const evLike = await publisher.react(ev, content);
|
||||||
publisher.broadcast(evLike);
|
publisher.broadcast(evLike);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -139,7 +139,7 @@ export default function NoteFooter(props: NoteFooterProps) {
|
|||||||
formatMessage(messages.ConfirmDeletion, { id: ev.Id.substring(0, 8) })
|
formatMessage(messages.ConfirmDeletion, { id: ev.Id.substring(0, 8) })
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
let evDelete = await publisher.delete(ev.Id);
|
const evDelete = await publisher.delete(ev.Id);
|
||||||
publisher.broadcast(evDelete);
|
publisher.broadcast(evDelete);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -150,14 +150,14 @@ export default function NoteFooter(props: NoteFooterProps) {
|
|||||||
!prefs.confirmReposts ||
|
!prefs.confirmReposts ||
|
||||||
window.confirm(formatMessage(messages.ConfirmRepost, { id: ev.Id }))
|
window.confirm(formatMessage(messages.ConfirmRepost, { id: ev.Id }))
|
||||||
) {
|
) {
|
||||||
let evRepost = await publisher.repost(ev);
|
const evRepost = await publisher.repost(ev);
|
||||||
publisher.broadcast(evRepost);
|
publisher.broadcast(evRepost);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function tipButton() {
|
function tipButton() {
|
||||||
let service = author?.lud16 || author?.lud06;
|
const service = author?.lud16 || author?.lud06;
|
||||||
if (service) {
|
if (service) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -246,7 +246,7 @@ export default function NoteFooter(props: NoteFooterProps) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
let result = await res.json();
|
const result = await res.json();
|
||||||
if (typeof props.onTranslated === "function" && result) {
|
if (typeof props.onTranslated === "function" && result) {
|
||||||
props.onTranslated({
|
props.onTranslated({
|
||||||
text: result.translatedText,
|
text: result.translatedText,
|
||||||
@ -332,7 +332,7 @@ export default function NoteFooter(props: NoteFooterProps) {
|
|||||||
{reactionIcons()}
|
{reactionIcons()}
|
||||||
<div
|
<div
|
||||||
className={`reaction-pill ${reply ? "reacted" : ""}`}
|
className={`reaction-pill ${reply ? "reacted" : ""}`}
|
||||||
onClick={(e) => setReply((s) => !s)}
|
onClick={() => setReply((s) => !s)}
|
||||||
>
|
>
|
||||||
<div className="reaction-pill-icon">
|
<div className="reaction-pill-icon">
|
||||||
<Reply />
|
<Reply />
|
||||||
|
@ -1,8 +1,13 @@
|
|||||||
import "./Note.css";
|
import "./Note.css";
|
||||||
import ProfileImage from "Element/ProfileImage";
|
import ProfileImage from "Element/ProfileImage";
|
||||||
|
|
||||||
export default function NoteGhost(props: any) {
|
interface NoteGhostProps {
|
||||||
const className = `note card ${props.className ? props.className : ""}`;
|
className?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NoteGhost(props: NoteGhostProps) {
|
||||||
|
const className = `note card ${props.className ?? ""}`;
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
<div className="header">
|
<div className="header">
|
||||||
|
@ -23,7 +23,7 @@ export default function NoteReaction(props: NoteReactionProps) {
|
|||||||
|
|
||||||
const refEvent = useMemo(() => {
|
const refEvent = useMemo(() => {
|
||||||
if (ev) {
|
if (ev) {
|
||||||
let eTags = ev.Tags.filter((a) => a.Key === "e");
|
const eTags = ev.Tags.filter((a) => a.Key === "e");
|
||||||
if (eTags.length > 0) {
|
if (eTags.length > 0) {
|
||||||
return eTags[0].Event;
|
return eTags[0].Event;
|
||||||
}
|
}
|
||||||
@ -45,7 +45,7 @@ export default function NoteReaction(props: NoteReactionProps) {
|
|||||||
ev.Content !== "#[0]"
|
ev.Content !== "#[0]"
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
let r: RawEvent = JSON.parse(ev.Content);
|
const r: RawEvent = JSON.parse(ev.Content);
|
||||||
return r as TaggedRawEvent;
|
return r as TaggedRawEvent;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Could not load reposted content", e);
|
console.error("Could not load reposted content", e);
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { FormattedRelativeTime } from "react-intl";
|
|
||||||
|
|
||||||
const MinuteInMs = 1_000 * 60;
|
const MinuteInMs = 1_000 * 60;
|
||||||
const HourInMs = MinuteInMs * 60;
|
const HourInMs = MinuteInMs * 60;
|
||||||
@ -19,10 +18,11 @@ export default function NoteTime(props: NoteTimeProps) {
|
|||||||
}).format(from);
|
}).format(from);
|
||||||
const fromDate = new Date(from);
|
const fromDate = new Date(from);
|
||||||
const isoDate = fromDate.toISOString();
|
const isoDate = fromDate.toISOString();
|
||||||
const ago = new Date().getTime() - from;
|
|
||||||
const absAgo = Math.abs(ago);
|
|
||||||
|
|
||||||
function calcTime() {
|
function calcTime() {
|
||||||
|
const fromDate = new Date(from);
|
||||||
|
const ago = new Date().getTime() - from;
|
||||||
|
const absAgo = Math.abs(ago);
|
||||||
if (absAgo > DayInMs) {
|
if (absAgo > DayInMs) {
|
||||||
return fromDate.toLocaleDateString(undefined, {
|
return fromDate.toLocaleDateString(undefined, {
|
||||||
year: "2-digit",
|
year: "2-digit",
|
||||||
@ -38,7 +38,7 @@ export default function NoteTime(props: NoteTimeProps) {
|
|||||||
} else if (absAgo < MinuteInMs) {
|
} else if (absAgo < MinuteInMs) {
|
||||||
return fallback;
|
return fallback;
|
||||||
} else {
|
} else {
|
||||||
let mins = Math.floor(absAgo / MinuteInMs);
|
const mins = Math.floor(absAgo / MinuteInMs);
|
||||||
if (ago < 0) {
|
if (ago < 0) {
|
||||||
return `in ${mins}m`;
|
return `in ${mins}m`;
|
||||||
}
|
}
|
||||||
@ -48,9 +48,9 @@ export default function NoteTime(props: NoteTimeProps) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTime(calcTime());
|
setTime(calcTime());
|
||||||
let t = setInterval(() => {
|
const t = setInterval(() => {
|
||||||
setTime((s) => {
|
setTime((s) => {
|
||||||
let newTime = calcTime();
|
const newTime = calcTime();
|
||||||
if (newTime !== s) {
|
if (newTime !== s) {
|
||||||
return newTime;
|
return newTime;
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@ export interface NoteToSelfProps {
|
|||||||
link?: string;
|
link?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function NoteLabel({ pubkey, link }: NoteToSelfProps) {
|
function NoteLabel({ pubkey }: NoteToSelfProps) {
|
||||||
const user = useUserProfile(pubkey);
|
const user = useUserProfile(pubkey);
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -63,10 +63,10 @@ export function getDisplayName(
|
|||||||
pubkey: HexKey
|
pubkey: HexKey
|
||||||
) {
|
) {
|
||||||
let name = hexToBech32("npub", pubkey).substring(0, 12);
|
let name = hexToBech32("npub", pubkey).substring(0, 12);
|
||||||
if ((user?.display_name?.length ?? 0) > 0) {
|
if (user?.display_name !== undefined && user.display_name.length > 0) {
|
||||||
name = user!.display_name!;
|
name = user.display_name;
|
||||||
} else if ((user?.name?.length ?? 0) > 0) {
|
} else if (user?.name !== undefined && user.name.length > 0) {
|
||||||
name = user!.name!;
|
name = user.name;
|
||||||
}
|
}
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,15 @@
|
|||||||
import useImgProxy from "Feed/ImgProxy";
|
import useImgProxy from "Feed/ImgProxy";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
export const ProxyImg = (props: any) => {
|
interface ProxyImgProps
|
||||||
|
extends React.DetailedHTMLProps<
|
||||||
|
React.ImgHTMLAttributes<HTMLImageElement>,
|
||||||
|
HTMLImageElement
|
||||||
|
> {
|
||||||
|
size?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProxyImg = (props: ProxyImgProps) => {
|
||||||
const { src, size, ...rest } = props;
|
const { src, size, ...rest } = props;
|
||||||
const [url, setUrl] = useState<string>();
|
const [url, setUrl] = useState<string>();
|
||||||
const { proxy } = useImgProxy();
|
const { proxy } = useImgProxy();
|
||||||
|
@ -15,7 +15,7 @@ export default function QrCode(props: QrCodeProps) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if ((props.data?.length ?? 0) > 0 && qrRef.current) {
|
if ((props.data?.length ?? 0) > 0 && qrRef.current) {
|
||||||
let qr = new QRCodeStyling({
|
const qr = new QRCodeStyling({
|
||||||
width: props.width || 256,
|
width: props.width || 256,
|
||||||
height: props.height || 256,
|
height: props.height || 256,
|
||||||
data: props.data,
|
data: props.data,
|
||||||
@ -35,9 +35,9 @@ export default function QrCode(props: QrCodeProps) {
|
|||||||
qrRef.current.innerHTML = "";
|
qrRef.current.innerHTML = "";
|
||||||
qr.append(qrRef.current);
|
qr.append(qrRef.current);
|
||||||
if (props.link) {
|
if (props.link) {
|
||||||
qrRef.current.onclick = function (e) {
|
qrRef.current.onclick = function () {
|
||||||
let elm = document.createElement("a");
|
const elm = document.createElement("a");
|
||||||
elm.href = props.link!;
|
elm.href = props.link ?? "";
|
||||||
elm.click();
|
elm.click();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -46,10 +46,5 @@ export default function QrCode(props: QrCodeProps) {
|
|||||||
}
|
}
|
||||||
}, [props.data, props.link]);
|
}, [props.data, props.link]);
|
||||||
|
|
||||||
return (
|
return <div className={`qr${props.className ?? ""}`} ref={qrRef}></div>;
|
||||||
<div
|
|
||||||
className={`qr${props.className ? ` ${props.className}` : ""}`}
|
|
||||||
ref={qrRef}
|
|
||||||
></div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@ -47,7 +47,7 @@ export default function Relay(props: RelayProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let latency = Math.floor(state?.avgLatency ?? 0);
|
const latency = Math.floor(state?.avgLatency ?? 0);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={`relay w-max`}>
|
<div className={`relay w-max`}>
|
||||||
@ -104,7 +104,10 @@ export default function Relay(props: RelayProps) {
|
|||||||
<FontAwesomeIcon icon={faPlugCircleXmark} /> {state?.disconnects}
|
<FontAwesomeIcon icon={faPlugCircleXmark} /> {state?.disconnects}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="icon-btn" onClick={() => navigate(state!.id)}>
|
<span
|
||||||
|
className="icon-btn"
|
||||||
|
onClick={() => navigate(state?.id ?? "")}
|
||||||
|
>
|
||||||
<FontAwesomeIcon icon={faGear} />
|
<FontAwesomeIcon icon={faGear} />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -50,7 +50,7 @@ export interface LNURLTipProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function LNURLTip(props: LNURLTipProps) {
|
export default function LNURLTip(props: LNURLTipProps) {
|
||||||
const onClose = props.onClose || (() => {});
|
const onClose = props.onClose || (() => undefined);
|
||||||
const service = props.svc;
|
const service = props.svc;
|
||||||
const show = props.show || false;
|
const show = props.show || false;
|
||||||
const { note, author, target } = props;
|
const { note, author, target } = props;
|
||||||
@ -83,7 +83,7 @@ export default function LNURLTip(props: LNURLTipProps) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (show && !props.invoice) {
|
if (show && !props.invoice) {
|
||||||
loadService()
|
loadService()
|
||||||
.then((a) => setPayService(a!))
|
.then((a) => setPayService(a ?? undefined))
|
||||||
.catch(() => setError(formatMessage(messages.LNURLFail)));
|
.catch(() => setError(formatMessage(messages.LNURLFail)));
|
||||||
} else {
|
} else {
|
||||||
setPayService(undefined);
|
setPayService(undefined);
|
||||||
@ -97,25 +97,14 @@ export default function LNURLTip(props: LNURLTipProps) {
|
|||||||
|
|
||||||
const serviceAmounts = useMemo(() => {
|
const serviceAmounts = useMemo(() => {
|
||||||
if (payService) {
|
if (payService) {
|
||||||
let min = (payService.minSendable ?? 0) / 1000;
|
const min = (payService.minSendable ?? 0) / 1000;
|
||||||
let max = (payService.maxSendable ?? 0) / 1000;
|
const max = (payService.maxSendable ?? 0) / 1000;
|
||||||
return amounts.filter((a) => a >= min && a <= max);
|
return amounts.filter((a) => a >= min && a <= max);
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
}, [payService]);
|
}, [payService]);
|
||||||
|
|
||||||
const metadata = useMemo(() => {
|
// TODO Why was this never used? I think this might be a bug, or was it just an oversight?
|
||||||
if (payService) {
|
|
||||||
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 {
|
|
||||||
description: desc ? desc[1] : null,
|
|
||||||
image: image ? image[1] : null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}, [payService]);
|
|
||||||
|
|
||||||
const selectAmount = (a: number) => {
|
const selectAmount = (a: number) => {
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
@ -124,9 +113,9 @@ export default function LNURLTip(props: LNURLTipProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
async function fetchJson<T>(url: string) {
|
async function fetchJson<T>(url: string) {
|
||||||
let rsp = await fetch(url);
|
const rsp = await fetch(url);
|
||||||
if (rsp.ok) {
|
if (rsp.ok) {
|
||||||
let data: T = await rsp.json();
|
const data: T = await rsp.json();
|
||||||
console.log(data);
|
console.log(data);
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
return data;
|
return data;
|
||||||
@ -136,12 +125,12 @@ export default function LNURLTip(props: LNURLTipProps) {
|
|||||||
|
|
||||||
async function loadService(): Promise<LNURLService | null> {
|
async function loadService(): Promise<LNURLService | null> {
|
||||||
if (service) {
|
if (service) {
|
||||||
let isServiceUrl = service.toLowerCase().startsWith("lnurl");
|
const isServiceUrl = service.toLowerCase().startsWith("lnurl");
|
||||||
if (isServiceUrl) {
|
if (isServiceUrl) {
|
||||||
let serviceUrl = bech32ToText(service);
|
const serviceUrl = bech32ToText(service);
|
||||||
return await fetchJson(serviceUrl);
|
return await fetchJson(serviceUrl);
|
||||||
} else {
|
} else {
|
||||||
let ns = service.split("@");
|
const ns = service.split("@");
|
||||||
return await fetchJson(`https://${ns[1]}/.well-known/lnurlp/${ns[0]}`);
|
return await fetchJson(`https://${ns[1]}/.well-known/lnurlp/${ns[0]}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -165,9 +154,9 @@ export default function LNURLTip(props: LNURLTipProps) {
|
|||||||
url = `${payService.callback}?${amountParam}${commentParam}`;
|
url = `${payService.callback}?${amountParam}${commentParam}`;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
let rsp = await fetch(url);
|
const rsp = await fetch(url);
|
||||||
if (rsp.ok) {
|
if (rsp.ok) {
|
||||||
let data = await rsp.json();
|
const data = await rsp.json();
|
||||||
console.log(data);
|
console.log(data);
|
||||||
if (data.status === "ERROR") {
|
if (data.status === "ERROR") {
|
||||||
setError(data.reason);
|
setError(data.reason);
|
||||||
@ -185,8 +174,8 @@ export default function LNURLTip(props: LNURLTipProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function custom() {
|
function custom() {
|
||||||
let min = (payService?.minSendable ?? 1000) / 1000;
|
const min = (payService?.minSendable ?? 1000) / 1000;
|
||||||
let max = (payService?.maxSendable ?? 21_000_000_000) / 1000;
|
const max = (payService?.maxSendable ?? 21_000_000_000) / 1000;
|
||||||
return (
|
return (
|
||||||
<div className="custom-amount flex">
|
<div className="custom-amount flex">
|
||||||
<input
|
<input
|
||||||
@ -201,8 +190,8 @@ export default function LNURLTip(props: LNURLTipProps) {
|
|||||||
<button
|
<button
|
||||||
className="secondary"
|
className="secondary"
|
||||||
type="button"
|
type="button"
|
||||||
disabled={!Boolean(customAmount)}
|
disabled={!customAmount}
|
||||||
onClick={() => selectAmount(customAmount!)}
|
onClick={() => selectAmount(customAmount ?? 0)}
|
||||||
>
|
>
|
||||||
<FormattedMessage {...messages.Confirm} />
|
<FormattedMessage {...messages.Confirm} />
|
||||||
</button>
|
</button>
|
||||||
@ -213,13 +202,15 @@ export default function LNURLTip(props: LNURLTipProps) {
|
|||||||
async function payWebLNIfEnabled(invoice: LNURLInvoice) {
|
async function payWebLNIfEnabled(invoice: LNURLInvoice) {
|
||||||
try {
|
try {
|
||||||
if (webln?.enabled) {
|
if (webln?.enabled) {
|
||||||
let res = await webln.sendPayment(invoice!.pr);
|
const res = await webln.sendPayment(invoice?.pr ?? "");
|
||||||
console.log(res);
|
console.log(res);
|
||||||
setSuccess(invoice!.successAction || {});
|
setSuccess(invoice?.successAction ?? {});
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
setError(e.toString());
|
|
||||||
console.warn(e);
|
console.warn(e);
|
||||||
|
if (e instanceof Error) {
|
||||||
|
setError(e.toString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ import ReactMarkdown from "react-markdown";
|
|||||||
import { visit, SKIP } from "unist-util-visit";
|
import { visit, SKIP } from "unist-util-visit";
|
||||||
|
|
||||||
import { UrlRegex, MentionRegex, InvoiceRegex, HashtagRegex } from "Const";
|
import { UrlRegex, MentionRegex, InvoiceRegex, HashtagRegex } from "Const";
|
||||||
import { eventLink, hexToBech32 } from "Util";
|
import { eventLink, hexToBech32, unwrap } from "Util";
|
||||||
import Invoice from "Element/Invoice";
|
import Invoice from "Element/Invoice";
|
||||||
import Hashtag from "Element/Hashtag";
|
import Hashtag from "Element/Hashtag";
|
||||||
|
|
||||||
@ -14,11 +14,12 @@ import { MetadataCache } from "State/Users";
|
|||||||
import Mention from "Element/Mention";
|
import Mention from "Element/Mention";
|
||||||
import HyperText from "Element/HyperText";
|
import HyperText from "Element/HyperText";
|
||||||
import { HexKey } from "Nostr";
|
import { HexKey } from "Nostr";
|
||||||
|
import * as unist from "unist";
|
||||||
|
|
||||||
export type Fragment = string | JSX.Element;
|
export type Fragment = string | React.ReactNode;
|
||||||
|
|
||||||
export interface TextFragment {
|
export interface TextFragment {
|
||||||
body: Fragment[];
|
body: React.ReactNode[];
|
||||||
tags: Tag[];
|
tags: Tag[];
|
||||||
users: Map<string, MetadataCache>;
|
users: Map<string, MetadataCache>;
|
||||||
}
|
}
|
||||||
@ -52,24 +53,24 @@ export default function Text({ content, tags, creator, users }: TextProps) {
|
|||||||
.map((f) => {
|
.map((f) => {
|
||||||
if (typeof f === "string") {
|
if (typeof f === "string") {
|
||||||
return f.split(MentionRegex).map((match) => {
|
return f.split(MentionRegex).map((match) => {
|
||||||
let matchTag = match.match(/#\[(\d+)\]/);
|
const matchTag = match.match(/#\[(\d+)\]/);
|
||||||
if (matchTag && matchTag.length === 2) {
|
if (matchTag && matchTag.length === 2) {
|
||||||
let idx = parseInt(matchTag[1]);
|
const idx = parseInt(matchTag[1]);
|
||||||
let ref = frag.tags?.find((a) => a.Index === idx);
|
const ref = frag.tags?.find((a) => a.Index === idx);
|
||||||
if (ref) {
|
if (ref) {
|
||||||
switch (ref.Key) {
|
switch (ref.Key) {
|
||||||
case "p": {
|
case "p": {
|
||||||
return <Mention pubkey={ref.PubKey!} />;
|
return <Mention pubkey={ref.PubKey ?? ""} />;
|
||||||
}
|
}
|
||||||
case "e": {
|
case "e": {
|
||||||
let eText = hexToBech32("note", ref.Event!).substring(
|
const eText = hexToBech32(
|
||||||
0,
|
"note",
|
||||||
12
|
ref.Event ?? ""
|
||||||
);
|
).substring(0, 12);
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
key={ref.Event}
|
key={ref.Event}
|
||||||
to={eventLink(ref.Event!)}
|
to={eventLink(ref.Event ?? "")}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
#{eText}
|
#{eText}
|
||||||
@ -77,7 +78,7 @@ export default function Text({ content, tags, creator, users }: TextProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
case "t": {
|
case "t": {
|
||||||
return <Hashtag tag={ref.Hashtag!} />;
|
return <Hashtag tag={ref.Hashtag ?? ""} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -127,7 +128,7 @@ export default function Text({ content, tags, creator, users }: TextProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function transformLi(frag: TextFragment) {
|
function transformLi(frag: TextFragment) {
|
||||||
let fragments = transformText(frag);
|
const fragments = transformText(frag);
|
||||||
return <li>{fragments}</li>;
|
return <li>{fragments}</li>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -140,9 +141,6 @@ export default function Text({ content, tags, creator, users }: TextProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function transformText(frag: TextFragment) {
|
function transformText(frag: TextFragment) {
|
||||||
if (frag.body === undefined) {
|
|
||||||
debugger;
|
|
||||||
}
|
|
||||||
let fragments = extractMentions(frag);
|
let fragments = extractMentions(frag);
|
||||||
fragments = extractLinks(fragments);
|
fragments = extractLinks(fragments);
|
||||||
fragments = extractInvoices(fragments);
|
fragments = extractInvoices(fragments);
|
||||||
@ -152,15 +150,22 @@ export default function Text({ content, tags, creator, users }: TextProps) {
|
|||||||
|
|
||||||
const components = useMemo(() => {
|
const components = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
p: (x: any) =>
|
p: (x: { children?: React.ReactNode[] }) =>
|
||||||
transformParagraph({ body: x.children ?? [], tags, users }),
|
transformParagraph({ body: x.children ?? [], tags, users }),
|
||||||
a: (x: any) => <HyperText link={x.href} creator={creator} />,
|
a: (x: { href?: string }) => (
|
||||||
li: (x: any) => transformLi({ body: x.children ?? [], tags, users }),
|
<HyperText link={x.href ?? ""} creator={creator} />
|
||||||
|
),
|
||||||
|
li: (x: { children?: Fragment[] }) =>
|
||||||
|
transformLi({ body: x.children ?? [], tags, users }),
|
||||||
};
|
};
|
||||||
}, [content]);
|
}, [content]);
|
||||||
|
|
||||||
|
interface Node extends unist.Node<unist.Data> {
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
const disableMarkdownLinks = useCallback(
|
const disableMarkdownLinks = useCallback(
|
||||||
() => (tree: any) => {
|
() => (tree: Node) => {
|
||||||
visit(tree, (node, index, parent) => {
|
visit(tree, (node, index, parent) => {
|
||||||
if (
|
if (
|
||||||
parent &&
|
parent &&
|
||||||
@ -172,8 +177,9 @@ export default function Text({ content, tags, creator, users }: TextProps) {
|
|||||||
node.type === "definition")
|
node.type === "definition")
|
||||||
) {
|
) {
|
||||||
node.type = "text";
|
node.type = "text";
|
||||||
|
const position = unwrap(node.position);
|
||||||
node.value = content
|
node.value = content
|
||||||
.slice(node.position.start.offset, node.position.end.offset)
|
.slice(position.start.offset, position.end.offset)
|
||||||
.replace(/\)$/, " )");
|
.replace(/\)$/, " )");
|
||||||
return SKIP;
|
return SKIP;
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import "@webscopeio/react-textarea-autocomplete/style.css";
|
|||||||
import "./Textarea.css";
|
import "./Textarea.css";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useIntl, FormattedMessage } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
import ReactTextareaAutocomplete from "@webscopeio/react-textarea-autocomplete";
|
import ReactTextareaAutocomplete from "@webscopeio/react-textarea-autocomplete";
|
||||||
import emoji from "@jukben/emoji-search";
|
import emoji from "@jukben/emoji-search";
|
||||||
import TextareaAutosize from "react-textarea-autosize";
|
import TextareaAutosize from "react-textarea-autosize";
|
||||||
@ -30,7 +30,7 @@ const EmojiItem = ({ entity: { name, char } }: { entity: EmojiItemProps }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const UserItem = (metadata: MetadataCache) => {
|
const UserItem = (metadata: MetadataCache) => {
|
||||||
const { pubkey, display_name, picture, nip05, ...rest } = metadata;
|
const { pubkey, display_name, nip05, ...rest } = metadata;
|
||||||
return (
|
return (
|
||||||
<div key={pubkey} className="user-item">
|
<div key={pubkey} className="user-item">
|
||||||
<div className="user-picture">
|
<div className="user-picture">
|
||||||
@ -44,7 +44,15 @@ const UserItem = (metadata: MetadataCache) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Textarea = ({ users, onChange, ...rest }: any) => {
|
interface TextareaProps {
|
||||||
|
autoFocus: boolean;
|
||||||
|
className: string;
|
||||||
|
onChange(ev: React.ChangeEvent<HTMLTextAreaElement>): void;
|
||||||
|
value: string;
|
||||||
|
onFocus(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Textarea = (props: TextareaProps) => {
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
@ -52,7 +60,7 @@ const Textarea = ({ users, onChange, ...rest }: any) => {
|
|||||||
|
|
||||||
const userDataProvider = (token: string) => {
|
const userDataProvider = (token: string) => {
|
||||||
setQuery(token);
|
setQuery(token);
|
||||||
return allUsers;
|
return allUsers ?? [];
|
||||||
};
|
};
|
||||||
|
|
||||||
const emojiDataProvider = (token: string) => {
|
const emojiDataProvider = (token: string) => {
|
||||||
@ -62,23 +70,26 @@ const Textarea = ({ users, onChange, ...rest }: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
// @ts-expect-error If anybody can figure out how to type this, please do
|
||||||
<ReactTextareaAutocomplete
|
<ReactTextareaAutocomplete
|
||||||
{...rest}
|
{...props}
|
||||||
loadingComponent={() => <span>Loading....</span>}
|
loadingComponent={() => <span>Loading...</span>}
|
||||||
placeholder={formatMessage(messages.NotePlaceholder)}
|
placeholder={formatMessage(messages.NotePlaceholder)}
|
||||||
onChange={onChange}
|
|
||||||
textAreaComponent={TextareaAutosize}
|
textAreaComponent={TextareaAutosize}
|
||||||
trigger={{
|
trigger={{
|
||||||
":": {
|
":": {
|
||||||
dataProvider: emojiDataProvider,
|
dataProvider: emojiDataProvider,
|
||||||
component: EmojiItem,
|
component: EmojiItem,
|
||||||
output: (item: EmojiItemProps, trigger) => item.char,
|
output: (item: EmojiItemProps) => item.char,
|
||||||
},
|
},
|
||||||
"@": {
|
"@": {
|
||||||
afterWhitespace: true,
|
afterWhitespace: true,
|
||||||
dataProvider: userDataProvider,
|
dataProvider: userDataProvider,
|
||||||
component: (props: any) => <UserItem {...props.entity} />,
|
component: (props: { entity: MetadataCache }) => (
|
||||||
output: (item: any) => `@${hexToBech32("npub", item.pubkey)}`,
|
<UserItem {...props.entity} />
|
||||||
|
),
|
||||||
|
output: (item: { pubkey: string }) =>
|
||||||
|
`@${hexToBech32("npub", item.pubkey)}`,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -6,19 +6,18 @@ import { useNavigate, useLocation, Link } from "react-router-dom";
|
|||||||
import { TaggedRawEvent, u256, HexKey } from "Nostr";
|
import { TaggedRawEvent, u256, HexKey } from "Nostr";
|
||||||
import { default as NEvent } from "Nostr/Event";
|
import { default as NEvent } from "Nostr/Event";
|
||||||
import EventKind from "Nostr/EventKind";
|
import EventKind from "Nostr/EventKind";
|
||||||
import { eventLink, hexToBech32, bech32ToHex } from "Util";
|
import { eventLink, bech32ToHex, unwrap } from "Util";
|
||||||
import BackButton from "Element/BackButton";
|
import BackButton from "Element/BackButton";
|
||||||
import Note from "Element/Note";
|
import Note from "Element/Note";
|
||||||
import NoteGhost from "Element/NoteGhost";
|
import NoteGhost from "Element/NoteGhost";
|
||||||
import Collapsed from "Element/Collapsed";
|
import Collapsed from "Element/Collapsed";
|
||||||
|
|
||||||
import messages from "./messages";
|
import messages from "./messages";
|
||||||
|
|
||||||
function getParent(
|
function getParent(
|
||||||
ev: HexKey,
|
ev: HexKey,
|
||||||
chains: Map<HexKey, NEvent[]>
|
chains: Map<HexKey, NEvent[]>
|
||||||
): HexKey | undefined {
|
): HexKey | undefined {
|
||||||
for (let [k, vs] of chains.entries()) {
|
for (const [k, vs] of chains.entries()) {
|
||||||
const fs = vs.map((a) => a.Id);
|
const fs = vs.map((a) => a.Id);
|
||||||
if (fs.includes(ev)) {
|
if (fs.includes(ev)) {
|
||||||
return k;
|
return k;
|
||||||
@ -53,7 +52,6 @@ interface SubthreadProps {
|
|||||||
const Subthread = ({
|
const Subthread = ({
|
||||||
active,
|
active,
|
||||||
path,
|
path,
|
||||||
from,
|
|
||||||
notes,
|
notes,
|
||||||
related,
|
related,
|
||||||
chains,
|
chains,
|
||||||
@ -332,20 +330,19 @@ export default function Thread(props: ThreadProps) {
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const urlNoteId = location?.pathname.slice(3);
|
const urlNoteId = location?.pathname.slice(3);
|
||||||
const urlNoteHex = urlNoteId && bech32ToHex(urlNoteId);
|
const urlNoteHex = urlNoteId && bech32ToHex(urlNoteId);
|
||||||
const rootNoteId = root && hexToBech32("note", root.Id);
|
|
||||||
|
|
||||||
const chains = useMemo(() => {
|
const chains = useMemo(() => {
|
||||||
let chains = new Map<u256, NEvent[]>();
|
const chains = new Map<u256, NEvent[]>();
|
||||||
parsedNotes
|
parsedNotes
|
||||||
?.filter((a) => a.Kind === EventKind.TextNote)
|
?.filter((a) => a.Kind === EventKind.TextNote)
|
||||||
.sort((a, b) => b.CreatedAt - a.CreatedAt)
|
.sort((a, b) => b.CreatedAt - a.CreatedAt)
|
||||||
.forEach((v) => {
|
.forEach((v) => {
|
||||||
let replyTo = v.Thread?.ReplyTo?.Event ?? v.Thread?.Root?.Event;
|
const replyTo = v.Thread?.ReplyTo?.Event ?? v.Thread?.Root?.Event;
|
||||||
if (replyTo) {
|
if (replyTo) {
|
||||||
if (!chains.has(replyTo)) {
|
if (!chains.has(replyTo)) {
|
||||||
chains.set(replyTo, [v]);
|
chains.set(replyTo, [v]);
|
||||||
} else {
|
} else {
|
||||||
chains.get(replyTo)!.push(v);
|
unwrap(chains.get(replyTo)).push(v);
|
||||||
}
|
}
|
||||||
} else if (v.Tags.length > 0) {
|
} else if (v.Tags.length > 0) {
|
||||||
console.log("Not replying to anything: ", v);
|
console.log("Not replying to anything: ", v);
|
||||||
@ -370,7 +367,7 @@ export default function Thread(props: ThreadProps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let subthreadPath = [];
|
const subthreadPath = [];
|
||||||
let parent = getParent(urlNoteHex, chains);
|
let parent = getParent(urlNoteHex, chains);
|
||||||
while (parent) {
|
while (parent) {
|
||||||
subthreadPath.unshift(parent);
|
subthreadPath.unshift(parent);
|
||||||
@ -414,7 +411,7 @@ export default function Thread(props: ThreadProps) {
|
|||||||
if (!from || !chains) {
|
if (!from || !chains) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let replies = chains.get(from);
|
const replies = chains.get(from);
|
||||||
if (replies) {
|
if (replies) {
|
||||||
return (
|
return (
|
||||||
<Subthread
|
<Subthread
|
||||||
@ -476,6 +473,6 @@ function getReplies(from: u256, chains?: Map<u256, NEvent[]>): NEvent[] {
|
|||||||
if (!from || !chains) {
|
if (!from || !chains) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
let replies = chains.get(from);
|
const replies = chains.get(from);
|
||||||
return replies ? replies : [];
|
return replies ? replies : [];
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { CSSProperties, useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { TidalRegex } from "Const";
|
import { TidalRegex } from "Const";
|
||||||
|
|
||||||
// Re-use dom parser across instances of TidalEmbed
|
// Re-use dom parser across instances of TidalEmbed
|
||||||
|
@ -83,7 +83,7 @@ export default function Timeline({
|
|||||||
}
|
}
|
||||||
case EventKind.Reaction:
|
case EventKind.Reaction:
|
||||||
case EventKind.Repost: {
|
case EventKind.Repost: {
|
||||||
let eRef = e.tags.find((a) => a[0] === "e")?.at(1);
|
const eRef = e.tags.find((a) => a[0] === "e")?.at(1);
|
||||||
return (
|
return (
|
||||||
<NoteReaction
|
<NoteReaction
|
||||||
data={e}
|
data={e}
|
||||||
|
@ -2,10 +2,10 @@ import "./Zap.css";
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
// @ts-expect-error
|
// @ts-expect-error No types available
|
||||||
import { decode as invoiceDecode } from "light-bolt11-decoder";
|
import { decode as invoiceDecode } from "light-bolt11-decoder";
|
||||||
import { bytesToHex } from "@noble/hashes/utils";
|
import { bytesToHex } from "@noble/hashes/utils";
|
||||||
import { sha256 } from "Util";
|
import { sha256, unwrap } from "Util";
|
||||||
|
|
||||||
//import { sha256 } from "Util";
|
//import { sha256 } from "Util";
|
||||||
import { formatShort } from "Number";
|
import { formatShort } from "Number";
|
||||||
@ -24,15 +24,19 @@ function findTag(e: TaggedRawEvent, tag: string) {
|
|||||||
return maybeTag && maybeTag[1];
|
return maybeTag && maybeTag[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Section {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
function getInvoice(zap: TaggedRawEvent) {
|
function getInvoice(zap: TaggedRawEvent) {
|
||||||
const bolt11 = findTag(zap, "bolt11");
|
const bolt11 = findTag(zap, "bolt11");
|
||||||
const decoded = invoiceDecode(bolt11);
|
const decoded = invoiceDecode(bolt11);
|
||||||
|
|
||||||
const amount = decoded.sections.find(
|
const amount = decoded.sections.find(
|
||||||
(section: any) => section.name === "amount"
|
(section: Section) => section.name === "amount"
|
||||||
)?.value;
|
)?.value;
|
||||||
const hash = decoded.sections.find(
|
const hash = decoded.sections.find(
|
||||||
(section: any) => section.name === "description_hash"
|
(section: Section) => section.name === "description_hash"
|
||||||
)?.value;
|
)?.value;
|
||||||
|
|
||||||
return { amount, hash: hash ? bytesToHex(hash) : undefined };
|
return { amount, hash: hash ? bytesToHex(hash) : undefined };
|
||||||
@ -72,7 +76,7 @@ export function parseZap(zap: TaggedRawEvent): ParsedZap {
|
|||||||
const { amount, hash } = getInvoice(zap);
|
const { amount, hash } = getInvoice(zap);
|
||||||
const zapper = hash ? getZapper(zap, hash) : { isValid: false };
|
const zapper = hash ? getZapper(zap, hash) : { isValid: false };
|
||||||
const e = findTag(zap, "e");
|
const e = findTag(zap, "e");
|
||||||
const p = findTag(zap, "p")!;
|
const p = unwrap(findTag(zap, "p"));
|
||||||
return {
|
return {
|
||||||
id: zap.id,
|
id: zap.id,
|
||||||
e,
|
e,
|
||||||
|
@ -6,8 +6,8 @@ import { useUserProfile } from "Feed/ProfileFeed";
|
|||||||
import { HexKey } from "Nostr";
|
import { HexKey } from "Nostr";
|
||||||
import SendSats from "Element/SendSats";
|
import SendSats from "Element/SendSats";
|
||||||
|
|
||||||
const ZapButton = ({ pubkey, svc }: { pubkey?: HexKey; svc?: string }) => {
|
const ZapButton = ({ pubkey, svc }: { pubkey: HexKey; svc?: string }) => {
|
||||||
const profile = useUserProfile(pubkey!);
|
const profile = useUserProfile(pubkey);
|
||||||
const [zap, setZap] = useState(false);
|
const [zap, setZap] = useState(false);
|
||||||
const service = svc ?? (profile?.lud16 || profile?.lud06);
|
const service = svc ?? (profile?.lud16 || profile?.lud06);
|
||||||
|
|
||||||
@ -15,7 +15,7 @@ const ZapButton = ({ pubkey, svc }: { pubkey?: HexKey; svc?: string }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="zap-button" onClick={(e) => setZap(true)}>
|
<div className="zap-button" onClick={() => setZap(true)}>
|
||||||
<FontAwesomeIcon icon={faBolt} />
|
<FontAwesomeIcon icon={faBolt} />
|
||||||
</div>
|
</div>
|
||||||
<SendSats
|
<SendSats
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
|
import { TaggedRawEvent } from "Nostr";
|
||||||
import { System } from "Nostr/System";
|
import { System } from "Nostr/System";
|
||||||
import { default as NEvent } from "Nostr/Event";
|
import { default as NEvent } from "Nostr/Event";
|
||||||
import EventKind from "Nostr/EventKind";
|
import EventKind from "Nostr/EventKind";
|
||||||
import Tag from "Nostr/Tag";
|
import Tag from "Nostr/Tag";
|
||||||
import { RootState } from "State/Store";
|
import { RootState } from "State/Store";
|
||||||
import { HexKey, RawEvent, u256, UserMetadata, Lists } from "Nostr";
|
import { HexKey, RawEvent, u256, UserMetadata, Lists } from "Nostr";
|
||||||
import { bech32ToHex } from "Util";
|
import { bech32ToHex, unwrap } from "Util";
|
||||||
import { DefaultRelays, HashtagRegex } from "Const";
|
import { DefaultRelays, HashtagRegex } from "Const";
|
||||||
import { RelaySettings } from "Nostr/Connection";
|
import { RelaySettings } from "Nostr/Connection";
|
||||||
|
|
||||||
@ -40,9 +41,12 @@ export default function useEventPublisher() {
|
|||||||
async function signEvent(ev: NEvent): Promise<NEvent> {
|
async function signEvent(ev: NEvent): Promise<NEvent> {
|
||||||
if (hasNip07 && !privKey) {
|
if (hasNip07 && !privKey) {
|
||||||
ev.Id = await ev.CreateId();
|
ev.Id = await ev.CreateId();
|
||||||
let tmpEv = await barierNip07(() =>
|
const tmpEv = (await barrierNip07(() =>
|
||||||
window.nostr.signEvent(ev.ToObject())
|
window.nostr.signEvent(ev.ToObject())
|
||||||
);
|
)) as TaggedRawEvent;
|
||||||
|
if (!tmpEv.relays) {
|
||||||
|
tmpEv.relays = [];
|
||||||
|
}
|
||||||
return new NEvent(tmpEv);
|
return new NEvent(tmpEv);
|
||||||
} else if (privKey) {
|
} else if (privKey) {
|
||||||
await ev.Sign(privKey);
|
await ev.Sign(privKey);
|
||||||
@ -111,14 +115,14 @@ export default function useEventPublisher() {
|
|||||||
*/
|
*/
|
||||||
broadcastForBootstrap: (ev: NEvent | undefined) => {
|
broadcastForBootstrap: (ev: NEvent | undefined) => {
|
||||||
if (ev) {
|
if (ev) {
|
||||||
for (let [k, _] of DefaultRelays) {
|
for (const [k] of DefaultRelays) {
|
||||||
System.WriteOnceToRelay(k, ev);
|
System.WriteOnceToRelay(k, ev);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
muted: async (keys: HexKey[], priv: HexKey[]) => {
|
muted: async (keys: HexKey[], priv: HexKey[]) => {
|
||||||
if (pubKey) {
|
if (pubKey) {
|
||||||
let ev = NEvent.ForPubKey(pubKey);
|
const ev = NEvent.ForPubKey(pubKey);
|
||||||
ev.Kind = EventKind.Lists;
|
ev.Kind = EventKind.Lists;
|
||||||
ev.Tags.push(new Tag(["d", Lists.Muted], ev.Tags.length));
|
ev.Tags.push(new Tag(["d", Lists.Muted], ev.Tags.length));
|
||||||
keys.forEach((p) => {
|
keys.forEach((p) => {
|
||||||
@ -129,7 +133,7 @@ export default function useEventPublisher() {
|
|||||||
const ps = priv.map((p) => ["p", p]);
|
const ps = priv.map((p) => ["p", p]);
|
||||||
const plaintext = JSON.stringify(ps);
|
const plaintext = JSON.stringify(ps);
|
||||||
if (hasNip07 && !privKey) {
|
if (hasNip07 && !privKey) {
|
||||||
content = await barierNip07(() =>
|
content = await barrierNip07(() =>
|
||||||
window.nostr.nip04.encrypt(pubKey, plaintext)
|
window.nostr.nip04.encrypt(pubKey, plaintext)
|
||||||
);
|
);
|
||||||
} else if (privKey) {
|
} else if (privKey) {
|
||||||
@ -142,7 +146,7 @@ export default function useEventPublisher() {
|
|||||||
},
|
},
|
||||||
metadata: async (obj: UserMetadata) => {
|
metadata: async (obj: UserMetadata) => {
|
||||||
if (pubKey) {
|
if (pubKey) {
|
||||||
let ev = NEvent.ForPubKey(pubKey);
|
const ev = NEvent.ForPubKey(pubKey);
|
||||||
ev.Kind = EventKind.SetMetadata;
|
ev.Kind = EventKind.SetMetadata;
|
||||||
ev.Content = JSON.stringify(obj);
|
ev.Content = JSON.stringify(obj);
|
||||||
return await signEvent(ev);
|
return await signEvent(ev);
|
||||||
@ -150,7 +154,7 @@ export default function useEventPublisher() {
|
|||||||
},
|
},
|
||||||
note: async (msg: string) => {
|
note: async (msg: string) => {
|
||||||
if (pubKey) {
|
if (pubKey) {
|
||||||
let ev = NEvent.ForPubKey(pubKey);
|
const ev = NEvent.ForPubKey(pubKey);
|
||||||
ev.Kind = EventKind.TextNote;
|
ev.Kind = EventKind.TextNote;
|
||||||
processContent(ev, msg);
|
processContent(ev, msg);
|
||||||
return await signEvent(ev);
|
return await signEvent(ev);
|
||||||
@ -158,18 +162,14 @@ export default function useEventPublisher() {
|
|||||||
},
|
},
|
||||||
zap: async (author: HexKey, note?: HexKey, msg?: string) => {
|
zap: async (author: HexKey, note?: HexKey, msg?: string) => {
|
||||||
if (pubKey) {
|
if (pubKey) {
|
||||||
let ev = NEvent.ForPubKey(pubKey);
|
const ev = NEvent.ForPubKey(pubKey);
|
||||||
ev.Kind = EventKind.ZapRequest;
|
ev.Kind = EventKind.ZapRequest;
|
||||||
if (note) {
|
if (note) {
|
||||||
// @ts-ignore
|
ev.Tags.push(new Tag(["e", note], 0));
|
||||||
ev.Tags.push(new Tag(["e", note]));
|
|
||||||
}
|
}
|
||||||
// @ts-ignore
|
ev.Tags.push(new Tag(["p", author], 0));
|
||||||
ev.Tags.push(new Tag(["p", author]));
|
|
||||||
// @ts-ignore
|
|
||||||
const relayTag = ["relays", ...Object.keys(relays).slice(0, 10)];
|
const relayTag = ["relays", ...Object.keys(relays).slice(0, 10)];
|
||||||
// @ts-ignore
|
ev.Tags.push(new Tag(relayTag, 0));
|
||||||
ev.Tags.push(new Tag(relayTag));
|
|
||||||
processContent(ev, msg || "");
|
processContent(ev, msg || "");
|
||||||
return await signEvent(ev);
|
return await signEvent(ev);
|
||||||
}
|
}
|
||||||
@ -179,15 +179,20 @@ export default function useEventPublisher() {
|
|||||||
*/
|
*/
|
||||||
reply: async (replyTo: NEvent, msg: string) => {
|
reply: async (replyTo: NEvent, msg: string) => {
|
||||||
if (pubKey) {
|
if (pubKey) {
|
||||||
let ev = NEvent.ForPubKey(pubKey);
|
const ev = NEvent.ForPubKey(pubKey);
|
||||||
ev.Kind = EventKind.TextNote;
|
ev.Kind = EventKind.TextNote;
|
||||||
|
|
||||||
let thread = replyTo.Thread;
|
const thread = replyTo.Thread;
|
||||||
if (thread) {
|
if (thread) {
|
||||||
if (thread.Root || thread.ReplyTo) {
|
if (thread.Root || thread.ReplyTo) {
|
||||||
ev.Tags.push(
|
ev.Tags.push(
|
||||||
new Tag(
|
new Tag(
|
||||||
["e", thread.Root?.Event ?? thread.ReplyTo?.Event!, "", "root"],
|
[
|
||||||
|
"e",
|
||||||
|
thread.Root?.Event ?? thread.ReplyTo?.Event ?? "",
|
||||||
|
"",
|
||||||
|
"root",
|
||||||
|
],
|
||||||
ev.Tags.length
|
ev.Tags.length
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@ -199,7 +204,7 @@ export default function useEventPublisher() {
|
|||||||
ev.Tags.push(new Tag(["p", replyTo.PubKey], ev.Tags.length));
|
ev.Tags.push(new Tag(["p", replyTo.PubKey], ev.Tags.length));
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let pk of thread.PubKeys) {
|
for (const pk of thread.PubKeys) {
|
||||||
if (pk === pubKey) {
|
if (pk === pubKey) {
|
||||||
continue; // dont tag self in replies
|
continue; // dont tag self in replies
|
||||||
}
|
}
|
||||||
@ -218,7 +223,7 @@ export default function useEventPublisher() {
|
|||||||
},
|
},
|
||||||
react: async (evRef: NEvent, content = "+") => {
|
react: async (evRef: NEvent, content = "+") => {
|
||||||
if (pubKey) {
|
if (pubKey) {
|
||||||
let ev = NEvent.ForPubKey(pubKey);
|
const ev = NEvent.ForPubKey(pubKey);
|
||||||
ev.Kind = EventKind.Reaction;
|
ev.Kind = EventKind.Reaction;
|
||||||
ev.Content = content;
|
ev.Content = content;
|
||||||
ev.Tags.push(new Tag(["e", evRef.Id], 0));
|
ev.Tags.push(new Tag(["e", evRef.Id], 0));
|
||||||
@ -228,10 +233,10 @@ export default function useEventPublisher() {
|
|||||||
},
|
},
|
||||||
saveRelays: async () => {
|
saveRelays: async () => {
|
||||||
if (pubKey) {
|
if (pubKey) {
|
||||||
let ev = NEvent.ForPubKey(pubKey);
|
const ev = NEvent.ForPubKey(pubKey);
|
||||||
ev.Kind = EventKind.ContactList;
|
ev.Kind = EventKind.ContactList;
|
||||||
ev.Content = JSON.stringify(relays);
|
ev.Content = JSON.stringify(relays);
|
||||||
for (let pk of follows) {
|
for (const pk of follows) {
|
||||||
ev.Tags.push(new Tag(["p", pk], ev.Tags.length));
|
ev.Tags.push(new Tag(["p", pk], ev.Tags.length));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -243,16 +248,16 @@ export default function useEventPublisher() {
|
|||||||
newRelays?: Record<string, RelaySettings>
|
newRelays?: Record<string, RelaySettings>
|
||||||
) => {
|
) => {
|
||||||
if (pubKey) {
|
if (pubKey) {
|
||||||
let ev = NEvent.ForPubKey(pubKey);
|
const ev = NEvent.ForPubKey(pubKey);
|
||||||
ev.Kind = EventKind.ContactList;
|
ev.Kind = EventKind.ContactList;
|
||||||
ev.Content = JSON.stringify(newRelays ?? relays);
|
ev.Content = JSON.stringify(newRelays ?? relays);
|
||||||
let temp = new Set(follows);
|
const temp = new Set(follows);
|
||||||
if (Array.isArray(pkAdd)) {
|
if (Array.isArray(pkAdd)) {
|
||||||
pkAdd.forEach((a) => temp.add(a));
|
pkAdd.forEach((a) => temp.add(a));
|
||||||
} else {
|
} else {
|
||||||
temp.add(pkAdd);
|
temp.add(pkAdd);
|
||||||
}
|
}
|
||||||
for (let pk of temp) {
|
for (const pk of temp) {
|
||||||
if (pk.length !== 64) {
|
if (pk.length !== 64) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -264,10 +269,10 @@ export default function useEventPublisher() {
|
|||||||
},
|
},
|
||||||
removeFollow: async (pkRemove: HexKey) => {
|
removeFollow: async (pkRemove: HexKey) => {
|
||||||
if (pubKey) {
|
if (pubKey) {
|
||||||
let ev = NEvent.ForPubKey(pubKey);
|
const ev = NEvent.ForPubKey(pubKey);
|
||||||
ev.Kind = EventKind.ContactList;
|
ev.Kind = EventKind.ContactList;
|
||||||
ev.Content = JSON.stringify(relays);
|
ev.Content = JSON.stringify(relays);
|
||||||
for (let pk of follows) {
|
for (const pk of follows) {
|
||||||
if (pk === pkRemove || pk.length !== 64) {
|
if (pk === pkRemove || pk.length !== 64) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -282,7 +287,7 @@ export default function useEventPublisher() {
|
|||||||
*/
|
*/
|
||||||
delete: async (id: u256) => {
|
delete: async (id: u256) => {
|
||||||
if (pubKey) {
|
if (pubKey) {
|
||||||
let ev = NEvent.ForPubKey(pubKey);
|
const ev = NEvent.ForPubKey(pubKey);
|
||||||
ev.Kind = EventKind.Deletion;
|
ev.Kind = EventKind.Deletion;
|
||||||
ev.Content = "";
|
ev.Content = "";
|
||||||
ev.Tags.push(new Tag(["e", id], 0));
|
ev.Tags.push(new Tag(["e", id], 0));
|
||||||
@ -290,11 +295,11 @@ export default function useEventPublisher() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Respot a note (NIP-18)
|
* Repost a note (NIP-18)
|
||||||
*/
|
*/
|
||||||
repost: async (note: NEvent) => {
|
repost: async (note: NEvent) => {
|
||||||
if (pubKey) {
|
if (pubKey) {
|
||||||
let ev = NEvent.ForPubKey(pubKey);
|
const ev = NEvent.ForPubKey(pubKey);
|
||||||
ev.Kind = EventKind.Repost;
|
ev.Kind = EventKind.Repost;
|
||||||
ev.Content = JSON.stringify(note.Original);
|
ev.Content = JSON.stringify(note.Original);
|
||||||
ev.Tags.push(new Tag(["e", note.Id], 0));
|
ev.Tags.push(new Tag(["e", note.Id], 0));
|
||||||
@ -311,12 +316,12 @@ export default function useEventPublisher() {
|
|||||||
return "<CANT DECRYPT>";
|
return "<CANT DECRYPT>";
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
let otherPubKey =
|
const otherPubKey =
|
||||||
note.PubKey === pubKey
|
note.PubKey === pubKey
|
||||||
? note.Tags.filter((a) => a.Key === "p")[0].PubKey!
|
? unwrap(note.Tags.filter((a) => a.Key === "p")[0].PubKey)
|
||||||
: note.PubKey;
|
: note.PubKey;
|
||||||
if (hasNip07 && !privKey) {
|
if (hasNip07 && !privKey) {
|
||||||
return await barierNip07(() =>
|
return await barrierNip07(() =>
|
||||||
window.nostr.nip04.decrypt(otherPubKey, note.Content)
|
window.nostr.nip04.decrypt(otherPubKey, note.Content)
|
||||||
);
|
);
|
||||||
} else if (privKey) {
|
} else if (privKey) {
|
||||||
@ -324,21 +329,21 @@ export default function useEventPublisher() {
|
|||||||
return note.Content;
|
return note.Content;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Decyrption failed", e);
|
console.error("Decryption failed", e);
|
||||||
return "<DECRYPTION FAILED>";
|
return "<DECRYPTION FAILED>";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
sendDm: async (content: string, to: HexKey) => {
|
sendDm: async (content: string, to: HexKey) => {
|
||||||
if (pubKey) {
|
if (pubKey) {
|
||||||
let ev = NEvent.ForPubKey(pubKey);
|
const ev = NEvent.ForPubKey(pubKey);
|
||||||
ev.Kind = EventKind.DirectMessage;
|
ev.Kind = EventKind.DirectMessage;
|
||||||
ev.Content = content;
|
ev.Content = content;
|
||||||
ev.Tags.push(new Tag(["p", to], 0));
|
ev.Tags.push(new Tag(["p", to], 0));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (hasNip07 && !privKey) {
|
if (hasNip07 && !privKey) {
|
||||||
let cx: string = await barierNip07(() =>
|
const cx: string = await barrierNip07(() =>
|
||||||
window.nostr.nip04.encrypt(to, content)
|
window.nostr.nip04.encrypt(to, content)
|
||||||
);
|
);
|
||||||
ev.Content = cx;
|
ev.Content = cx;
|
||||||
@ -358,12 +363,12 @@ export default function useEventPublisher() {
|
|||||||
let isNip07Busy = false;
|
let isNip07Busy = false;
|
||||||
|
|
||||||
const delay = (t: number) => {
|
const delay = (t: number) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve) => {
|
||||||
setTimeout(resolve, t);
|
setTimeout(resolve, t);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const barierNip07 = async (then: () => Promise<any>) => {
|
export const barrierNip07 = async <T>(then: () => Promise<T>): Promise<T> => {
|
||||||
while (isNip07Busy) {
|
while (isNip07Busy) {
|
||||||
await delay(10);
|
await delay(10);
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ import useSubscription from "Feed/Subscription";
|
|||||||
|
|
||||||
export default function useFollowersFeed(pubkey: HexKey) {
|
export default function useFollowersFeed(pubkey: HexKey) {
|
||||||
const sub = useMemo(() => {
|
const sub = useMemo(() => {
|
||||||
let x = new Subscriptions();
|
const x = new Subscriptions();
|
||||||
x.Id = `followers:${pubkey.slice(0, 12)}`;
|
x.Id = `followers:${pubkey.slice(0, 12)}`;
|
||||||
x.Kinds = new Set([EventKind.ContactList]);
|
x.Kinds = new Set([EventKind.ContactList]);
|
||||||
x.PTags = new Set([pubkey]);
|
x.PTags = new Set([pubkey]);
|
||||||
|
@ -6,7 +6,7 @@ import useSubscription, { NoteStore } from "Feed/Subscription";
|
|||||||
|
|
||||||
export default function useFollowsFeed(pubkey: HexKey) {
|
export default function useFollowsFeed(pubkey: HexKey) {
|
||||||
const sub = useMemo(() => {
|
const sub = useMemo(() => {
|
||||||
let x = new Subscriptions();
|
const x = new Subscriptions();
|
||||||
x.Id = `follows:${pubkey.slice(0, 12)}`;
|
x.Id = `follows:${pubkey.slice(0, 12)}`;
|
||||||
x.Kinds = new Set([EventKind.ContactList]);
|
x.Kinds = new Set([EventKind.ContactList]);
|
||||||
x.Authors = new Set([pubkey]);
|
x.Authors = new Set([pubkey]);
|
||||||
@ -18,10 +18,10 @@ export default function useFollowsFeed(pubkey: HexKey) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getFollowers(feed: NoteStore, pubkey: HexKey) {
|
export function getFollowers(feed: NoteStore, pubkey: HexKey) {
|
||||||
let contactLists = feed?.notes.filter(
|
const contactLists = feed?.notes.filter(
|
||||||
(a) => a.kind === EventKind.ContactList && a.pubkey === pubkey
|
(a) => a.kind === EventKind.ContactList && a.pubkey === pubkey
|
||||||
);
|
);
|
||||||
let pTags = contactLists?.map((a) =>
|
const pTags = contactLists?.map((a) =>
|
||||||
a.tags.filter((b) => b[0] === "p").map((c) => c[1])
|
a.tags.filter((b) => b[0] === "p").map((c) => c[1])
|
||||||
);
|
);
|
||||||
return [...new Set(pTags?.flat())];
|
return [...new Set(pTags?.flat())];
|
||||||
|
@ -2,6 +2,7 @@ import * as secp from "@noble/secp256k1";
|
|||||||
import * as base64 from "@protobufjs/base64";
|
import * as base64 from "@protobufjs/base64";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import { RootState } from "State/Store";
|
import { RootState } from "State/Store";
|
||||||
|
import { unwrap } from "Util";
|
||||||
|
|
||||||
export interface ImgProxySettings {
|
export interface ImgProxySettings {
|
||||||
url: string;
|
url: string;
|
||||||
@ -21,8 +22,8 @@ export default function useImgProxy() {
|
|||||||
|
|
||||||
async function signUrl(u: string) {
|
async function signUrl(u: string) {
|
||||||
const result = await secp.utils.hmacSha256(
|
const result = await secp.utils.hmacSha256(
|
||||||
secp.utils.hexToBytes(settings!.key),
|
secp.utils.hexToBytes(unwrap(settings).key),
|
||||||
secp.utils.hexToBytes(settings!.salt),
|
secp.utils.hexToBytes(unwrap(settings).salt),
|
||||||
te.encode(u)
|
te.encode(u)
|
||||||
);
|
);
|
||||||
return urlSafe(base64.encode(result, 0, result.byteLength));
|
return urlSafe(base64.encode(result, 0, result.byteLength));
|
||||||
|
@ -19,9 +19,10 @@ import { RootState } from "State/Store";
|
|||||||
import { mapEventToProfile, MetadataCache } from "State/Users";
|
import { mapEventToProfile, MetadataCache } from "State/Users";
|
||||||
import { useDb } from "State/Users/Db";
|
import { useDb } from "State/Users/Db";
|
||||||
import useSubscription from "Feed/Subscription";
|
import useSubscription from "Feed/Subscription";
|
||||||
import { barierNip07 } from "Feed/EventPublisher";
|
import { barrierNip07 } from "Feed/EventPublisher";
|
||||||
import { getMutedKeys, getNewest } from "Feed/MuteList";
|
import { getMutedKeys, getNewest } from "Feed/MuteList";
|
||||||
import useModeration from "Hooks/useModeration";
|
import useModeration from "Hooks/useModeration";
|
||||||
|
import { unwrap } from "Util";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Managed loading data for the current logged in user
|
* Managed loading data for the current logged in user
|
||||||
@ -40,7 +41,7 @@ export default function useLoginFeed() {
|
|||||||
const subMetadata = useMemo(() => {
|
const subMetadata = useMemo(() => {
|
||||||
if (!pubKey) return null;
|
if (!pubKey) return null;
|
||||||
|
|
||||||
let sub = new Subscriptions();
|
const sub = new Subscriptions();
|
||||||
sub.Id = `login:meta`;
|
sub.Id = `login:meta`;
|
||||||
sub.Authors = new Set([pubKey]);
|
sub.Authors = new Set([pubKey]);
|
||||||
sub.Kinds = new Set([EventKind.ContactList, EventKind.SetMetadata]);
|
sub.Kinds = new Set([EventKind.ContactList, EventKind.SetMetadata]);
|
||||||
@ -52,7 +53,7 @@ export default function useLoginFeed() {
|
|||||||
const subNotification = useMemo(() => {
|
const subNotification = useMemo(() => {
|
||||||
if (!pubKey) return null;
|
if (!pubKey) return null;
|
||||||
|
|
||||||
let sub = new Subscriptions();
|
const sub = new Subscriptions();
|
||||||
sub.Id = "login:notifications";
|
sub.Id = "login:notifications";
|
||||||
// todo: add zaps
|
// todo: add zaps
|
||||||
sub.Kinds = new Set([EventKind.TextNote]);
|
sub.Kinds = new Set([EventKind.TextNote]);
|
||||||
@ -64,7 +65,7 @@ export default function useLoginFeed() {
|
|||||||
const subMuted = useMemo(() => {
|
const subMuted = useMemo(() => {
|
||||||
if (!pubKey) return null;
|
if (!pubKey) return null;
|
||||||
|
|
||||||
let sub = new Subscriptions();
|
const sub = new Subscriptions();
|
||||||
sub.Id = "login:muted";
|
sub.Id = "login:muted";
|
||||||
sub.Kinds = new Set([EventKind.Lists]);
|
sub.Kinds = new Set([EventKind.Lists]);
|
||||||
sub.Authors = new Set([pubKey]);
|
sub.Authors = new Set([pubKey]);
|
||||||
@ -77,12 +78,12 @@ export default function useLoginFeed() {
|
|||||||
const subDms = useMemo(() => {
|
const subDms = useMemo(() => {
|
||||||
if (!pubKey) return null;
|
if (!pubKey) return null;
|
||||||
|
|
||||||
let dms = new Subscriptions();
|
const dms = new Subscriptions();
|
||||||
dms.Id = "login:dms";
|
dms.Id = "login:dms";
|
||||||
dms.Kinds = new Set([EventKind.DirectMessage]);
|
dms.Kinds = new Set([EventKind.DirectMessage]);
|
||||||
dms.PTags = new Set([pubKey]);
|
dms.PTags = new Set([pubKey]);
|
||||||
|
|
||||||
let dmsFromME = new Subscriptions();
|
const dmsFromME = new Subscriptions();
|
||||||
dmsFromME.Authors = new Set([pubKey]);
|
dmsFromME.Authors = new Set([pubKey]);
|
||||||
dmsFromME.Kinds = new Set([EventKind.DirectMessage]);
|
dmsFromME.Kinds = new Set([EventKind.DirectMessage]);
|
||||||
dms.AddSubscription(dmsFromME);
|
dms.AddSubscription(dmsFromME);
|
||||||
@ -102,28 +103,28 @@ export default function useLoginFeed() {
|
|||||||
const mutedFeed = useSubscription(subMuted, { leaveOpen: true, cache: true });
|
const mutedFeed = useSubscription(subMuted, { leaveOpen: true, cache: true });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let contactList = metadataFeed.store.notes.filter(
|
const contactList = metadataFeed.store.notes.filter(
|
||||||
(a) => a.kind === EventKind.ContactList
|
(a) => a.kind === EventKind.ContactList
|
||||||
);
|
);
|
||||||
let metadata = metadataFeed.store.notes.filter(
|
const metadata = metadataFeed.store.notes.filter(
|
||||||
(a) => a.kind === EventKind.SetMetadata
|
(a) => a.kind === EventKind.SetMetadata
|
||||||
);
|
);
|
||||||
let profiles = metadata
|
const profiles = metadata
|
||||||
.map((a) => mapEventToProfile(a))
|
.map((a) => mapEventToProfile(a))
|
||||||
.filter((a) => a !== undefined)
|
.filter((a) => a !== undefined)
|
||||||
.map((a) => a!);
|
.map((a) => unwrap(a));
|
||||||
|
|
||||||
for (let cl of contactList) {
|
for (const cl of contactList) {
|
||||||
if (cl.content !== "" && cl.content !== "{}") {
|
if (cl.content !== "" && cl.content !== "{}") {
|
||||||
let relays = JSON.parse(cl.content);
|
const relays = JSON.parse(cl.content);
|
||||||
dispatch(setRelays({ relays, createdAt: cl.created_at }));
|
dispatch(setRelays({ relays, createdAt: cl.created_at }));
|
||||||
}
|
}
|
||||||
let pTags = cl.tags.filter((a) => a[0] === "p").map((a) => a[1]);
|
const pTags = cl.tags.filter((a) => a[0] === "p").map((a) => a[1]);
|
||||||
dispatch(setFollows({ keys: pTags, createdAt: cl.created_at }));
|
dispatch(setFollows({ keys: pTags, createdAt: cl.created_at }));
|
||||||
}
|
}
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
let maxProfile = profiles.reduce(
|
const maxProfile = profiles.reduce(
|
||||||
(acc, v) => {
|
(acc, v) => {
|
||||||
if (v.created > acc.created) {
|
if (v.created > acc.created) {
|
||||||
acc.profile = v;
|
acc.profile = v;
|
||||||
@ -134,7 +135,7 @@ export default function useLoginFeed() {
|
|||||||
{ created: 0, profile: null as MetadataCache | null }
|
{ created: 0, profile: null as MetadataCache | null }
|
||||||
);
|
);
|
||||||
if (maxProfile.profile) {
|
if (maxProfile.profile) {
|
||||||
let existing = await db.find(maxProfile.profile.pubkey);
|
const existing = await db.find(maxProfile.profile.pubkey);
|
||||||
if ((existing?.created ?? 0) < maxProfile.created) {
|
if ((existing?.created ?? 0) < maxProfile.created) {
|
||||||
await db.put(maxProfile.profile);
|
await db.put(maxProfile.profile);
|
||||||
}
|
}
|
||||||
@ -153,7 +154,7 @@ export default function useLoginFeed() {
|
|||||||
dispatch(setLatestNotifications(nx.created_at));
|
dispatch(setLatestNotifications(nx.created_at));
|
||||||
makeNotification(db, nx).then((notification) => {
|
makeNotification(db, nx).then((notification) => {
|
||||||
if (notification) {
|
if (notification) {
|
||||||
// @ts-ignore
|
// @ts-expect-error This is typed wrong, but I don't have the time to fix it right now
|
||||||
dispatch(sendNotification(notification));
|
dispatch(sendNotification(notification));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -176,8 +177,8 @@ export default function useLoginFeed() {
|
|||||||
try {
|
try {
|
||||||
const blocked = JSON.parse(plaintext);
|
const blocked = JSON.parse(plaintext);
|
||||||
const keys = blocked
|
const keys = blocked
|
||||||
.filter((p: any) => p && p.length === 2 && p[0] === "p")
|
.filter((p: string) => p && p.length === 2 && p[0] === "p")
|
||||||
.map((p: any) => p[1]);
|
.map((p: string) => p[1]);
|
||||||
dispatch(
|
dispatch(
|
||||||
setBlocked({
|
setBlocked({
|
||||||
keys,
|
keys,
|
||||||
@ -193,7 +194,7 @@ export default function useLoginFeed() {
|
|||||||
}, [dispatch, mutedFeed.store]);
|
}, [dispatch, mutedFeed.store]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let dms = dmsFeed.store.notes.filter(
|
const dms = dmsFeed.store.notes.filter(
|
||||||
(a) => a.kind === EventKind.DirectMessage
|
(a) => a.kind === EventKind.DirectMessage
|
||||||
);
|
);
|
||||||
dispatch(addDirectMessage(dms));
|
dispatch(addDirectMessage(dms));
|
||||||
@ -209,7 +210,7 @@ async function decryptBlocked(
|
|||||||
if (pubKey && privKey) {
|
if (pubKey && privKey) {
|
||||||
return await ev.DecryptData(raw.content, privKey, pubKey);
|
return await ev.DecryptData(raw.content, privKey, pubKey);
|
||||||
} else {
|
} else {
|
||||||
return await barierNip07(() =>
|
return await barrierNip07(() =>
|
||||||
window.nostr.nip04.decrypt(pubKey, raw.content)
|
window.nostr.nip04.decrypt(pubKey, raw.content)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ import useSubscription, { NoteStore } from "Feed/Subscription";
|
|||||||
|
|
||||||
export default function useMutedFeed(pubkey: HexKey) {
|
export default function useMutedFeed(pubkey: HexKey) {
|
||||||
const sub = useMemo(() => {
|
const sub = useMemo(() => {
|
||||||
let sub = new Subscriptions();
|
const sub = new Subscriptions();
|
||||||
sub.Id = `muted:${pubkey.slice(0, 12)}`;
|
sub.Id = `muted:${pubkey.slice(0, 12)}`;
|
||||||
sub.Kinds = new Set([EventKind.Lists]);
|
sub.Kinds = new Set([EventKind.Lists]);
|
||||||
sub.Authors = new Set([pubkey]);
|
sub.Authors = new Set([pubkey]);
|
||||||
@ -44,7 +44,7 @@ export function getMutedKeys(rawNotes: TaggedRawEvent[]): {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getMuted(feed: NoteStore, pubkey: HexKey): HexKey[] {
|
export function getMuted(feed: NoteStore, pubkey: HexKey): HexKey[] {
|
||||||
let lists = feed?.notes.filter(
|
const lists = feed?.notes.filter(
|
||||||
(a) => a.kind === EventKind.Lists && a.pubkey === pubkey
|
(a) => a.kind === EventKind.Lists && a.pubkey === pubkey
|
||||||
);
|
);
|
||||||
return getMutedKeys(lists).keys;
|
return getMutedKeys(lists).keys;
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
import { useSyncExternalStore } from "react";
|
import { useSyncExternalStore } from "react";
|
||||||
import { System } from "Nostr/System";
|
import { System } from "Nostr/System";
|
||||||
import { CustomHook, StateSnapshot } from "Nostr/Connection";
|
import { StateSnapshot } from "Nostr/Connection";
|
||||||
|
|
||||||
const noop = (f: CustomHook) => {
|
const noop = () => {
|
||||||
return () => {};
|
return () => undefined;
|
||||||
};
|
};
|
||||||
const noopState = (): StateSnapshot | undefined => {
|
const noopState = (): StateSnapshot | undefined => {
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function useRelayState(addr: string) {
|
export default function useRelayState(addr: string) {
|
||||||
let c = System.Sockets.get(addr);
|
const c = System.Sockets.get(addr);
|
||||||
return useSyncExternalStore<StateSnapshot | undefined>(
|
return useSyncExternalStore<StateSnapshot | undefined>(
|
||||||
c?.StatusHook.bind(c) ?? noop,
|
c?.StatusHook.bind(c) ?? noop,
|
||||||
c?.GetState.bind(c) ?? noopState
|
c?.GetState.bind(c) ?? noopState
|
||||||
|
@ -2,7 +2,7 @@ import { useEffect, useMemo, useReducer, useState } from "react";
|
|||||||
import { System } from "Nostr/System";
|
import { System } from "Nostr/System";
|
||||||
import { TaggedRawEvent } from "Nostr";
|
import { TaggedRawEvent } from "Nostr";
|
||||||
import { Subscriptions } from "Nostr/Subscriptions";
|
import { Subscriptions } from "Nostr/Subscriptions";
|
||||||
import { debounce } from "Util";
|
import { debounce, unwrap } from "Util";
|
||||||
import { db } from "Db";
|
import { db } from "Db";
|
||||||
|
|
||||||
export type NoteStore = {
|
export type NoteStore = {
|
||||||
@ -17,7 +17,7 @@ export type UseSubscriptionOptions = {
|
|||||||
|
|
||||||
interface ReducerArg {
|
interface ReducerArg {
|
||||||
type: "END" | "EVENT" | "CLEAR";
|
type: "END" | "EVENT" | "CLEAR";
|
||||||
ev?: TaggedRawEvent | Array<TaggedRawEvent>;
|
ev?: TaggedRawEvent | TaggedRawEvent[];
|
||||||
end?: boolean;
|
end?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -25,7 +25,7 @@ function notesReducer(state: NoteStore, arg: ReducerArg) {
|
|||||||
if (arg.type === "END") {
|
if (arg.type === "END") {
|
||||||
return {
|
return {
|
||||||
notes: state.notes,
|
notes: state.notes,
|
||||||
end: arg.end!,
|
end: arg.end ?? false,
|
||||||
} as NoteStore;
|
} as NoteStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,11 +36,11 @@ function notesReducer(state: NoteStore, arg: ReducerArg) {
|
|||||||
} as NoteStore;
|
} as NoteStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
let evs = arg.ev!;
|
let evs = arg.ev;
|
||||||
if (!Array.isArray(evs)) {
|
if (!(evs instanceof Array)) {
|
||||||
evs = [evs];
|
evs = evs === undefined ? [] : [evs];
|
||||||
}
|
}
|
||||||
let existingIds = new Set(state.notes.map((a) => a.id));
|
const existingIds = new Set(state.notes.map((a) => a.id));
|
||||||
evs = evs.filter((a) => !existingIds.has(a.id));
|
evs = evs.filter((a) => !existingIds.has(a.id));
|
||||||
if (evs.length === 0) {
|
if (evs.length === 0) {
|
||||||
return state;
|
return state;
|
||||||
@ -175,7 +175,7 @@ const PreloadNotes = async (id: string): Promise<TaggedRawEvent[]> => {
|
|||||||
const feed = await db.feeds.get(id);
|
const feed = await db.feeds.get(id);
|
||||||
if (feed) {
|
if (feed) {
|
||||||
const events = await db.events.bulkGet(feed.ids);
|
const events = await db.events.bulkGet(feed.ids);
|
||||||
return events.filter((a) => a !== undefined).map((a) => a!);
|
return events.filter((a) => a !== undefined).map((a) => unwrap(a));
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
};
|
};
|
||||||
|
@ -16,9 +16,9 @@ export default function useThreadFeed(id: u256) {
|
|||||||
|
|
||||||
function addId(id: u256[]) {
|
function addId(id: u256[]) {
|
||||||
setTrackingEvent((s) => {
|
setTrackingEvent((s) => {
|
||||||
let orig = new Set(s);
|
const orig = new Set(s);
|
||||||
if (id.some((a) => !orig.has(a))) {
|
if (id.some((a) => !orig.has(a))) {
|
||||||
let tmp = new Set([...s, ...id]);
|
const tmp = new Set([...s, ...id]);
|
||||||
return Array.from(tmp);
|
return Array.from(tmp);
|
||||||
} else {
|
} else {
|
||||||
return s;
|
return s;
|
||||||
@ -55,16 +55,16 @@ export default function useThreadFeed(id: u256) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (main.store) {
|
if (main.store) {
|
||||||
return debounce(200, () => {
|
return debounce(200, () => {
|
||||||
let mainNotes = main.store.notes.filter(
|
const mainNotes = main.store.notes.filter(
|
||||||
(a) => a.kind === EventKind.TextNote
|
(a) => a.kind === EventKind.TextNote
|
||||||
);
|
);
|
||||||
|
|
||||||
let eTags = mainNotes
|
const eTags = mainNotes
|
||||||
.filter((a) => a.kind === EventKind.TextNote)
|
.filter((a) => a.kind === EventKind.TextNote)
|
||||||
.map((a) => a.tags.filter((b) => b[0] === "e").map((b) => b[1]))
|
.map((a) => a.tags.filter((b) => b[0] === "e").map((b) => b[1]))
|
||||||
.flat();
|
.flat();
|
||||||
let ids = mainNotes.map((a) => a.id);
|
const ids = mainNotes.map((a) => a.id);
|
||||||
let allEvents = new Set([...eTags, ...ids]);
|
const allEvents = new Set([...eTags, ...ids]);
|
||||||
addId(Array.from(allEvents));
|
addId(Array.from(allEvents));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
|||||||
import { u256 } from "Nostr";
|
import { u256 } from "Nostr";
|
||||||
import EventKind from "Nostr/EventKind";
|
import EventKind from "Nostr/EventKind";
|
||||||
import { Subscriptions } from "Nostr/Subscriptions";
|
import { Subscriptions } from "Nostr/Subscriptions";
|
||||||
import { unixNow } from "Util";
|
import { unixNow, unwrap } from "Util";
|
||||||
import useSubscription from "Feed/Subscription";
|
import useSubscription from "Feed/Subscription";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import { RootState } from "State/Store";
|
import { RootState } from "State/Store";
|
||||||
@ -38,7 +38,7 @@ export default function useTimelineFeed(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let sub = new Subscriptions();
|
const sub = new Subscriptions();
|
||||||
sub.Id = `timeline:${subject.type}:${subject.discriminator}`;
|
sub.Id = `timeline:${subject.type}:${subject.discriminator}`;
|
||||||
sub.Kinds = new Set([EventKind.TextNote, EventKind.Repost]);
|
sub.Kinds = new Set([EventKind.TextNote, EventKind.Repost]);
|
||||||
switch (subject.type) {
|
switch (subject.type) {
|
||||||
@ -64,7 +64,7 @@ export default function useTimelineFeed(
|
|||||||
}, [subject.type, subject.items, subject.discriminator]);
|
}, [subject.type, subject.items, subject.discriminator]);
|
||||||
|
|
||||||
const sub = useMemo(() => {
|
const sub = useMemo(() => {
|
||||||
let sub = createSub();
|
const sub = createSub();
|
||||||
if (sub) {
|
if (sub) {
|
||||||
if (options.method === "LIMIT_UNTIL") {
|
if (options.method === "LIMIT_UNTIL") {
|
||||||
sub.Until = until;
|
sub.Until = until;
|
||||||
@ -80,7 +80,7 @@ export default function useTimelineFeed(
|
|||||||
if (pref.autoShowLatest) {
|
if (pref.autoShowLatest) {
|
||||||
// copy properties of main sub but with limit 0
|
// copy properties of main sub but with limit 0
|
||||||
// this will put latest directly into main feed
|
// this will put latest directly into main feed
|
||||||
let latestSub = new Subscriptions();
|
const latestSub = new Subscriptions();
|
||||||
latestSub.Authors = sub.Authors;
|
latestSub.Authors = sub.Authors;
|
||||||
latestSub.HashTags = sub.HashTags;
|
latestSub.HashTags = sub.HashTags;
|
||||||
latestSub.PTags = sub.PTags;
|
latestSub.PTags = sub.PTags;
|
||||||
@ -97,7 +97,7 @@ export default function useTimelineFeed(
|
|||||||
const main = useSubscription(sub, { leaveOpen: true, cache: true });
|
const main = useSubscription(sub, { leaveOpen: true, cache: true });
|
||||||
|
|
||||||
const subRealtime = useMemo(() => {
|
const subRealtime = useMemo(() => {
|
||||||
let subLatest = createSub();
|
const subLatest = createSub();
|
||||||
if (subLatest && !pref.autoShowLatest) {
|
if (subLatest && !pref.autoShowLatest) {
|
||||||
subLatest.Id = `${subLatest.Id}:latest`;
|
subLatest.Id = `${subLatest.Id}:latest`;
|
||||||
subLatest.Limit = 1;
|
subLatest.Limit = 1;
|
||||||
@ -131,7 +131,7 @@ export default function useTimelineFeed(
|
|||||||
|
|
||||||
const subParents = useMemo(() => {
|
const subParents = useMemo(() => {
|
||||||
if (trackingParentEvents.length > 0) {
|
if (trackingParentEvents.length > 0) {
|
||||||
let parents = new Subscriptions();
|
const parents = new Subscriptions();
|
||||||
parents.Id = `timeline-parent:${subject.type}`;
|
parents.Id = `timeline-parent:${subject.type}`;
|
||||||
parents.Ids = new Set(trackingParentEvents);
|
parents.Ids = new Set(trackingParentEvents);
|
||||||
return parents;
|
return parents;
|
||||||
@ -144,21 +144,21 @@ export default function useTimelineFeed(
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (main.store.notes.length > 0) {
|
if (main.store.notes.length > 0) {
|
||||||
setTrackingEvent((s) => {
|
setTrackingEvent((s) => {
|
||||||
let ids = main.store.notes.map((a) => a.id);
|
const ids = main.store.notes.map((a) => a.id);
|
||||||
if (ids.some((a) => !s.includes(a))) {
|
if (ids.some((a) => !s.includes(a))) {
|
||||||
return Array.from(new Set([...s, ...ids]));
|
return Array.from(new Set([...s, ...ids]));
|
||||||
}
|
}
|
||||||
return s;
|
return s;
|
||||||
});
|
});
|
||||||
let reposts = main.store.notes
|
const reposts = main.store.notes
|
||||||
.filter((a) => a.kind === EventKind.Repost && a.content === "")
|
.filter((a) => a.kind === EventKind.Repost && a.content === "")
|
||||||
.map((a) => a.tags.find((b) => b[0] === "e"))
|
.map((a) => a.tags.find((b) => b[0] === "e"))
|
||||||
.filter((a) => a)
|
.filter((a) => a)
|
||||||
.map((a) => a![1]);
|
.map((a) => unwrap(a)[1]);
|
||||||
if (reposts.length > 0) {
|
if (reposts.length > 0) {
|
||||||
setTrackingParentEvents((s) => {
|
setTrackingParentEvents((s) => {
|
||||||
if (reposts.some((a) => !s.includes(a))) {
|
if (reposts.some((a) => !s.includes(a))) {
|
||||||
let temp = new Set([...s, ...reposts]);
|
const temp = new Set([...s, ...reposts]);
|
||||||
return Array.from(temp);
|
return Array.from(temp);
|
||||||
}
|
}
|
||||||
return s;
|
return s;
|
||||||
@ -175,7 +175,7 @@ export default function useTimelineFeed(
|
|||||||
loadMore: () => {
|
loadMore: () => {
|
||||||
console.debug("Timeline load more!");
|
console.debug("Timeline load more!");
|
||||||
if (options.method === "LIMIT_UNTIL") {
|
if (options.method === "LIMIT_UNTIL") {
|
||||||
let oldest = main.store.notes.reduce(
|
const oldest = main.store.notes.reduce(
|
||||||
(acc, v) => (acc = v.created_at < acc ? v.created_at : acc),
|
(acc, v) => (acc = v.created_at < acc ? v.created_at : acc),
|
||||||
unixNow()
|
unixNow()
|
||||||
);
|
);
|
||||||
|
@ -6,7 +6,7 @@ import useSubscription from "./Subscription";
|
|||||||
|
|
||||||
export default function useZapsFeed(pubkey: HexKey) {
|
export default function useZapsFeed(pubkey: HexKey) {
|
||||||
const sub = useMemo(() => {
|
const sub = useMemo(() => {
|
||||||
let x = new Subscriptions();
|
const x = new Subscriptions();
|
||||||
x.Id = `zaps:${pubkey.slice(0, 12)}`;
|
x.Id = `zaps:${pubkey.slice(0, 12)}`;
|
||||||
x.Kinds = new Set([EventKind.ZapReceipt]);
|
x.Kinds = new Set([EventKind.ZapReceipt]);
|
||||||
x.PTags = new Set([pubkey]);
|
x.PTags = new Set([pubkey]);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useRef, WheelEvent, LegacyRef } from "react";
|
import { useEffect, useRef, LegacyRef } from "react";
|
||||||
|
|
||||||
function useHorizontalScroll() {
|
function useHorizontalScroll() {
|
||||||
const elRef = useRef<HTMLDivElement>();
|
const elRef = useRef<HTMLDivElement>();
|
||||||
@ -10,9 +10,7 @@ function useHorizontalScroll() {
|
|||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
el.scrollTo({ left: el.scrollLeft + ev.deltaY, behavior: "smooth" });
|
el.scrollTo({ left: el.scrollLeft + ev.deltaY, behavior: "smooth" });
|
||||||
};
|
};
|
||||||
// @ts-ignore
|
|
||||||
el.addEventListener("wheel", onWheel);
|
el.addEventListener("wheel", onWheel);
|
||||||
// @ts-ignore
|
|
||||||
return () => el.removeEventListener("wheel", onWheel);
|
return () => el.removeEventListener("wheel", onWheel);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
@ -5,7 +5,7 @@ declare global {
|
|||||||
webln?: {
|
webln?: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
enable: () => Promise<void>;
|
enable: () => Promise<void>;
|
||||||
sendPayment: (pr: string) => Promise<any>;
|
sendPayment: (pr: string) => Promise<unknown>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -15,7 +15,7 @@ export default function useWebln(enable = true) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (maybeWebLn && !maybeWebLn.enabled && enable) {
|
if (maybeWebLn && !maybeWebLn.enabled && enable) {
|
||||||
maybeWebLn.enable().catch((error) => {
|
maybeWebLn.enable().catch(() => {
|
||||||
console.debug("Couldn't enable WebLN");
|
console.debug("Couldn't enable WebLN");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
import IconProps from "./IconProps";
|
const Attachment = () => {
|
||||||
|
|
||||||
const Attachment = (props: IconProps) => {
|
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
width="21"
|
width="21"
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
import IconProps from "./IconProps";
|
const Logout = () => {
|
||||||
|
|
||||||
const Logout = (props: IconProps) => {
|
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
width="22"
|
width="22"
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import IconProps from "./IconProps";
|
|
||||||
|
|
||||||
const Reply = () => {
|
const Reply = () => {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
|
@ -107,11 +107,11 @@ export class ServiceProvider {
|
|||||||
async _GetJson<T>(
|
async _GetJson<T>(
|
||||||
path: string,
|
path: string,
|
||||||
method?: "GET" | string,
|
method?: "GET" | string,
|
||||||
body?: any,
|
body?: { [key: string]: string },
|
||||||
headers?: any
|
headers?: { [key: string]: string }
|
||||||
): Promise<T | ServiceError> {
|
): Promise<T | ServiceError> {
|
||||||
try {
|
try {
|
||||||
let rsp = await fetch(`${this.url}${path}`, {
|
const rsp = await fetch(`${this.url}${path}`, {
|
||||||
method: method,
|
method: method,
|
||||||
body: body ? JSON.stringify(body) : undefined,
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
headers: {
|
headers: {
|
||||||
@ -121,7 +121,7 @@ export class ServiceProvider {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
let obj = await rsp.json();
|
const obj = await rsp.json();
|
||||||
if ("error" in obj) {
|
if ("error" in obj) {
|
||||||
return <ServiceError>obj;
|
return <ServiceError>obj;
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ import { RawEvent, TaggedRawEvent, u256 } from "Nostr";
|
|||||||
import { RelayInfo } from "./RelayInfo";
|
import { RelayInfo } from "./RelayInfo";
|
||||||
import Nips from "./Nips";
|
import Nips from "./Nips";
|
||||||
import { System } from "./System";
|
import { System } from "./System";
|
||||||
|
import { unwrap } from "Util";
|
||||||
|
|
||||||
export type CustomHook = (state: Readonly<StateSnapshot>) => void;
|
export type CustomHook = (state: Readonly<StateSnapshot>) => void;
|
||||||
|
|
||||||
@ -51,7 +52,7 @@ export default class Connection {
|
|||||||
LastState: Readonly<StateSnapshot>;
|
LastState: Readonly<StateSnapshot>;
|
||||||
IsClosed: boolean;
|
IsClosed: boolean;
|
||||||
ReconnectTimer: ReturnType<typeof setTimeout> | null;
|
ReconnectTimer: ReturnType<typeof setTimeout> | null;
|
||||||
EventsCallback: Map<u256, (msg?: any) => void>;
|
EventsCallback: Map<u256, (msg: boolean[]) => void>;
|
||||||
AwaitingAuth: Map<string, boolean>;
|
AwaitingAuth: Map<string, boolean>;
|
||||||
Authed: boolean;
|
Authed: boolean;
|
||||||
|
|
||||||
@ -87,15 +88,15 @@ export default class Connection {
|
|||||||
async Connect() {
|
async Connect() {
|
||||||
try {
|
try {
|
||||||
if (this.Info === undefined) {
|
if (this.Info === undefined) {
|
||||||
let u = new URL(this.Address);
|
const u = new URL(this.Address);
|
||||||
let rsp = await fetch(`https://${u.host}`, {
|
const rsp = await fetch(`https://${u.host}`, {
|
||||||
headers: {
|
headers: {
|
||||||
accept: "application/nostr+json",
|
accept: "application/nostr+json",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (rsp.ok) {
|
if (rsp.ok) {
|
||||||
let data = await rsp.json();
|
const data = await rsp.json();
|
||||||
for (let [k, v] of Object.entries(data)) {
|
for (const [k, v] of Object.entries(data)) {
|
||||||
if (v === "unset" || v === "") {
|
if (v === "unset" || v === "") {
|
||||||
data[k] = undefined;
|
data[k] = undefined;
|
||||||
}
|
}
|
||||||
@ -114,7 +115,7 @@ export default class Connection {
|
|||||||
|
|
||||||
this.IsClosed = false;
|
this.IsClosed = false;
|
||||||
this.Socket = new WebSocket(this.Address);
|
this.Socket = new WebSocket(this.Address);
|
||||||
this.Socket.onopen = (e) => this.OnOpen(e);
|
this.Socket.onopen = () => this.OnOpen();
|
||||||
this.Socket.onmessage = (e) => this.OnMessage(e);
|
this.Socket.onmessage = (e) => this.OnMessage(e);
|
||||||
this.Socket.onerror = (e) => this.OnError(e);
|
this.Socket.onerror = (e) => this.OnError(e);
|
||||||
this.Socket.onclose = (e) => this.OnClose(e);
|
this.Socket.onclose = (e) => this.OnClose(e);
|
||||||
@ -130,7 +131,7 @@ export default class Connection {
|
|||||||
this._UpdateState();
|
this._UpdateState();
|
||||||
}
|
}
|
||||||
|
|
||||||
OnOpen(e: Event) {
|
OnOpen() {
|
||||||
this.ConnectTimeout = DefaultConnectTimeout;
|
this.ConnectTimeout = DefaultConnectTimeout;
|
||||||
this._InitSubscriptions();
|
this._InitSubscriptions();
|
||||||
console.log(`[${this.Address}] Open!`);
|
console.log(`[${this.Address}] Open!`);
|
||||||
@ -157,10 +158,10 @@ export default class Connection {
|
|||||||
this._UpdateState();
|
this._UpdateState();
|
||||||
}
|
}
|
||||||
|
|
||||||
OnMessage(e: MessageEvent<any>) {
|
OnMessage(e: MessageEvent) {
|
||||||
if (e.data.length > 0) {
|
if (e.data.length > 0) {
|
||||||
let msg = JSON.parse(e.data);
|
const msg = JSON.parse(e.data);
|
||||||
let tag = msg[0];
|
const tag = msg[0];
|
||||||
switch (tag) {
|
switch (tag) {
|
||||||
case "AUTH": {
|
case "AUTH": {
|
||||||
this._OnAuthAsync(msg[1]);
|
this._OnAuthAsync(msg[1]);
|
||||||
@ -183,7 +184,7 @@ export default class Connection {
|
|||||||
console.debug("OK: ", msg);
|
console.debug("OK: ", msg);
|
||||||
const id = msg[1];
|
const id = msg[1];
|
||||||
if (this.EventsCallback.has(id)) {
|
if (this.EventsCallback.has(id)) {
|
||||||
let cb = this.EventsCallback.get(id)!;
|
const cb = unwrap(this.EventsCallback.get(id));
|
||||||
this.EventsCallback.delete(id);
|
this.EventsCallback.delete(id);
|
||||||
cb(msg);
|
cb(msg);
|
||||||
}
|
}
|
||||||
@ -213,7 +214,7 @@ export default class Connection {
|
|||||||
if (!this.Settings.write) {
|
if (!this.Settings.write) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let req = ["EVENT", e.ToObject()];
|
const req = ["EVENT", e.ToObject()];
|
||||||
this._SendJson(req);
|
this._SendJson(req);
|
||||||
this.Stats.EventsSent++;
|
this.Stats.EventsSent++;
|
||||||
this._UpdateState();
|
this._UpdateState();
|
||||||
@ -222,13 +223,13 @@ export default class Connection {
|
|||||||
/**
|
/**
|
||||||
* Send event on this connection and wait for OK response
|
* Send event on this connection and wait for OK response
|
||||||
*/
|
*/
|
||||||
async SendAsync(e: NEvent, timeout: number = 5000) {
|
async SendAsync(e: NEvent, timeout = 5000) {
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve) => {
|
||||||
if (!this.Settings.write) {
|
if (!this.Settings.write) {
|
||||||
resolve();
|
resolve();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let t = setTimeout(() => {
|
const t = setTimeout(() => {
|
||||||
resolve();
|
resolve();
|
||||||
}, timeout);
|
}, timeout);
|
||||||
this.EventsCallback.set(e.Id, () => {
|
this.EventsCallback.set(e.Id, () => {
|
||||||
@ -236,7 +237,7 @@ export default class Connection {
|
|||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
|
|
||||||
let req = ["EVENT", e.ToObject()];
|
const req = ["EVENT", e.ToObject()];
|
||||||
this._SendJson(req);
|
this._SendJson(req);
|
||||||
this.Stats.EventsSent++;
|
this.Stats.EventsSent++;
|
||||||
this._UpdateState();
|
this._UpdateState();
|
||||||
@ -269,7 +270,7 @@ export default class Connection {
|
|||||||
*/
|
*/
|
||||||
RemoveSubscription(subId: string) {
|
RemoveSubscription(subId: string) {
|
||||||
if (this.Subscriptions.has(subId)) {
|
if (this.Subscriptions.has(subId)) {
|
||||||
let req = ["CLOSE", subId];
|
const req = ["CLOSE", subId];
|
||||||
this._SendJson(req);
|
this._SendJson(req);
|
||||||
this.Subscriptions.delete(subId);
|
this.Subscriptions.delete(subId);
|
||||||
return true;
|
return true;
|
||||||
@ -281,7 +282,7 @@ export default class Connection {
|
|||||||
* Hook status for connection
|
* Hook status for connection
|
||||||
*/
|
*/
|
||||||
StatusHook(fnHook: CustomHook) {
|
StatusHook(fnHook: CustomHook) {
|
||||||
let id = uuid();
|
const id = uuid();
|
||||||
this.StateHooks.set(id, fnHook);
|
this.StateHooks.set(id, fnHook);
|
||||||
return () => {
|
return () => {
|
||||||
this.StateHooks.delete(id);
|
this.StateHooks.delete(id);
|
||||||
@ -324,20 +325,20 @@ export default class Connection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_NotifyState() {
|
_NotifyState() {
|
||||||
let state = this.GetState();
|
const state = this.GetState();
|
||||||
for (let [_, h] of this.StateHooks) {
|
for (const [, h] of this.StateHooks) {
|
||||||
h(state);
|
h(state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_InitSubscriptions() {
|
_InitSubscriptions() {
|
||||||
// send pending
|
// send pending
|
||||||
for (let p of this.Pending) {
|
for (const p of this.Pending) {
|
||||||
this._SendJson(p);
|
this._SendJson(p);
|
||||||
}
|
}
|
||||||
this.Pending = [];
|
this.Pending = [];
|
||||||
|
|
||||||
for (let [_, s] of this.Subscriptions) {
|
for (const [, s] of this.Subscriptions) {
|
||||||
this._SendSubscription(s);
|
this._SendSubscription(s);
|
||||||
}
|
}
|
||||||
this._UpdateState();
|
this._UpdateState();
|
||||||
@ -357,19 +358,20 @@ export default class Connection {
|
|||||||
this._SendJson(req);
|
this._SendJson(req);
|
||||||
}
|
}
|
||||||
|
|
||||||
_SendJson(obj: any) {
|
_SendJson(obj: Subscriptions | object) {
|
||||||
if (this.Socket?.readyState !== WebSocket.OPEN) {
|
if (this.Socket?.readyState !== WebSocket.OPEN) {
|
||||||
|
// @ts-expect-error TODO @v0l please figure this out... what the hell is going on
|
||||||
this.Pending.push(obj);
|
this.Pending.push(obj);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let json = JSON.stringify(obj);
|
const json = JSON.stringify(obj);
|
||||||
this.Socket.send(json);
|
this.Socket.send(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
_OnEvent(subId: string, ev: RawEvent) {
|
_OnEvent(subId: string, ev: RawEvent) {
|
||||||
if (this.Subscriptions.has(subId)) {
|
if (this.Subscriptions.has(subId)) {
|
||||||
//this._VerifySig(ev);
|
//this._VerifySig(ev);
|
||||||
let tagged: TaggedRawEvent = {
|
const tagged: TaggedRawEvent = {
|
||||||
...ev,
|
...ev,
|
||||||
relays: [this.Address],
|
relays: [this.Address],
|
||||||
};
|
};
|
||||||
@ -386,18 +388,18 @@ export default class Connection {
|
|||||||
};
|
};
|
||||||
this.AwaitingAuth.set(challenge, true);
|
this.AwaitingAuth.set(challenge, true);
|
||||||
const authEvent = await System.nip42Auth(challenge, this.Address);
|
const authEvent = await System.nip42Auth(challenge, this.Address);
|
||||||
return new Promise((resolve, _) => {
|
return new Promise((resolve) => {
|
||||||
if (!authEvent) {
|
if (!authEvent) {
|
||||||
authCleanup();
|
authCleanup();
|
||||||
return Promise.reject("no event");
|
return Promise.reject("no event");
|
||||||
}
|
}
|
||||||
|
|
||||||
let t = setTimeout(() => {
|
const t = setTimeout(() => {
|
||||||
authCleanup();
|
authCleanup();
|
||||||
resolve();
|
resolve();
|
||||||
}, 10_000);
|
}, 10_000);
|
||||||
|
|
||||||
this.EventsCallback.set(authEvent.Id, (msg: any[]) => {
|
this.EventsCallback.set(authEvent.Id, (msg: boolean[]) => {
|
||||||
clearTimeout(t);
|
clearTimeout(t);
|
||||||
authCleanup();
|
authCleanup();
|
||||||
if (msg.length > 3 && msg[2] === true) {
|
if (msg.length > 3 && msg[2] === true) {
|
||||||
@ -407,7 +409,7 @@ export default class Connection {
|
|||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
|
|
||||||
let req = ["AUTH", authEvent.ToObject()];
|
const req = ["AUTH", authEvent.ToObject()];
|
||||||
this._SendJson(req);
|
this._SendJson(req);
|
||||||
this.Stats.EventsSent++;
|
this.Stats.EventsSent++;
|
||||||
this._UpdateState();
|
this._UpdateState();
|
||||||
@ -415,13 +417,13 @@ export default class Connection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_OnEnd(subId: string) {
|
_OnEnd(subId: string) {
|
||||||
let sub = this.Subscriptions.get(subId);
|
const sub = this.Subscriptions.get(subId);
|
||||||
if (sub) {
|
if (sub) {
|
||||||
let now = new Date().getTime();
|
const now = new Date().getTime();
|
||||||
let started = sub.Started.get(this.Address);
|
const started = sub.Started.get(this.Address);
|
||||||
sub.Finished.set(this.Address, now);
|
sub.Finished.set(this.Address, now);
|
||||||
if (started) {
|
if (started) {
|
||||||
let responseTime = now - started;
|
const responseTime = now - started;
|
||||||
if (responseTime > 10_000) {
|
if (responseTime > 10_000) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`[${this.Address}][${subId}] Slow response time ${(
|
`[${this.Address}][${subId}] Slow response time ${(
|
||||||
@ -441,14 +443,14 @@ export default class Connection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_VerifySig(ev: RawEvent) {
|
_VerifySig(ev: RawEvent) {
|
||||||
let payload = [0, ev.pubkey, ev.created_at, ev.kind, ev.tags, ev.content];
|
const payload = [0, ev.pubkey, ev.created_at, ev.kind, ev.tags, ev.content];
|
||||||
|
|
||||||
let payloadData = new TextEncoder().encode(JSON.stringify(payload));
|
const payloadData = new TextEncoder().encode(JSON.stringify(payload));
|
||||||
if (secp.utils.sha256Sync === undefined) {
|
if (secp.utils.sha256Sync === undefined) {
|
||||||
throw "Cannot verify event, no sync sha256 method";
|
throw "Cannot verify event, no sync sha256 method";
|
||||||
}
|
}
|
||||||
let data = secp.utils.sha256Sync(payloadData);
|
const data = secp.utils.sha256Sync(payloadData);
|
||||||
let hash = secp.utils.bytesToHex(data);
|
const hash = secp.utils.bytesToHex(data);
|
||||||
if (!secp.schnorr.verifySync(ev.sig, hash, ev.pubkey)) {
|
if (!secp.schnorr.verifySync(ev.sig, hash, ev.pubkey)) {
|
||||||
throw "Sig verify failed";
|
throw "Sig verify failed";
|
||||||
}
|
}
|
||||||
|
@ -67,7 +67,7 @@ export default class Event {
|
|||||||
* Get the pub key of the creator of this event NIP-26
|
* Get the pub key of the creator of this event NIP-26
|
||||||
*/
|
*/
|
||||||
get RootPubKey() {
|
get RootPubKey() {
|
||||||
let delegation = this.Tags.find((a) => a.Key === "delegation");
|
const delegation = this.Tags.find((a) => a.Key === "delegation");
|
||||||
if (delegation?.PubKey) {
|
if (delegation?.PubKey) {
|
||||||
return delegation.PubKey;
|
return delegation.PubKey;
|
||||||
}
|
}
|
||||||
@ -80,7 +80,7 @@ export default class Event {
|
|||||||
async Sign(key: HexKey) {
|
async Sign(key: HexKey) {
|
||||||
this.Id = await this.CreateId();
|
this.Id = await this.CreateId();
|
||||||
|
|
||||||
let sig = await secp.schnorr.sign(this.Id, key);
|
const sig = await secp.schnorr.sign(this.Id, key);
|
||||||
this.Signature = secp.utils.bytesToHex(sig);
|
this.Signature = secp.utils.bytesToHex(sig);
|
||||||
if (!(await this.Verify())) {
|
if (!(await this.Verify())) {
|
||||||
throw "Signing failed";
|
throw "Signing failed";
|
||||||
@ -92,13 +92,13 @@ export default class Event {
|
|||||||
* @returns True if valid signature
|
* @returns True if valid signature
|
||||||
*/
|
*/
|
||||||
async Verify() {
|
async Verify() {
|
||||||
let id = await this.CreateId();
|
const id = await this.CreateId();
|
||||||
let result = await secp.schnorr.verify(this.Signature, id, this.PubKey);
|
const result = await secp.schnorr.verify(this.Signature, id, this.PubKey);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async CreateId() {
|
async CreateId() {
|
||||||
let payload = [
|
const payload = [
|
||||||
0,
|
0,
|
||||||
this.PubKey,
|
this.PubKey,
|
||||||
this.CreatedAt,
|
this.CreatedAt,
|
||||||
@ -107,9 +107,9 @@ export default class Event {
|
|||||||
this.Content,
|
this.Content,
|
||||||
];
|
];
|
||||||
|
|
||||||
let payloadData = new TextEncoder().encode(JSON.stringify(payload));
|
const payloadData = new TextEncoder().encode(JSON.stringify(payload));
|
||||||
let data = await secp.utils.sha256(payloadData);
|
const data = await secp.utils.sha256(payloadData);
|
||||||
let hash = secp.utils.bytesToHex(data);
|
const hash = secp.utils.bytesToHex(data);
|
||||||
if (this.Id !== "" && hash !== this.Id) {
|
if (this.Id !== "" && hash !== this.Id) {
|
||||||
console.debug(payload);
|
console.debug(payload);
|
||||||
throw "ID doesnt match!";
|
throw "ID doesnt match!";
|
||||||
@ -135,7 +135,7 @@ export default class Event {
|
|||||||
* Create a new event for a specific pubkey
|
* Create a new event for a specific pubkey
|
||||||
*/
|
*/
|
||||||
static ForPubKey(pubKey: HexKey) {
|
static ForPubKey(pubKey: HexKey) {
|
||||||
let ev = new Event();
|
const ev = new Event();
|
||||||
ev.PubKey = pubKey;
|
ev.PubKey = pubKey;
|
||||||
return ev;
|
return ev;
|
||||||
}
|
}
|
||||||
@ -144,10 +144,10 @@ export default class Event {
|
|||||||
* Encrypt the given message content
|
* Encrypt the given message content
|
||||||
*/
|
*/
|
||||||
async EncryptData(content: string, pubkey: HexKey, privkey: HexKey) {
|
async EncryptData(content: string, pubkey: HexKey, privkey: HexKey) {
|
||||||
let key = await this._GetDmSharedKey(pubkey, privkey);
|
const key = await this._GetDmSharedKey(pubkey, privkey);
|
||||||
let iv = window.crypto.getRandomValues(new Uint8Array(16));
|
const iv = window.crypto.getRandomValues(new Uint8Array(16));
|
||||||
let data = new TextEncoder().encode(content);
|
const data = new TextEncoder().encode(content);
|
||||||
let result = await window.crypto.subtle.encrypt(
|
const result = await window.crypto.subtle.encrypt(
|
||||||
{
|
{
|
||||||
name: "AES-CBC",
|
name: "AES-CBC",
|
||||||
iv: iv,
|
iv: iv,
|
||||||
@ -155,7 +155,7 @@ export default class Event {
|
|||||||
key,
|
key,
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
let uData = new Uint8Array(result);
|
const uData = new Uint8Array(result);
|
||||||
return `${base64.encode(uData, 0, result.byteLength)}?iv=${base64.encode(
|
return `${base64.encode(uData, 0, result.byteLength)}?iv=${base64.encode(
|
||||||
iv,
|
iv,
|
||||||
0,
|
0,
|
||||||
@ -174,15 +174,15 @@ export default class Event {
|
|||||||
* Decrypt the content of the message
|
* Decrypt the content of the message
|
||||||
*/
|
*/
|
||||||
async DecryptData(cyphertext: string, privkey: HexKey, pubkey: HexKey) {
|
async DecryptData(cyphertext: string, privkey: HexKey, pubkey: HexKey) {
|
||||||
let key = await this._GetDmSharedKey(pubkey, privkey);
|
const key = await this._GetDmSharedKey(pubkey, privkey);
|
||||||
let cSplit = cyphertext.split("?iv=");
|
const cSplit = cyphertext.split("?iv=");
|
||||||
let data = new Uint8Array(base64.length(cSplit[0]));
|
const data = new Uint8Array(base64.length(cSplit[0]));
|
||||||
base64.decode(cSplit[0], data, 0);
|
base64.decode(cSplit[0], data, 0);
|
||||||
|
|
||||||
let iv = new Uint8Array(base64.length(cSplit[1]));
|
const iv = new Uint8Array(base64.length(cSplit[1]));
|
||||||
base64.decode(cSplit[1], iv, 0);
|
base64.decode(cSplit[1], iv, 0);
|
||||||
|
|
||||||
let result = await window.crypto.subtle.decrypt(
|
const result = await window.crypto.subtle.decrypt(
|
||||||
{
|
{
|
||||||
name: "AES-CBC",
|
name: "AES-CBC",
|
||||||
iv: iv,
|
iv: iv,
|
||||||
@ -201,8 +201,8 @@ export default class Event {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async _GetDmSharedKey(pubkey: HexKey, privkey: HexKey) {
|
async _GetDmSharedKey(pubkey: HexKey, privkey: HexKey) {
|
||||||
let sharedPoint = secp.getSharedSecret(privkey, "02" + pubkey);
|
const sharedPoint = secp.getSharedSecret(privkey, "02" + pubkey);
|
||||||
let sharedX = sharedPoint.slice(1, 33);
|
const sharedX = sharedPoint.slice(1, 33);
|
||||||
return await window.crypto.subtle.importKey(
|
return await window.crypto.subtle.importKey(
|
||||||
"raw",
|
"raw",
|
||||||
sharedX,
|
sharedX,
|
||||||
|
@ -104,10 +104,10 @@ export class Subscriptions {
|
|||||||
this.Since = sub?.since ?? undefined;
|
this.Since = sub?.since ?? undefined;
|
||||||
this.Until = sub?.until ?? undefined;
|
this.Until = sub?.until ?? undefined;
|
||||||
this.Limit = sub?.limit ?? undefined;
|
this.Limit = sub?.limit ?? undefined;
|
||||||
this.OnEvent = (e) => {
|
this.OnEvent = () => {
|
||||||
console.warn(`No event handler was set on subscription: ${this.Id}`);
|
console.warn(`No event handler was set on subscription: ${this.Id}`);
|
||||||
};
|
};
|
||||||
this.OnEnd = (c) => {};
|
this.OnEnd = () => undefined;
|
||||||
this.OrSubs = [];
|
this.OrSubs = [];
|
||||||
this.Started = new Map<string, number>();
|
this.Started = new Map<string, number>();
|
||||||
this.Finished = new Map<string, number>();
|
this.Finished = new Map<string, number>();
|
||||||
@ -128,7 +128,7 @@ export class Subscriptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ToObject(): RawReqFilter {
|
ToObject(): RawReqFilter {
|
||||||
let ret: RawReqFilter = {};
|
const ret: RawReqFilter = {};
|
||||||
if (this.Ids) {
|
if (this.Ids) {
|
||||||
ret.ids = Array.from(this.Ids);
|
ret.ids = Array.from(this.Ids);
|
||||||
}
|
}
|
||||||
|
@ -5,9 +5,10 @@ import Connection, { RelaySettings } from "Nostr/Connection";
|
|||||||
import Event from "Nostr/Event";
|
import Event from "Nostr/Event";
|
||||||
import EventKind from "Nostr/EventKind";
|
import EventKind from "Nostr/EventKind";
|
||||||
import { Subscriptions } from "Nostr/Subscriptions";
|
import { Subscriptions } from "Nostr/Subscriptions";
|
||||||
|
import { unwrap } from "Util";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages nostr content retrival system
|
* Manages nostr content retrieval system
|
||||||
*/
|
*/
|
||||||
export class NostrSystem {
|
export class NostrSystem {
|
||||||
/**
|
/**
|
||||||
@ -49,14 +50,14 @@ export class NostrSystem {
|
|||||||
ConnectToRelay(address: string, options: RelaySettings) {
|
ConnectToRelay(address: string, options: RelaySettings) {
|
||||||
try {
|
try {
|
||||||
if (!this.Sockets.has(address)) {
|
if (!this.Sockets.has(address)) {
|
||||||
let c = new Connection(address, options);
|
const c = new Connection(address, options);
|
||||||
this.Sockets.set(address, c);
|
this.Sockets.set(address, c);
|
||||||
for (let [_, s] of this.Subscriptions) {
|
for (const [, s] of this.Subscriptions) {
|
||||||
c.AddSubscription(s);
|
c.AddSubscription(s);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// update settings if already connected
|
// update settings if already connected
|
||||||
this.Sockets.get(address)!.Settings = options;
|
unwrap(this.Sockets.get(address)).Settings = options;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
@ -67,7 +68,7 @@ export class NostrSystem {
|
|||||||
* Disconnect from a relay
|
* Disconnect from a relay
|
||||||
*/
|
*/
|
||||||
DisconnectRelay(address: string) {
|
DisconnectRelay(address: string) {
|
||||||
let c = this.Sockets.get(address);
|
const c = this.Sockets.get(address);
|
||||||
if (c) {
|
if (c) {
|
||||||
this.Sockets.delete(address);
|
this.Sockets.delete(address);
|
||||||
c.Close();
|
c.Close();
|
||||||
@ -75,14 +76,14 @@ export class NostrSystem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
AddSubscription(sub: Subscriptions) {
|
AddSubscription(sub: Subscriptions) {
|
||||||
for (let [a, s] of this.Sockets) {
|
for (const [, s] of this.Sockets) {
|
||||||
s.AddSubscription(sub);
|
s.AddSubscription(sub);
|
||||||
}
|
}
|
||||||
this.Subscriptions.set(sub.Id, sub);
|
this.Subscriptions.set(sub.Id, sub);
|
||||||
}
|
}
|
||||||
|
|
||||||
RemoveSubscription(subId: string) {
|
RemoveSubscription(subId: string) {
|
||||||
for (let [a, s] of this.Sockets) {
|
for (const [, s] of this.Sockets) {
|
||||||
s.RemoveSubscription(subId);
|
s.RemoveSubscription(subId);
|
||||||
}
|
}
|
||||||
this.Subscriptions.delete(subId);
|
this.Subscriptions.delete(subId);
|
||||||
@ -92,7 +93,7 @@ export class NostrSystem {
|
|||||||
* Send events to writable relays
|
* Send events to writable relays
|
||||||
*/
|
*/
|
||||||
BroadcastEvent(ev: Event) {
|
BroadcastEvent(ev: Event) {
|
||||||
for (let [_, s] of this.Sockets) {
|
for (const [, s] of this.Sockets) {
|
||||||
s.SendEvent(ev);
|
s.SendEvent(ev);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -101,7 +102,7 @@ export class NostrSystem {
|
|||||||
* Write an event to a relay then disconnect
|
* Write an event to a relay then disconnect
|
||||||
*/
|
*/
|
||||||
async WriteOnceToRelay(address: string, ev: Event) {
|
async WriteOnceToRelay(address: string, ev: Event) {
|
||||||
let c = new Connection(address, { write: true, read: false });
|
const c = new Connection(address, { write: true, read: false });
|
||||||
await c.SendAsync(ev);
|
await c.SendAsync(ev);
|
||||||
c.Close();
|
c.Close();
|
||||||
}
|
}
|
||||||
@ -110,7 +111,7 @@ export class NostrSystem {
|
|||||||
* Request profile metadata for a set of pubkeys
|
* Request profile metadata for a set of pubkeys
|
||||||
*/
|
*/
|
||||||
TrackMetadata(pk: HexKey | Array<HexKey>) {
|
TrackMetadata(pk: HexKey | Array<HexKey>) {
|
||||||
for (let p of Array.isArray(pk) ? pk : [pk]) {
|
for (const p of Array.isArray(pk) ? pk : [pk]) {
|
||||||
if (p.length > 0) {
|
if (p.length > 0) {
|
||||||
this.WantsMetadata.add(p);
|
this.WantsMetadata.add(p);
|
||||||
}
|
}
|
||||||
@ -121,7 +122,7 @@ export class NostrSystem {
|
|||||||
* Stop tracking metadata for a set of pubkeys
|
* Stop tracking metadata for a set of pubkeys
|
||||||
*/
|
*/
|
||||||
UntrackMetadata(pk: HexKey | Array<HexKey>) {
|
UntrackMetadata(pk: HexKey | Array<HexKey>) {
|
||||||
for (let p of Array.isArray(pk) ? pk : [pk]) {
|
for (const p of Array.isArray(pk) ? pk : [pk]) {
|
||||||
if (p.length > 0) {
|
if (p.length > 0) {
|
||||||
this.WantsMetadata.delete(p);
|
this.WantsMetadata.delete(p);
|
||||||
}
|
}
|
||||||
@ -132,16 +133,16 @@ export class NostrSystem {
|
|||||||
* Request/Response pattern
|
* Request/Response pattern
|
||||||
*/
|
*/
|
||||||
RequestSubscription(sub: Subscriptions) {
|
RequestSubscription(sub: Subscriptions) {
|
||||||
return new Promise<TaggedRawEvent[]>((resolve, reject) => {
|
return new Promise<TaggedRawEvent[]>((resolve) => {
|
||||||
let events: TaggedRawEvent[] = [];
|
const events: TaggedRawEvent[] = [];
|
||||||
|
|
||||||
// force timeout returning current results
|
// force timeout returning current results
|
||||||
let timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
this.RemoveSubscription(sub.Id);
|
this.RemoveSubscription(sub.Id);
|
||||||
resolve(events);
|
resolve(events);
|
||||||
}, 10_000);
|
}, 10_000);
|
||||||
|
|
||||||
let onEventPassthrough = sub.OnEvent;
|
const onEventPassthrough = sub.OnEvent;
|
||||||
sub.OnEvent = (ev) => {
|
sub.OnEvent = (ev) => {
|
||||||
if (typeof onEventPassthrough === "function") {
|
if (typeof onEventPassthrough === "function") {
|
||||||
onEventPassthrough(ev);
|
onEventPassthrough(ev);
|
||||||
@ -149,9 +150,9 @@ export class NostrSystem {
|
|||||||
if (!events.some((a) => a.id === ev.id)) {
|
if (!events.some((a) => a.id === ev.id)) {
|
||||||
events.push(ev);
|
events.push(ev);
|
||||||
} else {
|
} else {
|
||||||
let existing = events.find((a) => a.id === ev.id);
|
const existing = events.find((a) => a.id === ev.id);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
for (let v of ev.relays) {
|
for (const v of ev.relays) {
|
||||||
existing.relays.push(v);
|
existing.relays.push(v);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -171,11 +172,11 @@ export class NostrSystem {
|
|||||||
|
|
||||||
async _FetchMetadata() {
|
async _FetchMetadata() {
|
||||||
if (this.UserDb) {
|
if (this.UserDb) {
|
||||||
let missing = new Set<HexKey>();
|
const missing = new Set<HexKey>();
|
||||||
let meta = await this.UserDb.bulkGet(Array.from(this.WantsMetadata));
|
const meta = await this.UserDb.bulkGet(Array.from(this.WantsMetadata));
|
||||||
let expire = new Date().getTime() - ProfileCacheExpire;
|
const expire = new Date().getTime() - ProfileCacheExpire;
|
||||||
for (let pk of this.WantsMetadata) {
|
for (const pk of this.WantsMetadata) {
|
||||||
let m = meta.find((a) => a?.pubkey === pk);
|
const m = meta.find((a) => a?.pubkey === pk);
|
||||||
if (!m || m.loaded < expire) {
|
if (!m || m.loaded < expire) {
|
||||||
missing.add(pk);
|
missing.add(pk);
|
||||||
// cap 100 missing profiles
|
// cap 100 missing profiles
|
||||||
@ -188,35 +189,38 @@ export class NostrSystem {
|
|||||||
if (missing.size > 0) {
|
if (missing.size > 0) {
|
||||||
console.debug("Wants profiles: ", missing);
|
console.debug("Wants profiles: ", missing);
|
||||||
|
|
||||||
let sub = new Subscriptions();
|
const sub = new Subscriptions();
|
||||||
sub.Id = `profiles:${sub.Id.slice(0, 8)}`;
|
sub.Id = `profiles:${sub.Id.slice(0, 8)}`;
|
||||||
sub.Kinds = new Set([EventKind.SetMetadata]);
|
sub.Kinds = new Set([EventKind.SetMetadata]);
|
||||||
sub.Authors = missing;
|
sub.Authors = missing;
|
||||||
sub.OnEvent = async (e) => {
|
sub.OnEvent = async (e) => {
|
||||||
let profile = mapEventToProfile(e);
|
const profile = mapEventToProfile(e);
|
||||||
|
const userDb = unwrap(this.UserDb);
|
||||||
if (profile) {
|
if (profile) {
|
||||||
let existing = await this.UserDb!.find(profile.pubkey);
|
const existing = await userDb.find(profile.pubkey);
|
||||||
if ((existing?.created ?? 0) < profile.created) {
|
if ((existing?.created ?? 0) < profile.created) {
|
||||||
await this.UserDb!.put(profile);
|
await userDb.put(profile);
|
||||||
} else if (existing) {
|
} else if (existing) {
|
||||||
await this.UserDb!.update(profile.pubkey, {
|
await userDb.update(profile.pubkey, {
|
||||||
loaded: profile.loaded,
|
loaded: profile.loaded,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let results = await this.RequestSubscription(sub);
|
const results = await this.RequestSubscription(sub);
|
||||||
let couldNotFetch = Array.from(missing).filter(
|
const couldNotFetch = Array.from(missing).filter(
|
||||||
(a) => !results.some((b) => b.pubkey === a)
|
(a) => !results.some((b) => b.pubkey === a)
|
||||||
);
|
);
|
||||||
console.debug("No profiles: ", couldNotFetch);
|
console.debug("No profiles: ", couldNotFetch);
|
||||||
if (couldNotFetch.length > 0) {
|
if (couldNotFetch.length > 0) {
|
||||||
let updates = couldNotFetch.map((a) => {
|
const updates = couldNotFetch
|
||||||
|
.map((a) => {
|
||||||
return {
|
return {
|
||||||
pubkey: a,
|
pubkey: a,
|
||||||
loaded: new Date().getTime(),
|
loaded: new Date().getTime(),
|
||||||
};
|
};
|
||||||
}).map(a => this.UserDb!.update(a.pubkey, a));
|
})
|
||||||
|
.map((a) => unwrap(this.UserDb).update(a.pubkey, a));
|
||||||
await Promise.all(updates);
|
await Promise.all(updates);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -224,12 +228,8 @@ export class NostrSystem {
|
|||||||
setTimeout(() => this._FetchMetadata(), 500);
|
setTimeout(() => this._FetchMetadata(), 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
async nip42Auth(
|
nip42Auth: (challenge: string, relay: string) => Promise<Event | undefined> =
|
||||||
challenge: string,
|
async () => undefined;
|
||||||
relay: string
|
|
||||||
): Promise<Event | undefined> {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const System = new NostrSystem();
|
export const System = new NostrSystem();
|
||||||
|
@ -56,7 +56,7 @@ export default class Tag {
|
|||||||
switch (this.Key) {
|
switch (this.Key) {
|
||||||
case "e": {
|
case "e": {
|
||||||
let ret = ["e", this.Event, this.Relay, this.Marker];
|
let ret = ["e", this.Event, this.Relay, this.Marker];
|
||||||
let trimEnd = ret.reverse().findIndex((a) => a !== undefined);
|
const trimEnd = ret.reverse().findIndex((a) => a !== undefined);
|
||||||
ret = ret.reverse().slice(0, ret.length - trimEnd);
|
ret = ret.reverse().slice(0, ret.length - trimEnd);
|
||||||
return <string[]>ret;
|
return <string[]>ret;
|
||||||
}
|
}
|
||||||
@ -64,10 +64,10 @@ export default class Tag {
|
|||||||
return this.PubKey ? ["p", this.PubKey] : null;
|
return this.PubKey ? ["p", this.PubKey] : null;
|
||||||
}
|
}
|
||||||
case "t": {
|
case "t": {
|
||||||
return ["t", this.Hashtag!];
|
return ["t", this.Hashtag ?? ""];
|
||||||
}
|
}
|
||||||
case "d": {
|
case "d": {
|
||||||
return ["d", this.DTag!];
|
return ["d", this.DTag ?? ""];
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
return this.Original;
|
return this.Original;
|
||||||
|
@ -19,15 +19,15 @@ export default class Thread {
|
|||||||
* @param ev Event to extract thread from
|
* @param ev Event to extract thread from
|
||||||
*/
|
*/
|
||||||
static ExtractThread(ev: NEvent) {
|
static ExtractThread(ev: NEvent) {
|
||||||
let isThread = ev.Tags.some((a) => a.Key === "e");
|
const isThread = ev.Tags.some((a) => a.Key === "e");
|
||||||
if (!isThread) {
|
if (!isThread) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let shouldWriteMarkers = ev.Kind === EventKind.TextNote;
|
const shouldWriteMarkers = ev.Kind === EventKind.TextNote;
|
||||||
let ret = new Thread();
|
const ret = new Thread();
|
||||||
let eTags = ev.Tags.filter((a) => a.Key === "e");
|
const eTags = ev.Tags.filter((a) => a.Key === "e");
|
||||||
let marked = eTags.some((a) => a.Marker !== undefined);
|
const marked = eTags.some((a) => a.Marker !== undefined);
|
||||||
if (!marked) {
|
if (!marked) {
|
||||||
ret.Root = eTags[0];
|
ret.Root = eTags[0];
|
||||||
ret.Root.Marker = shouldWriteMarkers ? "root" : undefined;
|
ret.Root.Marker = shouldWriteMarkers ? "root" : undefined;
|
||||||
@ -42,8 +42,8 @@ export default class Thread {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let root = eTags.find((a) => a.Marker === "root");
|
const root = eTags.find((a) => a.Marker === "root");
|
||||||
let reply = eTags.find((a) => a.Marker === "reply");
|
const reply = eTags.find((a) => a.Marker === "reply");
|
||||||
ret.Root = root;
|
ret.Root = root;
|
||||||
ret.ReplyTo = reply;
|
ret.ReplyTo = reply;
|
||||||
ret.Mentions = eTags.filter((a) => a.Marker === "mention");
|
ret.Mentions = eTags.filter((a) => a.Marker === "mention");
|
||||||
|
@ -15,13 +15,12 @@ export async function makeNotification(
|
|||||||
case EventKind.TextNote: {
|
case EventKind.TextNote: {
|
||||||
const pubkeys = new Set([
|
const pubkeys = new Set([
|
||||||
ev.pubkey,
|
ev.pubkey,
|
||||||
...ev.tags.filter((a) => a[0] === "p").map((a) => a[1]!),
|
...ev.tags.filter((a) => a[0] === "p").map((a) => a[1]),
|
||||||
]);
|
]);
|
||||||
const users = await db.bulkGet(Array.from(pubkeys));
|
const users = await db.bulkGet(Array.from(pubkeys));
|
||||||
const fromUser = users.find((a) => a?.pubkey === ev.pubkey);
|
const fromUser = users.find((a) => a?.pubkey === ev.pubkey);
|
||||||
const name = getDisplayName(fromUser, ev.pubkey);
|
const name = getDisplayName(fromUser, ev.pubkey);
|
||||||
const avatarUrl =
|
const avatarUrl = fromUser?.picture || Nostrich;
|
||||||
(fromUser?.picture?.length ?? 0) === 0 ? Nostrich : fromUser?.picture;
|
|
||||||
return {
|
return {
|
||||||
title: `Reply from ${name}`,
|
title: `Reply from ${name}`,
|
||||||
body: replaceTagsWithUser(ev, users).substring(0, 50),
|
body: replaceTagsWithUser(ev, users).substring(0, 50),
|
||||||
@ -37,12 +36,12 @@ function replaceTagsWithUser(ev: TaggedRawEvent, users: MetadataCache[]) {
|
|||||||
return ev.content
|
return ev.content
|
||||||
.split(MentionRegex)
|
.split(MentionRegex)
|
||||||
.map((match) => {
|
.map((match) => {
|
||||||
let matchTag = match.match(/#\[(\d+)\]/);
|
const matchTag = match.match(/#\[(\d+)\]/);
|
||||||
if (matchTag && matchTag.length === 2) {
|
if (matchTag && matchTag.length === 2) {
|
||||||
let idx = parseInt(matchTag[1]);
|
const idx = parseInt(matchTag[1]);
|
||||||
let ref = ev.tags[idx];
|
const ref = ev.tags[idx];
|
||||||
if (ref && ref[0] === "p" && ref.length > 1) {
|
if (ref && ref[0] === "p" && ref.length > 1) {
|
||||||
let u = users.find((a) => a.pubkey === ref[1]);
|
const u = users.find((a) => a.pubkey === ref[1]);
|
||||||
return `@${getDisplayName(u, ref[1])}`;
|
return `@${getDisplayName(u, ref[1])}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ import { bech32ToHex } from "Util";
|
|||||||
import useEventPublisher from "Feed/EventPublisher";
|
import useEventPublisher from "Feed/EventPublisher";
|
||||||
|
|
||||||
import DM from "Element/DM";
|
import DM from "Element/DM";
|
||||||
import { RawEvent } from "Nostr";
|
import { RawEvent, TaggedRawEvent } from "Nostr";
|
||||||
import { dmsInChat, isToSelf } from "Pages/MessagesPage";
|
import { dmsInChat, isToSelf } from "Pages/MessagesPage";
|
||||||
import NoteToSelf from "Element/NoteToSelf";
|
import NoteToSelf from "Element/NoteToSelf";
|
||||||
|
|
||||||
@ -17,24 +17,32 @@ type RouterParams = {
|
|||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
login: {
|
||||||
|
dms: TaggedRawEvent[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export default function ChatPage() {
|
export default function ChatPage() {
|
||||||
const params = useParams<RouterParams>();
|
const params = useParams<RouterParams>();
|
||||||
const publisher = useEventPublisher();
|
const publisher = useEventPublisher();
|
||||||
const id = bech32ToHex(params.id ?? "");
|
const id = bech32ToHex(params.id ?? "");
|
||||||
const pubKey = useSelector<any>((s) => s.login.publicKey);
|
const pubKey = useSelector<{ login: { publicKey: string } }>(
|
||||||
const dms = useSelector<any, RawEvent[]>((s) => filterDms(s.login.dms));
|
(s) => s.login.publicKey
|
||||||
|
);
|
||||||
|
const dms = useSelector<State, RawEvent[]>((s) => filterDms(s.login.dms));
|
||||||
const [content, setContent] = useState<string>();
|
const [content, setContent] = useState<string>();
|
||||||
const { ref, inView, entry } = useInView();
|
const { ref, inView } = useInView();
|
||||||
const dmListRef = useRef<HTMLDivElement>(null);
|
const dmListRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
function filterDms(dms: RawEvent[]) {
|
function filterDms(dms: TaggedRawEvent[]) {
|
||||||
return dmsInChat(
|
return dmsInChat(
|
||||||
id === pubKey ? dms.filter((d) => isToSelf(d, pubKey)) : dms,
|
id === pubKey ? dms.filter((d) => isToSelf(d, pubKey)) : dms,
|
||||||
id
|
id
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sortedDms = useMemo<any[]>(() => {
|
const sortedDms = useMemo<RawEvent[]>(() => {
|
||||||
return [...dms].sort((a, b) => a.created_at - b.created_at);
|
return [...dms].sort((a, b) => a.created_at - b.created_at);
|
||||||
}, [dms]);
|
}, [dms]);
|
||||||
|
|
||||||
@ -46,7 +54,7 @@ export default function ChatPage() {
|
|||||||
|
|
||||||
async function sendDm() {
|
async function sendDm() {
|
||||||
if (content) {
|
if (content) {
|
||||||
let ev = await publisher.sendDm(content, id);
|
const ev = await publisher.sendDm(content, id);
|
||||||
console.debug(ev);
|
console.debug(ev);
|
||||||
publisher.broadcast(ev);
|
publisher.broadcast(ev);
|
||||||
setContent("");
|
setContent("");
|
||||||
@ -54,7 +62,7 @@ export default function ChatPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function onEnter(e: KeyboardEvent) {
|
async function onEnter(e: KeyboardEvent) {
|
||||||
let isEnter = e.code === "Enter";
|
const isEnter = e.code === "Enter";
|
||||||
if (isEnter && !e.shiftKey) {
|
if (isEnter && !e.shiftKey) {
|
||||||
await sendDm();
|
await sendDm();
|
||||||
}
|
}
|
||||||
@ -67,8 +75,9 @@ export default function ChatPage() {
|
|||||||
)) || <ProfileImage pubkey={id} className="f-grow mb10" />}
|
)) || <ProfileImage pubkey={id} className="f-grow mb10" />}
|
||||||
<div className="dm-list" ref={dmListRef}>
|
<div className="dm-list" ref={dmListRef}>
|
||||||
<div>
|
<div>
|
||||||
|
{/* TODO I need to look into this again, something's being bricked with the RawEvent and TaggedRawEvent */}
|
||||||
{sortedDms.map((a) => (
|
{sortedDms.map((a) => (
|
||||||
<DM data={a} key={a.id} />
|
<DM data={a as TaggedRawEvent} key={a.id} />
|
||||||
))}
|
))}
|
||||||
<div ref={ref} className="mb10"></div>
|
<div ref={ref} className="mb10"></div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -45,11 +45,11 @@ const DonatePage = () => {
|
|||||||
const [today, setSumToday] = useState<TotalToday>();
|
const [today, setSumToday] = useState<TotalToday>();
|
||||||
|
|
||||||
async function loadData() {
|
async function loadData() {
|
||||||
let rsp = await fetch(`${ApiHost}/api/v1/revenue/splits`);
|
const rsp = await fetch(`${ApiHost}/api/v1/revenue/splits`);
|
||||||
if (rsp.ok) {
|
if (rsp.ok) {
|
||||||
setSplits(await rsp.json());
|
setSplits(await rsp.json());
|
||||||
}
|
}
|
||||||
let rsp2 = await fetch(`${ApiHost}/api/v1/revenue/today`);
|
const rsp2 = await fetch(`${ApiHost}/api/v1/revenue/today`);
|
||||||
if (rsp2.ok) {
|
if (rsp2.ok) {
|
||||||
setSumToday(await rsp2.json());
|
setSumToday(await rsp2.json());
|
||||||
}
|
}
|
||||||
@ -60,7 +60,7 @@ const DonatePage = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
function actions(pk: HexKey) {
|
function actions(pk: HexKey) {
|
||||||
let split = splits.find((a) => bech32ToHex(a.pubKey) === pk);
|
const split = splits.find((a) => bech32ToHex(a.pubKey) === pk);
|
||||||
if (split) {
|
if (split) {
|
||||||
return <>{(100 * split.split).toLocaleString()}%</>;
|
return <>{(100 * split.split).toLocaleString()}%</>;
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ import { parseId } from "Util";
|
|||||||
|
|
||||||
export default function EventPage() {
|
export default function EventPage() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const id = parseId(params.id!);
|
const id = parseId(params.id ?? "");
|
||||||
const thread = useThreadFeed(id);
|
const thread = useThreadFeed(id);
|
||||||
|
|
||||||
return <Thread notes={thread.notes} this={id} />;
|
return <Thread notes={thread.notes} this={id} />;
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import Timeline from "Element/Timeline";
|
import Timeline from "Element/Timeline";
|
||||||
|
import { unwrap } from "Util";
|
||||||
|
|
||||||
const HashTagsPage = () => {
|
const HashTagsPage = () => {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const tag = params.tag!.toLowerCase();
|
const tag = unwrap(params.tag).toLowerCase();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -75,10 +75,10 @@ export default function Layout() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (relays) {
|
if (relays) {
|
||||||
for (let [k, v] of Object.entries(relays)) {
|
for (const [k, v] of Object.entries(relays)) {
|
||||||
System.ConnectToRelay(k, v);
|
System.ConnectToRelay(k, v);
|
||||||
}
|
}
|
||||||
for (let [k] of System.Sockets) {
|
for (const [k] of System.Sockets) {
|
||||||
if (!relays[k] && !SearchRelays.has(k)) {
|
if (!relays[k] && !SearchRelays.has(k)) {
|
||||||
System.DisconnectRelay(k);
|
System.DisconnectRelay(k);
|
||||||
}
|
}
|
||||||
@ -96,7 +96,7 @@ export default function Layout() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let osTheme = window.matchMedia("(prefers-color-scheme: light)");
|
const osTheme = window.matchMedia("(prefers-color-scheme: light)");
|
||||||
setTheme(
|
setTheme(
|
||||||
preferences.theme === "system" && osTheme.matches
|
preferences.theme === "system" && osTheme.matches
|
||||||
? "light"
|
? "light"
|
||||||
@ -139,24 +139,24 @@ export default function Layout() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
async function handleNewUser() {
|
async function handleNewUser() {
|
||||||
let newRelays: Record<string, RelaySettings> | undefined;
|
let newRelays: Record<string, RelaySettings> = {};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let rsp = await fetch("https://api.nostr.watch/v1/online");
|
const rsp = await fetch("https://api.nostr.watch/v1/online");
|
||||||
if (rsp.ok) {
|
if (rsp.ok) {
|
||||||
let online: string[] = await rsp.json();
|
const online: string[] = await rsp.json();
|
||||||
let pickRandom = online
|
const pickRandom = online
|
||||||
.sort((a, b) => (Math.random() >= 0.5 ? 1 : -1))
|
.sort(() => (Math.random() >= 0.5 ? 1 : -1))
|
||||||
.slice(0, 4); // pick 4 random relays
|
.slice(0, 4); // pick 4 random relays
|
||||||
|
|
||||||
let relayObjects = pickRandom.map((a) => [
|
const relayObjects = pickRandom.map((a) => [
|
||||||
a,
|
a,
|
||||||
{ read: true, write: true },
|
{ read: true, write: true },
|
||||||
]);
|
]);
|
||||||
newRelays = Object.fromEntries(relayObjects);
|
newRelays = Object.fromEntries(relayObjects);
|
||||||
dispatch(
|
dispatch(
|
||||||
setRelays({
|
setRelays({
|
||||||
relays: newRelays!,
|
relays: newRelays,
|
||||||
createdAt: 1,
|
createdAt: 1,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -175,13 +175,13 @@ export default function Layout() {
|
|||||||
}
|
}
|
||||||
}, [newUserKey]);
|
}, [newUserKey]);
|
||||||
|
|
||||||
async function goToNotifications(e: any) {
|
async function goToNotifications(e: React.MouseEvent) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
// request permissions to send notifications
|
// request permissions to send notifications
|
||||||
if ("Notification" in window) {
|
if ("Notification" in window) {
|
||||||
try {
|
try {
|
||||||
if (Notification.permission !== "granted") {
|
if (Notification.permission !== "granted") {
|
||||||
let res = await Notification.requestPermission();
|
const res = await Notification.requestPermission();
|
||||||
console.debug(res);
|
console.debug(res);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -194,14 +194,14 @@ export default function Layout() {
|
|||||||
function accountHeader() {
|
function accountHeader() {
|
||||||
return (
|
return (
|
||||||
<div className="header-actions">
|
<div className="header-actions">
|
||||||
<div className="btn btn-rnd" onClick={(e) => navigate("/search")}>
|
<div className="btn btn-rnd" onClick={() => navigate("/search")}>
|
||||||
<Search />
|
<Search />
|
||||||
</div>
|
</div>
|
||||||
<div className="btn btn-rnd" onClick={(e) => navigate("/messages")}>
|
<div className="btn btn-rnd" onClick={() => navigate("/messages")}>
|
||||||
<Envelope />
|
<Envelope />
|
||||||
{unreadDms > 0 && <span className="has-unread"></span>}
|
{unreadDms > 0 && <span className="has-unread"></span>}
|
||||||
</div>
|
</div>
|
||||||
<div className="btn btn-rnd" onClick={(e) => goToNotifications(e)}>
|
<div className="btn btn-rnd" onClick={goToNotifications}>
|
||||||
<Bell />
|
<Bell />
|
||||||
{hasNotifications && <span className="has-unread"></span>}
|
{hasNotifications && <span className="has-unread"></span>}
|
||||||
</div>
|
</div>
|
||||||
|
@ -30,15 +30,15 @@ export default function LoginPage() {
|
|||||||
}, [publicKey, navigate]);
|
}, [publicKey, navigate]);
|
||||||
|
|
||||||
async function getNip05PubKey(addr: string) {
|
async function getNip05PubKey(addr: string) {
|
||||||
let [username, domain] = addr.split("@");
|
const [username, domain] = addr.split("@");
|
||||||
let rsp = await fetch(
|
const rsp = await fetch(
|
||||||
`https://${domain}/.well-known/nostr.json?name=${encodeURIComponent(
|
`https://${domain}/.well-known/nostr.json?name=${encodeURIComponent(
|
||||||
username
|
username
|
||||||
)}`
|
)}`
|
||||||
);
|
);
|
||||||
if (rsp.ok) {
|
if (rsp.ok) {
|
||||||
let data = await rsp.json();
|
const data = await rsp.json();
|
||||||
let pKey = data.names[username];
|
const pKey = data.names[username];
|
||||||
if (pKey) {
|
if (pKey) {
|
||||||
return pKey;
|
return pKey;
|
||||||
}
|
}
|
||||||
@ -49,17 +49,17 @@ export default function LoginPage() {
|
|||||||
async function doLogin() {
|
async function doLogin() {
|
||||||
try {
|
try {
|
||||||
if (key.startsWith("nsec")) {
|
if (key.startsWith("nsec")) {
|
||||||
let hexKey = bech32ToHex(key);
|
const hexKey = bech32ToHex(key);
|
||||||
if (secp.utils.isValidPrivateKey(hexKey)) {
|
if (secp.utils.isValidPrivateKey(hexKey)) {
|
||||||
dispatch(setPrivateKey(hexKey));
|
dispatch(setPrivateKey(hexKey));
|
||||||
} else {
|
} else {
|
||||||
throw new Error("INVALID PRIVATE KEY");
|
throw new Error("INVALID PRIVATE KEY");
|
||||||
}
|
}
|
||||||
} else if (key.startsWith("npub")) {
|
} else if (key.startsWith("npub")) {
|
||||||
let hexKey = bech32ToHex(key);
|
const hexKey = bech32ToHex(key);
|
||||||
dispatch(setPublicKey(hexKey));
|
dispatch(setPublicKey(hexKey));
|
||||||
} else if (key.match(EmailRegex)) {
|
} else if (key.match(EmailRegex)) {
|
||||||
let hexKey = await getNip05PubKey(key);
|
const hexKey = await getNip05PubKey(key);
|
||||||
dispatch(setPublicKey(hexKey));
|
dispatch(setPublicKey(hexKey));
|
||||||
} else {
|
} else {
|
||||||
if (secp.utils.isValidPrivateKey(key)) {
|
if (secp.utils.isValidPrivateKey(key)) {
|
||||||
@ -75,17 +75,17 @@ export default function LoginPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function makeRandomKey() {
|
async function makeRandomKey() {
|
||||||
let newKey = secp.utils.bytesToHex(secp.utils.randomPrivateKey());
|
const newKey = secp.utils.bytesToHex(secp.utils.randomPrivateKey());
|
||||||
dispatch(setGeneratedPrivateKey(newKey));
|
dispatch(setGeneratedPrivateKey(newKey));
|
||||||
navigate("/new");
|
navigate("/new");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function doNip07Login() {
|
async function doNip07Login() {
|
||||||
let pubKey = await window.nostr.getPublicKey();
|
const pubKey = await window.nostr.getPublicKey();
|
||||||
dispatch(setPublicKey(pubKey));
|
dispatch(setPublicKey(pubKey));
|
||||||
|
|
||||||
if ("getRelays" in window.nostr) {
|
if ("getRelays" in window.nostr) {
|
||||||
let relays = await window.nostr.getRelays();
|
const relays = await window.nostr.getRelays();
|
||||||
dispatch(
|
dispatch(
|
||||||
setRelays({
|
setRelays({
|
||||||
relays: {
|
relays: {
|
||||||
@ -99,7 +99,7 @@ export default function LoginPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function altLogins() {
|
function altLogins() {
|
||||||
let nip07 = "nostr" in window;
|
const nip07 = "nostr" in window;
|
||||||
if (!nip07) {
|
if (!nip07) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -108,7 +108,7 @@ export default function LoginPage() {
|
|||||||
<>
|
<>
|
||||||
<h2>Other Login Methods</h2>
|
<h2>Other Login Methods</h2>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<button type="button" onClick={(e) => doNip07Login()}>
|
<button type="button" onClick={doNip07Login}>
|
||||||
Login with Extension (NIP-07)
|
Login with Extension (NIP-07)
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -129,7 +129,7 @@ export default function LoginPage() {
|
|||||||
</div>
|
</div>
|
||||||
{error.length > 0 ? <b className="error">{error}</b> : null}
|
{error.length > 0 ? <b className="error">{error}</b> : null}
|
||||||
<div className="tabs">
|
<div className="tabs">
|
||||||
<button type="button" onClick={(e) => doLogin()}>
|
<button type="button" onClick={doLogin}>
|
||||||
Login
|
Login
|
||||||
</button>
|
</button>
|
||||||
<button type="button" onClick={() => makeRandomKey()}>
|
<button type="button" onClick={() => makeRandomKey()}>
|
||||||
|
@ -33,7 +33,7 @@ export default function MessagesPage() {
|
|||||||
const chats = useMemo(() => {
|
const chats = useMemo(() => {
|
||||||
return extractChats(
|
return extractChats(
|
||||||
dms.filter((a) => !isMuted(a.pubkey)),
|
dms.filter((a) => !isMuted(a.pubkey)),
|
||||||
myPubKey!
|
myPubKey ?? ""
|
||||||
);
|
);
|
||||||
}, [dms, myPubKey, dmInteraction]);
|
}, [dms, myPubKey, dmInteraction]);
|
||||||
|
|
||||||
@ -65,7 +65,7 @@ export default function MessagesPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function markAllRead() {
|
function markAllRead() {
|
||||||
for (let c of chats) {
|
for (const c of chats) {
|
||||||
setLastReadDm(c.pubkey);
|
setLastReadDm(c.pubkey);
|
||||||
}
|
}
|
||||||
dispatch(incDmInteraction());
|
dispatch(incDmInteraction());
|
||||||
@ -95,23 +95,23 @@ export default function MessagesPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function lastReadDm(pk: HexKey) {
|
export function lastReadDm(pk: HexKey) {
|
||||||
let k = `dm:seen:${pk}`;
|
const k = `dm:seen:${pk}`;
|
||||||
return parseInt(window.localStorage.getItem(k) ?? "0");
|
return parseInt(window.localStorage.getItem(k) ?? "0");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setLastReadDm(pk: HexKey) {
|
export function setLastReadDm(pk: HexKey) {
|
||||||
const now = Math.floor(new Date().getTime() / 1000);
|
const now = Math.floor(new Date().getTime() / 1000);
|
||||||
let current = lastReadDm(pk);
|
const current = lastReadDm(pk);
|
||||||
if (current >= now) {
|
if (current >= now) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let k = `dm:seen:${pk}`;
|
const k = `dm:seen:${pk}`;
|
||||||
window.localStorage.setItem(k, now.toString());
|
window.localStorage.setItem(k, now.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
export function dmTo(e: RawEvent) {
|
export function dmTo(e: RawEvent) {
|
||||||
let firstP = e.tags.find((b) => b[0] === "p");
|
const firstP = e.tags.find((b) => b[0] === "p");
|
||||||
return firstP ? firstP[1] : "";
|
return firstP ? firstP[1] : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,7 +132,7 @@ export function totalUnread(dms: RawEvent[], myPubKey: HexKey) {
|
|||||||
|
|
||||||
function unreadDms(dms: RawEvent[], myPubKey: HexKey, pk: HexKey) {
|
function unreadDms(dms: RawEvent[], myPubKey: HexKey, pk: HexKey) {
|
||||||
if (pk === myPubKey) return 0;
|
if (pk === myPubKey) return 0;
|
||||||
let lastRead = lastReadDm(pk);
|
const lastRead = lastReadDm(pk);
|
||||||
return dmsInChat(dms, pk).filter(
|
return dmsInChat(dms, pk).filter(
|
||||||
(a) => a.created_at >= lastRead && a.pubkey !== myPubKey
|
(a) => a.created_at >= lastRead && a.pubkey !== myPubKey
|
||||||
).length;
|
).length;
|
||||||
|
@ -21,8 +21,8 @@ export default function NotificationsPage() {
|
|||||||
<Timeline
|
<Timeline
|
||||||
subject={{
|
subject={{
|
||||||
type: "ptag",
|
type: "ptag",
|
||||||
items: [pubkey!],
|
items: [pubkey],
|
||||||
discriminator: pubkey!.slice(0, 12),
|
discriminator: pubkey.slice(0, 12),
|
||||||
}}
|
}}
|
||||||
postsOnly={false}
|
postsOnly={false}
|
||||||
method={"TIME_RANGE"}
|
method={"TIME_RANGE"}
|
||||||
|
@ -51,7 +51,7 @@ export default function ProfilePage() {
|
|||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const id = useMemo(() => parseId(params.id!), [params]);
|
const id = useMemo(() => parseId(params.id ?? ""), [params]);
|
||||||
const user = useUserProfile(id);
|
const user = useUserProfile(id);
|
||||||
const loggedOut = useSelector<RootState, boolean | undefined>(
|
const loggedOut = useSelector<RootState, boolean | undefined>(
|
||||||
(s) => s.login.loggedOut
|
(s) => s.login.loggedOut
|
||||||
|
@ -10,7 +10,7 @@ import { System } from "Nostr/System";
|
|||||||
import messages from "./messages";
|
import messages from "./messages";
|
||||||
|
|
||||||
const SearchPage = () => {
|
const SearchPage = () => {
|
||||||
const params: any = useParams();
|
const params = useParams();
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const [search, setSearch] = useState<string>();
|
const [search, setSearch] = useState<string>();
|
||||||
const [keyword, setKeyword] = useState<string | undefined>(params.keyword);
|
const [keyword, setKeyword] = useState<string | undefined>(params.keyword);
|
||||||
@ -27,15 +27,15 @@ const SearchPage = () => {
|
|||||||
}, [search]);
|
}, [search]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let addedRelays: string[] = [];
|
const addedRelays: string[] = [];
|
||||||
for (let [k, v] of SearchRelays) {
|
for (const [k, v] of SearchRelays) {
|
||||||
if (!System.Sockets.has(k)) {
|
if (!System.Sockets.has(k)) {
|
||||||
System.ConnectToRelay(k, v);
|
System.ConnectToRelay(k, v);
|
||||||
addedRelays.push(k);
|
addedRelays.push(k);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return () => {
|
return () => {
|
||||||
for (let r of addedRelays) {
|
for (const r of addedRelays) {
|
||||||
System.DisconnectRelay(r);
|
System.DisconnectRelay(r);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -6,16 +6,16 @@ import { useNavigate } from "react-router-dom";
|
|||||||
export default function DiscoverFollows() {
|
export default function DiscoverFollows() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const sortedReccomends = useMemo(() => {
|
const sortedRecommends = useMemo(() => {
|
||||||
return RecommendedFollows.sort((a) => (Math.random() >= 0.5 ? -1 : 1));
|
return RecommendedFollows.sort(() => (Math.random() >= 0.5 ? -1 : 1));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h2>Follow some popular accounts</h2>
|
<h2>Follow some popular accounts</h2>
|
||||||
<button onClick={() => navigate("/")}>Skip</button>
|
<button onClick={() => navigate("/")}>Skip</button>
|
||||||
{sortedReccomends.length > 0 && (
|
{sortedRecommends.length > 0 && (
|
||||||
<FollowListBase pubkeys={sortedReccomends} />
|
<FollowListBase pubkeys={sortedRecommends} />
|
||||||
)}
|
)}
|
||||||
<button onClick={() => navigate("/")}>Done!</button>
|
<button onClick={() => navigate("/")}>Done!</button>
|
||||||
</>
|
</>
|
||||||
|
@ -20,15 +20,17 @@ export default function ImportFollows() {
|
|||||||
const sortedTwitterFollows = useMemo(() => {
|
const sortedTwitterFollows = useMemo(() => {
|
||||||
return follows
|
return follows
|
||||||
.map((a) => bech32ToHex(a))
|
.map((a) => bech32ToHex(a))
|
||||||
.sort((a, b) => (currentFollows.includes(a) ? 1 : -1));
|
.sort((a) => (currentFollows.includes(a) ? 1 : -1));
|
||||||
}, [follows, currentFollows]);
|
}, [follows, currentFollows]);
|
||||||
|
|
||||||
async function loadFollows() {
|
async function loadFollows() {
|
||||||
setFollows([]);
|
setFollows([]);
|
||||||
setError("");
|
setError("");
|
||||||
try {
|
try {
|
||||||
let rsp = await fetch(`${TwitterFollowsApi}?username=${twitterUsername}`);
|
const rsp = await fetch(
|
||||||
let data = await rsp.json();
|
`${TwitterFollowsApi}?username=${twitterUsername}`
|
||||||
|
);
|
||||||
|
const data = await rsp.json();
|
||||||
if (rsp.ok) {
|
if (rsp.ok) {
|
||||||
if (Array.isArray(data) && data.length === 0) {
|
if (Array.isArray(data) && data.length === 0) {
|
||||||
setError(`No nostr users found for "${twitterUsername}"`);
|
setError(`No nostr users found for "${twitterUsername}"`);
|
||||||
|
@ -7,6 +7,8 @@ import { DefaultImgProxy, setPreferences, UserPreferences } from "State/Login";
|
|||||||
import { RootState } from "State/Store";
|
import { RootState } from "State/Store";
|
||||||
|
|
||||||
import messages from "./messages";
|
import messages from "./messages";
|
||||||
|
import { unwrap } from "Util";
|
||||||
|
import "./Preferences.css";
|
||||||
|
|
||||||
const PreferencesPage = () => {
|
const PreferencesPage = () => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
@ -124,7 +126,7 @@ const PreferencesPage = () => {
|
|||||||
setPreferences({
|
setPreferences({
|
||||||
...perf,
|
...perf,
|
||||||
imgProxyConfig: {
|
imgProxyConfig: {
|
||||||
...perf.imgProxyConfig!,
|
...unwrap(perf.imgProxyConfig),
|
||||||
url: e.target.value,
|
url: e.target.value,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -147,7 +149,7 @@ const PreferencesPage = () => {
|
|||||||
setPreferences({
|
setPreferences({
|
||||||
...perf,
|
...perf,
|
||||||
imgProxyConfig: {
|
imgProxyConfig: {
|
||||||
...perf.imgProxyConfig!,
|
...unwrap(perf.imgProxyConfig),
|
||||||
key: e.target.value,
|
key: e.target.value,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -170,7 +172,7 @@ const PreferencesPage = () => {
|
|||||||
setPreferences({
|
setPreferences({
|
||||||
...perf,
|
...perf,
|
||||||
imgProxyConfig: {
|
imgProxyConfig: {
|
||||||
...perf.imgProxyConfig!,
|
...unwrap(perf.imgProxyConfig),
|
||||||
salt: e.target.value,
|
salt: e.target.value,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -31,7 +31,7 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
|
|||||||
const privKey = useSelector<RootState, HexKey | undefined>(
|
const privKey = useSelector<RootState, HexKey | undefined>(
|
||||||
(s) => s.login.privateKey
|
(s) => s.login.privateKey
|
||||||
);
|
);
|
||||||
const user = useUserProfile(id!);
|
const user = useUserProfile(id ?? "");
|
||||||
const publisher = useEventPublisher();
|
const publisher = useEventPublisher();
|
||||||
const uploader = useFileUpload();
|
const uploader = useFileUpload();
|
||||||
|
|
||||||
@ -61,7 +61,7 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
|
|||||||
|
|
||||||
async function saveProfile() {
|
async function saveProfile() {
|
||||||
// copy user object and delete internal fields
|
// copy user object and delete internal fields
|
||||||
let userCopy = {
|
const userCopy = {
|
||||||
...user,
|
...user,
|
||||||
name,
|
name,
|
||||||
display_name: displayName,
|
display_name: displayName,
|
||||||
@ -78,16 +78,16 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
|
|||||||
delete userCopy["npub"];
|
delete userCopy["npub"];
|
||||||
console.debug(userCopy);
|
console.debug(userCopy);
|
||||||
|
|
||||||
let ev = await publisher.metadata(userCopy);
|
const ev = await publisher.metadata(userCopy);
|
||||||
console.debug(ev);
|
console.debug(ev);
|
||||||
publisher.broadcast(ev);
|
publisher.broadcast(ev);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function uploadFile() {
|
async function uploadFile() {
|
||||||
let file = await openFile();
|
const file = await openFile();
|
||||||
if (file) {
|
if (file) {
|
||||||
console.log(file);
|
console.log(file);
|
||||||
let rsp = await uploader.upload(file, file.name);
|
const rsp = await uploader.upload(file, file.name);
|
||||||
console.log(rsp);
|
console.log(rsp);
|
||||||
if (typeof rsp?.error === "string") {
|
if (typeof rsp?.error === "string") {
|
||||||
throw new Error(`Upload failed ${rsp.error}`);
|
throw new Error(`Upload failed ${rsp.error}`);
|
||||||
|
@ -5,7 +5,7 @@ import { System } from "Nostr/System";
|
|||||||
import { useDispatch } from "react-redux";
|
import { useDispatch } from "react-redux";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import { removeRelay } from "State/Login";
|
import { removeRelay } from "State/Login";
|
||||||
import { parseId } from "Util";
|
import { parseId, unwrap } from "Util";
|
||||||
|
|
||||||
import messages from "./messages";
|
import messages from "./messages";
|
||||||
|
|
||||||
@ -100,7 +100,7 @@ const RelayInfo = () => {
|
|||||||
<div
|
<div
|
||||||
className="btn error"
|
className="btn error"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
dispatch(removeRelay(conn!.Address));
|
dispatch(removeRelay(unwrap(conn).Address));
|
||||||
navigate("/settings/relays");
|
navigate("/settings/relays");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -19,7 +19,7 @@ const RelaySettingsPage = () => {
|
|||||||
const [newRelay, setNewRelay] = useState<string>();
|
const [newRelay, setNewRelay] = useState<string>();
|
||||||
|
|
||||||
async function saveRelays() {
|
async function saveRelays() {
|
||||||
let ev = await publisher.saveRelays();
|
const ev = await publisher.saveRelays();
|
||||||
publisher.broadcast(ev);
|
publisher.broadcast(ev);
|
||||||
publisher.broadcastForBootstrap(ev);
|
publisher.broadcastForBootstrap(ev);
|
||||||
}
|
}
|
||||||
@ -48,7 +48,7 @@ const RelaySettingsPage = () => {
|
|||||||
|
|
||||||
function addNewRelay() {
|
function addNewRelay() {
|
||||||
if ((newRelay?.length ?? 0) > 0) {
|
if ((newRelay?.length ?? 0) > 0) {
|
||||||
const parsed = new URL(newRelay!);
|
const parsed = new URL(newRelay ?? "");
|
||||||
const payload = {
|
const payload = {
|
||||||
relays: {
|
relays: {
|
||||||
...relays,
|
...relays,
|
||||||
|
@ -215,26 +215,26 @@ const LoginSlice = createSlice({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// check pub key only
|
// check pub key only
|
||||||
let pubKey = window.localStorage.getItem(PublicKeyItem);
|
const pubKey = window.localStorage.getItem(PublicKeyItem);
|
||||||
if (pubKey && !state.privateKey) {
|
if (pubKey && !state.privateKey) {
|
||||||
state.publicKey = pubKey;
|
state.publicKey = pubKey;
|
||||||
state.loggedOut = false;
|
state.loggedOut = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let lastRelayList = window.localStorage.getItem(RelayListKey);
|
const lastRelayList = window.localStorage.getItem(RelayListKey);
|
||||||
if (lastRelayList) {
|
if (lastRelayList) {
|
||||||
state.relays = JSON.parse(lastRelayList);
|
state.relays = JSON.parse(lastRelayList);
|
||||||
} else {
|
} else {
|
||||||
state.relays = Object.fromEntries(DefaultRelays.entries());
|
state.relays = Object.fromEntries(DefaultRelays.entries());
|
||||||
}
|
}
|
||||||
|
|
||||||
let lastFollows = window.localStorage.getItem(FollowList);
|
const lastFollows = window.localStorage.getItem(FollowList);
|
||||||
if (lastFollows) {
|
if (lastFollows) {
|
||||||
state.follows = JSON.parse(lastFollows);
|
state.follows = JSON.parse(lastFollows);
|
||||||
}
|
}
|
||||||
|
|
||||||
// notifications
|
// notifications
|
||||||
let readNotif = parseInt(
|
const readNotif = parseInt(
|
||||||
window.localStorage.getItem(NotificationsReadItem) ?? "0"
|
window.localStorage.getItem(NotificationsReadItem) ?? "0"
|
||||||
);
|
);
|
||||||
if (!isNaN(readNotif)) {
|
if (!isNaN(readNotif)) {
|
||||||
@ -242,7 +242,7 @@ const LoginSlice = createSlice({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// preferences
|
// preferences
|
||||||
let pref = window.localStorage.getItem(UserPreferencesKey);
|
const pref = window.localStorage.getItem(UserPreferencesKey);
|
||||||
if (pref) {
|
if (pref) {
|
||||||
state.preferences = JSON.parse(pref);
|
state.preferences = JSON.parse(pref);
|
||||||
}
|
}
|
||||||
@ -270,15 +270,15 @@ const LoginSlice = createSlice({
|
|||||||
state.publicKey = action.payload;
|
state.publicKey = action.payload;
|
||||||
},
|
},
|
||||||
setRelays: (state, action: PayloadAction<SetRelaysPayload>) => {
|
setRelays: (state, action: PayloadAction<SetRelaysPayload>) => {
|
||||||
let relays = action.payload.relays;
|
const relays = action.payload.relays;
|
||||||
let createdAt = action.payload.createdAt;
|
const createdAt = action.payload.createdAt;
|
||||||
if (state.latestRelays > createdAt) {
|
if (state.latestRelays > createdAt) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// filter out non-websocket urls
|
// filter out non-websocket urls
|
||||||
let filtered = new Map<string, RelaySettings>();
|
const filtered = new Map<string, RelaySettings>();
|
||||||
for (let [k, v] of Object.entries(relays)) {
|
for (const [k, v] of Object.entries(relays)) {
|
||||||
if (k.startsWith("wss://") || k.startsWith("ws://")) {
|
if (k.startsWith("wss://") || k.startsWith("ws://")) {
|
||||||
filtered.set(k, v as RelaySettings);
|
filtered.set(k, v as RelaySettings);
|
||||||
}
|
}
|
||||||
@ -299,17 +299,17 @@ const LoginSlice = createSlice({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let existing = new Set(state.follows);
|
const existing = new Set(state.follows);
|
||||||
let update = Array.isArray(keys) ? keys : [keys];
|
const update = Array.isArray(keys) ? keys : [keys];
|
||||||
|
|
||||||
let changes = false;
|
let changes = false;
|
||||||
for (let pk of update.filter((a) => a.length === 64)) {
|
for (const pk of update.filter((a) => a.length === 64)) {
|
||||||
if (!existing.has(pk)) {
|
if (!existing.has(pk)) {
|
||||||
existing.add(pk);
|
existing.add(pk);
|
||||||
changes = true;
|
changes = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (let pk of existing) {
|
for (const pk of existing) {
|
||||||
if (!update.includes(pk)) {
|
if (!update.includes(pk)) {
|
||||||
existing.delete(pk);
|
existing.delete(pk);
|
||||||
changes = true;
|
changes = true;
|
||||||
@ -355,7 +355,7 @@ const LoginSlice = createSlice({
|
|||||||
}
|
}
|
||||||
|
|
||||||
let didChange = false;
|
let didChange = false;
|
||||||
for (let x of n) {
|
for (const x of n) {
|
||||||
if (!state.dms.some((a) => a.id === x.id)) {
|
if (!state.dms.some((a) => a.id === x.id)) {
|
||||||
state.dms.push(x);
|
state.dms.push(x);
|
||||||
didChange = true;
|
didChange = true;
|
||||||
@ -370,7 +370,7 @@ const LoginSlice = createSlice({
|
|||||||
state.dmInteraction += 1;
|
state.dmInteraction += 1;
|
||||||
},
|
},
|
||||||
logout: (state) => {
|
logout: (state) => {
|
||||||
let relays = { ...state.relays };
|
const relays = { ...state.relays };
|
||||||
Object.assign(state, InitState);
|
Object.assign(state, InitState);
|
||||||
state.loggedOut = true;
|
state.loggedOut = true;
|
||||||
window.localStorage.clear();
|
window.localStorage.clear();
|
||||||
@ -430,7 +430,7 @@ export function sendNotification({
|
|||||||
hasPermission && timestamp > readNotifications;
|
hasPermission && timestamp > readNotifications;
|
||||||
if (shouldShowNotification) {
|
if (shouldShowNotification) {
|
||||||
try {
|
try {
|
||||||
let worker = await navigator.serviceWorker.ready;
|
const worker = await navigator.serviceWorker.ready;
|
||||||
worker.showNotification(title, {
|
worker.showNotification(title, {
|
||||||
tag: "notification",
|
tag: "notification",
|
||||||
vibrate: [500],
|
vibrate: [500],
|
||||||
|
@ -26,7 +26,7 @@ export interface MetadataCache extends UserMetadata {
|
|||||||
|
|
||||||
export function mapEventToProfile(ev: TaggedRawEvent) {
|
export function mapEventToProfile(ev: TaggedRawEvent) {
|
||||||
try {
|
try {
|
||||||
let data: UserMetadata = JSON.parse(ev.content);
|
const data: UserMetadata = JSON.parse(ev.content);
|
||||||
return {
|
return {
|
||||||
pubkey: ev.pubkey,
|
pubkey: ev.pubkey,
|
||||||
npub: hexToBech32("npub", ev.pubkey),
|
npub: hexToBech32("npub", ev.pubkey),
|
||||||
@ -43,12 +43,12 @@ export interface UsersDb {
|
|||||||
isAvailable(): Promise<boolean>;
|
isAvailable(): Promise<boolean>;
|
||||||
query(str: string): Promise<MetadataCache[]>;
|
query(str: string): Promise<MetadataCache[]>;
|
||||||
find(key: HexKey): Promise<MetadataCache | undefined>;
|
find(key: HexKey): Promise<MetadataCache | undefined>;
|
||||||
add(user: MetadataCache): Promise<any>;
|
add(user: MetadataCache): Promise<void>;
|
||||||
put(user: MetadataCache): Promise<any>;
|
put(user: MetadataCache): Promise<void>;
|
||||||
bulkAdd(users: MetadataCache[]): Promise<any>;
|
bulkAdd(users: MetadataCache[]): Promise<void>;
|
||||||
bulkGet(keys: HexKey[]): Promise<MetadataCache[]>;
|
bulkGet(keys: HexKey[]): Promise<MetadataCache[]>;
|
||||||
bulkPut(users: MetadataCache[]): Promise<any>;
|
bulkPut(users: MetadataCache[]): Promise<void>;
|
||||||
update(key: HexKey, fields: Record<string, any>): Promise<any>;
|
update(key: HexKey, fields: Record<string, string | number>): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UsersStore {
|
export interface UsersStore {
|
||||||
|
@ -4,18 +4,19 @@ import { db as idb } from "Db";
|
|||||||
import { UsersDb, MetadataCache, setUsers } from "State/Users";
|
import { UsersDb, MetadataCache, setUsers } from "State/Users";
|
||||||
import store, { RootState } from "State/Store";
|
import store, { RootState } from "State/Store";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
|
import { unwrap } from "Util";
|
||||||
|
|
||||||
class IndexedUsersDb implements UsersDb {
|
class IndexedUsersDb implements UsersDb {
|
||||||
ready: boolean = false;
|
ready = false;
|
||||||
|
|
||||||
isAvailable() {
|
isAvailable() {
|
||||||
if ("indexedDB" in window) {
|
if ("indexedDB" in window) {
|
||||||
return new Promise<boolean>((resolve) => {
|
return new Promise<boolean>((resolve) => {
|
||||||
const req = window.indexedDB.open("dummy", 1);
|
const req = window.indexedDB.open("dummy", 1);
|
||||||
req.onsuccess = (ev) => {
|
req.onsuccess = () => {
|
||||||
resolve(true);
|
resolve(true);
|
||||||
};
|
};
|
||||||
req.onerror = (ev) => {
|
req.onerror = () => {
|
||||||
resolve(false);
|
resolve(false);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -41,30 +42,29 @@ class IndexedUsersDb implements UsersDb {
|
|||||||
.toArray();
|
.toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
bulkGet(keys: HexKey[]) {
|
async bulkGet(keys: HexKey[]) {
|
||||||
return idb.users
|
const ret = await idb.users.bulkGet(keys);
|
||||||
.bulkGet(keys)
|
return ret.filter((a) => a !== undefined).map((a_1) => unwrap(a_1));
|
||||||
.then((ret) => ret.filter((a) => a !== undefined).map((a) => a!));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
add(user: MetadataCache) {
|
async add(user: MetadataCache) {
|
||||||
return idb.users.add(user);
|
await idb.users.add(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
put(user: MetadataCache) {
|
async put(user: MetadataCache) {
|
||||||
return idb.users.put(user);
|
await idb.users.put(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
bulkAdd(users: MetadataCache[]) {
|
async bulkAdd(users: MetadataCache[]) {
|
||||||
return idb.users.bulkAdd(users);
|
await idb.users.bulkAdd(users);
|
||||||
}
|
}
|
||||||
|
|
||||||
bulkPut(users: MetadataCache[]) {
|
async bulkPut(users: MetadataCache[]) {
|
||||||
return idb.users.bulkPut(users);
|
await idb.users.bulkPut(users);
|
||||||
}
|
}
|
||||||
|
|
||||||
update(key: HexKey, fields: Record<string, any>) {
|
async update(key: HexKey, fields: Record<string, string>) {
|
||||||
return idb.users.update(key, fields);
|
await idb.users.update(key, fields);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -128,7 +128,7 @@ class ReduxUsersDb implements UsersDb {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(key: HexKey, fields: Record<string, any>) {
|
async update(key: HexKey, fields: Record<string, string>) {
|
||||||
const state = store.getState();
|
const state = store.getState();
|
||||||
const { users } = state.users;
|
const { users } = state.users;
|
||||||
const current = users[key];
|
const current = users[key];
|
||||||
|
@ -4,8 +4,9 @@ import { MetadataCache } from "State/Users";
|
|||||||
import type { RootState } from "State/Store";
|
import type { RootState } from "State/Store";
|
||||||
import { HexKey } from "Nostr";
|
import { HexKey } from "Nostr";
|
||||||
import { useDb } from "./Db";
|
import { useDb } from "./Db";
|
||||||
|
import { unwrap } from "Util";
|
||||||
|
|
||||||
export function useQuery(query: string, limit: number = 5) {
|
export function useQuery(query: string) {
|
||||||
const db = useDb();
|
const db = useDb();
|
||||||
return useLiveQuery(async () => db.query(query), [query]);
|
return useLiveQuery(async () => db.query(query), [query]);
|
||||||
}
|
}
|
||||||
@ -46,5 +47,5 @@ export function useKeys(pubKeys: HexKey[]): Map<HexKey, MetadataCache> {
|
|||||||
return new Map();
|
return new Map();
|
||||||
}, [pubKeys, users]);
|
}, [pubKeys, users]);
|
||||||
|
|
||||||
return dbUsers!;
|
return dbUsers ?? new Map();
|
||||||
}
|
}
|
||||||
|
@ -3,11 +3,11 @@ import { UploadResult } from "Upload";
|
|||||||
export default async function NostrBuild(
|
export default async function NostrBuild(
|
||||||
file: File | Blob
|
file: File | Blob
|
||||||
): Promise<UploadResult> {
|
): Promise<UploadResult> {
|
||||||
let fd = new FormData();
|
const fd = new FormData();
|
||||||
fd.append("fileToUpload", file);
|
fd.append("fileToUpload", file);
|
||||||
fd.append("submit", "Upload Image");
|
fd.append("submit", "Upload Image");
|
||||||
|
|
||||||
let rsp = await fetch("https://nostr.build/api/upload/snort.php", {
|
const rsp = await fetch("https://nostr.build/api/upload/snort.php", {
|
||||||
body: fd,
|
body: fd,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
@ -15,7 +15,7 @@ export default async function NostrBuild(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (rsp.ok) {
|
if (rsp.ok) {
|
||||||
let data = await rsp.json();
|
const data = await rsp.json();
|
||||||
return {
|
return {
|
||||||
url: new URL(data).toString(),
|
url: new URL(data).toString(),
|
||||||
};
|
};
|
||||||
|
@ -3,10 +3,10 @@ import { UploadResult } from "Upload";
|
|||||||
export default async function NostrImg(
|
export default async function NostrImg(
|
||||||
file: File | Blob
|
file: File | Blob
|
||||||
): Promise<UploadResult> {
|
): Promise<UploadResult> {
|
||||||
let fd = new FormData();
|
const fd = new FormData();
|
||||||
fd.append("image", file);
|
fd.append("image", file);
|
||||||
|
|
||||||
let rsp = await fetch("https://nostrimg.com/api/upload", {
|
const rsp = await fetch("https://nostrimg.com/api/upload", {
|
||||||
body: fd,
|
body: fd,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
@ -14,7 +14,7 @@ export default async function NostrImg(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (rsp.ok) {
|
if (rsp.ok) {
|
||||||
let data: UploadResponse = await rsp.json();
|
const data: UploadResponse = await rsp.json();
|
||||||
if (typeof data?.imageUrl === "string" && data.success) {
|
if (typeof data?.imageUrl === "string" && data.success) {
|
||||||
return {
|
return {
|
||||||
url: new URL(data.imageUrl).toString(),
|
url: new URL(data.imageUrl).toString(),
|
||||||
|
@ -13,7 +13,7 @@ export default async function VoidCat(
|
|||||||
const buf = await file.arrayBuffer();
|
const buf = await file.arrayBuffer();
|
||||||
const digest = await crypto.subtle.digest("SHA-256", buf);
|
const digest = await crypto.subtle.digest("SHA-256", buf);
|
||||||
|
|
||||||
let req = await fetch(`${VoidCatHost}/upload`, {
|
const req = await fetch(`${VoidCatHost}/upload`, {
|
||||||
mode: "cors",
|
mode: "cors",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: buf,
|
body: buf,
|
||||||
@ -28,7 +28,7 @@ export default async function VoidCat(
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (req.ok) {
|
if (req.ok) {
|
||||||
let rsp: VoidUploadResponse = await req.json();
|
const rsp: VoidUploadResponse = await req.json();
|
||||||
if (rsp.ok) {
|
if (rsp.ok) {
|
||||||
let ext = filename.match(FileExtensionRegex);
|
let ext = filename.match(FileExtensionRegex);
|
||||||
if (rsp.file?.metadata?.mimeType === "image/webp") {
|
if (rsp.file?.metadata?.mimeType === "image/webp") {
|
||||||
|
39
src/Util.ts
39
src/Util.ts
@ -1,7 +1,7 @@
|
|||||||
import * as secp from "@noble/secp256k1";
|
import * as secp from "@noble/secp256k1";
|
||||||
import { sha256 as hash } from "@noble/hashes/sha256";
|
import { sha256 as hash } from "@noble/hashes/sha256";
|
||||||
import { bech32 } from "bech32";
|
import { bech32 } from "bech32";
|
||||||
import { HexKey, RawEvent, TaggedRawEvent, u256 } from "Nostr";
|
import { HexKey, TaggedRawEvent, u256 } from "Nostr";
|
||||||
import EventKind from "Nostr/EventKind";
|
import EventKind from "Nostr/EventKind";
|
||||||
import { MessageDescriptor } from "react-intl";
|
import { MessageDescriptor } from "react-intl";
|
||||||
|
|
||||||
@ -10,11 +10,11 @@ export const sha256 = (str: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export async function openFile(): Promise<File | undefined> {
|
export async function openFile(): Promise<File | undefined> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve) => {
|
||||||
let elm = document.createElement("input");
|
const elm = document.createElement("input");
|
||||||
elm.type = "file";
|
elm.type = "file";
|
||||||
elm.onchange = (e: Event) => {
|
elm.onchange = (e: Event) => {
|
||||||
let elm = e.target as HTMLInputElement;
|
const elm = e.target as HTMLInputElement;
|
||||||
if (elm.files) {
|
if (elm.files) {
|
||||||
resolve(elm.files[0]);
|
resolve(elm.files[0]);
|
||||||
} else {
|
} else {
|
||||||
@ -36,13 +36,15 @@ export function parseId(id: string) {
|
|||||||
if (hrp.some((a) => id.startsWith(a))) {
|
if (hrp.some((a) => id.startsWith(a))) {
|
||||||
return bech32ToHex(id);
|
return bech32ToHex(id);
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) {
|
||||||
|
// Ignore the error.
|
||||||
|
}
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function bech32ToHex(str: string) {
|
export function bech32ToHex(str: string) {
|
||||||
let nKey = bech32.decode(str);
|
const nKey = bech32.decode(str);
|
||||||
let buff = bech32.fromWords(nKey.words);
|
const buff = bech32.fromWords(nKey.words);
|
||||||
return secp.utils.bytesToHex(Uint8Array.from(buff));
|
return secp.utils.bytesToHex(Uint8Array.from(buff));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,8 +54,8 @@ export function bech32ToHex(str: string) {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function bech32ToText(str: string) {
|
export function bech32ToText(str: string) {
|
||||||
let decoded = bech32.decode(str, 1000);
|
const decoded = bech32.decode(str, 1000);
|
||||||
let buf = bech32.fromWords(decoded.words);
|
const buf = bech32.fromWords(decoded.words);
|
||||||
return new TextDecoder().decode(Uint8Array.from(buf));
|
return new TextDecoder().decode(Uint8Array.from(buf));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,7 +78,7 @@ export function hexToBech32(hrp: string, hex: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let buf = secp.utils.hexToBytes(hex);
|
const buf = secp.utils.hexToBytes(hex);
|
||||||
return bech32.encode(hrp, bech32.toWords(buf));
|
return bech32.encode(hrp, bech32.toWords(buf));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Invalid hex", hex, e);
|
console.warn("Invalid hex", hex, e);
|
||||||
@ -140,13 +142,13 @@ export function getReactions(
|
|||||||
export function extractLnAddress(lnurl: string) {
|
export function extractLnAddress(lnurl: string) {
|
||||||
// some clients incorrectly set this to LNURL service, patch this
|
// some clients incorrectly set this to LNURL service, patch this
|
||||||
if (lnurl.toLowerCase().startsWith("lnurl")) {
|
if (lnurl.toLowerCase().startsWith("lnurl")) {
|
||||||
let url = bech32ToText(lnurl);
|
const url = bech32ToText(lnurl);
|
||||||
if (url.startsWith("http")) {
|
if (url.startsWith("http")) {
|
||||||
let parsedUri = new URL(url);
|
const parsedUri = new URL(url);
|
||||||
// is lightning address
|
// is lightning address
|
||||||
if (parsedUri.pathname.startsWith("/.well-known/lnurlp/")) {
|
if (parsedUri.pathname.startsWith("/.well-known/lnurlp/")) {
|
||||||
let pathParts = parsedUri.pathname.split("/");
|
const pathParts = parsedUri.pathname.split("/");
|
||||||
let username = pathParts[pathParts.length - 1];
|
const username = pathParts[pathParts.length - 1];
|
||||||
return `${username}@${parsedUri.hostname}`;
|
return `${username}@${parsedUri.hostname}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -165,7 +167,7 @@ export function unixNow() {
|
|||||||
* @returns Cancel timeout function
|
* @returns Cancel timeout function
|
||||||
*/
|
*/
|
||||||
export function debounce(timeout: number, fn: () => void) {
|
export function debounce(timeout: number, fn: () => void) {
|
||||||
let t = setTimeout(fn, timeout);
|
const t = setTimeout(fn, timeout);
|
||||||
return () => clearTimeout(t);
|
return () => clearTimeout(t);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -201,3 +203,10 @@ export function dedupeByPubkey(events: TaggedRawEvent[]) {
|
|||||||
);
|
);
|
||||||
return deduped.list as TaggedRawEvent[];
|
return deduped.list as TaggedRawEvent[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function unwrap<T>(v: T | undefined | null): T {
|
||||||
|
if (v === undefined || v === null) {
|
||||||
|
throw new Error("missing value");
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
@ -26,6 +26,7 @@ import SearchPage from "Pages/SearchPage";
|
|||||||
import HelpPage from "Pages/HelpPage";
|
import HelpPage from "Pages/HelpPage";
|
||||||
import { NewUserRoutes } from "Pages/new";
|
import { NewUserRoutes } from "Pages/new";
|
||||||
import { IntlProvider } from "./IntlProvider";
|
import { IntlProvider } from "./IntlProvider";
|
||||||
|
import { unwrap } from "Util";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP query provider
|
* HTTP query provider
|
||||||
@ -97,7 +98,7 @@ export const router = createBrowserRouter([
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(document.getElementById("root")!);
|
const root = ReactDOM.createRoot(unwrap(document.getElementById("root")));
|
||||||
root.render(
|
root.render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<Provider store={Store}>
|
<Provider store={Store}>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user