mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-04-08 20:08:02 +02:00
zap splits
This commit is contained in:
parent
c7956f00d2
commit
7a3674f8d2
5
.changeset/clever-pumas-build.md
Normal file
5
.changeset/clever-pumas-build.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"nostrudel": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add option to set zap splits when creating note
|
@ -98,7 +98,7 @@ export default function InputStep({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<CustomZapAmountOptions onSelect={(amount) => setValue("amount", amount)} />
|
<CustomZapAmountOptions onSelect={(amount) => setValue("amount", amount, { shouldDirty: true })} />
|
||||||
|
|
||||||
<Flex gap="2">
|
<Flex gap="2">
|
||||||
<Input
|
<Input
|
||||||
|
@ -50,6 +50,7 @@ export type NoteProps = Omit<CardProps, "children"> & {
|
|||||||
variant?: CardProps["variant"];
|
variant?: CardProps["variant"];
|
||||||
showReplyButton?: boolean;
|
showReplyButton?: boolean;
|
||||||
hideDrawerButton?: boolean;
|
hideDrawerButton?: boolean;
|
||||||
|
hideThreadLink?: boolean;
|
||||||
registerIntersectionEntity?: boolean;
|
registerIntersectionEntity?: boolean;
|
||||||
};
|
};
|
||||||
export const Note = React.memo(
|
export const Note = React.memo(
|
||||||
@ -58,6 +59,7 @@ export const Note = React.memo(
|
|||||||
variant = "outline",
|
variant = "outline",
|
||||||
showReplyButton,
|
showReplyButton,
|
||||||
hideDrawerButton,
|
hideDrawerButton,
|
||||||
|
hideThreadLink,
|
||||||
registerIntersectionEntity = true,
|
registerIntersectionEntity = true,
|
||||||
...props
|
...props
|
||||||
}: NoteProps) => {
|
}: NoteProps) => {
|
||||||
@ -96,6 +98,11 @@ export const Note = React.memo(
|
|||||||
<UserLink pubkey={event.pubkey} isTruncated fontWeight="bold" fontSize="lg" />
|
<UserLink pubkey={event.pubkey} isTruncated fontWeight="bold" fontSize="lg" />
|
||||||
<UserDnsIdentityIcon pubkey={event.pubkey} onlyIcon />
|
<UserDnsIdentityIcon pubkey={event.pubkey} onlyIcon />
|
||||||
<Flex grow={1} />
|
<Flex grow={1} />
|
||||||
|
{!hideThreadLink && (
|
||||||
|
<NoteLink noteId={event.id} whiteSpace="nowrap" color="current">
|
||||||
|
thread
|
||||||
|
</NoteLink>
|
||||||
|
)}
|
||||||
{showSignatureVerification && <EventVerificationIcon event={event} />}
|
{showSignatureVerification && <EventVerificationIcon event={event} />}
|
||||||
{!hideDrawerButton && (
|
{!hideDrawerButton && (
|
||||||
<OpenInDrawerButton to={`/n/${getSharableEventAddress(event)}`} size="sm" variant="ghost" />
|
<OpenInDrawerButton to={`/n/${getSharableEventAddress(event)}`} size="sm" variant="ghost" />
|
||||||
|
@ -22,10 +22,10 @@ import dayjs from "dayjs";
|
|||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { Kind } from "nostr-tools";
|
import { Kind } from "nostr-tools";
|
||||||
|
|
||||||
|
import { ArrowDownSIcon, ArrowUpSIcon, ImageIcon } from "../icons";
|
||||||
import NostrPublishAction from "../../classes/nostr-publish-action";
|
import NostrPublishAction from "../../classes/nostr-publish-action";
|
||||||
import { useWriteRelayUrls } from "../../hooks/use-client-relays";
|
import { useWriteRelayUrls } from "../../hooks/use-client-relays";
|
||||||
import { useSigningContext } from "../../providers/signing-provider";
|
import { useSigningContext } from "../../providers/signing-provider";
|
||||||
import { ArrowDownSIcon, ArrowUpSIcon, ImageIcon } from "../icons";
|
|
||||||
import { NoteContents } from "../note/note-contents";
|
import { NoteContents } from "../note/note-contents";
|
||||||
import { PublishDetails } from "../publish-details";
|
import { PublishDetails } from "../publish-details";
|
||||||
import { TrustProvider } from "../../providers/trust";
|
import { TrustProvider } from "../../providers/trust";
|
||||||
@ -35,12 +35,25 @@ import {
|
|||||||
ensureNotifyPubkeys,
|
ensureNotifyPubkeys,
|
||||||
finalizeNote,
|
finalizeNote,
|
||||||
getContentMentions,
|
getContentMentions,
|
||||||
|
setZapSplit,
|
||||||
} from "../../helpers/nostr/post";
|
} from "../../helpers/nostr/post";
|
||||||
import { UserAvatarStack } from "../compact-user-stack";
|
import { UserAvatarStack } from "../compact-user-stack";
|
||||||
import MagicTextArea, { RefType } from "../magic-textarea";
|
import MagicTextArea, { RefType } from "../magic-textarea";
|
||||||
import { useContextEmojis } from "../../providers/emoji-provider";
|
import { useContextEmojis } from "../../providers/emoji-provider";
|
||||||
import { nostrBuildUploadImage } from "../../helpers/nostr-build";
|
import { nostrBuildUploadImage } from "../../helpers/nostr-build";
|
||||||
import CommunitySelect from "./community-select";
|
import CommunitySelect from "./community-select";
|
||||||
|
import ZapSplitCreator, { fillRemainingPercent } from "./zap-split-creator";
|
||||||
|
import { EventSplit } from "../../helpers/nostr/zaps";
|
||||||
|
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||||
|
import useCacheForm from "../../hooks/use-cache-form";
|
||||||
|
|
||||||
|
type FormValues = {
|
||||||
|
content: string;
|
||||||
|
nsfw: boolean;
|
||||||
|
nsfwReason: string;
|
||||||
|
community: string;
|
||||||
|
split: EventSplit;
|
||||||
|
};
|
||||||
|
|
||||||
export default function PostModal({
|
export default function PostModal({
|
||||||
isOpen,
|
isOpen,
|
||||||
@ -48,23 +61,30 @@ export default function PostModal({
|
|||||||
initContent = "",
|
initContent = "",
|
||||||
}: Omit<ModalProps, "children"> & { initContent?: string }) {
|
}: Omit<ModalProps, "children"> & { initContent?: string }) {
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
const account = useCurrentAccount()!;
|
||||||
const { requestSignature } = useSigningContext();
|
const { requestSignature } = useSigningContext();
|
||||||
const writeRelays = useWriteRelayUrls();
|
const writeRelays = useWriteRelayUrls();
|
||||||
const [publishAction, setPublishAction] = useState<NostrPublishAction>();
|
const [publishAction, setPublishAction] = useState<NostrPublishAction>();
|
||||||
const emojis = useContextEmojis();
|
const emojis = useContextEmojis();
|
||||||
const moreOptions = useDisclosure();
|
const moreOptions = useDisclosure();
|
||||||
|
|
||||||
const { getValues, setValue, watch, register, handleSubmit, formState } = useForm({
|
const { getValues, setValue, watch, register, handleSubmit, formState, reset } = useForm<FormValues>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
content: initContent,
|
content: initContent,
|
||||||
nsfw: false,
|
nsfw: false,
|
||||||
nsfwReason: "",
|
nsfwReason: "",
|
||||||
community: "",
|
community: "",
|
||||||
|
split: [] as EventSplit,
|
||||||
},
|
},
|
||||||
|
mode: "all",
|
||||||
});
|
});
|
||||||
watch("content");
|
watch("content");
|
||||||
watch("nsfw");
|
watch("nsfw");
|
||||||
watch("nsfwReason");
|
watch("nsfwReason");
|
||||||
|
watch("split");
|
||||||
|
|
||||||
|
// cache form to localStorage
|
||||||
|
useCacheForm<FormValues>("new-note", getValues, setValue, formState);
|
||||||
|
|
||||||
const textAreaRef = useRef<RefType | null>(null);
|
const textAreaRef = useRef<RefType | null>(null);
|
||||||
const imageUploadRef = useRef<HTMLInputElement | null>(null);
|
const imageUploadRef = useRef<HTMLInputElement | null>(null);
|
||||||
@ -81,8 +101,10 @@ export default function PostModal({
|
|||||||
const content = getValues().content;
|
const content = getValues().content;
|
||||||
const position = textAreaRef.current?.getCaretPosition();
|
const position = textAreaRef.current?.getCaretPosition();
|
||||||
if (position !== undefined) {
|
if (position !== undefined) {
|
||||||
setValue("content", content.slice(0, position) + imageUrl + " " + content.slice(position));
|
setValue("content", content.slice(0, position) + imageUrl + " " + content.slice(position), {
|
||||||
} else setValue("content", content + imageUrl + " ");
|
shouldDirty: true,
|
||||||
|
});
|
||||||
|
} else setValue("content", content + imageUrl + " ", { shouldDirty: true });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||||
}
|
}
|
||||||
@ -92,7 +114,7 @@ export default function PostModal({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const getDraft = useCallback(() => {
|
const getDraft = useCallback(() => {
|
||||||
const { content, nsfw, nsfwReason, community } = getValues();
|
const { content, nsfw, nsfwReason, community, split } = getValues();
|
||||||
|
|
||||||
let updatedDraft = finalizeNote({
|
let updatedDraft = finalizeNote({
|
||||||
content: content,
|
content: content,
|
||||||
@ -113,6 +135,9 @@ export default function PostModal({
|
|||||||
const contentMentions = getContentMentions(updatedDraft.content);
|
const contentMentions = getContentMentions(updatedDraft.content);
|
||||||
updatedDraft = createEmojiTags(updatedDraft, emojis);
|
updatedDraft = createEmojiTags(updatedDraft, emojis);
|
||||||
updatedDraft = ensureNotifyPubkeys(updatedDraft, contentMentions);
|
updatedDraft = ensureNotifyPubkeys(updatedDraft, contentMentions);
|
||||||
|
if (split.length > 0) {
|
||||||
|
updatedDraft = setZapSplit(updatedDraft, fillRemainingPercent(split, account.pubkey));
|
||||||
|
}
|
||||||
return updatedDraft;
|
return updatedDraft;
|
||||||
}, [getValues, emojis]);
|
}, [getValues, emojis]);
|
||||||
|
|
||||||
@ -146,7 +171,7 @@ export default function PostModal({
|
|||||||
autoFocus
|
autoFocus
|
||||||
mb="2"
|
mb="2"
|
||||||
value={getValues().content}
|
value={getValues().content}
|
||||||
onChange={(e) => setValue("content", e.target.value)}
|
onChange={(e) => setValue("content", e.target.value, { shouldDirty: true })}
|
||||||
rows={5}
|
rows={5}
|
||||||
isRequired
|
isRequired
|
||||||
instanceRef={(inst) => (textAreaRef.current = inst)}
|
instanceRef={(inst) => (textAreaRef.current = inst)}
|
||||||
@ -192,9 +217,14 @@ export default function PostModal({
|
|||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
{mentions.length > 0 && <UserAvatarStack label="Mentions" pubkeys={mentions} />}
|
{mentions.length > 0 && <UserAvatarStack label="Mentions" pubkeys={mentions} />}
|
||||||
<Button onClick={onClose}>Cancel</Button>
|
<Button onClick={onClose} variant="ghost">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => reset()} isDisabled={!formState.isDirty}>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
colorScheme="blue"
|
colorScheme="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
isLoading={formState.isSubmitting}
|
isLoading={formState.isSubmitting}
|
||||||
onClick={submit}
|
onClick={submit}
|
||||||
@ -204,23 +234,32 @@ export default function PostModal({
|
|||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
{moreOptions.isOpen && (
|
{moreOptions.isOpen && (
|
||||||
<>
|
<Flex direction={{ base: "column", lg: "row" }} gap="4">
|
||||||
<FormControl>
|
<Flex direction="column" gap="2" flex={1}>
|
||||||
<FormLabel>Post to community</FormLabel>
|
<FormControl>
|
||||||
<CommunitySelect w="sm" {...register("community")} />
|
<FormLabel>Post to community</FormLabel>
|
||||||
</FormControl>
|
<CommunitySelect {...register("community")} />
|
||||||
<Flex gap="2" direction="column">
|
</FormControl>
|
||||||
<Switch {...register("nsfw")}>NSFW</Switch>
|
<Flex gap="2" direction="column">
|
||||||
{getValues().nsfw && <Input {...register("nsfwReason")} placeholder="Reason" maxW="50%" />}
|
<Switch {...register("nsfw")}>NSFW</Switch>
|
||||||
|
{getValues().nsfw && <Input {...register("nsfwReason")} placeholder="Reason" />}
|
||||||
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
</>
|
<Flex direction="column" gap="2" flex={1}>
|
||||||
|
<ZapSplitCreator
|
||||||
|
split={getValues().split}
|
||||||
|
onChange={(s) => setValue("split", s, { shouldDirty: true })}
|
||||||
|
authorPubkey={account?.pubkey}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onClose={onClose} size="4xl" closeOnOverlayClick={!!publishAction}>
|
<Modal isOpen={isOpen} onClose={onClose} size="4xl">
|
||||||
<ModalOverlay />
|
<ModalOverlay />
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<ModalBody display="flex" flexDirection="column" padding={["2", "2", "4"]} gap="2">
|
<ModalBody display="flex" flexDirection="column" padding={["2", "2", "4"]} gap="2">
|
||||||
|
179
src/components/post-modal/zap-split-creator.tsx
Normal file
179
src/components/post-modal/zap-split-creator.tsx
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
import {
|
||||||
|
Flex,
|
||||||
|
Heading,
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
NumberDecrementStepper,
|
||||||
|
NumberIncrementStepper,
|
||||||
|
NumberInput,
|
||||||
|
NumberInputField,
|
||||||
|
NumberInputStepper,
|
||||||
|
Text,
|
||||||
|
useToast,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import { CloseIcon } from "@chakra-ui/icons";
|
||||||
|
import { nip19 } from "nostr-tools";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
|
import { EventSplit } from "../../helpers/nostr/zaps";
|
||||||
|
import { AddIcon } from "../icons";
|
||||||
|
import { useUserDirectoryContext } from "../../providers/user-directory-provider";
|
||||||
|
import { useAsync } from "react-use";
|
||||||
|
import { getUserDisplayName } from "../../helpers/user-metadata";
|
||||||
|
import userMetadataService from "../../services/user-metadata";
|
||||||
|
import { normalizeToHex } from "../../helpers/nip19";
|
||||||
|
import { UserAvatar } from "../user-avatar";
|
||||||
|
import { UserLink } from "../user-link";
|
||||||
|
|
||||||
|
function getRemainingPercent(split: EventSplit) {
|
||||||
|
return Math.round((1 - split.reduce((v, p) => v + p.percent, 0)) * 100) / 100;
|
||||||
|
}
|
||||||
|
export function fillRemainingPercent(split: EventSplit, pubkey: string) {
|
||||||
|
const remainingPercent = getRemainingPercent(split);
|
||||||
|
if (remainingPercent === 0) return split;
|
||||||
|
return split.concat({ pubkey, percent: remainingPercent });
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateNpub(input: string) {
|
||||||
|
const pubkey = normalizeToHex(input);
|
||||||
|
if (!pubkey) {
|
||||||
|
return "Invalid npub";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function AddUserForm({
|
||||||
|
onSubmit,
|
||||||
|
remainingPercent,
|
||||||
|
}: {
|
||||||
|
onSubmit: (values: { pubkey: string; percent: number }) => void;
|
||||||
|
remainingPercent: number;
|
||||||
|
}) {
|
||||||
|
const toast = useToast();
|
||||||
|
const { register, handleSubmit, getValues, setValue, reset, watch } = useForm({
|
||||||
|
defaultValues: {
|
||||||
|
pubkey: "",
|
||||||
|
percent: Math.min(remainingPercent, 50),
|
||||||
|
},
|
||||||
|
mode: "all",
|
||||||
|
});
|
||||||
|
watch("percent");
|
||||||
|
|
||||||
|
const getDirectory = useUserDirectoryContext();
|
||||||
|
const { value: users } = useAsync(async () => {
|
||||||
|
const dir = await getDirectory();
|
||||||
|
return dir.map((pubkey) => ({ pubkey, metadata: userMetadataService.getSubject(pubkey).value }));
|
||||||
|
}, [getDirectory]);
|
||||||
|
|
||||||
|
const submit = handleSubmit((values) => {
|
||||||
|
try {
|
||||||
|
const pubkey = normalizeToHex(values.pubkey);
|
||||||
|
if (!pubkey) throw new Error("Invalid npub");
|
||||||
|
const percent = values.percent / 100;
|
||||||
|
onSubmit({ pubkey, percent });
|
||||||
|
reset();
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex as="form" gap="2" onSubmit={submit}>
|
||||||
|
<Input placeholder="npub..." list="users" {...register("pubkey", { required: true, validate: validateNpub })} />
|
||||||
|
{users && (
|
||||||
|
<datalist id="users">
|
||||||
|
{users
|
||||||
|
.filter((p) => !!p.metadata)
|
||||||
|
.map(({ metadata, pubkey }) => (
|
||||||
|
<option key={pubkey} value={nip19.npubEncode(pubkey)}>
|
||||||
|
{getUserDisplayName(metadata, pubkey)}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</datalist>
|
||||||
|
)}
|
||||||
|
<NumberInput
|
||||||
|
step={1}
|
||||||
|
min={1}
|
||||||
|
max={remainingPercent}
|
||||||
|
value={getValues().percent || 0}
|
||||||
|
onChange={(_, n) => setValue("percent", n, { shouldDirty: true })}
|
||||||
|
>
|
||||||
|
<NumberInputField size={8} />
|
||||||
|
<NumberInputStepper>
|
||||||
|
<NumberIncrementStepper />
|
||||||
|
<NumberDecrementStepper />
|
||||||
|
</NumberInputStepper>
|
||||||
|
</NumberInput>
|
||||||
|
<IconButton icon={<AddIcon />} aria-label="Add" type="submit" />
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function UserCard({
|
||||||
|
pubkey,
|
||||||
|
percent,
|
||||||
|
showRemove = true,
|
||||||
|
onRemove,
|
||||||
|
}: {
|
||||||
|
pubkey: string;
|
||||||
|
percent: number;
|
||||||
|
showRemove?: boolean;
|
||||||
|
onRemove?: () => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Flex gap="2" overflow="hidden" alignItems="center">
|
||||||
|
<UserAvatar pubkey={pubkey} size="sm" />
|
||||||
|
<UserLink pubkey={pubkey} fontWeight="bold" isTruncated />
|
||||||
|
<Text fontWeight="bold" fontSize="lg" ml="auto">
|
||||||
|
{Math.round(percent * 10000) / 100}%
|
||||||
|
</Text>
|
||||||
|
{showRemove && (
|
||||||
|
<IconButton
|
||||||
|
variant="ghost"
|
||||||
|
icon={<CloseIcon />}
|
||||||
|
aria-label="Remove from split"
|
||||||
|
title="Remove"
|
||||||
|
onClick={onRemove}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ZapSplitCreator({
|
||||||
|
split,
|
||||||
|
onChange,
|
||||||
|
authorPubkey,
|
||||||
|
}: {
|
||||||
|
split: EventSplit;
|
||||||
|
onChange: (split: EventSplit) => void;
|
||||||
|
authorPubkey?: string;
|
||||||
|
}) {
|
||||||
|
const remainingPercent = getRemainingPercent(split);
|
||||||
|
|
||||||
|
const addUser = ({ pubkey, percent }: { pubkey: string; percent: number }) => {
|
||||||
|
if (percent > remainingPercent) throw new Error("Not enough percent left");
|
||||||
|
if (split.some((s) => s.pubkey === pubkey)) throw new Error("User already in split");
|
||||||
|
onChange(split.concat({ pubkey, percent }));
|
||||||
|
};
|
||||||
|
const removeUser = (pubkey: string) => {
|
||||||
|
onChange(split.filter((p) => p.pubkey !== pubkey));
|
||||||
|
};
|
||||||
|
|
||||||
|
const displaySplit = authorPubkey ? fillRemainingPercent(split, authorPubkey) : split;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex gap="2" direction="column">
|
||||||
|
<Heading size="sm">Zap Splits</Heading>
|
||||||
|
{remainingPercent > 0 && <AddUserForm onSubmit={addUser} remainingPercent={remainingPercent * 100} />}
|
||||||
|
{displaySplit.map(({ pubkey, percent }) => (
|
||||||
|
<UserCard
|
||||||
|
key={pubkey}
|
||||||
|
pubkey={pubkey}
|
||||||
|
percent={percent}
|
||||||
|
showRemove={pubkey !== authorPubkey}
|
||||||
|
onRemove={() => removeUser(pubkey)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
@ -5,6 +5,7 @@ import { getEventRelays } from "../../services/event-relays";
|
|||||||
import relayScoreboardService from "../../services/relay-scoreboard";
|
import relayScoreboardService from "../../services/relay-scoreboard";
|
||||||
import { getPubkey, safeDecode } from "../nip19";
|
import { getPubkey, safeDecode } from "../nip19";
|
||||||
import { Emoji } from "../../providers/emoji-provider";
|
import { Emoji } from "../../providers/emoji-provider";
|
||||||
|
import { EventSplit } from "./zaps";
|
||||||
|
|
||||||
function addTag(tags: Tag[], tag: Tag, overwrite = false) {
|
function addTag(tags: Tag[], tag: Tag, overwrite = false) {
|
||||||
if (tags.some((t) => t[0] === tag[0] && t[1] === tag[1])) {
|
if (tags.some((t) => t[0] === tag[0] && t[1] === tag[1])) {
|
||||||
@ -110,6 +111,16 @@ export function createEmojiTags(draft: DraftNostrEvent, emojis: Emoji[]) {
|
|||||||
return updatedDraft;
|
return updatedDraft;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setZapSplit(draft: DraftNostrEvent, split: EventSplit) {
|
||||||
|
const updatedDraft: DraftNostrEvent = { ...draft, tags: Array.from(draft.tags) };
|
||||||
|
|
||||||
|
// TODO: get best input relay for user
|
||||||
|
const zapTags = split.map((p) => ["zap", p.pubkey, "", String(p.percent * 100)]);
|
||||||
|
updatedDraft.tags.push(...zapTags);
|
||||||
|
|
||||||
|
return updatedDraft;
|
||||||
|
}
|
||||||
|
|
||||||
export function finalizeNote(draft: DraftNostrEvent) {
|
export function finalizeNote(draft: DraftNostrEvent) {
|
||||||
let updated: DraftNostrEvent = { ...draft, tags: Array.from(draft.tags) };
|
let updated: DraftNostrEvent = { ...draft, tags: Array.from(draft.tags) };
|
||||||
updated.content = correctContentMentions(updated.content);
|
updated.content = correctContentMentions(updated.content);
|
||||||
|
40
src/hooks/use-cache-form.ts
Normal file
40
src/hooks/use-cache-form.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { useCallback, useRef } from "react";
|
||||||
|
import { FieldValues, UseFormGetValues, UseFormSetValue, UseFormStateReturn } from "react-hook-form";
|
||||||
|
import { useMount, useUnmount } from "react-use";
|
||||||
|
|
||||||
|
// TODO: make these caches expire
|
||||||
|
export default function useCacheForm<TFieldValues extends FieldValues = FieldValues>(
|
||||||
|
key: string,
|
||||||
|
getValues: UseFormGetValues<TFieldValues>,
|
||||||
|
setValue: UseFormSetValue<TFieldValues>,
|
||||||
|
state: UseFormStateReturn<TFieldValues>,
|
||||||
|
) {
|
||||||
|
const storageKey = key + "-form-values";
|
||||||
|
|
||||||
|
useMount(() => {
|
||||||
|
try {
|
||||||
|
const cached = localStorage.getItem(storageKey);
|
||||||
|
localStorage.removeItem(storageKey);
|
||||||
|
|
||||||
|
if (cached) {
|
||||||
|
const values = JSON.parse(cached) as TFieldValues;
|
||||||
|
for (const [key, value] of Object.entries(values)) {
|
||||||
|
// @ts-ignore
|
||||||
|
setValue(key, value, { shouldDirty: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
});
|
||||||
|
|
||||||
|
const stateRef = useRef<UseFormStateReturn<TFieldValues>>(state);
|
||||||
|
stateRef.current = state;
|
||||||
|
useUnmount(() => {
|
||||||
|
if (stateRef.current.isDirty && !stateRef.current.isSubmitted) {
|
||||||
|
localStorage.setItem(storageKey, JSON.stringify(getValues()));
|
||||||
|
} else localStorage.removeItem(storageKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
return useCallback(() => {
|
||||||
|
localStorage.removeItem(storageKey);
|
||||||
|
}, [storageKey]);
|
||||||
|
}
|
@ -26,7 +26,7 @@ export default function PostModalProvider({ children }: PropsWithChildren) {
|
|||||||
return (
|
return (
|
||||||
<PostModalContext.Provider value={context}>
|
<PostModalContext.Provider value={context}>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<PostModal isOpen={isOpen} onClose={onClose} initContent={initContent} />
|
{isOpen && <PostModal isOpen={isOpen} onClose={onClose} initContent={initContent} />}
|
||||||
{children}
|
{children}
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</PostModalContext.Provider>
|
</PostModalContext.Provider>
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Button, Flex, FormControl, FormHelperText, FormLabel, Input, Link, useToast } from "@chakra-ui/react";
|
import { Button, Flex, FormControl, FormHelperText, FormLabel, Input, Link, useToast } from "@chakra-ui/react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import { RelayUrlInput } from "../../components/relay-url-input";
|
import { RelayUrlInput } from "../../components/relay-url-input";
|
||||||
import { normalizeToHex } from "../../helpers/nip19";
|
import { normalizeToHex } from "../../helpers/nip19";
|
||||||
import accountService from "../../services/account";
|
import accountService from "../../services/account";
|
||||||
import clientRelaysService from "../../services/client-relays";
|
|
||||||
|
|
||||||
export default function LoginNpubView() {
|
export default function LoginNpubView() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
@ -66,8 +66,8 @@ export default function ReplyForm({ item, onCancel, onSubmitted }: ReplyFormProp
|
|||||||
const content = getValues().content;
|
const content = getValues().content;
|
||||||
const position = textAreaRef.current?.getCaretPosition();
|
const position = textAreaRef.current?.getCaretPosition();
|
||||||
if (position !== undefined) {
|
if (position !== undefined) {
|
||||||
setValue("content", content.slice(0, position) + imageUrl + content.slice(position));
|
setValue("content", content.slice(0, position) + imageUrl + content.slice(position), { shouldDirty: true });
|
||||||
} else setValue("content", content + imageUrl);
|
} else setValue("content", content + imageUrl, { shouldDirty: true });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||||
}
|
}
|
||||||
@ -105,7 +105,7 @@ export default function ReplyForm({ item, onCancel, onSubmitted }: ReplyFormProp
|
|||||||
rows={4}
|
rows={4}
|
||||||
isRequired
|
isRequired
|
||||||
value={getValues().content}
|
value={getValues().content}
|
||||||
onChange={(e) => setValue("content", e.target.value)}
|
onChange={(e) => setValue("content", e.target.value, { shouldDirty: true })}
|
||||||
instanceRef={(inst) => (textAreaRef.current = inst)}
|
instanceRef={(inst) => (textAreaRef.current = inst)}
|
||||||
onPaste={(e) => {
|
onPaste={(e) => {
|
||||||
const imageFile = Array.from(e.clipboardData.files).find((f) => f.type.includes("image"));
|
const imageFile = Array.from(e.clipboardData.files).find((f) => f.type.includes("image"));
|
||||||
|
@ -44,7 +44,12 @@ export const ThreadPost = ({ post, initShowReplies, focusId }: ThreadItemProps)
|
|||||||
muteAlert
|
muteAlert
|
||||||
) : (
|
) : (
|
||||||
<TrustProvider trust={focusId === post.event.id ? true : undefined}>
|
<TrustProvider trust={focusId === post.event.id ? true : undefined}>
|
||||||
<Note event={post.event} borderColor={focusId === post.event.id ? "blue.500" : undefined} hideDrawerButton />
|
<Note
|
||||||
|
event={post.event}
|
||||||
|
borderColor={focusId === post.event.id ? "blue.500" : undefined}
|
||||||
|
hideDrawerButton
|
||||||
|
hideThreadLink
|
||||||
|
/>
|
||||||
</TrustProvider>
|
</TrustProvider>
|
||||||
)}
|
)}
|
||||||
{showReplyForm.isOpen && (
|
{showReplyForm.isOpen && (
|
||||||
|
@ -53,13 +53,13 @@ export default function NoteView() {
|
|||||||
pageContent = (
|
pageContent = (
|
||||||
<>
|
<>
|
||||||
{parentPosts.map((parent) => (
|
{parentPosts.map((parent) => (
|
||||||
<Note key={parent.event.id + "-rely"} event={parent.event} hideDrawerButton />
|
<Note key={parent.event.id + "-rely"} event={parent.event} hideDrawerButton hideThreadLink />
|
||||||
))}
|
))}
|
||||||
<ThreadPost key={post.event.id} post={post} initShowReplies focusId={focusId} />
|
<ThreadPost key={post.event.id} post={post} initShowReplies focusId={focusId} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
} else if (events[focusId]) {
|
} else if (events[focusId]) {
|
||||||
pageContent = <Note event={events[focusId]} variant="filled" hideDrawerButton />;
|
pageContent = <Note event={events[focusId]} variant="filled" hideDrawerButton hideThreadLink />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <VerticalPageLayout>{pageContent}</VerticalPageLayout>;
|
return <VerticalPageLayout>{pageContent}</VerticalPageLayout>;
|
||||||
|
@ -51,7 +51,11 @@ export default function RelayReviewForm({
|
|||||||
<Flex as="form" direction="column" onSubmit={onSubmit} gap="2" mb="2" {...props}>
|
<Flex as="form" direction="column" onSubmit={onSubmit} gap="2" mb="2" {...props}>
|
||||||
<Flex gap="2">
|
<Flex gap="2">
|
||||||
<Heading size="md">Write review</Heading>
|
<Heading size="md">Write review</Heading>
|
||||||
<StarRating quality={getValues().quality} fontSize="1.5rem" onChange={(q) => setValue("quality", q)} />
|
<StarRating
|
||||||
|
quality={getValues().quality}
|
||||||
|
fontSize="1.5rem"
|
||||||
|
onChange={(q) => setValue("quality", q, { shouldDirty: true })}
|
||||||
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Textarea {...register("content")} rows={5} placeholder="A short description of your experience with the relay" />
|
<Textarea {...register("content")} rows={5} placeholder="A short description of your experience with the relay" />
|
||||||
<Flex gap="2" ml="auto">
|
<Flex gap="2" ml="auto">
|
||||||
|
@ -54,8 +54,8 @@ export default function ChatMessageForm({ stream, hideZapButton }: { stream: Par
|
|||||||
const content = getValues().content;
|
const content = getValues().content;
|
||||||
const position = textAreaRef.current?.getCaretPosition();
|
const position = textAreaRef.current?.getCaretPosition();
|
||||||
if (position !== undefined) {
|
if (position !== undefined) {
|
||||||
setValue("content", content.slice(0, position) + imageUrl + content.slice(position));
|
setValue("content", content.slice(0, position) + imageUrl + content.slice(position), { shouldDirty: true });
|
||||||
} else setValue("content", content + imageUrl);
|
} else setValue("content", content + imageUrl, { shouldDirty: true });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||||
}
|
}
|
||||||
@ -75,7 +75,7 @@ export default function ChatMessageForm({ stream, hideZapButton }: { stream: Par
|
|||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
isRequired
|
isRequired
|
||||||
value={getValues().content}
|
value={getValues().content}
|
||||||
onChange={(e) => setValue("content", e.target.value)}
|
onChange={(e) => setValue("content", e.target.value, { shouldDirty: true })}
|
||||||
onPaste={(e) => {
|
onPaste={(e) => {
|
||||||
const file = Array.from(e.clipboardData.files).find((f) => f.type.includes("image"));
|
const file = Array.from(e.clipboardData.files).find((f) => f.type.includes("image"));
|
||||||
if (file) uploadImage(file);
|
if (file) uploadImage(file);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user