This commit is contained in:
Shusui MOYATANI
2023-04-29 00:22:03 +09:00
parent 7ee7c9ec45
commit 5474c85ace
28 changed files with 204 additions and 78 deletions

View File

@@ -25,6 +25,8 @@ module.exports = {
}, },
plugins: ['import', 'solid', 'jsx-a11y', 'prettier', '@typescript-eslint', 'tailwindcss'], plugins: ['import', 'solid', 'jsx-a11y', 'prettier', '@typescript-eslint', 'tailwindcss'],
rules: { rules: {
'no-alert': ['off'],
'no-console': ['off'],
'import/extensions': [ 'import/extensions': [
'error', 'error',
'ignorePackages', 'ignorePackages',
@@ -33,9 +35,6 @@ module.exports = {
tsx: 'never', tsx: 'never',
}, },
], ],
'prettier/prettier': 'error',
'no-console': ['off'],
'no-alert': ['off'],
'import/order': [ 'import/order': [
'warn', 'warn',
{ {
@@ -49,6 +48,16 @@ module.exports = {
], ],
}, },
], ],
'jsx-a11y/label-has-associated-control': [
'error',
{
labelComponents: ['label'],
labelAttributes: ['inputLabel'],
assert: 'both',
depth: 3,
},
],
'prettier/prettier': 'error',
}, },
settings: { settings: {
linkComponents: ['Link'], linkComponents: ['Link'],

View File

@@ -3,7 +3,7 @@ import { createSignal, For, type JSX } from 'solid-js';
import XMark from 'heroicons/24/outline/x-mark.svg'; import XMark from 'heroicons/24/outline/x-mark.svg';
import Modal from '@/components/Modal'; import Modal from '@/components/Modal';
import useConfig, { type Config } from '@/nostr/useConfig'; import useConfig, { type Config } from '@/core/useConfig';
import UserNameDisplay from './UserDisplayName'; import UserNameDisplay from './UserDisplayName';
@@ -214,6 +214,13 @@ const OtherConfig = () => {
})); }));
}; };
const toggleHideCount = () => {
setConfig((current) => ({
...current,
hideCount: !(current.hideCount ?? false),
}));
};
return ( return (
<div class="py-2"> <div class="py-2">
<h3 class="font-bold"></h3> <h3 class="font-bold"></h3>
@@ -229,6 +236,10 @@ const OtherConfig = () => {
<div class="flex-1"></div> <div class="flex-1"></div>
<ToggleButton value={config().showImage} onClick={() => toggleShowImage()} /> <ToggleButton value={config().showImage} onClick={() => toggleShowImage()} />
</div> </div>
<div class="flex w-full">
<div class="flex-1"></div>
<ToggleButton value={config().hideCount} onClick={() => toggleHideCount()} />
</div>
{/* {/*
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1">リアクションのデフォルト</div> <div class="flex-1">リアクションのデフォルト</div>

View File

@@ -6,9 +6,9 @@ import { Event as NostrEvent } from 'nostr-tools';
import ColumnItem from '@/components/ColumnItem'; import ColumnItem from '@/components/ColumnItem';
import UserDisplayName from '@/components/UserDisplayName'; import UserDisplayName from '@/components/UserDisplayName';
import eventWrapper from '@/core/event';
import useFormatDate from '@/hooks/useFormatDate'; import useFormatDate from '@/hooks/useFormatDate';
import useModalState from '@/hooks/useModalState'; import useModalState from '@/hooks/useModalState';
import eventWrapper from '@/nostr/event';
import TextNoteDisplayById from './textNote/TextNoteDisplayById'; import TextNoteDisplayById from './textNote/TextNoteDisplayById';

View File

@@ -17,11 +17,11 @@ import uniq from 'lodash/uniq';
import { Event as NostrEvent } from 'nostr-tools'; import { Event as NostrEvent } from 'nostr-tools';
import UserNameDisplay from '@/components/UserDisplayName'; import UserNameDisplay from '@/components/UserDisplayName';
import eventWrapper from '@/core/event'; import useConfig from '@/core/useConfig';
import parseTextNote from '@/core/parseTextNote';
import { useHandleCommand } from '@/hooks/useCommandBus'; import { useHandleCommand } from '@/hooks/useCommandBus';
import eventWrapper from '@/nostr/event';
import parseTextNote, { ParsedTextNote } from '@/nostr/parseTextNote';
import useCommands, { PublishTextNoteParams } from '@/nostr/useCommands'; import useCommands, { PublishTextNoteParams } from '@/nostr/useCommands';
import useConfig from '@/nostr/useConfig';
import usePubkey from '@/nostr/usePubkey'; import usePubkey from '@/nostr/usePubkey';
import { uploadNostrBuild, uploadFiles } from '@/utils/imageUpload'; import { uploadNostrBuild, uploadFiles } from '@/utils/imageUpload';
@@ -43,9 +43,7 @@ const placeholder = (mode: NotePostFormProps['mode']) => {
} }
}; };
const parseAndExtract = (content: string) => { const extract = (parsed: ParsedTextNote) => {
const parsed = parseTextNote(content);
const hashtags: string[] = []; const hashtags: string[] = [];
const pubkeyReferences: string[] = []; const pubkeyReferences: string[] = [];
const eventReferences: string[] = []; const eventReferences: string[] = [];
@@ -73,6 +71,18 @@ const parseAndExtract = (content: string) => {
}; };
}; };
const format = (parsed: ParsedTextNote) => {
const content = [];
parsed.forEach((node) => {
if (node.type === 'Bech32Entity' && !node.isNIP19) {
content.push(`nostr:${node.content}`);
} else {
content.push(node.content);
}
});
return content.join('');
};
const NotePostForm: Component<NotePostFormProps> = (props) => { const NotePostForm: Component<NotePostFormProps> = (props) => {
let textAreaRef: HTMLTextAreaElement | undefined; let textAreaRef: HTMLTextAreaElement | undefined;
let fileInputRef: HTMLInputElement | undefined; let fileInputRef: HTMLInputElement | undefined;
@@ -166,12 +176,14 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
return; return;
} }
const { hashtags, pubkeyReferences, eventReferences, urlReferences } = parseAndExtract(text()); const parsed = parseTextNote(text());
const { hashtags, pubkeyReferences, eventReferences, urlReferences } = extract(parsed);
const formattedContent = format(parsed);
let textNote: PublishTextNoteParams = { let textNote: PublishTextNoteParams = {
relayUrls: config().relayUrls, relayUrls: config().relayUrls,
pubkey, pubkey,
content: text(), content: formattedContent,
notifyPubkeys: pubkeyReferences, notifyPubkeys: pubkeyReferences,
mentionEventIds: eventReferences, mentionEventIds: eventReferences,
hashtags, hashtags,

View File

@@ -7,13 +7,14 @@ import GlobeAlt from 'heroicons/24/outline/globe-alt.svg';
import XMark from 'heroicons/24/outline/x-mark.svg'; import XMark from 'heroicons/24/outline/x-mark.svg';
import CheckCircle from 'heroicons/24/solid/check-circle.svg'; import CheckCircle from 'heroicons/24/solid/check-circle.svg';
import ExclamationCircle from 'heroicons/24/solid/exclamation-circle.svg'; import ExclamationCircle from 'heroicons/24/solid/exclamation-circle.svg';
import uniq from 'lodash/uniq';
import Modal from '@/components/Modal'; import Modal from '@/components/Modal';
import Timeline from '@/components/Timeline'; import Timeline from '@/components/Timeline';
import Copy from '@/components/utils/Copy'; import Copy from '@/components/utils/Copy';
import SafeLink from '@/components/utils/SafeLink'; import SafeLink from '@/components/utils/SafeLink';
import useConfig from '@/core/useConfig';
import useCommands from '@/nostr/useCommands'; import useCommands from '@/nostr/useCommands';
import useConfig from '@/nostr/useConfig';
import useFollowers from '@/nostr/useFollowers'; import useFollowers from '@/nostr/useFollowers';
import useFollowings from '@/nostr/useFollowings'; import useFollowings from '@/nostr/useFollowings';
import useProfile from '@/nostr/useProfile'; import useProfile from '@/nostr/useProfile';
@@ -125,7 +126,7 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
relayUrls: config().relayUrls, relayUrls: config().relayUrls,
pubkey: p, pubkey: p,
content: myFollowingQuery.data?.content ?? '', content: myFollowingQuery.data?.content ?? '',
followingPubkeys: [...myFollowingPubkeys(), props.pubkey], followingPubkeys: uniq([...myFollowingPubkeys(), props.pubkey]),
}); });
}; };
@@ -325,25 +326,27 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
</Show> </Show>
</div> </div>
</div> </div>
<div class="flex flex-1 flex-col items-start"> <Show when={!config().hideCount}>
<div class="text-sm"></div> <div class="flex flex-1 flex-col items-start">
<div class="text-xl"> <div class="text-sm"></div>
<Show <div class="text-xl">
when={showFollowers()} <Show
fallback={ when={showFollowers()}
<button fallback={
class="text-sm hover:text-stone-800 hover:underline" <button
onClick={() => setShowFollowers(true)} class="text-sm hover:text-stone-800 hover:underline"
> onClick={() => setShowFollowers(true)}
>
</button>
} </button>
keyed }
> keyed
<FollowersCount pubkey={props.pubkey} /> >
</Show> <FollowersCount pubkey={props.pubkey} />
</Show>
</div>
</div> </div>
</div> </Show>
</div> </div>
<Show when={(profile()?.website ?? '').length > 0}> <Show when={(profile()?.website ?? '').length > 0}>
<ul class="border-t px-5 py-2 text-xs"> <ul class="border-t px-5 py-2 text-xs">

View File

@@ -6,8 +6,8 @@ import PencilSquare from 'heroicons/24/solid/pencil-square.svg';
import Config from '@/components/Config'; import Config from '@/components/Config';
import NotePostForm from '@/components/NotePostForm'; import NotePostForm from '@/components/NotePostForm';
import useConfig from '@/core/useConfig';
import { useHandleCommand } from '@/hooks/useCommandBus'; import { useHandleCommand } from '@/hooks/useCommandBus';
import useConfig from '@/nostr/useConfig';
const SideBar: Component = () => { const SideBar: Component = () => {
let textAreaRef: HTMLTextAreaElement | undefined; let textAreaRef: HTMLTextAreaElement | undefined;

View File

@@ -1,7 +1,7 @@
import { Show, type Component } from 'solid-js'; import { Show, type Component } from 'solid-js';
import ColumnItem from '@/components/ColumnItem'; import ColumnItem from '@/components/ColumnItem';
import useConfig from '@/nostr/useConfig'; import useConfig from '@/core/useConfig';
import TextNoteDisplay, { TextNoteDisplayProps } from './textNote/TextNoteDisplay'; import TextNoteDisplay, { TextNoteDisplayProps } from './textNote/TextNoteDisplay';

View File

@@ -5,8 +5,8 @@ import { Filter, Event as NostrEvent } from 'nostr-tools';
import Timeline from '@/components/Timeline'; import Timeline from '@/components/Timeline';
import { type TimelineContent } from '@/components/TimelineContext'; import { type TimelineContent } from '@/components/TimelineContext';
import eventWrapper from '@/core/event'; import useConfig from '@/core/useConfig';
import useConfig from '@/nostr/useConfig'; import eventWrapper from '@/nostr/event';
import useSubscription from '@/nostr/useSubscription'; import useSubscription from '@/nostr/useSubscription';
const relatedEvents = (rawEvent: NostrEvent) => { const relatedEvents = (rawEvent: NostrEvent) => {

View File

@@ -6,8 +6,8 @@ import { type Event as NostrEvent } from 'nostr-tools';
import ColumnItem from '@/components/ColumnItem'; import ColumnItem from '@/components/ColumnItem';
import TextNoteDisplay from '@/components/textNote/TextNoteDisplay'; import TextNoteDisplay from '@/components/textNote/TextNoteDisplay';
import UserDisplayName from '@/components/UserDisplayName'; import UserDisplayName from '@/components/UserDisplayName';
import eventWrapper from '@/core/event';
import useModalState from '@/hooks/useModalState'; import useModalState from '@/hooks/useModalState';
import eventWrapper from '@/nostr/event';
import useEvent from '@/nostr/useEvent'; import useEvent from '@/nostr/useEvent';
import useProfile from '@/nostr/useProfile'; import useProfile from '@/nostr/useProfile';

View File

@@ -1,6 +1,6 @@
import { createSignal, type Component, type JSX, Show } from 'solid-js'; import { createSignal, type Component, type JSX, Show } from 'solid-js';
import { ContentWarning } from '@/core/event'; import { ContentWarning } from '@/nostr/event';
export type ContentWarningDisplayProps = { export type ContentWarningDisplayProps = {
contentWarning: ContentWarning; contentWarning: ContentWarning;

View File

@@ -2,7 +2,7 @@ import { Show } from 'solid-js';
// eslint-disable-next-line import/no-cycle // eslint-disable-next-line import/no-cycle
import TextNoteDisplayById from '@/components/textNote/TextNoteDisplayById'; import TextNoteDisplayById from '@/components/textNote/TextNoteDisplayById';
import { type MentionedEvent } from '@/core/parseTextNote'; import { type MentionedEvent } from '@/nostr/parseTextNote';
import EventLink from '../EventLink'; import EventLink from '../EventLink';

View File

@@ -1,7 +1,7 @@
import GeneralUserMentionDisplay from '@/components/textNote/GeneralUserMentionDisplay'; import GeneralUserMentionDisplay from '@/components/textNote/GeneralUserMentionDisplay';
import useModalState from '@/hooks/useModalState'; import useModalState from '@/hooks/useModalState';
import type { MentionedUser } from '@/core/parseTextNote'; import type { MentionedUser } from '@/nostr/parseTextNote';
export type MentionedUserDisplayProps = { export type MentionedUserDisplayProps = {
pubkey: string; pubkey: string;

View File

@@ -1,4 +1,4 @@
import type { PlainText } from '@/core/parseTextNote'; import type { PlainText } from '@/nostr/parseTextNote';
export type PlainTextDisplayProps = { export type PlainTextDisplayProps = {
plainText: PlainText; plainText: PlainText;

View File

@@ -8,12 +8,12 @@ import MentionedUserDisplay from '@/components/textNote/MentionedUserDisplay';
import PlainTextDisplay from '@/components/textNote/PlainTextDisplay'; import PlainTextDisplay from '@/components/textNote/PlainTextDisplay';
import TextNoteDisplayById from '@/components/textNote/TextNoteDisplayById'; import TextNoteDisplayById from '@/components/textNote/TextNoteDisplayById';
import SafeLink from '@/components/utils/SafeLink'; import SafeLink from '@/components/utils/SafeLink';
import eventWrapper from '@/core/event'; import useConfig from '@/core/useConfig';
import parseTextNote, { resolveTagReference, type ParsedTextNoteNode } from '@/core/parseTextNote'; import eventWrapper from '@/nostr/event';
import parseTextNote, { resolveTagReference, type ParsedTextNoteNode } from '@/nostr/parseTextNote';
import type { Event as NostrEvent } from 'nostr-tools'; import type { Event as NostrEvent } from 'nostr-tools';
import useConfig from '@/nostr/useConfig';
import { isImageUrl } from '@/utils/imageUrl'; import { isImageUrl } from '@/utils/imageUrl';
export type TextNoteContentDisplayProps = { export type TextNoteContentDisplayProps = {

View File

@@ -15,11 +15,11 @@ import GeneralUserMentionDisplay from '@/components/textNote/GeneralUserMentionD
import TextNoteContentDisplay from '@/components/textNote/TextNoteContentDisplay'; import TextNoteContentDisplay from '@/components/textNote/TextNoteContentDisplay';
import TextNoteDisplayById from '@/components/textNote/TextNoteDisplayById'; import TextNoteDisplayById from '@/components/textNote/TextNoteDisplayById';
import { useTimelineContext } from '@/components/TimelineContext'; import { useTimelineContext } from '@/components/TimelineContext';
import eventWrapper from '@/core/event'; import useConfig from '@/core/useConfig';
import useFormatDate from '@/hooks/useFormatDate'; import useFormatDate from '@/hooks/useFormatDate';
import useModalState from '@/hooks/useModalState'; import useModalState from '@/hooks/useModalState';
import eventWrapper from '@/nostr/event';
import useCommands from '@/nostr/useCommands'; import useCommands from '@/nostr/useCommands';
import useConfig from '@/nostr/useConfig';
import useProfile from '@/nostr/useProfile'; import useProfile from '@/nostr/useProfile';
import usePubkey from '@/nostr/usePubkey'; import usePubkey from '@/nostr/usePubkey';
import useReactions from '@/nostr/useReactions'; import useReactions from '@/nostr/useReactions';
@@ -27,6 +27,7 @@ import useReposts from '@/nostr/useReposts';
import useSubscription from '@/nostr/useSubscription'; import useSubscription from '@/nostr/useSubscription';
import ensureNonNull from '@/utils/ensureNonNull'; import ensureNonNull from '@/utils/ensureNonNull';
import npubEncodeFallback from '@/utils/npubEncodeFallback'; import npubEncodeFallback from '@/utils/npubEncodeFallback';
import timeout from '@/utils/timeout';
import ContextMenu, { MenuItem } from '../ContextMenu'; import ContextMenu, { MenuItem } from '../ContextMenu';
@@ -41,23 +42,6 @@ const { noteEncode } = nip19;
const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => { const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
let contentRef: HTMLDivElement | undefined; let contentRef: HTMLDivElement | undefined;
const menu: MenuItem[] = [
{
content: () => 'IDをコピー',
onSelect: () => {
navigator.clipboard.writeText(noteEncode(props.event.id)).catch((err) => window.alert(err));
},
},
{
content: () => 'JSONとしてコピー',
onSelect: () => {
navigator.clipboard
.writeText(JSON.stringify(props.event))
.catch((err) => window.alert(err));
},
},
];
const { config } = useConfig(); const { config } = useConfig();
const formatDate = useFormatDate(); const formatDate = useFormatDate();
const pubkey = usePubkey(); const pubkey = usePubkey();
@@ -116,6 +100,68 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
}, },
}); });
const deleteMutation = createMutation({
mutationKey: ['delete', event().id],
mutationFn: (...params: Parameters<typeof commands.delete>) =>
commands
.delete(...params)
.then((promeses) => Promise.allSettled(promeses.map(timeout(10000)))),
onSuccess: (results) => {
// TODO タイムラインから削除する
const succeeded = results.filter((res) => res.status === 'fulfilled').length;
const failed = results.length - succeeded;
if (succeeded === results.length) {
window.alert('削除しました(画面の反映にはリロード)');
} else if (succeeded > 0) {
window.alert('一部のリレーで削除に失敗しました');
} else {
window.alert('すべてのリレーで削除に失敗しました');
}
},
onError: (err) => {
console.error('failed to delete', err);
},
});
const myPostMenu = (): MenuItem[] => {
if (event().pubkey !== pubkey()) return [];
return [
{
content: () => '削除',
onSelect: () => {
const p = pubkey();
if (p == null) return;
if (!window.confirm('本当に削除しますか?')) return;
deleteMutation.mutate({
relayUrls: config().relayUrls,
pubkey: p,
eventId: event().id,
});
},
},
];
};
const menu: MenuItem[] = [
...myPostMenu(),
{
content: () => 'IDをコピー',
onSelect: () => {
navigator.clipboard.writeText(noteEncode(props.event.id)).catch((err) => window.alert(err));
},
},
{
content: () => 'JSONとしてコピー',
onSelect: () => {
navigator.clipboard
.writeText(JSON.stringify(props.event))
.catch((err) => window.alert(err));
},
},
];
const isReactedByMe = createMemo(() => { const isReactedByMe = createMemo(() => {
const p = pubkey(); const p = pubkey();
return p != null && isReactedBy(p); return p != null && isReactedBy(p);
@@ -313,7 +359,7 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
> >
<ArrowPathRoundedSquare /> <ArrowPathRoundedSquare />
</button> </button>
<Show when={reposts().length > 0}> <Show when={!config().hideCount && reposts().length > 0}>
<div class="text-sm text-zinc-400">{reposts().length}</div> <div class="text-sm text-zinc-400">{reposts().length}</div>
</Show> </Show>
</div> </div>
@@ -333,7 +379,7 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
<HeartSolid /> <HeartSolid />
</Show> </Show>
</button> </button>
<Show when={reactions().length > 0}> <Show when={!config().hideCount && reactions().length > 0}>
<div class="text-sm text-zinc-400">{reactions().length}</div> <div class="text-sm text-zinc-400">{reactions().length}</div>
</Show> </Show>
</div> </div>

View File

@@ -2,7 +2,7 @@ import { Switch, Match, type Component } from 'solid-js';
// eslint-disable-next-line import/no-cycle // eslint-disable-next-line import/no-cycle
import TextNoteDisplay, { type TextNoteDisplayProps } from '@/components/textNote/TextNoteDisplay'; import TextNoteDisplay, { type TextNoteDisplayProps } from '@/components/textNote/TextNoteDisplay';
import useConfig from '@/nostr/useConfig'; import useConfig from '@/core/useConfig';
import useEvent from '@/nostr/useEvent'; import useEvent from '@/nostr/useEvent';
import ensureNonNull from '@/utils/ensureNonNull'; import ensureNonNull from '@/utils/ensureNonNull';

View File

@@ -0,0 +1,20 @@
import { createSignal, createEffect, type Component, type JSX } from 'solid-js';
export type PopupProps = {
baseElemRef: HTMLElement | null;
children: JSX.Element;
};
const Popup: Component<PopupProps> = (props) => {
let popupRef: HTMLDivElement | undefined;
const [style, setStyle] = createSignal({});
createEffect(() => {
if (props.baseElemRef == null || popupRef == null) return;
const baseElemRect = props.baseElemRef;
});
return null;
};

View File

@@ -15,6 +15,7 @@ export type Config = {
dateFormat: 'relative' | 'absolute-long' | 'absolute-short'; dateFormat: 'relative' | 'absolute-long' | 'absolute-short';
keepOpenPostForm: boolean; keepOpenPostForm: boolean;
showImage: boolean; showImage: boolean;
hideCount: boolean;
mutedPubkeys: string[]; mutedPubkeys: string[];
mutedKeywords: string[]; mutedKeywords: string[];
}; };
@@ -64,6 +65,7 @@ const InitialConfig = (): Config => {
dateFormat: 'relative', dateFormat: 'relative',
keepOpenPostForm: false, keepOpenPostForm: false,
showImage: true, showImage: true,
hideCount: false,
mutedPubkeys: [], mutedPubkeys: [],
mutedKeywords: [], mutedKeywords: [],
}; };

View File

@@ -1,5 +1,5 @@
import useConfig from '@/core/useConfig';
import useDatePulser from '@/hooks/useDatePulser'; import useDatePulser from '@/hooks/useDatePulser';
import useConfig from '@/nostr/useConfig';
import { formatRelative, formatAbsoluteLong, formatAbsoluteShort } from '@/utils/formatDate'; import { formatRelative, formatAbsoluteLong, formatAbsoluteShort } from '@/utils/formatDate';
// 7 seconds is used here so that the last digit of relative time is changed. // 7 seconds is used here so that the last digit of relative time is changed.

View File

@@ -162,6 +162,7 @@ describe('parseTextNote', () => {
{ {
type: 'Bech32Entity', type: 'Bech32Entity',
content: 'npub1srf6g8v2qpnecqg9l2kzehmkg0ym5f5rtnlsj6lhl8r6pmhger7q5mtt3q', content: 'npub1srf6g8v2qpnecqg9l2kzehmkg0ym5f5rtnlsj6lhl8r6pmhger7q5mtt3q',
isNIP19: false,
data: { data: {
type: 'npub', type: 'npub',
data: '80d3a41d8a00679c0105faac2cdf7643c9ba26835cff096bf7f9c7a0eee8c8fc', data: '80d3a41d8a00679c0105faac2cdf7643c9ba26835cff096bf7f9c7a0eee8c8fc',

View File

@@ -25,6 +25,7 @@ export type Bech32Entity = {
| { type: 'npub' | 'note'; data: string } | { type: 'npub' | 'note'; data: string }
| { type: 'nprofile'; data: ProfilePointer } | { type: 'nprofile'; data: ProfilePointer }
| { type: 'nevent'; data: EventPointer }; | { type: 'nevent'; data: EventPointer };
isNIP19: boolean;
}; };
export type HashTag = { export type HashTag = {
@@ -61,7 +62,8 @@ const tagRefRegex = /(?:#\[(?<idx>\d+)\])/g;
const hashTagRegex = /#(?<hashtag>[\p{Letter}\p{Number}_]+)/gu; const hashTagRegex = /#(?<hashtag>[\p{Letter}\p{Number}_]+)/gu;
// raw NIP-19 codes, NIP-21 links (NIP-27) // raw NIP-19 codes, NIP-21 links (NIP-27)
// nrelay and naddr is not supported by nostr-tools // nrelay and naddr is not supported by nostr-tools
const mentionRegex = /(?:nostr:)?(?<mention>(npub|note|nprofile|nevent)1[ac-hj-np-z02-9]+)/gi; const mentionRegex =
/(?<mention>(?<nip19>nostr:)?(?<bech32>(?:npub|note|nprofile|nevent)1[ac-hj-np-z02-9]+))/gi;
const urlRegex = const urlRegex =
/(?<url>(?:https?|wss?):\/\/[-a-zA-Z0-9.]+(:\d{1,5})?(?:\/[-[\]~!$&'()*+.,:;@&=%\w]+|\/)*(?:\?[-[\]~!$&'()*+.,/:;%@&=\w?]+)?(?:#[-[\]~!$&'()*+.,/:;%@\w&=?#]+)?)/g; /(?<url>(?:https?|wss?):\/\/[-a-zA-Z0-9.]+(:\d{1,5})?(?:\/[-[\]~!$&'()*+.,:;@&=%\w]+|\/)*(?:\?[-[\]~!$&'()*+.,/:;%@&=\w?]+)?(?:#[-[\]~!$&'()*+.,/:;%@\w&=?#]+)?)/g;
@@ -105,11 +107,12 @@ const parseTextNote = (textNoteContent: string) => {
} else if (match.groups?.mention) { } else if (match.groups?.mention) {
pushPlainText(index); pushPlainText(index);
try { try {
const decoded = decode(match[1]); const decoded = decode(match.groups.bech32);
const bech32Entity: Bech32Entity = { const bech32Entity: Bech32Entity = {
type: 'Bech32Entity', type: 'Bech32Entity',
content: match[0], content: match[0],
data: decoded as Bech32Entity['data'], data: decoded as Bech32Entity['data'],
isNIP19: match.groups.nip19 === 'nostr:',
}; };
result.push(bech32Entity); result.push(bech32Entity);
} catch (e) { } catch (e) {

View File

@@ -10,9 +10,9 @@ import {
import { createQuery, useQueryClient, type CreateQueryResult } from '@tanstack/solid-query'; import { createQuery, useQueryClient, type CreateQueryResult } from '@tanstack/solid-query';
import { type Event as NostrEvent, type Filter, Kind } from 'nostr-tools'; import { type Event as NostrEvent, type Filter, Kind } from 'nostr-tools';
import eventWrapper from '@/core/event'; import useConfig from '@/core/useConfig';
import eventWrapper from '@/nostr/event';
import useBatch, { type Task } from '@/nostr/useBatch'; import useBatch, { type Task } from '@/nostr/useBatch';
import useConfig from '@/nostr/useConfig';
import usePool from '@/nostr/usePool'; import usePool from '@/nostr/usePool';
import useStats from '@/nostr/useStats'; import useStats from '@/nostr/useStats';
import timeout from '@/utils/timeout'; import timeout from '@/utils/timeout';

View File

@@ -192,6 +192,24 @@ const useCommands = () => {
}; };
return publishEvent(relayUrls, preSignedEvent); return publishEvent(relayUrls, preSignedEvent);
}, },
async delete({
relayUrls,
pubkey,
eventId,
}: {
relayUrls: string[];
pubkey: string;
eventId: string;
}): Promise<Promise<void>[]> {
const preSignedEvent: UnsignedEvent = {
kind: Kind.EventDeletion,
pubkey,
created_at: epoch(),
tags: [['e', eventId, '']],
content: '',
};
return publishEvent(relayUrls, preSignedEvent);
},
}; };
}; };

View File

@@ -3,7 +3,7 @@ import { createMemo, createSignal } from 'solid-js';
import uniq from 'lodash/uniq'; import uniq from 'lodash/uniq';
import { Kind } from 'nostr-tools'; import { Kind } from 'nostr-tools';
import useConfig from '@/nostr/useConfig'; import useConfig from '@/core/useConfig';
import useSubscription from '@/nostr/useSubscription'; import useSubscription from '@/nostr/useSubscription';
export type UseFollowersProps = { export type UseFollowersProps = {

View File

@@ -2,10 +2,9 @@ import { createSignal, createEffect, onCleanup, on } from 'solid-js';
import uniqBy from 'lodash/uniqBy'; import uniqBy from 'lodash/uniqBy';
import useConfig from '@/core/useConfig';
import usePool from '@/nostr/usePool'; import usePool from '@/nostr/usePool';
import useStats from '@/nostr/useStats';
import useConfig from './useConfig';
import useStats from './useStats';
import type { Event as NostrEvent, Filter, SubscriptionOptions } from 'nostr-tools'; import type { Event as NostrEvent, Filter, SubscriptionOptions } from 'nostr-tools';

View File

@@ -18,10 +18,10 @@ import Notification from '@/components/Notification';
import ProfileDisplay from '@/components/ProfileDisplay'; import ProfileDisplay from '@/components/ProfileDisplay';
import SideBar from '@/components/SideBar'; import SideBar from '@/components/SideBar';
import Timeline from '@/components/Timeline'; import Timeline from '@/components/Timeline';
import useConfig from '@/core/useConfig';
import useModalState from '@/hooks/useModalState'; import useModalState from '@/hooks/useModalState';
import usePersistStatus from '@/hooks/usePersistStatus'; import usePersistStatus from '@/hooks/usePersistStatus';
import { useMountShortcutKeys } from '@/hooks/useShortcutKeys'; import { useMountShortcutKeys } from '@/hooks/useShortcutKeys';
import useConfig from '@/nostr/useConfig';
import useFollowings from '@/nostr/useFollowings'; import useFollowings from '@/nostr/useFollowings';
import usePool from '@/nostr/usePool'; import usePool from '@/nostr/usePool';
import usePubkey from '@/nostr/usePubkey'; import usePubkey from '@/nostr/usePubkey';

View File

@@ -1,3 +1,5 @@
const epoch = (): number => Math.floor(Date.now() / 1000); export const toEpoch = (date: Date) => Math.floor(+date / 1000);
const epoch = (): number => toEpoch(new Date());
export default epoch; export default epoch;