From 57969c2c0936df1cf76c7e31e427808c50b0bb27 Mon Sep 17 00:00:00 2001 From: Shusui MOYATANI Date: Tue, 21 Feb 2023 20:39:37 +0900 Subject: [PATCH] implement parseTextNote --- package-lock.json | 38 ++++++++ package.json | 1 + src/App.tsx | 15 ++- src/clients/useCachedEvents.ts | 67 +++++++++++++ src/clients/useConfig.ts | 35 +++++++ src/clients/useEvent.ts | 28 ++++++ src/clients/useFollowings.ts | 54 +++++++++++ src/clients/useProfile.ts | 52 ++++++++++ src/clients/useSubscription.ts | 38 ++++++-- src/components/Column.tsx | 17 ++-- src/components/SideBar.tsx | 6 +- src/components/TextNote.tsx | 68 ++++++++++--- .../textNote/GeneralUserMentionDisplay.tsx | 24 +++++ .../textNote/MentionedEventDisplay.tsx | 11 +++ .../textNote/MentionedUserDisplay.tsx | 16 ++++ src/components/textNote/PlainTextDisplay.tsx | 11 +++ .../textNote/TextNoteContentDisplay.tsx | 34 +++++++ src/core/parseTextNote.ts | 95 +++++++++++++++++++ src/hooks/useShortcutKeys.ts | 4 +- src/pages/Home.tsx | 70 +++++++------- 20 files changed, 605 insertions(+), 79 deletions(-) create mode 100644 src/clients/useCachedEvents.ts create mode 100644 src/clients/useConfig.ts create mode 100644 src/clients/useEvent.ts create mode 100644 src/clients/useFollowings.ts create mode 100644 src/clients/useProfile.ts create mode 100644 src/components/textNote/GeneralUserMentionDisplay.tsx create mode 100644 src/components/textNote/MentionedEventDisplay.tsx create mode 100644 src/components/textNote/MentionedUserDisplay.tsx create mode 100644 src/components/textNote/PlainTextDisplay.tsx create mode 100644 src/components/textNote/TextNoteContentDisplay.tsx create mode 100644 src/core/parseTextNote.ts diff --git a/package-lock.json b/package-lock.json index 98dfa02..8cfb5b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 870fa33..b5f0f6a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/App.tsx b/src/App.tsx index 25dfef8..c8a0c31 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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 = () => ( - - } /> - } /> - } /> - + + + } /> + } /> + } /> + + ); export default App; diff --git a/src/clients/useCachedEvents.ts b/src/clients/useCachedEvents.ts new file mode 100644 index 0000000..a07d3d1 --- /dev/null +++ b/src/clients/useCachedEvents.ts @@ -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 => { + 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; diff --git a/src/clients/useConfig.ts b/src/clients/useConfig.ts new file mode 100644 index 0000000..7603a81 --- /dev/null +++ b/src/clients/useConfig.ts @@ -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 => { + return [config, setConfig]; +}; + +export default useConfig; diff --git a/src/clients/useEvent.ts b/src/clients/useEvent.ts new file mode 100644 index 0000000..36722b7 --- /dev/null +++ b/src/clients/useEvent.ts @@ -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; diff --git a/src/clients/useFollowings.ts b/src/clients/useFollowings.ts new file mode 100644 index 0000000..45cda79 --- /dev/null +++ b/src/clients/useFollowings.ts @@ -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; diff --git a/src/clients/useProfile.ts b/src/clients/useProfile.ts new file mode 100644 index 0000000..de7a8b6 --- /dev/null +++ b/src/clients/useProfile.ts @@ -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; diff --git a/src/clients/useSubscription.ts b/src/clients/useSubscription.ts index c204049..6c1e69b 100644 --- a/src/clients/useSubscription.ts +++ b/src/clients/useSubscription.ts @@ -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([]); + const [events, setEvents] = createSignal([]); 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(); diff --git a/src/components/Column.tsx b/src/components/Column.tsx index a6c66b0..2aaf7e6 100644 --- a/src/components/Column.tsx +++ b/src/components/Column.tsx @@ -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 = (props) => { const width = () => { if (props.width == null) { @@ -23,8 +24,8 @@ const Column: Component = (props) => { return (
- 🏠 - Home + {/* 🏠 */} + {props.name}
{props.children}
diff --git a/src/components/SideBar.tsx b/src/components/SideBar.tsx index d3847d9..f74d3e2 100644 --- a/src/components/SideBar.tsx +++ b/src/components/SideBar.tsx @@ -8,7 +8,7 @@ type SideBarProps = { }; const SideBar: Component = (props) => { - const [formOpened, setFormOpened] = createSignal(true); + const [formOpened, setFormOpened] = createSignal(false); return (
@@ -22,8 +22,8 @@ const SideBar: Component = (props) => { -
column 1
-
column 2
+ {/*
column 1
*/} + {/*
column 2
*/}
{() => props.postForm()} diff --git a/src/components/TextNote.tsx b/src/components/TextNote.tsx index a982b02..ade6309 100644 --- a/src/components/TextNote.tsx +++ b/src/components/TextNote.tsx @@ -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 = (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 (
-
- author icon +
+ }> + icon +
-
- {/* TODO link to profile */} -
Author
-
@aauthorauthorauthorauthorauthoruthor
+
+ {/* TODO link to author */} + +
{author()?.display_name}
+
+
+ + @{author()?.name} + +
-
{props.createdAt.toLocaleTimeString()}
+
{createdAt()}
+
+ 0}> +
+ {'Replying to '} + + {(pubkey: string) => ( + + + + )} + +
+
+
+
-
{props.content}
); diff --git a/src/components/textNote/GeneralUserMentionDisplay.tsx b/src/components/textNote/GeneralUserMentionDisplay.tsx new file mode 100644 index 0000000..140fdc2 --- /dev/null +++ b/src/components/textNote/GeneralUserMentionDisplay.tsx @@ -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 ( + + @{profile()?.display_name ?? props.pubkey} + + ); +}; + +export default GeneralUserMentionDisplay; diff --git a/src/components/textNote/MentionedEventDisplay.tsx b/src/components/textNote/MentionedEventDisplay.tsx new file mode 100644 index 0000000..176769a --- /dev/null +++ b/src/components/textNote/MentionedEventDisplay.tsx @@ -0,0 +1,11 @@ +import type { MentionedEvent } from '@/core/parseTextNote'; + +export type MentionedEventDisplayProps = { + mentionedEvent: MentionedEvent; +}; + +const MentionedEventDisplay = (props: MentionedEventDisplayProps) => { + return @{props.mentionedEvent.eventId}; +}; + +export default MentionedEventDisplay; diff --git a/src/components/textNote/MentionedUserDisplay.tsx b/src/components/textNote/MentionedUserDisplay.tsx new file mode 100644 index 0000000..3d82f66 --- /dev/null +++ b/src/components/textNote/MentionedUserDisplay.tsx @@ -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 ( + + + + ); +}; + +export default MentionedUserDisplay; diff --git a/src/components/textNote/PlainTextDisplay.tsx b/src/components/textNote/PlainTextDisplay.tsx new file mode 100644 index 0000000..a43b3f9 --- /dev/null +++ b/src/components/textNote/PlainTextDisplay.tsx @@ -0,0 +1,11 @@ +import type { PlainText } from '@/core/parseTextNote'; + +export type PlainTextDisplayProps = { + plainText: PlainText; +}; + +const PlainTextDisplay = (props: PlainTextDisplayProps) => { + return {props.plainText.content}; +}; + +export default PlainTextDisplay; diff --git a/src/components/textNote/TextNoteContentDisplay.tsx b/src/components/textNote/TextNoteContentDisplay.tsx new file mode 100644 index 0000000..8b13fcc --- /dev/null +++ b/src/components/textNote/TextNoteContentDisplay.tsx @@ -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 ( + + {(item: ParsedTextNoteNode) => { + if (item.type === 'PlainText') { + return ; + } + if (item.type === 'MentionedUser') { + return ; + } + if (item.type === 'MentionedEvent') { + return ; + } + if (item.type === 'HashTag') { + return {item.content}; + } + return null; + }} + + ); +}; + +export default TextNoteContentDisplay; diff --git a/src/core/parseTextNote.ts b/src/core/parseTextNote.ts new file mode 100644 index 0000000..5e56f2f --- /dev/null +++ b/src/core/parseTextNote.ts @@ -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(/(?:#\[(?\d+)\]|#(?[^\[\]\(\)\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; diff --git a/src/hooks/useShortcutKeys.ts b/src/hooks/useShortcutKeys.ts index daeda7a..b42a94b 100644 --- a/src/hooks/useShortcutKeys.ts +++ b/src/hooks/useShortcutKeys.ts @@ -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 = (ev) => { console.log(ev); if (ev.type !== 'keydown') return; diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index f075e8f..b9b3b65 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -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(); +useShortcutKeys({ + onShortcut: (s) => console.log(s), +}); - 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 (
} /> -
- - - {(ev) => } - +
+ + {(ev) => } - - - {(ev) => } - - - - - {(ev) => } - - - - - {(ev) => } - + + {(ev) => }