mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-04-09 20:29:17 +02:00
fix bug where mentioning npub would freeze app
This commit is contained in:
parent
f83d1ad7df
commit
fbc1ea4ebb
5
.changeset/stupid-dodos-design.md
Normal file
5
.changeset/stupid-dodos-design.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": patch
|
||||
---
|
||||
|
||||
fix bug where mentioning npub would freeze app
|
@ -1,16 +1,28 @@
|
||||
import { useContext } from "react";
|
||||
import { IconButton } from "@chakra-ui/react";
|
||||
import { Kind } from "nostr-tools";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import { QuoteRepostIcon } from "../../icons";
|
||||
import { PostModalContext } from "../../../providers/post-modal-provider";
|
||||
import { buildQuoteRepost } from "../../../helpers/nostr/event";
|
||||
import { useCurrentAccount } from "../../../hooks/use-current-account";
|
||||
import { getSharableNoteId } from "../../../helpers/nip19";
|
||||
|
||||
export function QuoteRepostButton({ event }: { event: NostrEvent }) {
|
||||
const account = useCurrentAccount();
|
||||
const { openModal } = useContext(PostModalContext);
|
||||
|
||||
const handleClick = () => openModal(buildQuoteRepost(event));
|
||||
const handleClick = () => {
|
||||
const nevent = getSharableNoteId(event.id);
|
||||
const draft = {
|
||||
kind: Kind.Text,
|
||||
tags: [],
|
||||
content: "nostr:" + nevent,
|
||||
created_at: dayjs().unix(),
|
||||
};
|
||||
openModal(draft);
|
||||
};
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
|
@ -1,24 +0,0 @@
|
||||
import { useContext } from "react";
|
||||
import { IconButton } from "@chakra-ui/react";
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import { ReplyIcon } from "../../icons";
|
||||
import { PostModalContext } from "../../../providers/post-modal-provider";
|
||||
import { buildReply } from "../../../helpers/nostr/event";
|
||||
import { useCurrentAccount } from "../../../hooks/use-current-account";
|
||||
|
||||
export function ReplyButton({ event }: { event: NostrEvent }) {
|
||||
const account = useCurrentAccount();
|
||||
const { openModal } = useContext(PostModalContext);
|
||||
|
||||
const reply = () => openModal(buildReply(event));
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
icon={<ReplyIcon />}
|
||||
title="Reply"
|
||||
aria-label="Reply"
|
||||
onClick={reply}
|
||||
isDisabled={account?.readonly ?? true}
|
||||
/>
|
||||
);
|
||||
}
|
@ -14,6 +14,7 @@ import {
|
||||
useToast,
|
||||
} from "@chakra-ui/react";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import NostrPublishAction from "../../classes/nostr-publish-action";
|
||||
import { getReferences } from "../../helpers/nostr/event";
|
||||
import { useWriteRelayUrls } from "../../hooks/use-client-relays";
|
||||
@ -24,7 +25,8 @@ import { NoteLink } from "../note-link";
|
||||
import { NoteContents } from "../note/note-contents";
|
||||
import { PublishDetails } from "../publish-details";
|
||||
import { TrustProvider } from "../../providers/trust";
|
||||
import { finalizeNote } from "../../helpers/nostr/post";
|
||||
import { ensureNotifyPubkeys, finalizeNote, getContentMentions } from "../../helpers/nostr/post";
|
||||
import { UserAvatarStack } from "../user-avatar-stack";
|
||||
|
||||
function emptyDraft(): DraftNostrEvent {
|
||||
return {
|
||||
@ -77,7 +79,9 @@ export const PostModal = ({ isOpen, onClose, initialDraft }: PostModalProps) =>
|
||||
|
||||
const handleSubmit = async () => {
|
||||
setWaiting(true);
|
||||
const updatedDraft = finalizeNote(draft);
|
||||
let updatedDraft = finalizeNote(draft);
|
||||
const contentMentions = getContentMentions(draft.content);
|
||||
updatedDraft = ensureNotifyPubkeys(updatedDraft, contentMentions);
|
||||
const signed = await requestSignature(updatedDraft);
|
||||
setWaiting(false);
|
||||
if (!signed) return;
|
||||
@ -144,6 +148,7 @@ export const PostModal = ({ isOpen, onClose, initialDraft }: PostModalProps) =>
|
||||
isLoading={uploading}
|
||||
/>
|
||||
</Flex>
|
||||
<UserAvatarStack label="Mentions" users={getContentMentions(draft.content)} />
|
||||
{draft.content.length > 0 && <Button onClick={togglePreview}>Preview</Button>}
|
||||
<Button onClick={onClose}>Cancel</Button>
|
||||
<Button colorScheme="blue" type="submit" isLoading={waiting} onClick={handleSubmit} isDisabled={!canSubmit}>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { bech32 } from "bech32";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import { getPublicKey, nip19 } from "nostr-tools";
|
||||
import { getEventRelays } from "../services/event-relays";
|
||||
import relayScoreboardService from "../services/relay-scoreboard";
|
||||
|
||||
@ -69,6 +69,18 @@ export function safeDecode(str: string) {
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
export function getPubkey(result: nip19.DecodeResult){
|
||||
switch(result.type){
|
||||
case 'naddr':
|
||||
case 'nprofile':
|
||||
return result.data.pubkey
|
||||
case 'npub':
|
||||
return result.data;
|
||||
case 'nsec':
|
||||
return getPublicKey(result.data)
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeToBech32(key: string, prefix: Bech32Prefix = Bech32Prefix.Pubkey) {
|
||||
if (isHex(key)) return hexToBech32(key, prefix);
|
||||
if (isBech32Key(key)) return key;
|
||||
|
@ -2,10 +2,8 @@ import dayjs from "dayjs";
|
||||
import { getEventRelays } from "../../services/event-relays";
|
||||
import { DraftNostrEvent, isETag, isPTag, NostrEvent, RTag, Tag } from "../../types/nostr-event";
|
||||
import { RelayConfig, RelayMode } from "../../classes/relay";
|
||||
import accountService from "../../services/account";
|
||||
import { Kind, nip19 } from "nostr-tools";
|
||||
import { getMatchNostrLink } from "../regexp";
|
||||
import { getSharableNoteId } from "../nip19";
|
||||
import relayScoreboardService from "../../services/relay-scoreboard";
|
||||
import { getAddr } from "../../services/replaceable-event-requester";
|
||||
|
||||
@ -134,37 +132,6 @@ export function getReferences(event: NostrEvent | DraftNostrEvent) {
|
||||
};
|
||||
}
|
||||
|
||||
export function buildReply(event: NostrEvent, account = accountService.current.value): DraftNostrEvent {
|
||||
const refs = getReferences(event);
|
||||
const relay = getEventRelays(event.id).value?.[0] ?? "";
|
||||
|
||||
const tags: NostrEvent["tags"] = [];
|
||||
|
||||
const rootId = refs.rootId ?? event.id;
|
||||
const replyId = event.id;
|
||||
|
||||
tags.push(["e", rootId, relay, "root"]);
|
||||
if (replyId !== rootId) {
|
||||
tags.push(["e", replyId, relay, "reply"]);
|
||||
}
|
||||
// add all ptags
|
||||
// TODO: omit my own pubkey
|
||||
const ptags = event.tags.filter(isPTag).filter((t) => !account || t[1] !== account.pubkey);
|
||||
tags.push(...ptags);
|
||||
// add the original authors pubkey if its not already there
|
||||
if (!ptags.some((t) => t[1] === event.pubkey)) {
|
||||
tags.push(["p", event.pubkey]);
|
||||
}
|
||||
|
||||
return {
|
||||
kind: Kind.Text,
|
||||
// TODO: be smarter about picking relay
|
||||
tags,
|
||||
content: "",
|
||||
created_at: dayjs().unix(),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildRepost(event: NostrEvent): DraftNostrEvent {
|
||||
const relays = getEventRelays(event.id).value;
|
||||
const topRelay = relayScoreboardService.getRankedRelays(relays)[0] ?? "";
|
||||
@ -180,17 +147,6 @@ export function buildRepost(event: NostrEvent): DraftNostrEvent {
|
||||
};
|
||||
}
|
||||
|
||||
export function buildQuoteRepost(event: NostrEvent): DraftNostrEvent {
|
||||
const nevent = getSharableNoteId(event.id);
|
||||
|
||||
return {
|
||||
kind: Kind.Text,
|
||||
tags: [],
|
||||
content: "nostr:" + nevent,
|
||||
created_at: dayjs().unix(),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildDeleteEvent(eventIds: string[], reason = ""): DraftNostrEvent {
|
||||
return {
|
||||
kind: Kind.EventDeletion,
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { DraftNostrEvent, NostrEvent, PTag, Tag } from "../../types/nostr-event";
|
||||
import { getMatchHashtag, getMentionNpubOrNote } from "../regexp";
|
||||
import { normalizeToHex } from "../nip19";
|
||||
import { getMatchHashtag } from "../regexp";
|
||||
import { getReferences } from "./event";
|
||||
import { getEventRelays } from "../../services/event-relays";
|
||||
import relayScoreboardService from "../../services/relay-scoreboard";
|
||||
import { getPubkey, safeDecode } from "../nip19";
|
||||
|
||||
function addTag(tags: Tag[], tag: Tag, overwrite = false) {
|
||||
if (tags.some((t) => t[0] === tag[0] && t[1] === tag[1])) {
|
||||
@ -50,7 +50,7 @@ export function addReplyTags(draft: DraftNostrEvent, replyTo: NostrEvent) {
|
||||
}
|
||||
|
||||
/** ensure a list of pubkeys are present on an event */
|
||||
export function ensureNotifyUsers(draft: DraftNostrEvent, pubkeys: string[]) {
|
||||
export function ensureNotifyPubkeys(draft: DraftNostrEvent, pubkeys: string[]) {
|
||||
const updated: DraftNostrEvent = { ...draft, tags: Array.from(draft.tags) };
|
||||
|
||||
for (const pubkey of pubkeys) {
|
||||
@ -60,30 +60,19 @@ export function ensureNotifyUsers(draft: DraftNostrEvent, pubkeys: string[]) {
|
||||
return updated;
|
||||
}
|
||||
|
||||
export function replaceAtMentions(draft: DraftNostrEvent) {
|
||||
const updatedDraft: DraftNostrEvent = { ...draft, tags: Array.from(draft.tags) };
|
||||
export function getContentMentions(content: string) {
|
||||
const matched = content.matchAll(/nostr:(npub1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{58})/gi);
|
||||
return Array.from(matched)
|
||||
.map((m) => {
|
||||
const parsed = safeDecode(m[1]);
|
||||
return parsed && getPubkey(parsed);
|
||||
})
|
||||
.filter(Boolean) as string[];
|
||||
}
|
||||
|
||||
// replace all occurrences of @npub and @note
|
||||
while (true) {
|
||||
const match = getMentionNpubOrNote().exec(updatedDraft.content);
|
||||
if (!match || match.index === undefined) break;
|
||||
|
||||
const hex = normalizeToHex(match[1]);
|
||||
if (!hex) continue;
|
||||
const mentionType = match[2] === "npub1" ? "p" : "e";
|
||||
|
||||
// TODO: find the best relay for this user or note
|
||||
const existingMention = updatedDraft.tags.find((t) => t[0] === mentionType && t[1] === hex);
|
||||
const index = existingMention
|
||||
? updatedDraft.tags.indexOf(existingMention)
|
||||
: updatedDraft.tags.push([mentionType, hex, "", "mention"]) - 1;
|
||||
|
||||
// replace the npub1 or note1 with a mention tag #[0]
|
||||
const c = updatedDraft.content;
|
||||
updatedDraft.content = c.slice(0, match.index) + `#[${index}]` + c.slice(match.index + match[0].length);
|
||||
}
|
||||
|
||||
return updatedDraft;
|
||||
export function ensureNotifyContentMentions(draft: DraftNostrEvent) {
|
||||
const mentions = getContentMentions(draft.content);
|
||||
return mentions.length > 0 ? ensureNotifyPubkeys(draft, mentions) : draft;
|
||||
}
|
||||
|
||||
export function createHashtagTags(draft: DraftNostrEvent) {
|
||||
@ -103,7 +92,6 @@ export function createHashtagTags(draft: DraftNostrEvent) {
|
||||
|
||||
export function finalizeNote(draft: DraftNostrEvent) {
|
||||
let updated = draft;
|
||||
updated = replaceAtMentions(updated);
|
||||
updated = createHashtagTags(updated);
|
||||
return updated;
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import dayjs from "dayjs";
|
||||
import { DraftNostrEvent, NostrEvent, isPTag } from "../../types/nostr-event";
|
||||
import { unique } from "../array";
|
||||
import { getAddr } from "../../services/replaceable-event-requester";
|
||||
import { ensureNotifyContentMentions } from "./post";
|
||||
|
||||
export const STREAM_KIND = 30311;
|
||||
export const STREAM_CHAT_MESSAGE_KIND = 1311;
|
||||
@ -83,12 +84,14 @@ export function getATag(stream: ParsedStream) {
|
||||
}
|
||||
|
||||
export function buildChatMessage(stream: ParsedStream, content: string) {
|
||||
const template: DraftNostrEvent = {
|
||||
let draft: DraftNostrEvent = {
|
||||
tags: [["a", getATag(stream), "", "root"]],
|
||||
content,
|
||||
created_at: dayjs().unix(),
|
||||
kind: STREAM_CHAT_MESSAGE_KIND,
|
||||
};
|
||||
|
||||
return template;
|
||||
draft = ensureNotifyContentMentions(draft);
|
||||
|
||||
return draft;
|
||||
}
|
||||
|
@ -1,5 +1,3 @@
|
||||
export const getMentionNpubOrNote = () =>
|
||||
/(?:\s|^)(@|nostr:)?((npub1|note1)[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{58})(?:\s|$)/gi;
|
||||
export const getMatchNostrLink = () =>
|
||||
/(nostr:|@)?((npub|note|nprofile|nevent)1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{58,})/gi;
|
||||
export const getMatchHashtag = () => /(^|[^\p{L}])#([\p{L}\p{N}]+)/gu;
|
||||
|
@ -8,11 +8,13 @@ import { NostrEvent } from "../../../types/nostr-event";
|
||||
import { UserAvatarStack } from "../../../components/user-avatar-stack";
|
||||
import { ThreadItem, getThreadMembers } from "../../../helpers/thread";
|
||||
import { NoteContents } from "../../../components/note/note-contents";
|
||||
import { addReplyTags, ensureNotifyUsers, finalizeNote } from "../../../helpers/nostr/post";
|
||||
import { addReplyTags, ensureNotifyPubkeys, finalizeNote, getContentMentions } from "../../../helpers/nostr/post";
|
||||
import { useCurrentAccount } from "../../../hooks/use-current-account";
|
||||
import { useSigningContext } from "../../../providers/signing-provider";
|
||||
import { useWriteRelayUrls } from "../../../hooks/use-client-relays";
|
||||
import NostrPublishAction from "../../../classes/nostr-publish-action";
|
||||
import { getContentTagRefs } from "../../../helpers/nostr/event";
|
||||
import { unique } from "../../../helpers/array";
|
||||
|
||||
function NoteContentPreview({ content }: { content: string }) {
|
||||
const draft = useMemo(
|
||||
@ -32,6 +34,7 @@ export type ReplyFormProps = {
|
||||
export default function ReplyForm({ item, onCancel, onSubmitted }: ReplyFormProps) {
|
||||
const toast = useToast();
|
||||
const account = useCurrentAccount();
|
||||
const showPreview = useDisclosure();
|
||||
const { requestSignature } = useSigningContext();
|
||||
const writeRelays = useWriteRelayUrls();
|
||||
|
||||
@ -41,8 +44,8 @@ export default function ReplyForm({ item, onCancel, onSubmitted }: ReplyFormProp
|
||||
content: "",
|
||||
},
|
||||
});
|
||||
|
||||
const showPreview = useDisclosure();
|
||||
const contentMentions = getContentMentions(getValues().content);
|
||||
const notifyPubkeys = unique([...threadMembers, ...contentMentions]);
|
||||
|
||||
watch("content");
|
||||
|
||||
@ -50,13 +53,12 @@ export default function ReplyForm({ item, onCancel, onSubmitted }: ReplyFormProp
|
||||
try {
|
||||
let draft = finalizeNote({ kind: Kind.Text, content: values.content, created_at: dayjs().unix(), tags: [] });
|
||||
draft = addReplyTags(draft, item.event);
|
||||
draft = ensureNotifyUsers(draft, threadMembers);
|
||||
draft = ensureNotifyPubkeys(draft, notifyPubkeys);
|
||||
|
||||
const signed = await requestSignature(draft);
|
||||
if (!signed) return;
|
||||
// TODO: write to other users inbox relays
|
||||
const pub = new NostrPublishAction("Reply", writeRelays, signed);
|
||||
await pub.onComplete;
|
||||
|
||||
if (onSubmitted) onSubmitted(signed);
|
||||
} catch (e) {
|
||||
@ -78,7 +80,7 @@ export default function ReplyForm({ item, onCancel, onSubmitted }: ReplyFormProp
|
||||
<Button onClick={onCancel}>Cancel</Button>
|
||||
<Button type="submit">Submit</Button>
|
||||
</ButtonGroup>
|
||||
<UserAvatarStack label="Notify" users={threadMembers} />
|
||||
<UserAvatarStack label="Notify" users={notifyPubkeys} />
|
||||
{getValues().content.length > 0 && (
|
||||
<Button size="sm" ml="auto" onClick={showPreview.onToggle}>
|
||||
Preview
|
||||
|
@ -41,6 +41,7 @@ import useUserMuteList from "../../../../hooks/use-user-mute-list";
|
||||
import { NostrEvent, isPTag } from "../../../../types/nostr-event";
|
||||
import { useCurrentAccount } from "../../../../hooks/use-current-account";
|
||||
import NostrPublishAction from "../../../../classes/nostr-publish-action";
|
||||
import { ensureNotifyContentMentions } from "../../../../helpers/nostr/post";
|
||||
|
||||
const hideScrollbar = css`
|
||||
scrollbar-width: 0;
|
||||
|
Loading…
x
Reference in New Issue
Block a user