This commit is contained in:
Shusui MOYATANI
2023-03-18 01:44:02 +09:00
parent 5a37842702
commit 3f19aa120d
12 changed files with 144 additions and 138 deletions

View File

@@ -12,9 +12,9 @@ const RelayConfig = () => {
const handleClickAddRelay: JSX.EventHandler<HTMLFormElement, Event> = (ev) => { const handleClickAddRelay: JSX.EventHandler<HTMLFormElement, Event> = (ev) => {
ev.preventDefault(); ev.preventDefault();
const relayUrl = ev.currentTarget?.relayUrl?.value as string | undefined; if (relayUrlInput().length > 0) return;
if (relayUrl == null) return;
addRelay(relayUrlInput()); addRelay(relayUrlInput());
setRelayUrlInput('');
}; };
return ( return (
@@ -80,13 +80,13 @@ const DateFormatConfig = () => {
return ( return (
<div> <div>
<h3 class="font-bold"></h3> <h3 class="font-bold"></h3>
<div class="flex flex-col justify-evenly gap-2 md:flex-row"> <div class="flex flex-col justify-evenly gap-2 sm:flex-row">
<For each={dateFormats}> <For each={dateFormats}>
{({ id, name, example }) => ( {({ id, name, example }) => (
<div class="flex flex-1 flex-row items-center gap-1 md:flex-col"> <div class="flex flex-1 flex-row items-center gap-1 sm:flex-col">
<button <button
type="button" type="button"
class="w-48 rounded border border-rose-300 p-2 font-bold md:w-full" class="w-48 rounded border border-rose-300 p-2 font-bold sm:w-full"
classList={{ classList={{
'bg-rose-300': config().dateFormat === id, 'bg-rose-300': config().dateFormat === id,
'text-white': config().dateFormat === id, 'text-white': config().dateFormat === id,

View File

@@ -8,7 +8,9 @@ import {
type JSX, type JSX,
type Accessor, type Accessor,
} from 'solid-js'; } from 'solid-js';
import { createMutation } from '@tanstack/solid-query';
import { Event as NostrEvent } from 'nostr-tools'; import { Event as NostrEvent } from 'nostr-tools';
import uniq from 'lodash/uniq';
import PaperAirplane from 'heroicons/24/solid/paper-airplane.svg'; import PaperAirplane from 'heroicons/24/solid/paper-airplane.svg';
import XMark from 'heroicons/24/outline/x-mark.svg'; import XMark from 'heroicons/24/outline/x-mark.svg';
@@ -41,26 +43,35 @@ const placeholder = (mode: NotePostFormProps['mode']) => {
const NotePostForm: Component<NotePostFormProps> = (props) => { const NotePostForm: Component<NotePostFormProps> = (props) => {
let textAreaRef: HTMLTextAreaElement | undefined; let textAreaRef: HTMLTextAreaElement | undefined;
const [text, setText] = createSignal<string>('');
const clearText = () => setText('');
const { config } = useConfig(); const { config } = useConfig();
const getPubkey = usePubkey(); const getPubkey = usePubkey();
const commands = useCommands(); const commands = useCommands();
const [text, setText] = createSignal<string>('');
const clearText = () => setText('');
const replyTo = () => props.replyTo && eventWrapper(props.replyTo); const replyTo = () => props.replyTo && eventWrapper(props.replyTo);
const mode = () => props.mode ?? 'normal'; const mode = () => props.mode ?? 'normal';
const publishTextNoteMutation = createMutation({
mutationKey: ['publishTextNote'],
mutationFn: commands.publishTextNote.bind(commands),
onSuccess: () => {
console.log('succeeded to post');
clearText();
props?.onClose();
},
onError: (err) => {
console.error('error', err);
},
});
const mentionedPubkeys: Accessor<string[]> = createMemo( const mentionedPubkeys: Accessor<string[]> = createMemo(
() => replyTo()?.mentionedPubkeysWithoutAuthor() ?? [], () => replyTo()?.mentionedPubkeysWithoutAuthor() ?? [],
); );
const notifyPubkeys = (pubkey: string): string[] | undefined => { const notifyPubkeys = (pubkey: string): string[] | undefined => {
if (mentionedPubkeys().length === 0) return undefined; if (props.replyTo === undefined) return undefined;
return [...mentionedPubkeys(), pubkey]; return uniq([props.replyTo.pubkey, ...mentionedPubkeys(), pubkey]);
};
const handleChangeText: JSX.EventHandler<HTMLTextAreaElement, Event> = (ev) => {
setText(ev.currentTarget.value);
}; };
const submit = () => { const submit = () => {
@@ -69,23 +80,14 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
console.error('pubkey is not available'); console.error('pubkey is not available');
return; return;
} }
commands publishTextNoteMutation.mutate({
.publishTextNote({ relayUrls: config().relayUrls,
relayUrls: config().relayUrls, pubkey,
pubkey, content: text(),
content: text(), notifyPubkeys: notifyPubkeys(pubkey),
notifyPubkeys: notifyPubkeys(pubkey), rootEventId: replyTo()?.rootEvent()?.id ?? replyTo()?.id,
rootEventId: replyTo()?.rootEvent()?.id ?? replyTo()?.id, replyEventId: replyTo()?.id,
replyEventId: replyTo()?.id, });
})
.then(() => {
console.log('succeeded to post');
clearText();
props?.onClose();
})
.catch((err) => {
console.error('error', err);
});
}; };
const handleSubmit: JSX.EventHandler<HTMLFormElement, Event> = (ev) => { const handleSubmit: JSX.EventHandler<HTMLFormElement, Event> = (ev) => {
@@ -101,7 +103,9 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
} }
}; };
const submitDisabled = createMemo(() => text().trim().length === 0); const submitDisabled = createMemo(
() => text().trim().length === 0 || publishTextNoteMutation.isLoading,
);
onMount(() => { onMount(() => {
setTimeout(() => { setTimeout(() => {
@@ -130,7 +134,7 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
class="rounded border-none" class="rounded border-none"
rows={4} rows={4}
placeholder={placeholder(mode())} placeholder={placeholder(mode())}
onInput={handleChangeText} onInput={(ev) => setText(ev.currentTarget.value)}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
value={text()} value={text()}
/> />

View File

@@ -10,11 +10,7 @@ export type MentionedEventDisplayProps = {
const MentionedEventDisplay = (props: MentionedEventDisplayProps) => { const MentionedEventDisplay = (props: MentionedEventDisplayProps) => {
return ( return (
<Show <Show
when={ when={props.mentionedEvent.marker != null && props.mentionedEvent.marker.length > 0}
props.mentionedEvent.marker == null ||
props.mentionedEvent.marker.length === 0 ||
props.mentionedEvent.marker === 'mention'
}
fallback={() => <EventLink eventId={props.mentionedEvent.eventId} />} fallback={() => <EventLink eventId={props.mentionedEvent.eventId} />}
> >
<div class="my-1 rounded border p-1"> <div class="my-1 rounded border p-1">

View File

@@ -1,14 +1,6 @@
import { import { Show, For, createSignal, createMemo, type JSX, type Component } from 'solid-js';
Show,
For,
createSignal,
createMemo,
type JSX,
type Component,
Match,
Switch,
} from 'solid-js';
import type { Event as NostrEvent } from 'nostr-tools'; import type { Event as NostrEvent } from 'nostr-tools';
import { createMutation } from '@tanstack/solid-query';
import HeartOutlined from 'heroicons/24/outline/heart.svg'; import HeartOutlined from 'heroicons/24/outline/heart.svg';
import HeartSolid from 'heroicons/24/solid/heart.svg'; import HeartSolid from 'heroicons/24/solid/heart.svg';
@@ -47,13 +39,10 @@ export type TextNoteDisplayProps = {
const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => { const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
const { config } = useConfig(); const { config } = useConfig();
const formatDate = useFormatDate(); const formatDate = useFormatDate();
const commands = useCommands();
const pubkey = usePubkey(); const pubkey = usePubkey();
const [showReplyForm, setShowReplyForm] = createSignal(false); const [showReplyForm, setShowReplyForm] = createSignal(false);
const [showMenu, setShowMenu] = createSignal(false); const [showMenu, setShowMenu] = createSignal(false);
const [postingRepost, setPostingRepost] = createSignal(false);
const [postingReaction, setPostingReaction] = createSignal(false);
const event = createMemo(() => eventWrapper(props.event)); const event = createMemo(() => eventWrapper(props.event));
@@ -75,6 +64,34 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
eventId: props.event.id as string, // TODO いつかなおす eventId: props.event.id as string, // TODO いつかなおす
})); }));
const commands = useCommands();
const publishReactionMutation = createMutation({
mutationKey: ['publishReaction', event().id],
mutationFn: commands.publishReaction.bind(commands),
onSuccess: () => {
console.log('succeeded to publish reaction');
invalidateReactions().catch((err) => console.error('failed to refetch reactions', err));
},
onError: (err) => {
console.error('failed to publish reaction: ', err);
},
});
const publishDeprecatedRepostMutation = createMutation({
mutationKey: ['publishDeprecatedRepost', event().id],
mutationFn: commands.publishDeprecatedRepost.bind(commands),
onSuccess: () => {
console.log('succeeded to publish deprecated reposts');
invalidateDeprecatedReposts().catch((err) =>
console.error('failed to refetch deprecated reposts', err),
);
},
onError: (err) => {
console.error('failed to publish deprecated repost: ', err);
},
});
const isReactedByMe = createMemo(() => isReactedBy(pubkey())); const isReactedByMe = createMemo(() => isReactedBy(pubkey()));
const isRepostedByMe = createMemo(() => isRepostedBy(pubkey())); const isRepostedByMe = createMemo(() => isRepostedBy(pubkey()));
@@ -97,22 +114,14 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
// TODO remove reaction // TODO remove reaction
return; return;
} }
if (postingRepost()) {
return;
}
setPostingRepost(true);
ensureNonNull([pubkey(), props.event.id] as const)(([pubkeyNonNull, eventIdNonNull]) => { ensureNonNull([pubkey(), props.event.id] as const)(([pubkeyNonNull, eventIdNonNull]) => {
commands publishDeprecatedRepostMutation.mutate({
.publishDeprecatedRepost({ relayUrls: config().relayUrls,
relayUrls: config().relayUrls, pubkey: pubkeyNonNull,
pubkey: pubkeyNonNull, eventId: eventIdNonNull,
eventId: eventIdNonNull, notifyPubkey: props.event.pubkey,
notifyPubkey: props.event.pubkey, });
})
.then(() => invalidateDeprecatedReposts())
.catch((err) => console.error('failed to repost: ', err))
.finally(() => setPostingRepost(false));
}); });
}; };
@@ -121,23 +130,15 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
// TODO remove reaction // TODO remove reaction
return; return;
} }
if (postingReaction()) {
return;
}
setPostingReaction(true);
ensureNonNull([pubkey(), props.event.id] as const)(([pubkeyNonNull, eventIdNonNull]) => { ensureNonNull([pubkey(), props.event.id] as const)(([pubkeyNonNull, eventIdNonNull]) => {
commands publishReactionMutation.mutate({
.publishReaction({ relayUrls: config().relayUrls,
relayUrls: config().relayUrls, pubkey: pubkeyNonNull,
pubkey: pubkeyNonNull, content: '+',
content: '+', eventId: eventIdNonNull,
eventId: eventIdNonNull, notifyPubkey: props.event.pubkey,
notifyPubkey: props.event.pubkey, });
})
.then(() => invalidateReactions())
.catch((err) => console.error('failed to publish reaction: ', err))
.finally(() => setPostingReaction(false));
}); });
}; };
@@ -173,14 +174,14 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
</div> </div>
<Show when={showReplyEvent()} keyed> <Show when={showReplyEvent()} keyed>
{(id) => ( {(id) => (
<div class="border p-1"> <div class="mt-1 rounded border p-1">
<TextNoteDisplayById eventId={id} actions={false} embedding={false} /> <TextNoteDisplayById eventId={id} actions={false} embedding={false} />
</div> </div>
)} )}
</Show> </Show>
<Show when={event().mentionedPubkeysWithoutAuthor().length > 0}> <Show when={event().mentionedPubkeys().length > 0}>
<div class="text-xs"> <div class="text-xs">
<For each={event().mentionedPubkeysWithoutAuthor()}> <For each={event().mentionedPubkeys()}>
{(replyToPubkey: string) => ( {(replyToPubkey: string) => (
<span class="pr-1 text-blue-500 underline"> <span class="pr-1 text-blue-500 underline">
<GeneralUserMentionDisplay pubkey={replyToPubkey} /> <GeneralUserMentionDisplay pubkey={replyToPubkey} />
@@ -207,10 +208,14 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
class="flex shrink-0 items-center gap-1" class="flex shrink-0 items-center gap-1"
classList={{ classList={{
'text-zinc-400': !isRepostedByMe(), 'text-zinc-400': !isRepostedByMe(),
'text-green-400': isRepostedByMe(), 'text-green-400': isRepostedByMe() || publishDeprecatedRepostMutation.isLoading,
}} }}
> >
<button class="h-4 w-4" onClick={handleRepost} disabled={postingRepost()}> <button
class="h-4 w-4"
onClick={handleRepost}
disabled={publishDeprecatedRepostMutation.isLoading}
>
<ArrowPathRoundedSquare /> <ArrowPathRoundedSquare />
</button> </button>
<Show when={reposts().length > 0}> <Show when={reposts().length > 0}>
@@ -221,10 +226,14 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
class="flex shrink-0 items-center gap-1" class="flex shrink-0 items-center gap-1"
classList={{ classList={{
'text-zinc-400': !isReactedByMe(), 'text-zinc-400': !isReactedByMe(),
'text-rose-400': isReactedByMe(), 'text-rose-400': isReactedByMe() || publishReactionMutation.isLoading,
}} }}
> >
<button class="h-4 w-4" onClick={handleReaction} disabled={postingReaction()}> <button
class="h-4 w-4"
onClick={handleReaction}
disabled={publishReactionMutation.isLoading}
>
<Show when={isReactedByMe()} fallback={<HeartOutlined />}> <Show when={isReactedByMe()} fallback={<HeartOutlined />}>
<HeartSolid /> <HeartSolid />
</Show> </Show>

View File

@@ -12,7 +12,7 @@ export type MentionedEvent = {
content: string; content: string;
tagIndex: number; tagIndex: number;
eventId: string; eventId: string;
marker: string | null; // TODO 'reply' | 'root' | 'mention' | null; marker: 'reply' | 'root' | 'mention' | undefined;
}; };
export type MentionedUser = { export type MentionedUser = {
@@ -98,22 +98,21 @@ export const parseTextNote = (event: NostrEvent): ParsedTextNote => {
result.push(mentionedUser); result.push(mentionedUser);
} else if (tagName === 'e') { } else if (tagName === 'e') {
const mention = eventWrapper(event) const mention = eventWrapper(event)
.mentionedEvents() .taggedEvents()
.find((ev) => ev.index === tagIndex); .find((ev) => ev.index === tagIndex);
if (mention == null) return;
const mentionedEvent: MentionedEvent = { const mentionedEvent: MentionedEvent = {
type: 'MentionedEvent', type: 'MentionedEvent',
tagIndex, tagIndex,
content: match[0], content: match[0],
eventId: tag[1], eventId: tag[1],
marker: mention.marker, marker: mention?.marker,
}; };
result.push(mentionedEvent); result.push(mentionedEvent);
} }
} else if (match.groups?.nip19 && match.index >= pos) { } else if (match.groups?.nip19 && match.index >= pos) {
try { try {
const decoded = decode(match[0]); const decoded = decode(match[0].toLowerCase());
const bech32Entity: Bech32Entity = { const bech32Entity: Bech32Entity = {
type: 'Bech32Entity', type: 'Bech32Entity',
content: match[0], content: match[0],

View File

@@ -45,4 +45,3 @@ const applyContentFilter = (contentFilter: ContentFilter): boolean => {
// DOUBLEQUOTE // DOUBLEQUOTE
// A filter '"HELLO WORLD"' should match 'HELLO WORLD' // A filter '"HELLO WORLD"' should match 'HELLO WORLD'
// A filter '"HELLO WORLD"' should not match 'hello world' // A filter '"HELLO WORLD"' should not match 'hello world'

View File

@@ -38,46 +38,48 @@ const useCommands = () => {
}); });
}; };
return { // NIP-01
// NIP-01 const publishTextNote = ({
publishTextNote({ relayUrls,
relayUrls, pubkey,
content,
tags,
notifyPubkeys,
rootEventId,
mentionEventIds,
replyEventId,
}: {
relayUrls: string[];
pubkey: string;
content: string;
tags?: string[][];
notifyPubkeys?: string[];
rootEventId?: string;
mentionEventIds?: string[];
replyEventId?: string;
}): Promise<Promise<void>[]> => {
const pTags = notifyPubkeys?.map((p) => ['p', p]) ?? [];
const eTags = [];
// NIP-10
if (rootEventId != null) eTags.push(['e', rootEventId, '', 'root']);
if (mentionEventIds != null)
mentionEventIds.forEach((id) => eTags.push(['e', id, '', 'mention']));
if (replyEventId != null) eTags.push(['e', replyEventId, '', 'reply']);
const mergedTags = [...eTags, ...pTags, ...(tags ?? [])];
const preSignedEvent: NostrEvent = {
kind: 1,
pubkey, pubkey,
created_at: currentDate(),
tags: mergedTags,
content, content,
tags, };
notifyPubkeys, return publishEvent(relayUrls, preSignedEvent);
rootEventId, };
mentionEventIds,
replyEventId,
}: {
relayUrls: string[];
pubkey: string;
content: string;
tags?: string[][];
notifyPubkeys?: string[];
rootEventId?: string;
mentionEventIds?: string[];
replyEventId?: string;
}): Promise<Promise<void>[]> {
const pTags = notifyPubkeys?.map((p) => ['p', p]) ?? [];
const eTags = [];
// NIP-10
if (rootEventId != null) eTags.push(['e', rootEventId, '', 'root']);
if (mentionEventIds != null)
mentionEventIds.forEach((id) => eTags.push(['e', id, '', 'mention']));
if (replyEventId != null) eTags.push(['e', replyEventId, '', 'reply']);
const mergedTags = [...eTags, ...pTags, ...(tags ?? [])]; return {
publishTextNote,
const preSignedEvent: NostrEvent = {
kind: 1,
pubkey,
created_at: currentDate(),
tags: mergedTags,
content,
};
return publishEvent(relayUrls, preSignedEvent);
},
// NIP-25 // NIP-25
publishReaction({ publishReaction({
relayUrls, relayUrls,

View File

@@ -18,7 +18,7 @@ export type UseDeprecatedReposts = {
}; };
const { exec } = useBatchedEvents<UseDeprecatedRepostsProps>(() => ({ const { exec } = useBatchedEvents<UseDeprecatedRepostsProps>(() => ({
interval: 5000, interval: 3400,
generateKey: ({ eventId }) => eventId, generateKey: ({ eventId }) => eventId,
mergeFilters: (args) => { mergeFilters: (args) => {
const eventIds = args.map((arg) => arg.eventId); const eventIds = args.map((arg) => arg.eventId);

View File

@@ -19,7 +19,7 @@ export type UseReactions = {
}; };
const { exec } = useBatchedEvents<UseReactionsProps>(() => ({ const { exec } = useBatchedEvents<UseReactionsProps>(() => ({
interval: 5000, interval: 3400,
generateKey: ({ eventId }) => eventId, generateKey: ({ eventId }) => eventId,
mergeFilters: (args) => { mergeFilters: (args) => {
const eventIds = args.map((arg) => arg.eventId); const eventIds = args.map((arg) => arg.eventId);

View File

@@ -1,7 +1,4 @@
const filter = { const filter = {
kinds: [1], kinds: [1],
'#e': [ '#e': [rootEventId, currentEventId],
rootEventId,
currentEventId,
],
}; };

View File

@@ -48,7 +48,7 @@ const Home: Component = () => {
filters: [ filters: [
{ {
kinds: [1, 6], kinds: [1, 6],
authors: [...followingPubkeys(), pubkeyNonNull], authors: followingPubkeys(),
limit: 25, limit: 25,
since: Math.floor(Date.now() / 1000) - 12 * 60 * 60, since: Math.floor(Date.now() / 1000) - 12 * 60 * 60,
}, },

View File

@@ -59,7 +59,7 @@ const defaultAbsoluteDateShortFormatter = (parsedDate: AbsoluteDate): string =>
}; };
const calcDiffSec = (date: Date, currentDate: Date): number => const calcDiffSec = (date: Date, currentDate: Date): number =>
(Number(currentDate) - Number(date)) / 1000; Math.round(Number(currentDate) - Number(date)) / 1000;
const parseDateDiff = (date: Date, currentDate: Date): RelativeDate => { const parseDateDiff = (date: Date, currentDate: Date): RelativeDate => {
const diffSec = calcDiffSec(date, currentDate); const diffSec = calcDiffSec(date, currentDate);