-
-

+
+
}>
+
?.picture})
+
-
- {/* 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 (