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.current.fyi',
'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 { createQuery, type CreateQueryResult } from '@tanstack/solid-query';
import useConfig from '@/clients/useConfig';
import useBatch, { type Task } from '@/clients/useBatch';
import useSubscription from '@/clients/useSubscription';
import useBatchedEvent from '@/clients/useBatchedEvent';
export type UseEventProps = {
// TODO リレーURLを考慮したい
relayUrls: string[];
eventId: string;
};
@@ -16,36 +15,14 @@ export type UseEvent = {
query: CreateQueryResult<NostrEvent>;
};
const { exec } = useBatch<UseEventProps, NostrEvent>(() => {
return {
executor: (tasks) => {
// TODO relayUrlsを考慮する
const [config] = useConfig();
const eventIdTaskMap = new Map<string, Task<UseEventProps, NostrEvent>>(
tasks.map((task) => [task.args.eventId, task]),
);
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 { exec } = useBatchedEvent<UseEventProps>(() => ({
generateKey: ({ eventId }: UseEventProps) => eventId,
mergeFilters: (args: UseEventProps[]) => {
const eventIds = args.map((arg) => arg.eventId);
return [{ kinds: [1], ids: eventIds }];
},
extractKey: (event: NostrEvent) => event.id,
}));
},
};
});
const useEvent = (propsProvider: () => UseEventProps): UseEvent => {
const props = createMemo(propsProvider);

View File

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

View File

@@ -1,5 +1,5 @@
// 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 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 useProfile from '@/clients/useProfile';
import UserNameDisplay from '@/components/UserNameDisplay';
import TextNote from '@/components/TextNote';
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">
<ArrowPathRoundedSquare />
</div>
<div class="truncate">
<Show when={(profile()?.display_name?.length ?? 0) > 0} fallback={props.event.pubkey}>
{profile()?.display_name}
</Show>
<div class="truncate break-all">
<UserNameDisplay pubkey={props.event.pubkey} />
{' Reposted'}
</div>
</div>
<Show
when={event() != null}
fallback={<Show when={eventQuery.isLoading}>loading {eventId()}</Show>}
>
<Switch fallback="failed to load">
<Match when={event() != null}>
<TextNote event={event()} />
</Show>
</Match>
<Match when={eventQuery.isLoading}>
<div class="truncate">
{'loading '}
<span>{eventId()}</span>
</div>
</Match>
</Switch>
</div>
);
};

View File

@@ -3,6 +3,7 @@ import { Kind, type Event as NostrEvent } from 'nostr-tools/event';
import TextNote from '@/components/TextNote';
import Reaction from '@/components/notification/Reaction';
import DeprecatedRepost from '@/components/DeprecatedRepost';
export type TimelineProps = {
events: NostrEvent[];
@@ -19,6 +20,10 @@ const Timeline: Component<TimelineProps> = (props) => {
<Match when={event.kind === Kind.Reaction}>
<Reaction event={event} />
</Match>
{/* TODO ちゃんとnotification用のコンポーネント使う */}
<Match when={event.kind === 1}>
<DeprecatedRepost event={event} />
</Match>
</Switch>
)}
</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 HeartSolid from 'heroicons/24/solid/heart.svg';
import UserNameDisplay from '@/components/UserNameDisplay';
import TextNote from '@/components/TextNote';
import useConfig from '@/clients/useConfig';
import useProfile from '@/clients/useProfile';
import useEvent from '@/clients/useEvent';
import TextNote from '../TextNote';
type ReactionProps = {
event: NostrEvent;
@@ -52,9 +54,7 @@ const Reaction: Component<ReactionProps> = (props) => {
</div>
<div>
<span class="truncate whitespace-pre-wrap break-all font-bold">
<Show when={profile() != null} fallback={props.event.pubkey}>
{profile()?.display_name}
</Show>
<UserNameDisplay pubkey={props.event.pubkey} />
</span>
{' reacted'}
</div>

View File

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

View File

@@ -10,49 +10,21 @@ import TextNote from '@/components/TextNote';
import useCommands from '@/clients/useCommands';
import useConfig from '@/clients/useConfig';
import useSubscription from '@/clients/useSubscription';
import useShortcutKeys from '@/hooks/useShortcutKeys';
import useFollowings from '@/clients/useFollowings';
/*
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';
import usePubkey from '@/clients/usePubkey';
import useShortcutKeys from '@/hooks/useShortcutKeys';
useShortcutKeys({
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 [config] = useConfig();
const pubkey = usePubkey();
const commands = useCommands();
const { followings } = useFollowings(() => ({
relayUrls: config().relayUrls,
pubkey: pubkeyHex,
pubkey: pubkey(),
}));
const { events: followingsPosts } = useSubscription(() => ({
@@ -60,7 +32,7 @@ const Home: Component = () => {
filters: [
{
kinds: [1, 6],
authors: followings()?.map((f) => f.pubkey) ?? [pubkeyHex],
authors: [...followings()?.map((f) => f.pubkey), pubkey()] ?? [pubkey()],
limit: 25,
since: Math.floor(Date.now() / 1000) - 12 * 60 * 60,
},
@@ -72,7 +44,7 @@ const Home: Component = () => {
filters: [
{
kinds: [1, 6],
authors: [pubkeyHex],
authors: [pubkey()],
limit: 25,
},
],
@@ -83,7 +55,7 @@ const Home: Component = () => {
filters: [
{
kinds: [1, 6, 7],
'#p': [pubkeyHex],
'#p': [pubkey()],
limit: 25,
since: Math.floor(Date.now() / 1000) - 12 * 60 * 60,
},
@@ -101,6 +73,7 @@ const Home: Component = () => {
],
}));
/*
const { events: searchPosts } = useSubscription(() => ({
relayUrls: ['wss://relay.nostr.band/'],
filters: [
@@ -112,12 +85,13 @@ const Home: Component = () => {
},
],
}));
*/
const handlePost = ({ content }: { content: string }) => {
commands
.publishTextNote({
relayUrls: config().relayUrls,
pubkey: pubkeyHex,
pubkey: pubkey(),
content,
})
.then(() => {
@@ -144,9 +118,6 @@ const Home: Component = () => {
<Column name="自分の投稿" width="medium">
<Timeline events={myPosts()} />
</Column>
<Column name="#nostrstudy" width="medium">
<Timeline events={searchPosts()} />
</Column>
</div>
</div>
);