use swr for note fetcher

This commit is contained in:
Ren Amamiya
2023-04-27 08:57:41 +07:00
parent 38c6b2c76d
commit 8cadb7467f
7 changed files with 204 additions and 312 deletions

View File

@@ -1,4 +1,3 @@
import { AccountContext } from '@components/accountProvider';
import { NoteMetadata } from '@components/note/metadata';
import { RelayContext } from '@components/relaysProvider';
import { UserExtend } from '@components/user/extend';
@@ -6,126 +5,90 @@ import { UserExtend } from '@components/user/extend';
import { READONLY_RELAYS } from '@stores/constants';
import { contentParser } from '@utils/parser';
import { createNote, getNoteByID } from '@utils/storage';
import { getParentID } from '@utils/transform';
import { memo, useCallback, useContext, useEffect, useState } from 'react';
import { memo, useContext } from 'react';
import useSWRSubscription from 'swr/subscription';
export const NoteParent = memo(function NoteParent({ id }: { id: string }) {
const pool: any = useContext(RelayContext);
const activeAccount: any = useContext(AccountContext);
const [event, setEvent] = useState(null);
const content = event ? contentParser(event.content, event.tags) : '';
const fetchEvent = useCallback(async () => {
const unsubscribe = pool.subscribe(
[
{
ids: [id],
kinds: [1],
const { data, error } = useSWRSubscription(
id
? [
{
ids: [id],
kinds: [1],
},
]
: null,
(key, { next }) => {
const unsubscribe = pool.subscribe(
key,
READONLY_RELAYS,
(event: any) => {
next(null, event);
},
],
READONLY_RELAYS,
(event: any) => {
// update state
setEvent(event);
// insert to database
const parentID = getParentID(event.tags, event.id);
// insert event to local database
createNote(
event.id,
activeAccount.id,
event.pubkey,
event.kind,
event.tags,
event.content,
event.created_at,
parentID
);
},
undefined,
undefined,
{
unsubscribeOnEose: true,
}
);
return () => {
unsubscribe();
};
}, [activeAccount.id, id, pool]);
const checkNoteIsSaved = useCallback(async () => {
getNoteByID(id)
.then((res) => {
if (res) {
setEvent(res);
} else {
fetchEvent();
undefined,
undefined,
{
unsubscribeOnEose: true,
}
})
.catch(console.error);
}, [fetchEvent, id]);
);
useEffect(() => {
let ignore = false;
if (!ignore) {
checkNoteIsSaved();
return () => {
unsubscribe();
};
}
);
return () => {
ignore = true;
};
}, [checkNoteIsSaved]);
if (event) {
return (
<div className="relative pb-5">
<div className="absolute left-[21px] top-0 h-full w-0.5 bg-gradient-to-t from-zinc-800 to-zinc-600"></div>
<div className="relative z-10 flex flex-col">
<UserExtend pubkey={event.pubkey} time={event.created_at} />
<div className="mt-1 pl-[52px]">
<div className="whitespace-pre-line break-words text-[15px] leading-tight text-zinc-100">{content}</div>
return (
<div className="relative pb-5">
{error && <div>failed to load</div>}
{!data ? (
<div className="animated-pulse">
<div className="flex items-start gap-2">
<div className="relative h-11 w-11 shrink overflow-hidden rounded-md bg-zinc-700" />
<div className="flex w-full flex-1 items-start justify-between">
<div className="flex w-full items-center justify-between">
<div className="flex items-center gap-2 text-sm">
<div className="h-4 w-16 rounded bg-zinc-700" />
<span className="text-zinc-500">·</span>
<div className="h-4 w-12 rounded bg-zinc-700" />
</div>
</div>
</div>
</div>
<div onClick={(e) => e.stopPropagation()} className="mt-5 pl-[52px]">
<NoteMetadata
eventID={event.event_id}
eventPubkey={event.pubkey}
eventContent={event.content}
eventTime={event.created_at}
/>
</div>
</div>
</div>
);
} else {
return (
<div className="relative z-10 flex h-min animate-pulse select-text flex-col pb-5">
<div className="flex items-start gap-2">
<div className="relative h-11 w-11 shrink overflow-hidden rounded-md bg-zinc-700" />
<div className="flex w-full flex-1 items-start justify-between">
<div className="flex w-full items-center justify-between">
<div className="flex items-center gap-2 text-sm">
<div className="h-4 w-16 rounded bg-zinc-700" />
<span className="text-zinc-500">·</span>
<div className="-mt-5 pl-[52px]">
<div className="flex flex-col gap-6">
<div className="h-16 w-full rounded bg-zinc-700" />
<div className="flex items-center gap-8">
<div className="h-4 w-12 rounded bg-zinc-700" />
<div className="h-4 w-12 rounded bg-zinc-700" />
</div>
<div className="h-3 w-3 rounded-full bg-zinc-700" />
</div>
</div>
</div>
<div className="-mt-5 pl-[52px]">
<div className="flex flex-col gap-6">
<div className="h-16 w-full rounded bg-zinc-700" />
<div className="flex items-center gap-8">
<div className="h-4 w-12 rounded bg-zinc-700" />
<div className="h-4 w-12 rounded bg-zinc-700" />
) : (
<>
<div className="absolute left-[21px] top-0 h-full w-0.5 bg-gradient-to-t from-zinc-800 to-zinc-600"></div>
<div className="relative z-10 flex flex-col">
<UserExtend pubkey={data.pubkey} time={data.created_at} />
<div className="mt-1 pl-[52px]">
<div className="whitespace-pre-line break-words text-[15px] leading-tight text-zinc-100">
{contentParser(data.content, data.tags)}
</div>
</div>
<div onClick={(e) => e.stopPropagation()} className="mt-5 pl-[52px]">
<NoteMetadata
eventID={data.event_id}
eventPubkey={data.pubkey}
eventContent={data.content}
eventTime={data.created_at}
/>
</div>
</div>
</div>
</div>
);
}
</>
)}
</div>
);
});

View File

@@ -1,95 +1,60 @@
import { AccountContext } from '@components/accountProvider';
import { RelayContext } from '@components/relaysProvider';
import { UserExtend } from '@components/user/extend';
import { READONLY_RELAYS } from '@stores/constants';
import { contentParser } from '@utils/parser';
import { createNote, getNoteByID } from '@utils/storage';
import { getParentID } from '@utils/transform';
import { memo, useCallback, useContext, useEffect, useState } from 'react';
import { memo, useContext } from 'react';
import useSWRSubscription from 'swr/subscription';
export const NoteQuote = memo(function NoteQuote({ id }: { id: string }) {
const pool: any = useContext(RelayContext);
const activeAccount: any = useContext(AccountContext);
const [event, setEvent] = useState(null);
const content = event ? contentParser(event.content, event.tags) : '';
const fetchEvent = useCallback(async () => {
const unsubscribe = pool.subscribe(
[
{
ids: [id],
kinds: [1],
const { data, error } = useSWRSubscription(
id
? [
{
ids: [id],
kinds: [1],
},
]
: null,
(key, { next }) => {
const unsubscribe = pool.subscribe(
key,
READONLY_RELAYS,
(event: any) => {
next(null, event);
},
],
READONLY_RELAYS,
(event: any) => {
// update state
setEvent(event);
// insert to database
const parentID = getParentID(event.tags, event.id);
createNote(
event.id,
activeAccount.id,
event.pubkey,
event.kind,
event.tags,
event.content,
event.created_at,
parentID
);
},
undefined,
undefined,
{
unsubscribeOnEose: true,
}
);
return () => {
unsubscribe();
};
}, [activeAccount.id, id, pool]);
const checkNoteIsSaved = useCallback(async () => {
getNoteByID(id)
.then((res) => {
if (res) {
setEvent(res);
} else {
fetchEvent();
undefined,
undefined,
{
unsubscribeOnEose: true,
}
})
.catch(console.error);
}, [fetchEvent, id]);
);
useEffect(() => {
let ignore = false;
if (!ignore) {
checkNoteIsSaved();
return () => {
unsubscribe();
};
}
);
return () => {
ignore = true;
};
}, [checkNoteIsSaved]);
if (event) {
return (
<div className="relative mb-2 mt-3 rounded-lg border border-zinc-700 bg-zinc-800 p-2 py-3">
return (
<div className="relative mb-2 mt-3 rounded-lg border border-zinc-700 bg-zinc-800 p-2 py-3">
{error && <div>failed to load</div>}
{!data ? (
<div className="h-6 w-full animate-pulse select-text flex-col rounded bg-zinc-800"></div>
) : (
<div className="relative z-10 flex flex-col">
<UserExtend pubkey={event.pubkey} time={event.created_at} />
<UserExtend pubkey={data.pubkey} time={data.created_at} />
<div className="mt-1 pl-[52px]">
<div className="whitespace-pre-line break-words text-[15px] leading-tight text-zinc-100">{content}</div>
<div className="whitespace-pre-line break-words text-[15px] leading-tight text-zinc-100">
{contentParser(data.content, data.tags)}
</div>
</div>
</div>
</div>
);
} else {
return <div className="mt-2 h-6 animate-pulse select-text flex-col rounded bg-zinc-700 pb-5"></div>;
}
)}
</div>
);
});

View File

@@ -1,24 +1,12 @@
import { RootNote } from '@components/note/rootNote';
import { UserQuoteRepost } from '@components/user/quoteRepost';
import destr from 'destr';
import { getQuoteID } from '@utils/transform';
import { memo } from 'react';
export const NoteQuoteRepost = memo(function NoteQuoteRepost({ event }: { event: any }) {
const rootNote = () => {
let note = null;
if (event.content) {
const content = destr(event.content);
if (content) {
note = <RootNote event={content} />;
}
} else {
note = <RootNote event={event.tags[0][1]} />;
}
return note;
};
const rootID = getQuoteID(event.tags);
return (
<div className="relative z-10 flex h-min min-h-min w-full select-text flex-col border-b border-zinc-800 px-3 py-5 hover:bg-black/20">
@@ -26,7 +14,7 @@ export const NoteQuoteRepost = memo(function NoteQuoteRepost({ event }: { event:
<div className="absolute left-[21px] top-0 h-full w-0.5 bg-gradient-to-t from-zinc-800 to-zinc-600"></div>
<UserQuoteRepost pubkey={event.pubkey} time={event.created_at} />
</div>
{rootNote()}
<RootNote id={rootID} />
</div>
);
});

View File

@@ -6,42 +6,37 @@ import { READONLY_RELAYS } from '@stores/constants';
import { contentParser } from '@utils/parser';
import { memo, useCallback, useContext, useEffect, useState } from 'react';
import { memo, useContext } from 'react';
import useSWRSubscription from 'swr/subscription';
import { navigate } from 'vite-plugin-ssr/client/router';
export const RootNote = memo(function RootNote({ event }: { event: any }) {
export const RootNote = memo(function RootNote({ id }: { id: string }) {
const pool: any = useContext(RelayContext);
const [data, setData] = useState(null);
const [content, setContent] = useState('');
const openUserPage = (e) => {
e.stopPropagation();
navigate(`/user?pubkey=${event.pubkey}`);
};
const openThread = (e) => {
const selection = window.getSelection();
if (selection.toString().length === 0) {
navigate(`/newsfeed/note?id=${event.parent_id}`);
navigate(`/newsfeed/note?id=${id}`);
} else {
e.stopPropagation();
}
};
const fetchEvent = useCallback(
async (id: string) => {
const unsubscribe = pool.subscribe(
[
const { data, error } = useSWRSubscription(
id
? [
{
ids: [id],
kinds: [1],
},
],
]
: null,
(key, { next }) => {
const unsubscribe = pool.subscribe(
key,
READONLY_RELAYS,
(event: any) => {
setData(event);
setContent(contentParser(event.content, event.tags));
next(null, event);
},
undefined,
undefined,
@@ -53,72 +48,32 @@ export const RootNote = memo(function RootNote({ event }: { event: any }) {
return () => {
unsubscribe();
};
},
[pool]
}
);
useEffect(() => {
let ignore = false;
if (!ignore) {
if (typeof event === 'object') {
setData(event);
setContent(contentParser(event.content, event.tags));
} else {
fetchEvent(event);
}
}
return () => {
ignore = true;
};
}, [event, fetchEvent]);
if (data) {
return (
<div onClick={(e) => openThread(e)} className="relative z-10 flex flex-col">
<div onClick={(e) => openUserPage(e)}>
return (
<>
{error && <div>failed to load</div>}
{!data ? (
<div className="h-6 w-full animate-pulse select-text flex-col rounded bg-zinc-800"></div>
) : (
<div onClick={(e) => openThread(e)} className="relative z-10 flex flex-col">
<UserExtend pubkey={data.pubkey} time={data.created_at} />
</div>
<div className="mt-1 pl-[52px]">
<div className="whitespace-pre-line break-words text-[15px] leading-tight text-zinc-100">{content}</div>
</div>
<div onClick={(e) => e.stopPropagation()} className="mt-5 pl-[52px]">
<NoteMetadata
eventID={data.id}
eventPubkey={data.pubkey}
eventContent={data.content}
eventTime={data.created_at}
/>
</div>
</div>
);
} else {
return (
<div className="relative z-10 flex h-min animate-pulse select-text flex-col pb-5">
<div className="flex items-start gap-2">
<div className="relative h-11 w-11 shrink overflow-hidden rounded-md bg-zinc-700" />
<div className="flex w-full flex-1 items-start justify-between">
<div className="flex w-full items-center justify-between">
<div className="flex items-center gap-2 text-sm">
<div className="h-4 w-16 rounded bg-zinc-700" />
<span className="text-zinc-500">·</span>
<div className="h-4 w-12 rounded bg-zinc-700" />
</div>
<div className="h-3 w-3 rounded-full bg-zinc-700" />
<div className="mt-1 pl-[52px]">
<div className="whitespace-pre-line break-words text-[15px] leading-tight text-zinc-100">
{contentParser(data.content, data.tags)}
</div>
</div>
</div>
<div className="-mt-5 pl-[52px]">
<div className="flex flex-col gap-6">
<div className="h-16 w-full rounded bg-zinc-700" />
<div className="flex items-center gap-8">
<div className="h-4 w-12 rounded bg-zinc-700" />
<div className="h-4 w-12 rounded bg-zinc-700" />
</div>
<div onClick={(e) => e.stopPropagation()} className="mt-5 pl-[52px]">
<NoteMetadata
eventID={data.id}
eventPubkey={data.pubkey}
eventContent={data.content}
eventTime={data.created_at}
/>
</div>
</div>
</div>
);
}
)}
</>
);
});

View File

@@ -1,6 +1,6 @@
import { RelayContext } from '@components/relaysProvider';
import { FULL_RELAYS } from '@stores/constants';
import { READONLY_RELAYS } from '@stores/constants';
import { dateToUnix, hoursAgo } from '@utils/getDate';
import {
@@ -16,17 +16,18 @@ import { getParentID } from '@utils/transform';
import LumeSymbol from '@assets/icons/Lume';
import { useCallback, useContext, useEffect, useRef } from 'react';
import { useContext, useEffect, useRef } from 'react';
import { navigate } from 'vite-plugin-ssr/client/router';
export function Page() {
const pool: any = useContext(RelayContext);
const now = useRef(new Date());
const timeout = useRef(null);
const fetchData = useCallback(
async (account: { id: number; pubkey: string; chats: string[] }, tags: any) => {
useEffect(() => {
let unsubscribe: () => void;
let timeout: any;
const fetchInitalData = async (account: { pubkey: string; id: number }, tags: string) => {
const lastLogin = await getLastLogin();
const notes = await countTotalNotes();
@@ -52,6 +53,7 @@ export function Page() {
since: since,
until: dateToUnix(now.current),
});
// kind 4 (chats) query
query.push({
kinds: [4],
@@ -59,6 +61,7 @@ export function Page() {
since: 0,
until: dateToUnix(now.current),
});
// kind 43, 43 (mute user, hide message) query
query.push({
authors: [account.pubkey],
@@ -66,10 +69,11 @@ export function Page() {
since: 0,
until: dateToUnix(now.current),
});
// subscribe relays
const unsubscribe = pool.subscribe(
unsubscribe = pool.subscribe(
query,
FULL_RELAYS,
READONLY_RELAYS,
(event: { kind: number; tags: string[]; id: string; pubkey: string; content: string; created_at: number }) => {
switch (event.kind) {
// short text note
@@ -123,44 +127,30 @@ export function Page() {
undefined,
() => {
updateLastLogin(dateToUnix(now.current));
timeout.current = setTimeout(() => {
timeout = setTimeout(() => {
navigate('/newsfeed/following', { overwriteLastHistoryEntry: true });
}, 5000);
},
{
unsubscribeOnEose: true,
logAllEvents: false,
}
);
};
return () => {
unsubscribe();
};
},
[pool]
);
useEffect(() => {
let ignore = false;
if (!ignore) {
getActiveAccount()
.then((res: any) => {
if (res) {
const account = res;
fetchData(account, account.follows);
} else {
navigate('/onboarding', { overwriteLastHistoryEntry: true });
}
})
.catch(console.error);
}
getActiveAccount()
.then((res: any) => {
if (res) {
fetchInitalData(res, res.follows);
} else {
navigate('/onboarding', { overwriteLastHistoryEntry: true });
}
})
.catch(console.error);
return () => {
ignore = true;
clearTimeout(timeout.current);
if (unsubscribe) {
unsubscribe();
}
clearTimeout(timeout);
};
}, [fetchData]);
}, []);
return (
<div className="h-screen w-screen bg-zinc-50 text-zinc-900 dark:bg-black dark:text-white">

11
src/utils/broadcast.tsx Normal file
View File

@@ -0,0 +1,11 @@
import { WRITEONLY_RELAYS } from '@stores/constants';
import { getEventHash, signEvent } from 'nostr-tools';
export const broadcast = ({ pool, data, privkey }: { pool: any; data: any; privkey: string }) => {
const event = data;
event.id = getEventHash(event);
event.sig = signEvent(event, privkey);
pool.publish(event, WRITEONLY_RELAYS);
};

View File

@@ -50,6 +50,26 @@ export const getParentID = (arr: string[], fallback: string) => {
return parentID;
};
// get parent id from event tags
export const getQuoteID = (arr: string[]) => {
const tags = destr(arr);
let quoteID = null;
if (tags.length > 0) {
if (tags[0][0] === 'e') {
quoteID = tags[0][1];
} else {
tags.forEach((tag) => {
if (tag[0] === 'e') {
quoteID = tag[1];
}
});
}
}
return quoteID;
};
// sort messages by timestamp
export const sortMessages = (arr: any) => {
arr.sort((a, b) => {