implement parseTextNote

This commit is contained in:
Shusui MOYATANI
2023-02-21 20:39:37 +09:00
parent 2aa85b3ed9
commit 57969c2c09
20 changed files with 605 additions and 79 deletions

38
package-lock.json generated
View File

@@ -12,6 +12,7 @@
"@solidjs/meta": "^0.28.2",
"@solidjs/router": "^0.6.0",
"@tailwindcss/forms": "^0.5.3",
"@tanstack/solid-query": "^4.24.6",
"@thisbeyond/solid-dnd": "^0.7.3",
"heroicons": "^2.0.15",
"nostr-tools": "^1.3.2",
@@ -1346,6 +1347,30 @@
"tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1"
}
},
"node_modules/@tanstack/query-core": {
"version": "4.24.6",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.24.6.tgz",
"integrity": "sha512-Tfru6YTDTCpX7dKVwHp/sosw/dNjEdzrncduUjIkQxn7n7u+74HT2ZrGtwwrU6Orws4x7zp3FKRqBPWVVhpx9w==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/solid-query": {
"version": "4.24.6",
"resolved": "https://registry.npmjs.org/@tanstack/solid-query/-/solid-query-4.24.6.tgz",
"integrity": "sha512-ksUfW4Lwwl85kogQuP46oyqPGBqbSfNMRTu9Ey3FDPjfYzObW4j9opI3TjRoSkOapqVg5KOaobhzu8N2Wp0JBg==",
"dependencies": {
"@tanstack/query-core": "4.24.6"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"solid-js": "^1.5.7"
}
},
"node_modules/@thisbeyond/solid-dnd": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/@thisbeyond/solid-dnd/-/solid-dnd-0.7.3.tgz",
@@ -9312,6 +9337,19 @@
"mini-svg-data-uri": "^1.2.3"
}
},
"@tanstack/query-core": {
"version": "4.24.6",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.24.6.tgz",
"integrity": "sha512-Tfru6YTDTCpX7dKVwHp/sosw/dNjEdzrncduUjIkQxn7n7u+74HT2ZrGtwwrU6Orws4x7zp3FKRqBPWVVhpx9w=="
},
"@tanstack/solid-query": {
"version": "4.24.6",
"resolved": "https://registry.npmjs.org/@tanstack/solid-query/-/solid-query-4.24.6.tgz",
"integrity": "sha512-ksUfW4Lwwl85kogQuP46oyqPGBqbSfNMRTu9Ey3FDPjfYzObW4j9opI3TjRoSkOapqVg5KOaobhzu8N2Wp0JBg==",
"requires": {
"@tanstack/query-core": "4.24.6"
}
},
"@thisbeyond/solid-dnd": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/@thisbeyond/solid-dnd/-/solid-dnd-0.7.3.tgz",

View File

@@ -50,6 +50,7 @@
"@solidjs/meta": "^0.28.2",
"@solidjs/router": "^0.6.0",
"@tailwindcss/forms": "^0.5.3",
"@tanstack/solid-query": "^4.24.6",
"@thisbeyond/solid-dnd": "^0.7.3",
"heroicons": "^2.0.15",
"nostr-tools": "^1.3.2",

View File

@@ -1,16 +1,21 @@
import type { Component } from 'solid-js';
import { Routes, Route } from '@solidjs/router';
import { QueryClient, QueryClientProvider } from '@tanstack/solid-query';
import Home from '@/pages/Home';
import NotFound from '@/pages/NotFound';
import AccountRecovery from '@/pages/AccountRecovery';
const queryClient = new QueryClient();
const App: Component = () => (
<QueryClientProvider client={queryClient}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/recovery" element={<AccountRecovery />} />
<Route path="/*" element={<NotFound />} />
</Routes>
</QueryClientProvider>
);
export default App;

View File

@@ -0,0 +1,67 @@
import { createQuery } from '@tanstack/solid-query';
import { UseSubscriptionProps } from '@/clients/useSubscription';
import type { Event as NostrEvent } from 'nostr-tools/event';
import type { Filter } from 'nostr-tools/filter';
import type { SimplePool } from 'nostr-tools/pool';
import type { SubscriptionOptions } from 'nostr-tools/relay';
import usePool from './usePool';
type GetEventsArgs = {
pool: SimplePool;
relayUrls: string[];
filters: Filter[];
options?: SubscriptionOptions;
signal?: AbortSignal;
};
const getEvents = async ({
pool,
relayUrls,
filters,
options,
signal,
}: GetEventsArgs): Promise<NostrEvent[]> => {
const result: NostrEvent[] = [];
const sub = pool.sub(relayUrls, filters, options);
sub.on('event', (event: NostrEvent) => result.push(event));
return new Promise((resolve, reject) => {
sub.on('eose', () => {
sub.unsub();
resolve(result);
});
if (signal != null) {
signal.addEventListener('abort', () => {
sub.unsub();
reject(signal.reason);
});
}
});
};
/**
* This aims to fetch stored data, and doesn't support fetching streaming data continuously.
*
* This will be useful when you want to fetch profile or following list, reactions, and something like that.
*/
const useCachedEvents = (propsProvider: () => UseSubscriptionProps) => {
const pool = usePool();
return createQuery(
() => {
const { relayUrls, filters, options } = propsProvider();
return ['useCachedEvents', relayUrls, filters, options];
},
({ queryKey, signal }) => {
const [, relayUrls, filters, options] = queryKey;
return getEvents({ pool: pool(), relayUrls, filters, options, signal });
},
{
staleTime: 5 * 60 * 1000, // 5 minutes in ms
},
);
};
export default useCachedEvents;

35
src/clients/useConfig.ts Normal file
View File

@@ -0,0 +1,35 @@
import { Signal } from 'solid-js';
import {
createStorageWithSerializer,
createSignalWithStorage,
} from '@/hooks/createSignalWithStorage';
type Config = {
relayUrls: string[];
};
const InitialConfig: Config = {
relayUrls: [
'wss://relay-jp.nostr.wirednet.jp',
'wss://nostr.h3z.jp/',
'wss://relay.damus.io',
'wss://nos.lol',
'wss://brb.io',
'wss://relay.snort.social',
'wss://relay.current.fyi',
'wss://relay.nostr.wirednet.jp',
],
};
const serializer = (config: Config): string => JSON.stringify(config);
// TODO zod使う
const deserializer = (json: string): Config => JSON.parse(json) as Config;
const storage = createStorageWithSerializer(() => window.localStorage, serializer, deserializer);
const [config, setConfig] = createSignalWithStorage('rabbit_config', InitialConfig, storage);
const useConfig = (): Signal<Config> => {
return [config, setConfig];
};
export default useConfig;

28
src/clients/useEvent.ts Normal file
View File

@@ -0,0 +1,28 @@
import useCachedEvents from '@/clients/useCachedEvents';
type UseEventProps = {
relayUrls: string[];
eventId: string;
};
const useEvent = (propsProvider: () => UseEventProps) => {
const query = useCachedEvents(() => {
const { relayUrls, eventId } = propsProvider();
return {
relayUrls,
filters: [
{
ids: [eventId],
kinds: [1],
limit: 1,
},
],
};
});
const event = () => query.data?.[0];
return { event };
};
export default useEvent;

View File

@@ -0,0 +1,54 @@
import useCachedEvents from '@/clients/useCachedEvents';
type UseFollowingsProps = {
relayUrls: string[];
pubkey: string;
};
type Following = {
pubkey: string;
mainRelayUrl?: string;
petname?: string;
};
const useFollowings = (propsProvider: () => UseFollowingsProps) => {
const query = useCachedEvents(() => {
const { relayUrls, pubkey } = propsProvider();
return {
relayUrls,
filters: [
{
kinds: [3],
authors: [pubkey],
limit: 1,
},
],
};
});
const followings = () => {
const event = query?.data?.[0];
if (event == null) return [];
const result: Following[] = [];
event.tags.forEach((tag) => {
// TODO zodにする
const [tagName, followingPubkey, mainRelayUrl, petname] = tag;
if (!tag.every((e) => typeof e === 'string')) return;
if (tagName !== 'p') return;
const following: Following = { pubkey: followingPubkey, petname };
if (mainRelayUrl != null && mainRelayUrl.length > 0) {
following.mainRelayUrl = mainRelayUrl;
}
result.push(following);
});
return result;
};
return { followings };
};
export default useFollowings;

52
src/clients/useProfile.ts Normal file
View File

@@ -0,0 +1,52 @@
import useCachedEvents from '@/clients/useCachedEvents';
type UseProfileProps = {
relayUrls: string[];
pubkey: string;
};
// TODO zodにする
// deleted等の特殊なもの
type StandardProfile = {
name?: string;
about?: string;
picture?: string;
nip05?: string; // NIP-05
lud06?: string; // NIP-57
lud16?: string; // NIP-57
};
type NonStandardProfile = {
display_name?: string;
website?: string;
};
type Profile = StandardProfile & NonStandardProfile;
const useProfile = (propsProvider: () => UseProfileProps) => {
const query = useCachedEvents(() => {
const { relayUrls, pubkey } = propsProvider();
return {
relayUrls,
filters: [
{
kinds: [0],
authors: [pubkey],
limit: 1,
},
],
};
});
const profile = () => {
const maybeProfile = query.data?.[0];
if (maybeProfile == null) return null;
// TODO 大きすぎたりしないかどうか、JSONかどうかのチェック
return JSON.parse(maybeProfile.content) as Profile;
};
return { profile };
};
export default useProfile;

View File

@@ -1,30 +1,48 @@
import { createSignal, createEffect } from 'solid-js';
import type { Event as NostrEvent } from 'nostr-tools/event';
import type { Filter } from 'nostr-tools/filter';
import type { SubscriptionOptions } from 'nostr-tools/relay';
import usePool from '@/clients/usePool';
type UseSubscriptionProps = {
export type UseSubscriptionProps = {
relayUrls: string[];
filters: Filter[];
options?: SubscriptionOptions;
};
const useSubscription = ({ relayUrls, filters, options }: UseSubscriptionProps) => {
const sortEvents = (events: NostrEvent[]) => events.sort((a, b) => b.created_at - a.created_at);
const useSubscription = (propsProvider: () => UseSubscriptionProps) => {
const pool = usePool();
const [events, setEvents] = createSignal<Event[]>([]);
const [events, setEvents] = createSignal<NostrEvent[]>([]);
createEffect(() => {
const sub = pool().sub(relayUrls, filters, options);
const tempEvents: Event[] = [];
const { relayUrls, filters, options } = propsProvider();
sub.on('event', (event: Event) => {
tempEvents.push(event);
const sub = pool().sub(relayUrls, filters, options);
let eose = false;
const storedEvents: NostrEvent[] = [];
sub.on('event', (event: NostrEvent) => {
if (!eose) {
storedEvents.push(event);
} else {
setEvents((prevEvents) => sortEvents([event, ...prevEvents]));
}
});
sub.on('eose', () => {
eose = true;
setEvents(sortEvents(storedEvents));
});
const intervalId = setInterval(() => {
const newEvents = tempEvents.splice(0, tempEvents.length);
setEvents((prevEvents) => [...newEvents, ...prevEvents]);
}, 500);
if (eose) {
clearInterval(intervalId);
return;
}
setEvents(sortEvents(storedEvents));
}, 100);
return () => {
sub.unsub();

View File

@@ -1,9 +1,4 @@
import type { Component } from 'solid-js';
type ColumnProps = {
width: 'wide' | 'medium' | 'narrow' | null | undefined;
children: JSX.Element;
};
import type { Component, JSX } from 'solid-js';
const widthToClass = {
widest: 'w-[500px]',
@@ -12,6 +7,12 @@ const widthToClass = {
narrow: 'w-[270px]',
} as const;
type ColumnProps = {
name: string;
width: keyof typeof widthToClass | null | undefined;
children: JSX.Element;
};
const Column: Component<ColumnProps> = (props) => {
const width = () => {
if (props.width == null) {
@@ -23,8 +24,8 @@ const Column: Component<ColumnProps> = (props) => {
return (
<div class={`h-full shrink-0 border-r ${width()}`}>
<div class="flex h-8 items-center border-b bg-white px-2">
<span class="column-icon">🏠</span>
<span class="column-name">Home</span>
{/* <span class="column-icon">🏠</span> */}
<span class="column-name">{props.name}</span>
</div>
<div class="h-full overflow-y-scroll pb-8">{props.children}</div>
</div>

View File

@@ -8,7 +8,7 @@ type SideBarProps = {
};
const SideBar: Component<SideBarProps> = (props) => {
const [formOpened, setFormOpened] = createSignal(true);
const [formOpened, setFormOpened] = createSignal(false);
return (
<div class="flex shrink-0 flex-row border-r bg-sidebar-bg">
@@ -22,8 +22,8 @@ const SideBar: Component<SideBarProps> = (props) => {
<button class="h-9 w-9 rounded-full border border-primary p-2 text-2xl font-bold text-primary">
<MagnifyingGlass />
</button>
<div>column 1</div>
<div>column 2</div>
{/* <div>column 1</div> */}
{/* <div>column 2</div> */}
</div>
<Show when={formOpened()}>{() => props.postForm()}</Show>
</div>

View File

@@ -1,31 +1,69 @@
import { createMemo, Show, For } from 'solid-js';
import type { Component } from 'solid-js';
import type { Event as NostrEvent } from 'nostr-tools/event';
import useProfile from '@/clients/useProfile';
import useConfig from '@/clients/useConfig';
import TextNoteContentDisplay from '@/components/textNote/TextNoteContentDisplay';
import GeneralUserMentionDisplay from './textNote/GeneralUserMentionDisplay';
export type TextNoteProps = {
content: string;
createdAt: Date;
event: NostrEvent;
};
const TextNote: Component<TextNoteProps> = (props) => {
const [config] = useConfig();
const { profile: author } = useProfile(() => ({
relayUrls: config().relayUrls,
pubkey: props.event.pubkey,
}));
const replyingToPubKeys = createMemo(() =>
props.event.tags.filter((tag) => tag[0] === 'p').map((e) => e[1]),
);
// TODO 日付をいい感じにフォーマットする関数を作る
const createdAt = () => new Date(props.event.created_at * 1000).toLocaleTimeString();
return (
<div class="textnote flex w-full flex-row gap-1 overflow-hidden border-b p-1">
<div class="author-icon shrink-0">
<div class="author-icon max-w-10 max-h-10 shrink-0">
<Show when={author()?.picture} fallback={<div class="h-10 w-10" />}>
<img
src="https://i.gyazo.com/883119a7763e594d30c5706a62969d52.jpg"
alt="author icon"
src={author()?.picture}
alt="icon"
// TODO autofit
class="w-10 rounded"
class="h-10 w-10 rounded"
/>
</Show>
</div>
<div class="min-w-0 flex-auto">
<div class="flex justify-between gap-1 text-xs">
<div class="author flex min-w-0">
{/* TODO link to profile */}
<div class="author-name font-bold">Author</div>
<div class="author-username truncate pl-1">@aauthorauthorauthorauthorauthoruthor</div>
<div class="author flex min-w-0 truncate">
{/* TODO link to author */}
<Show when={author()?.display_name}>
<div class="author-name pr-1 font-bold">{author()?.display_name}</div>
</Show>
<div class="author-username truncate">
<Show when={author()?.name} fallback={props.event.pubkey}>
@{author()?.name}
</Show>
</div>
<div class="created-at shrink-0">{props.createdAt.toLocaleTimeString()}</div>
</div>
<div class="content whitespace-pre-wrap break-all">{props.content}</div>
<div class="created-at shrink-0">{createdAt()}</div>
</div>
<Show when={replyingToPubKeys().length > 0}>
<div class="text-xs">
{'Replying to '}
<For each={replyingToPubKeys()}>
{(pubkey: string) => (
<span class="pr-1 text-blue-500 underline">
<GeneralUserMentionDisplay pubkey={pubkey} />
</span>
)}
</For>
</div>
</Show>
<div class="content whitespace-pre-wrap break-all">
<TextNoteContentDisplay event={props.event} />
</div>
</div>
</div>
);

View File

@@ -0,0 +1,24 @@
import type { MentionedUser } from '@/core/parseTextNote';
import useProfile from '@/clients/useProfile';
import useConfig from '@/clients/useConfig';
import { Show } from 'solid-js';
export type GeneralUserMentionDisplayProps = {
pubkey: string;
};
const GeneralUserMentionDisplay = (props: GeneralUserMentionDisplayProps) => {
const [config] = useConfig();
const { profile } = useProfile(() => ({
relayUrls: config().relayUrls,
pubkey: props.pubkey,
}));
return (
<Show when={profile() != null} fallback={`@${props.pubkey}`}>
@{profile()?.display_name ?? props.pubkey}
</Show>
);
};
export default GeneralUserMentionDisplay;

View File

@@ -0,0 +1,11 @@
import type { MentionedEvent } from '@/core/parseTextNote';
export type MentionedEventDisplayProps = {
mentionedEvent: MentionedEvent;
};
const MentionedEventDisplay = (props: MentionedEventDisplayProps) => {
return <span class="text-blue-500 underline">@{props.mentionedEvent.eventId}</span>;
};
export default MentionedEventDisplay;

View File

@@ -0,0 +1,16 @@
import type { MentionedUser } from '@/core/parseTextNote';
import GeneralUserMentionDisplay from '@/components/textNote/GeneralUserMentionDisplay';
export type MentionedUserDisplayProps = {
mentionedUser: MentionedUser;
};
const MentionedUserDisplay = (props: MentionedUserDisplayProps) => {
return (
<span class="text-blue-500 underline">
<GeneralUserMentionDisplay pubkey={props.mentionedUser.pubkey} />
</span>
);
};
export default MentionedUserDisplay;

View File

@@ -0,0 +1,11 @@
import type { PlainText } from '@/core/parseTextNote';
export type PlainTextDisplayProps = {
plainText: PlainText;
};
const PlainTextDisplay = (props: PlainTextDisplayProps) => {
return <span>{props.plainText.content}</span>;
};
export default PlainTextDisplay;

View File

@@ -0,0 +1,34 @@
import { For } from 'solid-js';
import parseTextNote, { type ParsedTextNoteNode } from '@/core/parseTextNote';
import type { Event as NostrEvent } from 'nostr-tools/event';
import PlainTextDisplay from '@/components/textNote/PlainTextDisplay';
import MentionedUserDisplay from '@/components/textNote/MentionedUserDisplay';
import MentionedEventDisplay from '@/components/textNote/MentionedEventDisplay';
export type TextNoteContentDisplayProps = {
event: NostrEvent;
};
export const TextNoteContentDisplay = (props: TextNoteContentDisplayProps) => {
return (
<For each={parseTextNote(props.event)}>
{(item: ParsedTextNoteNode) => {
if (item.type === 'PlainText') {
return <PlainTextDisplay plainText={item} />;
}
if (item.type === 'MentionedUser') {
return <MentionedUserDisplay mentionedUser={item} />;
}
if (item.type === 'MentionedEvent') {
return <MentionedEventDisplay mentionedEvent={item} />;
}
if (item.type === 'HashTag') {
return <span class="text-blue-500 underline ">{item.content}</span>;
}
return null;
}}
</For>
);
};
export default TextNoteContentDisplay;

95
src/core/parseTextNote.ts Normal file
View File

@@ -0,0 +1,95 @@
import type { Event as NostrEvent } from 'nostr-tools/event';
export type PlainText = {
type: 'PlainText';
content: string;
};
export type MentionedEvent = {
type: 'MentionedEvent';
tagIndex: number;
content: string;
eventId: string;
marker: string | null; // TODO 'reply' | 'root' | 'mention' | null;
};
export type MentionedUser = {
type: 'MentionedUser';
tagIndex: number;
content: string;
pubkey: string;
};
export type HashTag = {
type: 'HashTag';
content: string;
tagName: string;
};
export type ParsedTextNoteNode = PlainText | MentionedEvent | MentionedUser | HashTag;
export type ParsedTextNote = ParsedTextNoteNode[];
export const parseTextNote = (event: NostrEvent): ParsedTextNote => {
const matches = Array.from(
event.content.matchAll(/(?:#\[(?<idx>\d+)\]|#(?<hashtag>[^\[\]\(\)\s]+))/g),
);
let pos = 0;
const result: ParsedTextNote = [];
matches.forEach((match) => {
if (match.groups?.hashtag) {
const tagName = match.groups?.hashtag;
if (pos !== match.index) {
const content = event.content.slice(pos, match.index);
const plainText: PlainText = { type: 'PlainText', content };
result.push(plainText);
}
const hashtag: HashTag = {
type: 'HashTag',
content: match[0],
tagName,
};
result.push(hashtag);
} else if (match.groups?.idx) {
const tagIndex = parseInt(match.groups.idx, 10);
const tag = event.tags[tagIndex];
if (tag == null) return;
if (pos !== match.index) {
const content = event.content.slice(pos, match.index);
const plainText: PlainText = { type: 'PlainText', content };
result.push(plainText);
}
const tagName = tag[0];
if (tagName === 'p') {
const mentionedUser: MentionedUser = {
type: 'MentionedUser',
tagIndex,
content: match[0],
pubkey: tag[1],
};
result.push(mentionedUser);
} else if (tagName === 'e') {
const mentionedEvent: MentionedEvent = {
type: 'MentionedEvent',
tagIndex,
content: match[0],
eventId: tag[1],
marker: tag[2],
};
result.push(mentionedEvent);
}
}
pos = match.index + match[0].length;
});
if (pos !== event.content.length) {
const content = event.content.slice(pos);
const plainText: PlainText = { type: 'PlainText', content };
result.push(plainText);
}
return result;
};
export default parseTextNote;

View File

@@ -1,7 +1,7 @@
// const commands = ['openPostForm'] as const;
// type Commands = (typeof commands)[number];
import { createMemo, createEffect } from 'solid-js';
import { onMount, type JSX } from 'solid-js';
type Shortcut = { key: string; command: string };
@@ -39,7 +39,7 @@ const createShortcutsMap = (shortcuts: Shortcut[]) => {
const useShortcutKeys = ({ shortcuts = defaultShortcut, onShortcut }: UseShortcutKeysProps) => {
const shortcutsMap = createShortcutsMap(shortcuts);
createEffect(() => {
onMount(() => {
const handleKeydown: JSX.EventHandler<Window, KeyboardEvent> = (ev) => {
console.log(ev);
if (ev.type !== 'keydown') return;

View File

@@ -6,8 +6,10 @@ import NotePostForm from '@/components/NotePostForm';
import SideBar from '@/components/SideBar';
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 };
@@ -21,61 +23,57 @@ const publish = async (pool, event) => {
*/
// const relays = ['ws://localhost:8008'];
//
// 'wss://relay.damus.io',
// 'wss://nos.lol',
// 'wss://brb.io',
// 'wss://relay.snort.social',
// 'wss://relay.current.fyi',
// 'wss://relay.nostr.wirednet.jp',
const relayUrls = ['wss://relay-jp.nostr.wirednet.jp', 'wss://nostr.h3z.jp/'];
const pubkey = 'npub1jcsr6e38dcepf65nkmrc54mu8jd8y70eael9rv308wxpwep6sxwqgsscyc';
const pubkeyHex = '96203d66276e3214ea93b6c78a577c3c9a7279f9ee7e51b22f3b8c17643a819c';
const Home: Component = () => {
useShortcutKeys({
onShortcut: (s) => console.log(s),
});
const { publishTextNote } = useCommands();
const { events } = useSubscription({
relayUrls,
const Home: Component = () => {
const [config] = useConfig();
const commands = useCommands();
const { followings } = useFollowings(() => ({
relayUrls: config().relayUrls,
pubkey: pubkeyHex,
}));
const { events: myPosts } = useSubscription(() => ({
relayUrls: config().relayUrls,
filters: [
{
kinds: [1],
authors: [pubkeyHex],
limit: 100,
since: Math.floor(Date.now() / 1000) - 48 * 60 * 60,
},
],
});
}));
const { events: followingsPosts } = useSubscription(() => ({
relayUrls: config().relayUrls,
filters: [
{
kinds: [1],
authors: followings()?.map((f) => f.pubkey) ?? [pubkeyHex],
limit: 100,
since: Math.floor(Date.now() / 1000) - 12 * 60 * 60,
},
],
}));
const handlePost = ({ content }) => {
publishTextNote({ relayUrls, pubkey: pubkeyHex, content });
commands.publishTextNote({ relayUrls: config().relayUrls, pubkey: pubkeyHex, content });
};
return (
<div class="flex h-screen w-screen flex-row overflow-hidden">
<SideBar postForm={() => <NotePostForm onPost={handlePost} />} />
<div class="flex flex-row overflow-x-scroll">
<Column width="widest">
<For each={events()}>
{(ev) => <TextNote content={ev.content} createdAt={new Date(ev.created_at * 1000)} />}
</For>
<div class="flex flex-row overflow-y-hidden overflow-x-scroll">
<Column name="ホーム" width="widest">
<For each={followingsPosts()}>{(ev) => <TextNote event={ev} />}</For>
</Column>
<Column width="medium">
<For each={events()}>
{(ev) => <TextNote content={ev.content} createdAt={new Date(ev.created_at * 1000)} />}
</For>
</Column>
<Column width="narrow">
<For each={events()}>
{(ev) => <TextNote content={ev.content} createdAt={new Date(ev.created_at * 1000)} />}
</For>
</Column>
<Column width="narrow">
<For each={events()}>
{(ev) => <TextNote content={ev.content} createdAt={new Date(ev.created_at * 1000)} />}
</For>
<Column name="自分の投稿" width="medium">
<For each={myPosts()}>{(ev) => <TextNote event={ev} />}</For>
</Column>
</div>
</div>