This commit is contained in:
Shusui MOYATANI
2023-03-02 02:27:53 +09:00
parent b1aa63d6a3
commit 3ce64a449d
10 changed files with 133 additions and 129 deletions

View File

@@ -0,0 +1,45 @@
import { type Event as NostrEvent } from 'nostr-tools/event';
import { type Filter } from 'nostr-tools/filter';
import useConfig from '@/clients/useConfig';
import useBatch, { type Task } from '@/clients/useBatch';
import useSubscription from '@/clients/useSubscription';
export type UseBatchedEventProps<TaskArgs> = {
generateKey: (args: TaskArgs) => string | number;
mergeFilters: (args: TaskArgs[]) => Filter[];
extractKey: (event: NostrEvent) => string | number | undefined;
};
const useBatchedEvent = <TaskArgs>(propsProvider: () => UseBatchedEventProps<TaskArgs>) => {
return useBatch<TaskArgs, NostrEvent>(() => {
return {
executor: (tasks) => {
const { generateKey, mergeFilters, extractKey } = propsProvider();
// TODO relayUrlsを考慮する
const [config] = useConfig();
const keyTaskMap = new Map<string | number, Task<TaskArgs, NostrEvent>>(
tasks.map((task) => [generateKey(task.args), task]),
);
const filters = mergeFilters(tasks.map((task) => task.args));
useSubscription(() => ({
relayUrls: config().relayUrls,
filters,
continuous: false,
onEvent: (event: NostrEvent) => {
const key = extractKey(event);
if (key == null) return;
const task = keyTaskMap.get(key);
// possibly, the new event received
if (task == null) return;
task.resolve(event);
},
}));
},
};
});
};
export default useBatchedEvent;

View File

@@ -17,6 +17,7 @@ const InitialConfig: Config = {
'wss://relay.snort.social', 'wss://relay.snort.social',
'wss://relay.current.fyi', 'wss://relay.current.fyi',
'wss://relay.nostr.wirednet.jp', 'wss://relay.nostr.wirednet.jp',
'wss://relay.mostr.pub',
], ],
}; };

View File

@@ -2,11 +2,10 @@ import { createMemo, type Accessor } from 'solid-js';
import { type Event as NostrEvent } from 'nostr-tools/event'; import { type Event as NostrEvent } from 'nostr-tools/event';
import { createQuery, type CreateQueryResult } from '@tanstack/solid-query'; import { createQuery, type CreateQueryResult } from '@tanstack/solid-query';
import useConfig from '@/clients/useConfig'; import useBatchedEvent from '@/clients/useBatchedEvent';
import useBatch, { type Task } from '@/clients/useBatch';
import useSubscription from '@/clients/useSubscription';
export type UseEventProps = { export type UseEventProps = {
// TODO リレーURLを考慮したい
relayUrls: string[]; relayUrls: string[];
eventId: string; eventId: string;
}; };
@@ -16,36 +15,14 @@ export type UseEvent = {
query: CreateQueryResult<NostrEvent>; query: CreateQueryResult<NostrEvent>;
}; };
const { exec } = useBatch<UseEventProps, NostrEvent>(() => { const { exec } = useBatchedEvent<UseEventProps>(() => ({
return { generateKey: ({ eventId }: UseEventProps) => eventId,
executor: (tasks) => { mergeFilters: (args: UseEventProps[]) => {
// TODO relayUrlsを考慮する const eventIds = args.map((arg) => arg.eventId);
const [config] = useConfig(); return [{ kinds: [1], ids: eventIds }];
const eventIdTaskMap = new Map<string, Task<UseEventProps, NostrEvent>>( },
tasks.map((task) => [task.args.eventId, task]), extractKey: (event: NostrEvent) => event.id,
); }));
const eventIds = Array.from(eventIdTaskMap.keys());
useSubscription(() => ({
relayUrls: config().relayUrls,
filters: [
{
ids: eventIds,
kinds: [1],
},
],
continuous: false,
onEvent: (event: NostrEvent) => {
if (event.id == null) return;
const task = eventIdTaskMap.get(event.id);
// possibly, the new event received
if (task == null) return;
task.resolve(event);
},
}));
},
};
});
const useEvent = (propsProvider: () => UseEventProps): UseEvent => { const useEvent = (propsProvider: () => UseEventProps): UseEvent => {
const props = createMemo(propsProvider); const props = createMemo(propsProvider);

View File

@@ -1,14 +1,14 @@
import { createMemo, type Accessor } from 'solid-js'; import { createMemo, type Accessor } from 'solid-js';
import { type Event as NostrEvent } from 'nostr-tools/event'; import { type Event as NostrEvent } from 'nostr-tools/event';
import { type Filter } from 'nostr-tools/filter';
import { createQuery, type CreateQueryResult } from '@tanstack/solid-query'; import { createQuery, type CreateQueryResult } from '@tanstack/solid-query';
import useConfig from '@/clients/useConfig'; import useBatchedEvent from '@/clients/useBatchedEvent';
import useBatch, { type Task } from '@/clients/useBatch'; import { Task } from './useBatch';
import useSubscription from '@/clients/useSubscription';
// TODO zodにする // TODO zodにする
// deleted等の特殊なもの // deleted等の特殊なもの
type StandardProfile = { export type StandardProfile = {
name?: string; name?: string;
about?: string; about?: string;
picture?: string; picture?: string;
@@ -17,14 +17,14 @@ type StandardProfile = {
lud16?: string; // NIP-57 lud16?: string; // NIP-57
}; };
type NonStandardProfile = { export type NonStandardProfile = {
display_name?: string; display_name?: string;
website?: string; website?: string;
}; };
type Profile = StandardProfile & NonStandardProfile; export type Profile = StandardProfile & NonStandardProfile;
type UseProfileProps = { export type UseProfileProps = {
relayUrls: string[]; relayUrls: string[];
pubkey: string; pubkey: string;
}; };
@@ -34,40 +34,17 @@ type UseProfile = {
query: CreateQueryResult<NostrEvent>; query: CreateQueryResult<NostrEvent>;
}; };
const { exec } = useBatch<UseProfileProps, NostrEvent>(() => { const { exec } = useBatchedEvent<UseProfileProps>(() => ({
return { generateKey: ({ pubkey }: UseProfileProps): string => pubkey,
executor: (tasks) => { mergeFilters: (args: UseProfileProps[]): Filter[] => {
// TODO relayUrlsを考慮する const pubkeys = args.map((arg) => arg.pubkey);
const [config] = useConfig(); return [{ kinds: [0], authors: pubkeys }];
const pubkeyTaskMap = new Map<string, Task<UseProfileProps, NostrEvent>>( },
tasks.map((task) => [task.args.pubkey, task]), extractKey: (event: NostrEvent): string => event.pubkey,
); }));
const pubkeys = Array.from(pubkeyTaskMap.keys());
useSubscription(() => ({
relayUrls: config().relayUrls,
filters: [
{
kinds: [0],
authors: pubkeys,
},
],
continuous: false,
onEvent: (event: NostrEvent) => {
if (event.id == null) return;
const task = pubkeyTaskMap.get(event.pubkey);
// possibly, the new event received
if (task == null) return;
task.resolve(event);
},
}));
},
};
});
const useProfile = (propsProvider: () => UseProfileProps): UseProfile => { const useProfile = (propsProvider: () => UseProfileProps): UseProfile => {
const props = createMemo(propsProvider); const props = createMemo(propsProvider);
const query = createQuery( const query = createQuery(
() => ['useProfile', props()] as const, () => ['useProfile', props()] as const,
({ queryKey, signal }) => { ({ queryKey, signal }) => {
@@ -82,11 +59,9 @@ const useProfile = (propsProvider: () => UseProfileProps): UseProfile => {
); );
const profile = () => { const profile = () => {
const maybeProfile = query.data; if (query.data == null) return undefined;
if (maybeProfile == null) return undefined;
// TODO 大きすぎたりしないかどうか、JSONかどうかのチェック // TODO 大きすぎたりしないかどうか、JSONかどうかのチェック
return JSON.parse(maybeProfile.content) as Profile; return JSON.parse(query.data.content) as Profile;
}; };
return { profile, query }; return { profile, query };

View File

@@ -1,5 +1,5 @@
// NIP-18 (DEPRECATED) // NIP-18 (DEPRECATED)
import { Show, type Component } from 'solid-js'; import { Show, Switch, Match, type Component } from 'solid-js';
import { Event as NostrEvent } from 'nostr-tools/event'; import { Event as NostrEvent } from 'nostr-tools/event';
import ArrowPathRoundedSquare from 'heroicons/24/outline/arrow-path-rounded-square.svg'; import ArrowPathRoundedSquare from 'heroicons/24/outline/arrow-path-rounded-square.svg';
@@ -7,6 +7,7 @@ import useConfig from '@/clients/useConfig';
import useEvent from '@/clients/useEvent'; import useEvent from '@/clients/useEvent';
import useProfile from '@/clients/useProfile'; import useProfile from '@/clients/useProfile';
import UserNameDisplay from '@/components/UserNameDisplay';
import TextNote from '@/components/TextNote'; import TextNote from '@/components/TextNote';
export type DeprecatedRepostProps = { export type DeprecatedRepostProps = {
@@ -30,19 +31,22 @@ const DeprecatedRepost: Component<DeprecatedRepostProps> = (props) => {
<div class="h-5 w-5 shrink-0 pr-1 text-green-500" aria-hidden="true"> <div class="h-5 w-5 shrink-0 pr-1 text-green-500" aria-hidden="true">
<ArrowPathRoundedSquare /> <ArrowPathRoundedSquare />
</div> </div>
<div class="truncate"> <div class="truncate break-all">
<Show when={(profile()?.display_name?.length ?? 0) > 0} fallback={props.event.pubkey}> <UserNameDisplay pubkey={props.event.pubkey} />
{profile()?.display_name}
</Show>
{' Reposted'} {' Reposted'}
</div> </div>
</div> </div>
<Show <Switch fallback="failed to load">
when={event() != null} <Match when={event() != null}>
fallback={<Show when={eventQuery.isLoading}>loading {eventId()}</Show>} <TextNote event={event()} />
> </Match>
<TextNote event={event()} /> <Match when={eventQuery.isLoading}>
</Show> <div class="truncate">
{'loading '}
<span>{eventId()}</span>
</div>
</Match>
</Switch>
</div> </div>
); );
}; };

View File

@@ -3,6 +3,7 @@ import { Kind, type Event as NostrEvent } from 'nostr-tools/event';
import TextNote from '@/components/TextNote'; import TextNote from '@/components/TextNote';
import Reaction from '@/components/notification/Reaction'; import Reaction from '@/components/notification/Reaction';
import DeprecatedRepost from '@/components/DeprecatedRepost';
export type TimelineProps = { export type TimelineProps = {
events: NostrEvent[]; events: NostrEvent[];
@@ -19,6 +20,10 @@ const Timeline: Component<TimelineProps> = (props) => {
<Match when={event.kind === Kind.Reaction}> <Match when={event.kind === Kind.Reaction}>
<Reaction event={event} /> <Reaction event={event} />
</Match> </Match>
{/* TODO ちゃんとnotification用のコンポーネント使う */}
<Match when={event.kind === 1}>
<DeprecatedRepost event={event} />
</Match>
</Switch> </Switch>
)} )}
</For> </For>

View File

@@ -0,0 +1,25 @@
import { Component, Switch, Match } from 'solid-js';
import useConfig from '@/clients/useConfig';
import useProfile, { type Profile } from '@/clients/useProfile';
type UserNameDisplayProps = {
pubkey: string;
};
const UserNameDisplay: Component<UserNameDisplayProps> = (props) => {
const [config] = useConfig();
const { profile } = useProfile(() => ({
relayUrls: config().relayUrls,
pubkey: props.pubkey,
}));
return (
<Switch fallback={`@${props.pubkey}`}>
<Match when={(profile()?.display_name?.length ?? 0) > 0}>{profile()?.display_name}</Match>
<Match when={(profile()?.name?.length ?? 0) > 0}>@{profile()?.name}</Match>
</Switch>
);
};
export default UserNameDisplay;

View File

@@ -2,10 +2,12 @@ import { Switch, Match, type Component, Show } from 'solid-js';
import { type Event as NostrEvent } from 'nostr-tools/event'; import { type Event as NostrEvent } from 'nostr-tools/event';
import HeartSolid from 'heroicons/24/solid/heart.svg'; import HeartSolid from 'heroicons/24/solid/heart.svg';
import UserNameDisplay from '@/components/UserNameDisplay';
import TextNote from '@/components/TextNote';
import useConfig from '@/clients/useConfig'; import useConfig from '@/clients/useConfig';
import useProfile from '@/clients/useProfile'; import useProfile from '@/clients/useProfile';
import useEvent from '@/clients/useEvent'; import useEvent from '@/clients/useEvent';
import TextNote from '../TextNote';
type ReactionProps = { type ReactionProps = {
event: NostrEvent; event: NostrEvent;
@@ -52,9 +54,7 @@ const Reaction: Component<ReactionProps> = (props) => {
</div> </div>
<div> <div>
<span class="truncate whitespace-pre-wrap break-all font-bold"> <span class="truncate whitespace-pre-wrap break-all font-bold">
<Show when={profile() != null} fallback={props.event.pubkey}> <UserNameDisplay pubkey={props.event.pubkey} />
{profile()?.display_name}
</Show>
</span> </span>
{' reacted'} {' reacted'}
</div> </div>

View File

@@ -2,6 +2,7 @@ import { createSignal, type Accessor } from 'solid-js';
const [currentDate, setCurrentDate] = createSignal(new Date()); const [currentDate, setCurrentDate] = createSignal(new Date());
// 7 seconds is used for the interval so that the last digit of relative time is changed.
setInterval(() => { setInterval(() => {
setCurrentDate(new Date()); setCurrentDate(new Date());
}, 7000); }, 7000);

View File

@@ -10,49 +10,21 @@ import TextNote from '@/components/TextNote';
import useCommands from '@/clients/useCommands'; import useCommands from '@/clients/useCommands';
import useConfig from '@/clients/useConfig'; import useConfig from '@/clients/useConfig';
import useSubscription from '@/clients/useSubscription'; import useSubscription from '@/clients/useSubscription';
import useShortcutKeys from '@/hooks/useShortcutKeys';
import useFollowings from '@/clients/useFollowings'; import useFollowings from '@/clients/useFollowings';
import usePubkey from '@/clients/usePubkey';
/* import useShortcutKeys from '@/hooks/useShortcutKeys';
type UseRelayProps = { pubkey: string };
const publish = async (pool, event) => {
const pub = pool.publish(writeRelays, event);
return new Promise((resolve, reject) => {});
};
*/
// const relays = ['ws://localhost:8008'];
//
const pubkey = 'npub1jcsr6e38dcepf65nkmrc54mu8jd8y70eael9rv308wxpwep6sxwqgsscyc';
const pubkeyHex = '96203d66276e3214ea93b6c78a577c3c9a7279f9ee7e51b22f3b8c17643a819c';
useShortcutKeys({ useShortcutKeys({
onShortcut: (s) => console.log(s), onShortcut: (s) => console.log(s),
}); });
const dummyTextNote = (
<TextNote
event={
{
id: 12345,
kind: 1,
pubkey: pubkeyHex,
created_at: Math.floor(Date.now() / 1000),
tags: [],
content: 'hello',
} as NostrEvent
}
/>
);
const Home: Component = () => { const Home: Component = () => {
const [config] = useConfig(); const [config] = useConfig();
const pubkey = usePubkey();
const commands = useCommands(); const commands = useCommands();
const { followings } = useFollowings(() => ({ const { followings } = useFollowings(() => ({
relayUrls: config().relayUrls, relayUrls: config().relayUrls,
pubkey: pubkeyHex, pubkey: pubkey(),
})); }));
const { events: followingsPosts } = useSubscription(() => ({ const { events: followingsPosts } = useSubscription(() => ({
@@ -60,7 +32,7 @@ const Home: Component = () => {
filters: [ filters: [
{ {
kinds: [1, 6], kinds: [1, 6],
authors: followings()?.map((f) => f.pubkey) ?? [pubkeyHex], authors: [...followings()?.map((f) => f.pubkey), pubkey()] ?? [pubkey()],
limit: 25, limit: 25,
since: Math.floor(Date.now() / 1000) - 12 * 60 * 60, since: Math.floor(Date.now() / 1000) - 12 * 60 * 60,
}, },
@@ -72,7 +44,7 @@ const Home: Component = () => {
filters: [ filters: [
{ {
kinds: [1, 6], kinds: [1, 6],
authors: [pubkeyHex], authors: [pubkey()],
limit: 25, limit: 25,
}, },
], ],
@@ -83,7 +55,7 @@ const Home: Component = () => {
filters: [ filters: [
{ {
kinds: [1, 6, 7], kinds: [1, 6, 7],
'#p': [pubkeyHex], '#p': [pubkey()],
limit: 25, limit: 25,
since: Math.floor(Date.now() / 1000) - 12 * 60 * 60, since: Math.floor(Date.now() / 1000) - 12 * 60 * 60,
}, },
@@ -101,6 +73,7 @@ const Home: Component = () => {
], ],
})); }));
/*
const { events: searchPosts } = useSubscription(() => ({ const { events: searchPosts } = useSubscription(() => ({
relayUrls: ['wss://relay.nostr.band/'], relayUrls: ['wss://relay.nostr.band/'],
filters: [ filters: [
@@ -112,12 +85,13 @@ const Home: Component = () => {
}, },
], ],
})); }));
*/
const handlePost = ({ content }: { content: string }) => { const handlePost = ({ content }: { content: string }) => {
commands commands
.publishTextNote({ .publishTextNote({
relayUrls: config().relayUrls, relayUrls: config().relayUrls,
pubkey: pubkeyHex, pubkey: pubkey(),
content, content,
}) })
.then(() => { .then(() => {
@@ -144,9 +118,6 @@ const Home: Component = () => {
<Column name="自分の投稿" width="medium"> <Column name="自分の投稿" width="medium">
<Timeline events={myPosts()} /> <Timeline events={myPosts()} />
</Column> </Column>
<Column name="#nostrstudy" width="medium">
<Timeline events={searchPosts()} />
</Column>
</div> </div>
</div> </div>
); );