mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-04-07 03:18:02 +02:00
blur images in stream chat
This commit is contained in:
parent
5a537ab9ab
commit
e4b40dda68
5
.changeset/brave-dolls-glow.md
Normal file
5
.changeset/brave-dolls-glow.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Blur images in stream chat
|
@ -2,7 +2,7 @@ import { Box, Image, ImageProps, Link, useDisclosure } from "@chakra-ui/react";
|
||||
import appSettings from "../../services/app-settings";
|
||||
import { ImageGalleryLink } from "../image-gallery";
|
||||
import { useIsMobile } from "../../hooks/use-is-mobile";
|
||||
import { useTrusted } from "../note/trust";
|
||||
import { useTrusted } from "../../providers/trust";
|
||||
import OpenGraphCard from "../open-graph-card";
|
||||
|
||||
const BlurredImage = (props: ImageProps) => {
|
||||
|
@ -11,10 +11,9 @@ import {
|
||||
useDisclosure,
|
||||
useToast,
|
||||
} from "@chakra-ui/react";
|
||||
import { ExternalLinkIcon, LightningIcon, QrCodeIcon } from "./icons";
|
||||
import { ExternalLinkIcon, QrCodeIcon } from "./icons";
|
||||
import QrCodeSvg from "./qr-code-svg";
|
||||
import { CopyIconButton } from "./copy-icon-button";
|
||||
import { useIsMobile } from "../hooks/use-is-mobile";
|
||||
|
||||
export default function InvoiceModal({
|
||||
invoice,
|
||||
@ -22,17 +21,20 @@ export default function InvoiceModal({
|
||||
onPaid,
|
||||
...props
|
||||
}: Omit<ModalProps, "children"> & { invoice: string; onPaid: () => void }) {
|
||||
const isMobile = useIsMobile();
|
||||
const toast = useToast();
|
||||
const showQr = useDisclosure();
|
||||
|
||||
const payWithWebLn = async (invoice: string) => {
|
||||
if (window.webln && invoice) {
|
||||
if (!window.webln.enabled) await window.webln.enable();
|
||||
await window.webln.sendPayment(invoice);
|
||||
try {
|
||||
if (window.webln && invoice) {
|
||||
if (!window.webln.enabled) await window.webln.enable();
|
||||
await window.webln.sendPayment(invoice);
|
||||
|
||||
if (onPaid) onPaid();
|
||||
onClose();
|
||||
if (onPaid) onPaid();
|
||||
onClose();
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||
}
|
||||
};
|
||||
const payWithApp = async (invoice: string) => {
|
||||
|
@ -37,9 +37,7 @@ export function RepostButton({ event }: { event: NostrEvent }) {
|
||||
await nostrPostAction(clientRelaysService.getWriteUrls(), repost);
|
||||
onClose();
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
toast({ status: "error", description: e.message });
|
||||
}
|
||||
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
@ -9,7 +9,7 @@ import { UserDnsIdentityIcon } from "../user-dns-identity-icon";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import appSettings from "../../services/app-settings";
|
||||
import EventVerificationIcon from "../event-verification-icon";
|
||||
import { TrustProvider } from "./trust";
|
||||
import { TrustProvider } from "../../providers/trust";
|
||||
import { NoteLink } from "../note-link";
|
||||
|
||||
export default function EmbeddedNote({ note }: { note: NostrEvent }) {
|
||||
|
@ -32,7 +32,7 @@ import { RepostButton } from "./buttons/repost-button";
|
||||
import { QuoteRepostButton } from "./buttons/quote-repost-button";
|
||||
import { ExternalLinkIcon } from "../icons";
|
||||
import NoteContentWithWarning from "./note-content-with-warning";
|
||||
import { TrustProvider } from "./trust";
|
||||
import { TrustProvider } from "../../providers/trust";
|
||||
import { NoteLink } from "../note-link";
|
||||
import { useRegisterIntersectionEntity } from "../../providers/intersection-observer";
|
||||
|
||||
|
@ -21,11 +21,10 @@ import {
|
||||
renderOpenGraphUrl,
|
||||
} from "../embed-types";
|
||||
import { ImageGalleryProvider } from "../image-gallery";
|
||||
import { useTrusted } from "./trust";
|
||||
import { renderRedditUrl } from "../embed-types/reddit";
|
||||
import EmbeddedContent from "../embeded-content";
|
||||
|
||||
function buildContents(event: NostrEvent | DraftNostrEvent, trusted = false) {
|
||||
function buildContents(event: NostrEvent | DraftNostrEvent) {
|
||||
let content: EmbedableContent = [event.content.trim()];
|
||||
|
||||
// common
|
||||
@ -70,8 +69,7 @@ export type NoteContentsProps = {
|
||||
};
|
||||
|
||||
export const NoteContents = React.memo(({ event, maxHeight }: NoteContentsProps) => {
|
||||
const trusted = useTrusted();
|
||||
const content = buildContents(event, trusted);
|
||||
const content = buildContents(event);
|
||||
const expand = useExpand();
|
||||
const [innerHeight, setInnerHeight] = useState(0);
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
|
@ -53,12 +53,7 @@ export const NoteMenu = ({ event, ...props }: { event: NostrEvent } & Omit<MenuI
|
||||
await results.onComplete;
|
||||
deleteModal.onClose();
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
toast({
|
||||
status: "error",
|
||||
description: e.message,
|
||||
});
|
||||
}
|
||||
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||
} finally {
|
||||
setDeleting(false);
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ import { ImageIcon } from "../icons";
|
||||
import { NoteLink } from "../note-link";
|
||||
import { NoteContents } from "../note/note-contents";
|
||||
import { PostResults } from "./post-results";
|
||||
import { TrustProvider } from "../note/trust";
|
||||
import { TrustProvider } from "../../providers/trust";
|
||||
|
||||
function emptyDraft(): DraftNostrEvent {
|
||||
return {
|
||||
@ -96,12 +96,7 @@ export const PostModal = ({ isOpen, onClose, initialDraft }: PostModalProps) =>
|
||||
setDraft((d) => ({ ...d, content: (d.content += imageUrl) }));
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
toast({
|
||||
status: "error",
|
||||
description: e.message,
|
||||
});
|
||||
}
|
||||
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||
}
|
||||
setUploading(false);
|
||||
};
|
||||
|
@ -9,7 +9,7 @@ import { NoteMenu } from "./note/note-menu";
|
||||
import { UserAvatar } from "./user-avatar";
|
||||
import { UserDnsIdentityIcon } from "./user-dns-identity-icon";
|
||||
import { UserLink } from "./user-link";
|
||||
import { TrustProvider } from "./note/trust";
|
||||
import { TrustProvider } from "../providers/trust";
|
||||
import { safeJson } from "../helpers/parse";
|
||||
import { verifySignature } from "nostr-tools";
|
||||
import { useReadRelayUrls } from "../hooks/use-client-relays";
|
||||
|
@ -59,13 +59,13 @@ export default function ZapModal({
|
||||
initialAmount,
|
||||
...props
|
||||
}: ZapModalProps) {
|
||||
const isMobile = useIsMobile();
|
||||
const metadata = useUserMetadata(pubkey);
|
||||
const { requestSignature } = useSigningContext();
|
||||
const toast = useToast();
|
||||
const [promptInvoice, setPromptInvoice] = useState<string>();
|
||||
const { isOpen: showQr, onToggle: toggleQr } = useDisclosure();
|
||||
const isMobile = useIsMobile();
|
||||
const { zapAmounts } = useSubject(appSettings);
|
||||
const { customZapAmounts } = useSubject(appSettings);
|
||||
|
||||
const {
|
||||
register,
|
||||
@ -76,7 +76,7 @@ export default function ZapModal({
|
||||
} = useForm<FormValues>({
|
||||
mode: "onBlur",
|
||||
defaultValues: {
|
||||
amount: initialAmount ?? zapAmounts[0],
|
||||
amount: initialAmount ?? (parseInt(customZapAmounts.split(",")[0]) || 100),
|
||||
comment: initialComment ?? "",
|
||||
},
|
||||
});
|
||||
@ -160,19 +160,11 @@ export default function ZapModal({
|
||||
}, 1000 * 2);
|
||||
};
|
||||
|
||||
const payInvoice = (invoice: string) => {
|
||||
switch (appSettings.value.lightningPayMode) {
|
||||
case "webln":
|
||||
payWithWebLn(invoice);
|
||||
break;
|
||||
case "external":
|
||||
payWithApp(invoice);
|
||||
break;
|
||||
default:
|
||||
case "prompt":
|
||||
setPromptInvoice(invoice);
|
||||
break;
|
||||
const payInvoice = async (invoice: string) => {
|
||||
if (appSettings.value.autoPayWithWebLN) {
|
||||
await payWithWebLn(invoice);
|
||||
}
|
||||
setPromptInvoice(invoice);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
@ -228,11 +220,14 @@ export default function ZapModal({
|
||||
<UserLink pubkey={pubkey} />
|
||||
</Flex>
|
||||
<Flex gap="2" alignItems="center" flexWrap="wrap">
|
||||
{zapAmounts.map((amount, i) => (
|
||||
<Button key={amount + i} onClick={() => setValue("amount", amount)} size="sm" variant="outline">
|
||||
{amount}
|
||||
</Button>
|
||||
))}
|
||||
{customZapAmounts
|
||||
.split(",")
|
||||
.map((v) => parseInt(v))
|
||||
.map((amount, i) => (
|
||||
<Button key={amount + i} onClick={() => setValue("amount", amount)} size="sm" variant="outline">
|
||||
{amount}
|
||||
</Button>
|
||||
))}
|
||||
</Flex>
|
||||
<Flex gap="2">
|
||||
<InputGroup maxWidth={32}>
|
||||
|
@ -13,12 +13,7 @@ export default function useAppSettings() {
|
||||
try {
|
||||
return replaceSettings({ ...settings, ...newSettings });
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
toast({
|
||||
status: "error",
|
||||
description: e.message,
|
||||
});
|
||||
}
|
||||
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||
}
|
||||
},
|
||||
[settings]
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React, { useCallback, useContext, useState } from "react";
|
||||
import InvoiceModal from "../components/invoice-modal";
|
||||
import createDefer, { Deferred } from "../classes/deferred";
|
||||
import appSettings from "../services/app-settings";
|
||||
|
||||
export type InvoiceModalContext = {
|
||||
requestPay: (invoice: string) => Promise<void>;
|
||||
@ -20,7 +21,17 @@ export const InvoiceModalProvider = ({ children }: { children: React.ReactNode }
|
||||
const [invoice, setInvoice] = useState<string>();
|
||||
const [defer, setDefer] = useState<Deferred<void>>();
|
||||
|
||||
const requestPay = useCallback((invoice: string) => {
|
||||
const requestPay = useCallback(async (invoice: string) => {
|
||||
if (window.webln && appSettings.value.autoPayWithWebLN) {
|
||||
try {
|
||||
if (!window.webln.enabled) await window.webln.enable();
|
||||
await window.webln.sendPayment(invoice);
|
||||
|
||||
handlePaid();
|
||||
return;
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
const defer = createDefer<void>();
|
||||
setDefer(defer);
|
||||
setInvoice(invoice);
|
||||
|
@ -37,12 +37,7 @@ export const SigningProvider = ({ children }: { children: React.ReactNode }) =>
|
||||
if (!current) throw new Error("No account");
|
||||
return await signingService.requestSignature(draft, current);
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
toast({
|
||||
status: "error",
|
||||
description: e.message,
|
||||
});
|
||||
}
|
||||
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||
}
|
||||
},
|
||||
[toast, current]
|
||||
@ -53,12 +48,7 @@ export const SigningProvider = ({ children }: { children: React.ReactNode }) =>
|
||||
if (!current) throw new Error("No account");
|
||||
return await signingService.requestDecrypt(data, pubkey, current);
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
toast({
|
||||
status: "error",
|
||||
description: e.message,
|
||||
});
|
||||
}
|
||||
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||
}
|
||||
},
|
||||
[toast, current]
|
||||
@ -69,12 +59,7 @@ export const SigningProvider = ({ children }: { children: React.ReactNode }) =>
|
||||
if (!current) throw new Error("No account");
|
||||
return await signingService.requestEncrypt(data, pubkey, current);
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
toast({
|
||||
status: "error",
|
||||
description: e.message,
|
||||
});
|
||||
}
|
||||
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||
}
|
||||
},
|
||||
[toast, current]
|
||||
|
@ -1,8 +1,8 @@
|
||||
import React, { PropsWithChildren, useContext } from "react";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
import clientFollowingService from "../../services/client-following";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import { NostrEvent } from "../types/nostr-event";
|
||||
import { useCurrentAccount } from "../hooks/use-current-account";
|
||||
import clientFollowingService from "../services/client-following";
|
||||
import useSubject from "../hooks/use-subject";
|
||||
|
||||
const TrustContext = React.createContext<boolean>(false);
|
||||
|
@ -9,12 +9,6 @@ import db from "./db";
|
||||
|
||||
const DTAG = "nostrudel-settings";
|
||||
|
||||
export enum LightningPayMode {
|
||||
Prompt = "prompt",
|
||||
Webln = "webln",
|
||||
External = "external",
|
||||
}
|
||||
|
||||
export type AppSettings = {
|
||||
colorMode: ColorMode;
|
||||
blurImages: boolean;
|
||||
@ -22,8 +16,10 @@ export type AppSettings = {
|
||||
proxyUserMedia: boolean;
|
||||
showReactions: boolean;
|
||||
showSignatureVerification: boolean;
|
||||
lightningPayMode: LightningPayMode;
|
||||
zapAmounts: number[];
|
||||
|
||||
autoPayWithWebLN: boolean;
|
||||
customZapAmounts: string;
|
||||
|
||||
primaryColor: string;
|
||||
imageProxy: string;
|
||||
corsProxy: string;
|
||||
@ -40,8 +36,10 @@ export const defaultSettings: AppSettings = {
|
||||
proxyUserMedia: false,
|
||||
showReactions: true,
|
||||
showSignatureVerification: false,
|
||||
lightningPayMode: LightningPayMode.Prompt,
|
||||
zapAmounts: [50, 200, 500, 1000],
|
||||
|
||||
autoPayWithWebLN: true,
|
||||
customZapAmounts: "50,200,500,1000,2000,5000",
|
||||
|
||||
primaryColor: "#8DB600",
|
||||
imageProxy: "",
|
||||
corsProxy: "",
|
||||
|
@ -4,7 +4,7 @@ import { ArrowDownSIcon, ArrowUpSIcon } from "../../components/icons";
|
||||
import { Note } from "../../components/note";
|
||||
import { countReplies, ThreadItem as ThreadItemData } from "../../helpers/thread";
|
||||
import { useIsMobile } from "../../hooks/use-is-mobile";
|
||||
import { TrustProvider } from "../../components/note/trust";
|
||||
import { TrustProvider } from "../../providers/trust";
|
||||
|
||||
export type ThreadItemProps = {
|
||||
post: ThreadItemData;
|
||||
|
@ -243,12 +243,7 @@ export const ProfileEditView = () => {
|
||||
|
||||
await results.onComplete;
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
toast({
|
||||
status: "error",
|
||||
description: e.message,
|
||||
});
|
||||
}
|
||||
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -61,9 +61,7 @@ function RelaysPage() {
|
||||
}
|
||||
setRelayInputValue("");
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
toast({ status: "error", description: e.message });
|
||||
}
|
||||
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||
}
|
||||
};
|
||||
const savePending = async () => {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Button, Flex, Accordion, Link } from "@chakra-ui/react";
|
||||
import { Button, Flex, Accordion, Link, useToast } from "@chakra-ui/react";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import { GithubIcon, ToolsIcon } from "../../components/icons";
|
||||
import LightningSettings from "./lightning-settings";
|
||||
@ -10,6 +10,7 @@ import useAppSettings from "../../hooks/use-app-settings";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
|
||||
export default function SettingsView() {
|
||||
const toast = useToast();
|
||||
const { updateSettings, ...settings } = useAppSettings();
|
||||
|
||||
const form = useForm({
|
||||
@ -18,7 +19,12 @@ export default function SettingsView() {
|
||||
});
|
||||
|
||||
const saveSettings = form.handleSubmit(async (values) => {
|
||||
await updateSettings(values);
|
||||
try {
|
||||
await updateSettings(values);
|
||||
toast({ title: "Settings saved", status: "success" });
|
||||
} catch (e) {
|
||||
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
|
@ -10,13 +10,15 @@ import {
|
||||
FormHelperText,
|
||||
Input,
|
||||
Select,
|
||||
Switch,
|
||||
FormErrorMessage,
|
||||
} from "@chakra-ui/react";
|
||||
import { LightningIcon } from "../../components/icons";
|
||||
import { AppSettings } from "../../services/user-app-settings";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
export default function LightningSettings() {
|
||||
const { register } = useFormContext<AppSettings>();
|
||||
const { register, formState } = useFormContext<AppSettings>();
|
||||
|
||||
return (
|
||||
<AccordionItem>
|
||||
@ -31,44 +33,35 @@ export default function LightningSettings() {
|
||||
<AccordionPanel>
|
||||
<Flex direction="column" gap="4">
|
||||
<FormControl>
|
||||
<FormLabel htmlFor="lightningPayMode" mb="0">
|
||||
Payment mode
|
||||
</FormLabel>
|
||||
<Select id="lightningPayMode" {...register("lightningPayMode")}>
|
||||
<option value="prompt">Prompt</option>
|
||||
<option value="webln">WebLN</option>
|
||||
<option value="external">External</option>
|
||||
</Select>
|
||||
<Flex alignItems="center">
|
||||
<FormLabel htmlFor="autoPayWithWebLN" mb="0">
|
||||
Auto pay with WebLN
|
||||
</FormLabel>
|
||||
<Switch id="autoPayWithWebLN" {...register("autoPayWithWebLN")} />
|
||||
</Flex>
|
||||
|
||||
<FormHelperText>
|
||||
<span>Prompt: Ask every time</span>
|
||||
<br />
|
||||
<span>WebLN: Use browser extension</span>
|
||||
<br />
|
||||
<span>External: Open an external app using "lightning:" link</span>
|
||||
<span>Enabled: Attempt to automatically pay with WebLN if its available</span>
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
|
||||
<FormControl>
|
||||
<FormLabel htmlFor="zap-amounts" mb="0">
|
||||
<FormLabel htmlFor="customZapAmounts" mb="0">
|
||||
Zap Amounts
|
||||
</FormLabel>
|
||||
<Input
|
||||
id="zap-amounts"
|
||||
id="customZapAmounts"
|
||||
autoComplete="off"
|
||||
{...register("zapAmounts", {
|
||||
setValueAs: (value: number[] | string) => {
|
||||
if (Array.isArray(value)) {
|
||||
return Array.from(value).join(",");
|
||||
} else {
|
||||
return value
|
||||
.split(",")
|
||||
.map((v) => parseInt(v))
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => a - b);
|
||||
}
|
||||
{...register("customZapAmounts", {
|
||||
validate: (v) => {
|
||||
if (!/^[\d,]*$/.test(v)) return "Must be a list of comma separated numbers";
|
||||
return true;
|
||||
},
|
||||
})}
|
||||
/>
|
||||
{formState.errors.customZapAmounts && (
|
||||
<FormErrorMessage>{formState.errors.customZapAmounts.message}</FormErrorMessage>
|
||||
)}
|
||||
<FormHelperText>
|
||||
<span>Comma separated list of custom zap amounts</span>
|
||||
</FormHelperText>
|
||||
|
@ -36,7 +36,7 @@ import RawValue from "../../../components/debug-modals/raw-value";
|
||||
import RawJson from "../../../components/debug-modals/raw-json";
|
||||
|
||||
export default function StreamCard({ stream, ...props }: CardProps & { stream: ParsedStream }) {
|
||||
const { title, summary, identifier, image } = stream;
|
||||
const { title, identifier, image } = stream;
|
||||
const devModal = useDisclosure();
|
||||
|
||||
const naddr = useMemo(() => {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useCallback, useMemo, useRef } from "react";
|
||||
import { useMemo, useRef } from "react";
|
||||
import dayjs from "dayjs";
|
||||
import {
|
||||
Box,
|
||||
@ -11,8 +11,15 @@ import {
|
||||
Heading,
|
||||
IconButton,
|
||||
Input,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalOverlay,
|
||||
Spacer,
|
||||
Text,
|
||||
useDisclosure,
|
||||
useToast,
|
||||
} from "@chakra-ui/react";
|
||||
import { ParsedStream, buildChatMessage, getATag } from "../../../helpers/nostr/stream";
|
||||
@ -26,8 +33,15 @@ import { UserLink } from "../../../components/user-link";
|
||||
import { DraftNostrEvent, NostrEvent } from "../../../types/nostr-event";
|
||||
import IntersectionObserverProvider, { useRegisterIntersectionEntity } from "../../../providers/intersection-observer";
|
||||
import { useTimelineCurserIntersectionCallback } from "../../../hooks/use-timeline-cursor-intersection-callback";
|
||||
import { embedUrls } from "../../../helpers/embeds";
|
||||
import { embedEmoji, renderGenericUrl, renderImageUrl } from "../../../components/embed-types";
|
||||
import { EmbedableContent, embedUrls } from "../../../helpers/embeds";
|
||||
import {
|
||||
embedEmoji,
|
||||
embedNostrHashtags,
|
||||
embedNostrLinks,
|
||||
embedNostrMentions,
|
||||
renderGenericUrl,
|
||||
renderImageUrl,
|
||||
} from "../../../components/embed-types";
|
||||
import EmbeddedContent from "../../../components/embeded-content";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useSigningContext } from "../../../providers/signing-provider";
|
||||
@ -41,33 +55,50 @@ import { readablizeSats } from "../../../helpers/bolt11";
|
||||
import { Kind } from "nostr-tools";
|
||||
import useUserLNURLMetadata from "../../../hooks/use-user-lnurl-metadata";
|
||||
import { useInvoiceModalContext } from "../../../providers/invoice-modal";
|
||||
import { ImageGalleryProvider } from "../../../components/image-gallery";
|
||||
import appSettings from "../../../services/app-settings";
|
||||
import { TrustProvider } from "../../../providers/trust";
|
||||
|
||||
function ChatMessageContent({ event }: { event: NostrEvent }) {
|
||||
const content = useMemo(() => {
|
||||
let c: EmbedableContent = [event.content];
|
||||
|
||||
c = embedUrls(c, [renderImageUrl, renderGenericUrl]);
|
||||
|
||||
// nostr
|
||||
c = embedNostrLinks(c);
|
||||
c = embedNostrMentions(c, event);
|
||||
c = embedNostrHashtags(c, event);
|
||||
c = embedEmoji(c, event);
|
||||
|
||||
return c;
|
||||
}, [event.content]);
|
||||
|
||||
return <EmbeddedContent content={content} />;
|
||||
}
|
||||
|
||||
function ChatMessage({ event, stream }: { event: NostrEvent; stream: ParsedStream }) {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
useRegisterIntersectionEntity(ref, event.id);
|
||||
|
||||
const content = useMemo(() => {
|
||||
let c = embedUrls([event.content], [renderImageUrl, renderGenericUrl]);
|
||||
c = embedEmoji(c, event);
|
||||
return c;
|
||||
}, [event.content]);
|
||||
|
||||
return (
|
||||
<Flex direction="column" ref={ref}>
|
||||
<Flex gap="2" alignItems="center">
|
||||
<UserAvatar pubkey={event.pubkey} size="xs" />
|
||||
<UserLink
|
||||
pubkey={event.pubkey}
|
||||
fontWeight="bold"
|
||||
color={event.pubkey === stream.author ? "rgb(248, 56, 217)" : "cyan"}
|
||||
/>
|
||||
<Spacer />
|
||||
<Text>{dayjs.unix(event.created_at).fromNow()}</Text>
|
||||
<TrustProvider event={event}>
|
||||
<Flex direction="column" ref={ref}>
|
||||
<Flex gap="2" alignItems="center">
|
||||
<UserAvatar pubkey={event.pubkey} size="xs" />
|
||||
<UserLink
|
||||
pubkey={event.pubkey}
|
||||
fontWeight="bold"
|
||||
color={event.pubkey === stream.author ? "rgb(248, 56, 217)" : "cyan"}
|
||||
/>
|
||||
<Spacer />
|
||||
<Text>{dayjs.unix(event.created_at).fromNow()}</Text>
|
||||
</Flex>
|
||||
<Box>
|
||||
<ChatMessageContent event={event} />
|
||||
</Box>
|
||||
</Flex>
|
||||
<Box>
|
||||
<EmbeddedContent content={content} />
|
||||
</Box>
|
||||
</Flex>
|
||||
</TrustProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@ -76,28 +107,24 @@ function ZapMessage({ zap, stream }: { zap: NostrEvent; stream: ParsedStream })
|
||||
useRegisterIntersectionEntity(ref, zap.id);
|
||||
|
||||
const { request, payment } = parseZapEvent(zap);
|
||||
const content = useMemo(() => {
|
||||
let c = embedUrls([request.content], [renderImageUrl, renderGenericUrl]);
|
||||
c = embedEmoji(c, request);
|
||||
return c;
|
||||
}, [request.content]);
|
||||
|
||||
if (!payment.amount) return null;
|
||||
|
||||
return (
|
||||
<Flex direction="column" borderRadius="md" borderColor="yellow.400" borderWidth="1px" p="2" ref={ref}>
|
||||
<Flex gap="2">
|
||||
<LightningIcon color="yellow.400" />
|
||||
<UserAvatar pubkey={request.pubkey} size="xs" />
|
||||
<UserLink pubkey={request.pubkey} fontWeight="bold" color="yellow.400" />
|
||||
<Text>zapped {readablizeSats(payment.amount / 1000)} sats</Text>
|
||||
<Spacer />
|
||||
<Text>{dayjs.unix(request.created_at).fromNow()}</Text>
|
||||
<TrustProvider event={request}>
|
||||
<Flex direction="column" borderRadius="md" borderColor="yellow.400" borderWidth="1px" p="2" ref={ref}>
|
||||
<Flex gap="2">
|
||||
<LightningIcon color="yellow.400" />
|
||||
<UserAvatar pubkey={request.pubkey} size="xs" />
|
||||
<UserLink pubkey={request.pubkey} fontWeight="bold" color="yellow.400" />
|
||||
<Text>zapped {readablizeSats(payment.amount / 1000)} sats</Text>
|
||||
<Spacer />
|
||||
<Text>{dayjs.unix(request.created_at).fromNow()}</Text>
|
||||
</Flex>
|
||||
<Box>
|
||||
<ChatMessageContent event={request} />
|
||||
</Box>
|
||||
</Flex>
|
||||
<Box>
|
||||
<EmbeddedContent content={content} />
|
||||
</Box>
|
||||
</Flex>
|
||||
</TrustProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@ -106,6 +133,7 @@ export default function StreamChat({
|
||||
actions,
|
||||
...props
|
||||
}: CardProps & { stream: ParsedStream; actions?: React.ReactNode }) {
|
||||
const { customZapAmounts } = useSubject(appSettings);
|
||||
const toast = useToast();
|
||||
const contextRelays = useAdditionalRelayContext();
|
||||
const readRelays = useReadRelayUrls(contextRelays);
|
||||
@ -135,18 +163,18 @@ export default function StreamChat({
|
||||
nostrPostAction(unique([...contextRelays, ...writeRelays]), signed);
|
||||
reset();
|
||||
} catch (e) {
|
||||
if (e instanceof Error) toast({ description: e.message });
|
||||
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||
}
|
||||
});
|
||||
|
||||
const zapAmountModal = useDisclosure();
|
||||
const { requestPay } = useInvoiceModalContext();
|
||||
const zapMetadata = useUserLNURLMetadata(stream.author);
|
||||
const zapMessage = async () => {
|
||||
const zapMessage = async (amount: number) => {
|
||||
try {
|
||||
if (!zapMetadata.metadata?.callback) throw new Error("bad lnurl endpoint");
|
||||
|
||||
const content = getValues().content;
|
||||
const amount = 100;
|
||||
const zapRequest: DraftNostrEvent = {
|
||||
kind: Kind.ZapRequest,
|
||||
created_at: dayjs().unix(),
|
||||
@ -167,53 +195,93 @@ export default function StreamChat({
|
||||
|
||||
reset();
|
||||
} catch (e) {
|
||||
if (e instanceof Error) toast({ description: e.message });
|
||||
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<IntersectionObserverProvider callback={callback} root={scrollBox}>
|
||||
<Card {...props} overflow="hidden">
|
||||
<CardHeader py="3" display="flex" justifyContent="space-between" alignItems="center">
|
||||
<Heading size="md">Stream Chat</Heading>
|
||||
{actions}
|
||||
</CardHeader>
|
||||
<CardBody display="flex" flexDirection="column" gap="2" overflow="hidden" p={0}>
|
||||
<Flex
|
||||
overflowY="scroll"
|
||||
overflowX="hidden"
|
||||
ref={scrollBox}
|
||||
direction="column-reverse"
|
||||
flex={1}
|
||||
px="4"
|
||||
py="2"
|
||||
gap="2"
|
||||
>
|
||||
{events.map((event) =>
|
||||
event.kind === 1311 ? (
|
||||
<ChatMessage key={event.id} event={event} stream={stream} />
|
||||
) : (
|
||||
<ZapMessage key={event.id} zap={event} stream={stream} />
|
||||
)
|
||||
)}
|
||||
</Flex>
|
||||
<Box as="form" borderRadius="md" flexShrink={0} display="flex" gap="2" px="2" pb="2" onSubmit={sendMessage}>
|
||||
<Input placeholder="Message" {...register("content", { required: true })} autoComplete="off" />
|
||||
<Button colorScheme="brand" type="submit" isLoading={formState.isSubmitting}>
|
||||
Send
|
||||
</Button>
|
||||
{zapMetadata.metadata?.allowsNostr && (
|
||||
<IconButton
|
||||
icon={<LightningIcon color="yellow.400" />}
|
||||
aria-label="Zap stream"
|
||||
borderColor="yellow.400"
|
||||
variant="outline"
|
||||
onClick={zapMessage}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</IntersectionObserverProvider>
|
||||
<>
|
||||
<IntersectionObserverProvider callback={callback} root={scrollBox}>
|
||||
<ImageGalleryProvider>
|
||||
<Card {...props} overflow="hidden">
|
||||
<CardHeader py="3" display="flex" justifyContent="space-between" alignItems="center">
|
||||
<Heading size="md">Stream Chat</Heading>
|
||||
{actions}
|
||||
</CardHeader>
|
||||
<CardBody display="flex" flexDirection="column" gap="2" overflow="hidden" p={0}>
|
||||
<Flex
|
||||
overflowY="scroll"
|
||||
overflowX="hidden"
|
||||
ref={scrollBox}
|
||||
direction="column-reverse"
|
||||
flex={1}
|
||||
px="4"
|
||||
py="2"
|
||||
gap="2"
|
||||
>
|
||||
{events.map((event) =>
|
||||
event.kind === 1311 ? (
|
||||
<ChatMessage key={event.id} event={event} stream={stream} />
|
||||
) : (
|
||||
<ZapMessage key={event.id} zap={event} stream={stream} />
|
||||
)
|
||||
)}
|
||||
</Flex>
|
||||
<Box
|
||||
as="form"
|
||||
borderRadius="md"
|
||||
flexShrink={0}
|
||||
display="flex"
|
||||
gap="2"
|
||||
px="2"
|
||||
pb="2"
|
||||
onSubmit={sendMessage}
|
||||
>
|
||||
<Input placeholder="Message" {...register("content", { required: true })} autoComplete="off" />
|
||||
<Button colorScheme="brand" type="submit" isLoading={formState.isSubmitting}>
|
||||
Send
|
||||
</Button>
|
||||
{zapMetadata.metadata?.allowsNostr && (
|
||||
<IconButton
|
||||
icon={<LightningIcon color="yellow.400" />}
|
||||
aria-label="Zap stream"
|
||||
borderColor="yellow.400"
|
||||
variant="outline"
|
||||
onClick={zapAmountModal.onOpen}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</ImageGalleryProvider>
|
||||
</IntersectionObserverProvider>
|
||||
<Modal isOpen={zapAmountModal.isOpen} onClose={zapAmountModal.onClose}>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader pb="0">Zap Amount</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<Flex gap="2" alignItems="center" flexWrap="wrap">
|
||||
{customZapAmounts
|
||||
.split(",")
|
||||
.map((v) => parseInt(v))
|
||||
.map((amount, i) => (
|
||||
<Button
|
||||
key={amount + i}
|
||||
onClick={() => {
|
||||
zapAmountModal.onClose();
|
||||
zapMessage(amount);
|
||||
}}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
>
|
||||
{amount}
|
||||
</Button>
|
||||
))}
|
||||
</Flex>
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -45,9 +45,7 @@ function EncodeForm() {
|
||||
|
||||
setOutput(nprofile);
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
toast({ description: e.message });
|
||||
}
|
||||
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||
}
|
||||
});
|
||||
|
||||
@ -92,9 +90,7 @@ function DecodeForm() {
|
||||
try {
|
||||
setOutput(nip19.decode(values.input));
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
toast({ description: e.message });
|
||||
}
|
||||
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||
}
|
||||
});
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user