mirror of
https://github.com/aljazceru/rabbit.git
synced 2025-12-17 22:14:26 +01:00
update
This commit is contained in:
15
.eslintrc.js
15
.eslintrc.js
@@ -25,6 +25,8 @@ module.exports = {
|
||||
},
|
||||
plugins: ['import', 'solid', 'jsx-a11y', 'prettier', '@typescript-eslint', 'tailwindcss'],
|
||||
rules: {
|
||||
'no-alert': ['off'],
|
||||
'no-console': ['off'],
|
||||
'import/extensions': [
|
||||
'error',
|
||||
'ignorePackages',
|
||||
@@ -33,9 +35,6 @@ module.exports = {
|
||||
tsx: 'never',
|
||||
},
|
||||
],
|
||||
'prettier/prettier': 'error',
|
||||
'no-console': ['off'],
|
||||
'no-alert': ['off'],
|
||||
'import/order': [
|
||||
'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: {
|
||||
linkComponents: ['Link'],
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createSignal, For, type JSX } from 'solid-js';
|
||||
import XMark from 'heroicons/24/outline/x-mark.svg';
|
||||
|
||||
import Modal from '@/components/Modal';
|
||||
import useConfig, { type Config } from '@/nostr/useConfig';
|
||||
import useConfig, { type Config } from '@/core/useConfig';
|
||||
|
||||
import UserNameDisplay from './UserDisplayName';
|
||||
|
||||
@@ -214,6 +214,13 @@ const OtherConfig = () => {
|
||||
}));
|
||||
};
|
||||
|
||||
const toggleHideCount = () => {
|
||||
setConfig((current) => ({
|
||||
...current,
|
||||
hideCount: !(current.hideCount ?? false),
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="py-2">
|
||||
<h3 class="font-bold">その他</h3>
|
||||
@@ -229,6 +236,10 @@ const OtherConfig = () => {
|
||||
<div class="flex-1">画像をデフォルトで表示する</div>
|
||||
<ToggleButton value={config().showImage} onClick={() => toggleShowImage()} />
|
||||
</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-1">リアクションのデフォルト</div>
|
||||
|
||||
@@ -6,9 +6,9 @@ import { Event as NostrEvent } from 'nostr-tools';
|
||||
|
||||
import ColumnItem from '@/components/ColumnItem';
|
||||
import UserDisplayName from '@/components/UserDisplayName';
|
||||
import eventWrapper from '@/core/event';
|
||||
import useFormatDate from '@/hooks/useFormatDate';
|
||||
import useModalState from '@/hooks/useModalState';
|
||||
import eventWrapper from '@/nostr/event';
|
||||
|
||||
import TextNoteDisplayById from './textNote/TextNoteDisplayById';
|
||||
|
||||
|
||||
@@ -17,11 +17,11 @@ import uniq from 'lodash/uniq';
|
||||
import { Event as NostrEvent } from 'nostr-tools';
|
||||
|
||||
import UserNameDisplay from '@/components/UserDisplayName';
|
||||
import eventWrapper from '@/core/event';
|
||||
import parseTextNote from '@/core/parseTextNote';
|
||||
import useConfig from '@/core/useConfig';
|
||||
import { useHandleCommand } from '@/hooks/useCommandBus';
|
||||
import eventWrapper from '@/nostr/event';
|
||||
import parseTextNote, { ParsedTextNote } from '@/nostr/parseTextNote';
|
||||
import useCommands, { PublishTextNoteParams } from '@/nostr/useCommands';
|
||||
import useConfig from '@/nostr/useConfig';
|
||||
import usePubkey from '@/nostr/usePubkey';
|
||||
import { uploadNostrBuild, uploadFiles } from '@/utils/imageUpload';
|
||||
|
||||
@@ -43,9 +43,7 @@ const placeholder = (mode: NotePostFormProps['mode']) => {
|
||||
}
|
||||
};
|
||||
|
||||
const parseAndExtract = (content: string) => {
|
||||
const parsed = parseTextNote(content);
|
||||
|
||||
const extract = (parsed: ParsedTextNote) => {
|
||||
const hashtags: string[] = [];
|
||||
const pubkeyReferences: 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) => {
|
||||
let textAreaRef: HTMLTextAreaElement | undefined;
|
||||
let fileInputRef: HTMLInputElement | undefined;
|
||||
@@ -166,12 +176,14 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
|
||||
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 = {
|
||||
relayUrls: config().relayUrls,
|
||||
pubkey,
|
||||
content: text(),
|
||||
content: formattedContent,
|
||||
notifyPubkeys: pubkeyReferences,
|
||||
mentionEventIds: eventReferences,
|
||||
hashtags,
|
||||
|
||||
@@ -7,13 +7,14 @@ import GlobeAlt from 'heroicons/24/outline/globe-alt.svg';
|
||||
import XMark from 'heroicons/24/outline/x-mark.svg';
|
||||
import CheckCircle from 'heroicons/24/solid/check-circle.svg';
|
||||
import ExclamationCircle from 'heroicons/24/solid/exclamation-circle.svg';
|
||||
import uniq from 'lodash/uniq';
|
||||
|
||||
import Modal from '@/components/Modal';
|
||||
import Timeline from '@/components/Timeline';
|
||||
import Copy from '@/components/utils/Copy';
|
||||
import SafeLink from '@/components/utils/SafeLink';
|
||||
import useConfig from '@/core/useConfig';
|
||||
import useCommands from '@/nostr/useCommands';
|
||||
import useConfig from '@/nostr/useConfig';
|
||||
import useFollowers from '@/nostr/useFollowers';
|
||||
import useFollowings from '@/nostr/useFollowings';
|
||||
import useProfile from '@/nostr/useProfile';
|
||||
@@ -125,7 +126,7 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
|
||||
relayUrls: config().relayUrls,
|
||||
pubkey: p,
|
||||
content: myFollowingQuery.data?.content ?? '',
|
||||
followingPubkeys: [...myFollowingPubkeys(), props.pubkey],
|
||||
followingPubkeys: uniq([...myFollowingPubkeys(), props.pubkey]),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -325,25 +326,27 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-1 flex-col items-start">
|
||||
<div class="text-sm">フォロワー</div>
|
||||
<div class="text-xl">
|
||||
<Show
|
||||
when={showFollowers()}
|
||||
fallback={
|
||||
<button
|
||||
class="text-sm hover:text-stone-800 hover:underline"
|
||||
onClick={() => setShowFollowers(true)}
|
||||
>
|
||||
読み込む
|
||||
</button>
|
||||
}
|
||||
keyed
|
||||
>
|
||||
<FollowersCount pubkey={props.pubkey} />
|
||||
</Show>
|
||||
<Show when={!config().hideCount}>
|
||||
<div class="flex flex-1 flex-col items-start">
|
||||
<div class="text-sm">フォロワー</div>
|
||||
<div class="text-xl">
|
||||
<Show
|
||||
when={showFollowers()}
|
||||
fallback={
|
||||
<button
|
||||
class="text-sm hover:text-stone-800 hover:underline"
|
||||
onClick={() => setShowFollowers(true)}
|
||||
>
|
||||
読み込む
|
||||
</button>
|
||||
}
|
||||
keyed
|
||||
>
|
||||
<FollowersCount pubkey={props.pubkey} />
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={(profile()?.website ?? '').length > 0}>
|
||||
<ul class="border-t px-5 py-2 text-xs">
|
||||
|
||||
@@ -6,8 +6,8 @@ import PencilSquare from 'heroicons/24/solid/pencil-square.svg';
|
||||
|
||||
import Config from '@/components/Config';
|
||||
import NotePostForm from '@/components/NotePostForm';
|
||||
import useConfig from '@/core/useConfig';
|
||||
import { useHandleCommand } from '@/hooks/useCommandBus';
|
||||
import useConfig from '@/nostr/useConfig';
|
||||
|
||||
const SideBar: Component = () => {
|
||||
let textAreaRef: HTMLTextAreaElement | undefined;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Show, type Component } from 'solid-js';
|
||||
|
||||
import ColumnItem from '@/components/ColumnItem';
|
||||
import useConfig from '@/nostr/useConfig';
|
||||
import useConfig from '@/core/useConfig';
|
||||
|
||||
import TextNoteDisplay, { TextNoteDisplayProps } from './textNote/TextNoteDisplay';
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ import { Filter, Event as NostrEvent } from 'nostr-tools';
|
||||
|
||||
import Timeline from '@/components/Timeline';
|
||||
import { type TimelineContent } from '@/components/TimelineContext';
|
||||
import eventWrapper from '@/core/event';
|
||||
import useConfig from '@/nostr/useConfig';
|
||||
import useConfig from '@/core/useConfig';
|
||||
import eventWrapper from '@/nostr/event';
|
||||
import useSubscription from '@/nostr/useSubscription';
|
||||
|
||||
const relatedEvents = (rawEvent: NostrEvent) => {
|
||||
|
||||
@@ -6,8 +6,8 @@ import { type Event as NostrEvent } from 'nostr-tools';
|
||||
import ColumnItem from '@/components/ColumnItem';
|
||||
import TextNoteDisplay from '@/components/textNote/TextNoteDisplay';
|
||||
import UserDisplayName from '@/components/UserDisplayName';
|
||||
import eventWrapper from '@/core/event';
|
||||
import useModalState from '@/hooks/useModalState';
|
||||
import eventWrapper from '@/nostr/event';
|
||||
import useEvent from '@/nostr/useEvent';
|
||||
import useProfile from '@/nostr/useProfile';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createSignal, type Component, type JSX, Show } from 'solid-js';
|
||||
|
||||
import { ContentWarning } from '@/core/event';
|
||||
import { ContentWarning } from '@/nostr/event';
|
||||
|
||||
export type ContentWarningDisplayProps = {
|
||||
contentWarning: ContentWarning;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Show } from 'solid-js';
|
||||
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import TextNoteDisplayById from '@/components/textNote/TextNoteDisplayById';
|
||||
import { type MentionedEvent } from '@/core/parseTextNote';
|
||||
import { type MentionedEvent } from '@/nostr/parseTextNote';
|
||||
|
||||
import EventLink from '../EventLink';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import GeneralUserMentionDisplay from '@/components/textNote/GeneralUserMentionDisplay';
|
||||
import useModalState from '@/hooks/useModalState';
|
||||
|
||||
import type { MentionedUser } from '@/core/parseTextNote';
|
||||
import type { MentionedUser } from '@/nostr/parseTextNote';
|
||||
|
||||
export type MentionedUserDisplayProps = {
|
||||
pubkey: string;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { PlainText } from '@/core/parseTextNote';
|
||||
import type { PlainText } from '@/nostr/parseTextNote';
|
||||
|
||||
export type PlainTextDisplayProps = {
|
||||
plainText: PlainText;
|
||||
|
||||
@@ -8,12 +8,12 @@ import MentionedUserDisplay from '@/components/textNote/MentionedUserDisplay';
|
||||
import PlainTextDisplay from '@/components/textNote/PlainTextDisplay';
|
||||
import TextNoteDisplayById from '@/components/textNote/TextNoteDisplayById';
|
||||
import SafeLink from '@/components/utils/SafeLink';
|
||||
import eventWrapper from '@/core/event';
|
||||
import parseTextNote, { resolveTagReference, type ParsedTextNoteNode } from '@/core/parseTextNote';
|
||||
import useConfig from '@/core/useConfig';
|
||||
import eventWrapper from '@/nostr/event';
|
||||
import parseTextNote, { resolveTagReference, type ParsedTextNoteNode } from '@/nostr/parseTextNote';
|
||||
|
||||
import type { Event as NostrEvent } from 'nostr-tools';
|
||||
|
||||
import useConfig from '@/nostr/useConfig';
|
||||
import { isImageUrl } from '@/utils/imageUrl';
|
||||
|
||||
export type TextNoteContentDisplayProps = {
|
||||
|
||||
@@ -15,11 +15,11 @@ import GeneralUserMentionDisplay from '@/components/textNote/GeneralUserMentionD
|
||||
import TextNoteContentDisplay from '@/components/textNote/TextNoteContentDisplay';
|
||||
import TextNoteDisplayById from '@/components/textNote/TextNoteDisplayById';
|
||||
import { useTimelineContext } from '@/components/TimelineContext';
|
||||
import eventWrapper from '@/core/event';
|
||||
import useConfig from '@/core/useConfig';
|
||||
import useFormatDate from '@/hooks/useFormatDate';
|
||||
import useModalState from '@/hooks/useModalState';
|
||||
import eventWrapper from '@/nostr/event';
|
||||
import useCommands from '@/nostr/useCommands';
|
||||
import useConfig from '@/nostr/useConfig';
|
||||
import useProfile from '@/nostr/useProfile';
|
||||
import usePubkey from '@/nostr/usePubkey';
|
||||
import useReactions from '@/nostr/useReactions';
|
||||
@@ -27,6 +27,7 @@ import useReposts from '@/nostr/useReposts';
|
||||
import useSubscription from '@/nostr/useSubscription';
|
||||
import ensureNonNull from '@/utils/ensureNonNull';
|
||||
import npubEncodeFallback from '@/utils/npubEncodeFallback';
|
||||
import timeout from '@/utils/timeout';
|
||||
|
||||
import ContextMenu, { MenuItem } from '../ContextMenu';
|
||||
|
||||
@@ -41,23 +42,6 @@ const { noteEncode } = nip19;
|
||||
const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
||||
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 formatDate = useFormatDate();
|
||||
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 p = pubkey();
|
||||
return p != null && isReactedBy(p);
|
||||
@@ -313,7 +359,7 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
||||
>
|
||||
<ArrowPathRoundedSquare />
|
||||
</button>
|
||||
<Show when={reposts().length > 0}>
|
||||
<Show when={!config().hideCount && reposts().length > 0}>
|
||||
<div class="text-sm text-zinc-400">{reposts().length}</div>
|
||||
</Show>
|
||||
</div>
|
||||
@@ -333,7 +379,7 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
||||
<HeartSolid />
|
||||
</Show>
|
||||
</button>
|
||||
<Show when={reactions().length > 0}>
|
||||
<Show when={!config().hideCount && reactions().length > 0}>
|
||||
<div class="text-sm text-zinc-400">{reactions().length}</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Switch, Match, type Component } from 'solid-js';
|
||||
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import TextNoteDisplay, { type TextNoteDisplayProps } from '@/components/textNote/TextNoteDisplay';
|
||||
import useConfig from '@/nostr/useConfig';
|
||||
import useConfig from '@/core/useConfig';
|
||||
import useEvent from '@/nostr/useEvent';
|
||||
import ensureNonNull from '@/utils/ensureNonNull';
|
||||
|
||||
|
||||
20
src/components/utils/Popup.tsx
Normal file
20
src/components/utils/Popup.tsx
Normal 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;
|
||||
};
|
||||
@@ -15,6 +15,7 @@ export type Config = {
|
||||
dateFormat: 'relative' | 'absolute-long' | 'absolute-short';
|
||||
keepOpenPostForm: boolean;
|
||||
showImage: boolean;
|
||||
hideCount: boolean;
|
||||
mutedPubkeys: string[];
|
||||
mutedKeywords: string[];
|
||||
};
|
||||
@@ -64,6 +65,7 @@ const InitialConfig = (): Config => {
|
||||
dateFormat: 'relative',
|
||||
keepOpenPostForm: false,
|
||||
showImage: true,
|
||||
hideCount: false,
|
||||
mutedPubkeys: [],
|
||||
mutedKeywords: [],
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import useConfig from '@/core/useConfig';
|
||||
import useDatePulser from '@/hooks/useDatePulser';
|
||||
import useConfig from '@/nostr/useConfig';
|
||||
import { formatRelative, formatAbsoluteLong, formatAbsoluteShort } from '@/utils/formatDate';
|
||||
|
||||
// 7 seconds is used here so that the last digit of relative time is changed.
|
||||
|
||||
@@ -162,6 +162,7 @@ describe('parseTextNote', () => {
|
||||
{
|
||||
type: 'Bech32Entity',
|
||||
content: 'npub1srf6g8v2qpnecqg9l2kzehmkg0ym5f5rtnlsj6lhl8r6pmhger7q5mtt3q',
|
||||
isNIP19: false,
|
||||
data: {
|
||||
type: 'npub',
|
||||
data: '80d3a41d8a00679c0105faac2cdf7643c9ba26835cff096bf7f9c7a0eee8c8fc',
|
||||
@@ -25,6 +25,7 @@ export type Bech32Entity = {
|
||||
| { type: 'npub' | 'note'; data: string }
|
||||
| { type: 'nprofile'; data: ProfilePointer }
|
||||
| { type: 'nevent'; data: EventPointer };
|
||||
isNIP19: boolean;
|
||||
};
|
||||
|
||||
export type HashTag = {
|
||||
@@ -61,7 +62,8 @@ const tagRefRegex = /(?:#\[(?<idx>\d+)\])/g;
|
||||
const hashTagRegex = /#(?<hashtag>[\p{Letter}\p{Number}_]+)/gu;
|
||||
// raw NIP-19 codes, NIP-21 links (NIP-27)
|
||||
// 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 =
|
||||
/(?<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) {
|
||||
pushPlainText(index);
|
||||
try {
|
||||
const decoded = decode(match[1]);
|
||||
const decoded = decode(match.groups.bech32);
|
||||
const bech32Entity: Bech32Entity = {
|
||||
type: 'Bech32Entity',
|
||||
content: match[0],
|
||||
data: decoded as Bech32Entity['data'],
|
||||
isNIP19: match.groups.nip19 === 'nostr:',
|
||||
};
|
||||
result.push(bech32Entity);
|
||||
} catch (e) {
|
||||
@@ -10,9 +10,9 @@ import {
|
||||
import { createQuery, useQueryClient, type CreateQueryResult } from '@tanstack/solid-query';
|
||||
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 useConfig from '@/nostr/useConfig';
|
||||
import usePool from '@/nostr/usePool';
|
||||
import useStats from '@/nostr/useStats';
|
||||
import timeout from '@/utils/timeout';
|
||||
|
||||
@@ -192,6 +192,24 @@ const useCommands = () => {
|
||||
};
|
||||
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);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createMemo, createSignal } from 'solid-js';
|
||||
import uniq from 'lodash/uniq';
|
||||
import { Kind } from 'nostr-tools';
|
||||
|
||||
import useConfig from '@/nostr/useConfig';
|
||||
import useConfig from '@/core/useConfig';
|
||||
import useSubscription from '@/nostr/useSubscription';
|
||||
|
||||
export type UseFollowersProps = {
|
||||
|
||||
@@ -2,10 +2,9 @@ import { createSignal, createEffect, onCleanup, on } from 'solid-js';
|
||||
|
||||
import uniqBy from 'lodash/uniqBy';
|
||||
|
||||
import useConfig from '@/core/useConfig';
|
||||
import usePool from '@/nostr/usePool';
|
||||
|
||||
import useConfig from './useConfig';
|
||||
import useStats from './useStats';
|
||||
import useStats from '@/nostr/useStats';
|
||||
|
||||
import type { Event as NostrEvent, Filter, SubscriptionOptions } from 'nostr-tools';
|
||||
|
||||
|
||||
@@ -18,10 +18,10 @@ import Notification from '@/components/Notification';
|
||||
import ProfileDisplay from '@/components/ProfileDisplay';
|
||||
import SideBar from '@/components/SideBar';
|
||||
import Timeline from '@/components/Timeline';
|
||||
import useConfig from '@/core/useConfig';
|
||||
import useModalState from '@/hooks/useModalState';
|
||||
import usePersistStatus from '@/hooks/usePersistStatus';
|
||||
import { useMountShortcutKeys } from '@/hooks/useShortcutKeys';
|
||||
import useConfig from '@/nostr/useConfig';
|
||||
import useFollowings from '@/nostr/useFollowings';
|
||||
import usePool from '@/nostr/usePool';
|
||||
import usePubkey from '@/nostr/usePubkey';
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user