mirror of
https://github.com/aljazceru/rabbit.git
synced 2025-12-18 06:24:25 +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'],
|
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'],
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
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';
|
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: [],
|
||||||
};
|
};
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -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',
|
||||||
@@ -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) {
|
||||||
@@ -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';
|
||||||
|
|||||||
@@ -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);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user