major overhaul (not finished)

This commit is contained in:
Ren Amamiya 2023-03-22 09:22:34 +07:00
parent 3c3ee2fc88
commit 49b7ebf32b
17 changed files with 150 additions and 68 deletions

View File

@ -20,6 +20,8 @@
"@radix-ui/react-tabs": "^1.0.3", "@radix-ui/react-tabs": "^1.0.3",
"@rehooks/local-storage": "^2.4.4", "@rehooks/local-storage": "^2.4.4",
"@supabase/supabase-js": "^2.12.0", "@supabase/supabase-js": "^2.12.0",
"@tanstack/query-core": "^4.27.0",
"@tanstack/react-virtual": "3.0.0-beta.54",
"@tauri-apps/api": "^1.2.0", "@tauri-apps/api": "^1.2.0",
"@uiw/react-markdown-preview": "^4.1.10", "@uiw/react-markdown-preview": "^4.1.10",
"@uiw/react-md-editor": "^3.20.5", "@uiw/react-md-editor": "^3.20.5",
@ -28,6 +30,7 @@
"dayjs": "^1.11.7", "dayjs": "^1.11.7",
"framer-motion": "^9.1.7", "framer-motion": "^9.1.7",
"jotai": "^2.0.3", "jotai": "^2.0.3",
"jotai-tanstack-query": "^0.6.0",
"moment": "^2.29.4", "moment": "^2.29.4",
"next": "^13.2.4", "next": "^13.2.4",
"next-remove-imports": "^1.0.10", "next-remove-imports": "^1.0.10",

View File

@ -10,6 +10,8 @@ specifiers:
'@rehooks/local-storage': ^2.4.4 '@rehooks/local-storage': ^2.4.4
'@supabase/supabase-js': ^2.12.0 '@supabase/supabase-js': ^2.12.0
'@tailwindcss/typography': ^0.5.9 '@tailwindcss/typography': ^0.5.9
'@tanstack/query-core': ^4.27.0
'@tanstack/react-virtual': 3.0.0-beta.54
'@tauri-apps/api': ^1.2.0 '@tauri-apps/api': ^1.2.0
'@tauri-apps/cli': ^1.2.3 '@tauri-apps/cli': ^1.2.3
'@trivago/prettier-plugin-sort-imports': ^4.1.1 '@trivago/prettier-plugin-sort-imports': ^4.1.1
@ -33,6 +35,7 @@ specifiers:
framer-motion: ^9.1.7 framer-motion: ^9.1.7
husky: ^8.0.3 husky: ^8.0.3
jotai: ^2.0.3 jotai: ^2.0.3
jotai-tanstack-query: ^0.6.0
lint-staged: ^13.2.0 lint-staged: ^13.2.0
moment: ^2.29.4 moment: ^2.29.4
next: ^13.2.4 next: ^13.2.4
@ -65,6 +68,8 @@ dependencies:
'@radix-ui/react-tabs': 1.0.3_biqbaboplfbrettd7655fr4n2y '@radix-ui/react-tabs': 1.0.3_biqbaboplfbrettd7655fr4n2y
'@rehooks/local-storage': 2.4.4_react@18.2.0 '@rehooks/local-storage': 2.4.4_react@18.2.0
'@supabase/supabase-js': 2.12.0 '@supabase/supabase-js': 2.12.0
'@tanstack/query-core': 4.27.0
'@tanstack/react-virtual': 3.0.0-beta.54_react@18.2.0
'@tauri-apps/api': 1.2.0 '@tauri-apps/api': 1.2.0
'@uiw/react-markdown-preview': 4.1.10_zula6vjvt3wdocc4mwcxqa6nzi '@uiw/react-markdown-preview': 4.1.10_zula6vjvt3wdocc4mwcxqa6nzi
'@uiw/react-md-editor': 3.20.5_zula6vjvt3wdocc4mwcxqa6nzi '@uiw/react-md-editor': 3.20.5_zula6vjvt3wdocc4mwcxqa6nzi
@ -73,6 +78,7 @@ dependencies:
dayjs: 1.11.7 dayjs: 1.11.7
framer-motion: 9.1.7_biqbaboplfbrettd7655fr4n2y framer-motion: 9.1.7_biqbaboplfbrettd7655fr4n2y
jotai: 2.0.3_react@18.2.0 jotai: 2.0.3_react@18.2.0
jotai-tanstack-query: 0.6.0_jqgumvl52k2nlr5n23qdneaa6y
moment: 2.29.4 moment: 2.29.4
next: 13.2.4_biqbaboplfbrettd7655fr4n2y next: 13.2.4_biqbaboplfbrettd7655fr4n2y
next-remove-imports: 1.0.10 next-remove-imports: 1.0.10
@ -1302,6 +1308,26 @@ packages:
tailwindcss: 3.2.7_postcss@8.4.21 tailwindcss: 3.2.7_postcss@8.4.21
dev: true dev: true
/@tanstack/query-core/4.27.0:
resolution:
{ integrity: sha512-sm+QncWaPmM73IPwFlmWSKPqjdTXZeFf/7aEmWh00z7yl2FjqophPt0dE1EHW9P1giMC5rMviv7OUbSDmWzXXA== }
dev: false
/@tanstack/react-virtual/3.0.0-beta.54_react@18.2.0:
resolution:
{ integrity: sha512-D1mDMf4UPbrtHRZZriCly5bXTBMhylslm4dhcHqTtDJ6brQcgGmk8YD9JdWBGWfGSWPKoh2x1H3e7eh+hgPXtQ== }
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
dependencies:
'@tanstack/virtual-core': 3.0.0-beta.54
react: 18.2.0
dev: false
/@tanstack/virtual-core/3.0.0-beta.54:
resolution:
{ integrity: sha512-jtkwqdP2rY2iCCDVAFuaNBH3fiEi29aTn2RhtIoky8DTTiCdc48plpHHreLwmv1PICJ4AJUUESaq3xa8fZH8+g== }
dev: false
/@tauri-apps/api/1.2.0: /@tauri-apps/api/1.2.0:
resolution: resolution:
{ integrity: sha512-lsI54KI6HGf7VImuf/T9pnoejfgkNoXveP14pVV7XarrQ46rOejIVJLFqHI9sRReJMGdh2YuCoI3cc/yCWCsrw== } { integrity: sha512-lsI54KI6HGf7VImuf/T9pnoejfgkNoXveP14pVV7XarrQ46rOejIVJLFqHI9sRReJMGdh2YuCoI3cc/yCWCsrw== }
@ -3836,6 +3862,17 @@ packages:
{ integrity: sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw== } { integrity: sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw== }
dev: true dev: true
/jotai-tanstack-query/0.6.0_jqgumvl52k2nlr5n23qdneaa6y:
resolution:
{ integrity: sha512-87oD6MnjrgfLWeCJXB/dQt4xyCmyFYZeG9jw4Y2lIprtwLKS5s/vCEjNP5fnYG2nuPBkopiRECTEqtIysvHSxg== }
peerDependencies:
'@tanstack/query-core': '*'
jotai: '>=1.11.0'
dependencies:
'@tanstack/query-core': 4.27.0
jotai: 2.0.3_react@18.2.0
dev: false
/jotai/2.0.3_react@18.2.0: /jotai/2.0.3_react@18.2.0:
resolution: resolution:
{ integrity: sha512-MMjhSPAL3RoeZD9WbObufRT2quThEAEknHHridf2ma8Ml7ZVQmUiHk0ssdbR3F0h3kcwhYqSGJ59OjhPge7RRg== } { integrity: sha512-MMjhSPAL3RoeZD9WbObufRT2quThEAEknHHridf2ma8Ml7ZVQmUiHk0ssdbR3F0h3kcwhYqSGJ59OjhPge7RRg== }

View File

@ -48,7 +48,7 @@ CREATE TABLE
npub TEXT NOT NULL, npub TEXT NOT NULL,
nsec TEXT NOT NULL, nsec TEXT NOT NULL,
is_active INTEGER NOT NULL DEFAULT 0, is_active INTEGER NOT NULL DEFAULT 0,
metadata JSON metadata TEXT
); );
-- create follows -- create follows
@ -61,7 +61,7 @@ CREATE TABLE
pubkey TEXT NOT NULL, pubkey TEXT NOT NULL,
account TEXT NOT NULL, account TEXT NOT NULL,
kind INTEGER NOT NULL DEFAULT 0, kind INTEGER NOT NULL DEFAULT 0,
metadata JSON metadata TEXT
); );
-- create index for pubkey in follows -- create index for pubkey in follows
@ -71,7 +71,7 @@ CREATE UNIQUE INDEX index_pubkey_on_follows ON follows (pubkey);
CREATE TABLE CREATE TABLE
cache_profiles ( cache_profiles (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
metadata JSON, metadata TEXT,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
); );
@ -84,5 +84,8 @@ CREATE TABLE
created_at TEXT, created_at TEXT,
kind INTEGER NOT NULL DEFAULT 1, kind INTEGER NOT NULL DEFAULT 1,
tags TEXT NOT NULL, tags TEXT NOT NULL,
content TEXT NOT NULL content TEXT NOT NULL,
is_circle INTEGER NOT NULL DEFAULT 0,
is_root INTEGER NOT NULL DEFAULT 0,
is_reply INTEGER NOT NULL DEFAULT 0
); );

View File

@ -48,17 +48,25 @@ export default function DatabaseProvider({ children }: { children: React.ReactNo
return; return;
}, []); }, []);
const clearCacheNote = useCallback(async () => {
const result: any = await db.select('SELECT COUNT(*) AS "total" FROM cache_notes');
if (result[0].total >= 1000) {
await db.execute('DELETE FROM cache_notes');
}
}, []);
useEffect(() => { useEffect(() => {
getRelays().catch(console.error); getRelays().catch(console.error);
getAccount() getAccount()
.then((res) => { .then((res) => {
if (res) { if (res) {
getFollows(res.id).catch(console.error); getFollows(res.id).catch(console.error);
clearCacheNote().catch(console.error);
} }
setDone(true); setDone(true);
}) })
.catch(console.error); .catch(console.error);
}, [getAccount, getFollows, getRelays]); }, [getAccount, getFollows, clearCacheNote, getRelays]);
if (!done) { if (!done) {
return <></>; return <></>;

View File

@ -1,7 +1,7 @@
import { DatabaseContext } from '@components/contexts/database'; import { DatabaseContext } from '@components/contexts/database';
import { RelayContext } from '@components/contexts/relay'; import { RelayContext } from '@components/contexts/relay';
import { atomHasNewerNote } from '@stores/note'; import { hasNewerNoteAtom } from '@stores/note';
import { dateToUnix, hoursAgo } from '@utils/getDate'; import { dateToUnix, hoursAgo } from '@utils/getDate';
@ -16,7 +16,7 @@ export const NoteConnector = memo(function NoteConnector() {
const [follows]: any = useLocalStorage('follows'); const [follows]: any = useLocalStorage('follows');
const [relays]: any = useLocalStorage('relays'); const [relays]: any = useLocalStorage('relays');
const setHasNewerNote = useSetAtom(atomHasNewerNote); const setHasNewerNote = useSetAtom(hasNewerNoteAtom);
const [isOnline, setIsOnline] = useState(navigator.onLine); const [isOnline, setIsOnline] = useState(navigator.onLine);
const now = useRef(new Date()); const now = useRef(new Date());
@ -25,7 +25,7 @@ export const NoteConnector = memo(function NoteConnector() {
// insert to local database // insert to local database
await db.execute( await db.execute(
'INSERT OR IGNORE INTO cache_notes (id, pubkey, created_at, kind, content, tags) VALUES (?, ?, ?, ?, ?, ?);', 'INSERT OR IGNORE INTO cache_notes (id, pubkey, created_at, kind, content, tags) VALUES (?, ?, ?, ?, ?, ?);',
[event.id, event.pubkey, event.created_at, event.kind, event.content, JSON.stringify(event.tags)] [event.id, event.pubkey, event.created_at, event.kind, event.content, String(event.tags)]
); );
}, },
[db] [db]

View File

@ -9,11 +9,9 @@ import reactStringReplace from 'react-string-replace';
export const Content = memo(function Content({ data }: { data: any }) { export const Content = memo(function Content({ data }: { data: any }) {
const content = useMemo(() => { const content = useMemo(() => {
let parsedContent; let parsedContent;
let tags;
// get data tags // get data tags
if (data.tags.length > 1) { const tags = String(data.tags).replaceAll("'", '"');
tags = JSON.parse(data.tags); const parseTags = JSON.parse(tags);
}
// remove all image urls // remove all image urls
parsedContent = data.content.replace(/(https?:\/\/.*\.(jpg|jpeg|gif|png|webp|mp4|webm)((\?.*)$|$))/gim, ''); parsedContent = data.content.replace(/(https?:\/\/.*\.(jpg|jpeg|gif|png|webp|mp4|webm)((\?.*)$|$))/gim, '');
// handle urls // handle urls
@ -29,10 +27,10 @@ export const Content = memo(function Content({ data }: { data: any }) {
</span> </span>
)); ));
// handle mentions // handle mentions
if (tags.length > 0) { if (parseTags.length > 0) {
parsedContent = reactStringReplace(parsedContent, /\#\[(\d+)\]/gm, (match, i) => { parsedContent = reactStringReplace(parsedContent, /\#\[(\d+)\]/gm, (match, i) => {
if (tags[match][0] === 'p') { if (parseTags[match][0] === 'p') {
return <UserMention key={match + i} pubkey={tags[match][1]} />; return <UserMention key={match + i} pubkey={parseTags[match][1]} />;
} else { } else {
// #TODO: handle mention other note // #TODO: handle mention other note
// console.log(tags[match]); // console.log(tags[match]);
@ -48,7 +46,7 @@ export const Content = memo(function Content({ data }: { data: any }) {
<UserExtend pubkey={data.pubkey} time={data.created_at} /> <UserExtend pubkey={data.pubkey} time={data.created_at} />
<div className="-mt-5 pl-[52px]"> <div className="-mt-5 pl-[52px]">
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex flex-col"> <div className="flex flex-col gap-2">
<div className="prose prose-zinc max-w-none break-words text-[15px] leading-tight dark:prose-invert prose-headings:mt-3 prose-headings:mb-2 prose-p:m-0 prose-p:text-[15px] prose-p:leading-tight prose-a:font-normal prose-a:text-fuchsia-500 prose-a:no-underline prose-ul:mt-2 prose-li:my-1"> <div className="prose prose-zinc max-w-none break-words text-[15px] leading-tight dark:prose-invert prose-headings:mt-3 prose-headings:mb-2 prose-p:m-0 prose-p:text-[15px] prose-p:leading-tight prose-a:font-normal prose-a:text-fuchsia-500 prose-a:no-underline prose-ul:mt-2 prose-li:my-1">
{content} {content}
</div> </div>

View File

@ -7,7 +7,7 @@ import LikedIcon from '@assets/icons/liked';
import { useLocalStorage } from '@rehooks/local-storage'; import { useLocalStorage } from '@rehooks/local-storage';
import { getEventHash, signEvent } from 'nostr-tools'; import { getEventHash, signEvent } from 'nostr-tools';
import { memo, useContext, useState } from 'react'; import { memo, useContext, useEffect, useState } from 'react';
export const LikesCounter = memo(function LikesCounter({ export const LikesCounter = memo(function LikesCounter({
count, count,
@ -24,7 +24,7 @@ export const LikesCounter = memo(function LikesCounter({
const [currentUser]: any = useLocalStorage('current-user'); const [currentUser]: any = useLocalStorage('current-user');
const [isReact, setIsReact] = useState(false); const [isReact, setIsReact] = useState(false);
const [like, setLike] = useState(count); const [like, setLike] = useState(0);
const handleLike = (e: any) => { const handleLike = (e: any) => {
e.stopPropagation(); e.stopPropagation();
@ -49,6 +49,10 @@ export const LikesCounter = memo(function LikesCounter({
setLike(like + 1); setLike(like + 1);
}; };
useEffect(() => {
setLike(count);
}, [count]);
return ( return (
<button onClick={(e) => handleLike(e)} className="group flex w-16 items-center gap-1 text-sm text-zinc-500"> <button onClick={(e) => handleLike(e)} className="group flex w-16 items-center gap-1 text-sm text-zinc-500">
<div className="rounded-md p-1 group-hover:bg-zinc-800"> <div className="rounded-md p-1 group-hover:bg-zinc-800">

View File

@ -4,16 +4,17 @@ import { RootNote } from '@components/note/root';
import { memo, useMemo } from 'react'; import { memo, useMemo } from 'react';
export const Note = memo(function Note({ event }: { event: any }) { export const Note = memo(function Note({ event }: { event: any }) {
const tags = JSON.parse(event.tags); const tags = event.tags.replaceAll("'", '"');
const parseTags = JSON.parse(tags);
const fetchRootEvent = useMemo(() => { const fetchRootEvent = useMemo(() => {
if (tags.length > 0) { if (parseTags.length > 0) {
if (tags[0][0] === 'e') { if (parseTags[0][0] === 'e' || parseTags[0][2] === 'root') {
return <RootNote id={tags[0][1]} />; return <RootNote id={parseTags[0][1]} />;
} else { } else {
tags.every((tag) => { parseTags.every((tag) => {
if (tag[2] === 'root') { if (tag[0] === 'e' && tag[2] === 'root') {
return <RootNote id={tags[1]} />; return <RootNote id={parseTags[1]} />;
} }
return <></>; return <></>;
}); });
@ -21,7 +22,7 @@ export const Note = memo(function Note({ event }: { event: any }) {
} else { } else {
return <></>; return <></>;
} }
}, [tags]); }, [parseTags]);
return ( return (
<div className="relative z-10 flex h-min min-h-min w-full cursor-pointer select-text flex-col border-b border-zinc-800 py-5 px-3 hover:bg-black/20"> <div className="relative z-10 flex h-min min-h-min w-full cursor-pointer select-text flex-col border-b border-zinc-800 py-5 px-3 hover:bg-black/20">

View File

@ -4,7 +4,7 @@ export const Placeholder = memo(function Placeholder() {
return ( return (
<div className="relative z-10 flex h-min animate-pulse select-text flex-col py-5 px-3"> <div className="relative z-10 flex h-min animate-pulse select-text flex-col py-5 px-3">
<div className="flex items-start gap-2"> <div className="flex items-start gap-2">
<div className="relative h-11 w-11 shrink overflow-hidden rounded-full bg-zinc-700" /> <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 flex-1 items-start justify-between">
<div className="flex w-full items-center justify-between"> <div className="flex w-full items-center justify-between">
<div className="flex items-center gap-2 text-sm"> <div className="flex items-center gap-2 text-sm">

View File

@ -16,8 +16,8 @@ export const RootNote = memo(function RootNote({ id }: { id: string }) {
async (event: any) => { async (event: any) => {
// insert to local database // insert to local database
await db.execute( await db.execute(
'INSERT OR IGNORE INTO cache_notes (id, pubkey, created_at, kind, content, tags) VALUES (?, ?, ?, ?, ?, ?);', 'INSERT OR IGNORE INTO cache_notes (id, pubkey, created_at, kind, content, tags, is_root) VALUES (?, ?, ?, ?, ?, ?, ?);',
[event.id, event.pubkey, event.created_at, event.kind, event.content, JSON.stringify(event.tags)] [event.id, event.pubkey, event.created_at, event.kind, event.content, String(event.tags), 1]
); );
}, },
[db] [db]
@ -76,7 +76,7 @@ export const RootNote = memo(function RootNote({ id }: { id: string }) {
return ( return (
<div className="relative z-10 flex h-min animate-pulse select-text flex-col pb-5"> <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="flex items-start gap-2">
<div className="relative h-11 w-11 shrink overflow-hidden rounded-full bg-zinc-700" /> <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 flex-1 items-start justify-between">
<div className="flex w-full items-center justify-between"> <div className="flex w-full items-center justify-between">
<div className="flex items-center gap-2 text-sm"> <div className="flex items-center gap-2 text-sm">

View File

@ -4,12 +4,11 @@ import { ImageWithFallback } from '@components/imageWithFallback';
import { truncate } from '@utils/truncate'; import { truncate } from '@utils/truncate';
import { fetch } from '@tauri-apps/api/http'; import { fetch } from '@tauri-apps/api/http';
import Avatar from 'boring-avatars';
import { memo, useCallback, useContext, useEffect, useState } from 'react'; import { memo, useCallback, useContext, useEffect, useState } from 'react';
export const UserBase = memo(function UserBase({ pubkey }: { pubkey: string }) { export const UserBase = memo(function UserBase({ pubkey }: { pubkey: string }) {
const { db }: any = useContext(DatabaseContext); const { db }: any = useContext(DatabaseContext);
const [profile, setProfile] = useState({ picture: null, display_name: null, name: null }); const [profile, setProfile] = useState(null);
const fetchProfile = useCallback(async (id: string) => { const fetchProfile = useCallback(async (id: string) => {
const res = await fetch(`https://rbr.bio/${id}/metadata.json`, { const res = await fetch(`https://rbr.bio/${id}/metadata.json`, {
@ -38,22 +37,13 @@ export const UserBase = memo(function UserBase({ pubkey }: { pubkey: string }) {
return ( return (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="relative h-11 w-11 shrink overflow-hidden rounded-full border border-white/10"> <div className="relative h-11 w-11 shrink overflow-hidden rounded-full border border-white/10">
{profile.picture ? ( {profile?.picture && (
<ImageWithFallback src={profile.picture} alt={pubkey} fill={true} className="rounded-full object-cover" /> <ImageWithFallback src={profile.picture} alt={pubkey} fill={true} className="rounded-full object-cover" />
) : (
<Avatar
size={44}
name={pubkey}
variant="beam"
colors={['#FEE2E2', '#FEF3C7', '#F59E0B', '#EC4899', '#D946EF', '#8B5CF6']}
/>
)} )}
</div> </div>
<div className="flex w-full flex-1 flex-col items-start"> <div className="flex w-full flex-1 flex-col items-start">
<span className="font-medium leading-tight text-zinc-200"> <span className="font-medium leading-tight text-zinc-200">{profile?.display_name || profile?.name}</span>
{profile.display_name ? profile.display_name : truncate(pubkey, 16, ' .... ')} <span className="text-sm leading-tight text-zinc-400">{truncate(pubkey, 16, ' .... ')}</span>
</span>
<span className="text-sm leading-tight text-zinc-400">{profile.name}</span>
</div> </div>
</div> </div>
); );

View File

@ -1,8 +1,6 @@
import { DatabaseContext } from '@components/contexts/database'; import { DatabaseContext } from '@components/contexts/database';
import { ImageWithFallback } from '@components/imageWithFallback'; import { ImageWithFallback } from '@components/imageWithFallback';
import { truncate } from '@utils/truncate';
import { DotsHorizontalIcon } from '@radix-ui/react-icons'; import { DotsHorizontalIcon } from '@radix-ui/react-icons';
import { fetch } from '@tauri-apps/api/http'; import { fetch } from '@tauri-apps/api/http';
import Avatar from 'boring-avatars'; import Avatar from 'boring-avatars';
@ -14,7 +12,7 @@ dayjs.extend(relativeTime);
export const UserExtend = memo(function UserExtend({ pubkey, time }: { pubkey: string; time: any }) { export const UserExtend = memo(function UserExtend({ pubkey, time }: { pubkey: string; time: any }) {
const { db }: any = useContext(DatabaseContext); const { db }: any = useContext(DatabaseContext);
const [profile, setProfile] = useState({ picture: null, name: null, username: null }); const [profile, setProfile] = useState(null);
const fetchProfile = useCallback(async (id: string) => { const fetchProfile = useCallback(async (id: string) => {
const res = await fetch(`https://rbr.bio/${id}/metadata.json`, { const res = await fetch(`https://rbr.bio/${id}/metadata.json`, {
@ -31,10 +29,10 @@ export const UserExtend = memo(function UserExtend({ pubkey, time }: { pubkey: s
const insertCacheProfile = useCallback( const insertCacheProfile = useCallback(
async (event) => { async (event) => {
// insert to database
await db.execute('INSERT OR IGNORE INTO cache_profiles (id, metadata) VALUES (?, ?);', [pubkey, event.content]);
// update state // update state
setProfile(JSON.parse(event.content)); setProfile(JSON.parse(event.content));
// insert to database
await db.execute('INSERT OR IGNORE INTO cache_profiles (id, metadata) VALUES (?, ?);', [pubkey, event.content]);
}, },
[db, pubkey] [db, pubkey]
); );
@ -56,7 +54,7 @@ export const UserExtend = memo(function UserExtend({ pubkey, time }: { pubkey: s
return ( return (
<div className="flex items-start gap-2"> <div className="flex items-start gap-2">
<div className="relative h-11 w-11 shrink overflow-hidden rounded-md bg-zinc-900"> <div className="relative h-11 w-11 shrink overflow-hidden rounded-md bg-zinc-900">
{profile.picture ? ( {profile?.picture ? (
<ImageWithFallback <ImageWithFallback
src={profile.picture} src={profile.picture}
alt={pubkey} alt={pubkey}
@ -76,9 +74,7 @@ export const UserExtend = memo(function UserExtend({ pubkey, time }: { pubkey: s
<div className="flex w-full flex-1 items-start justify-between"> <div className="flex w-full flex-1 items-start justify-between">
<div className="flex w-full justify-between"> <div className="flex w-full justify-between">
<div className="flex items-baseline gap-2 text-sm"> <div className="flex items-baseline gap-2 text-sm">
<span className="font-bold leading-tight"> <span className="font-bold leading-tight">{profile?.name}</span>
{profile.name ? profile.name : truncate(pubkey, 16, ' .... ')}
</span>
<span className="leading-tight text-zinc-500">·</span> <span className="leading-tight text-zinc-500">·</span>
<span className="text-zinc-500">{dayjs().to(dayjs.unix(time))}</span> <span className="text-zinc-500">{dayjs().to(dayjs.unix(time))}</span>
</div> </div>

View File

@ -46,5 +46,5 @@ export const UserMention = memo(function UserMention({ pubkey }: { pubkey: strin
.catch(console.error); .catch(console.error);
}, [fetchProfile, getCacheProfile, insertCacheProfile, pubkey]); }, [fetchProfile, getCacheProfile, insertCacheProfile, pubkey]);
return <span className="text-fuchsia-500">@{profile ? profile.name : truncate(pubkey, 16, ' .... ')}</span>; return <span className="text-fuchsia-500">@{profile?.name || truncate(pubkey, 16, ' .... ')}</span>;
}); });

View File

@ -9,7 +9,7 @@ import { memo, useCallback, useContext, useEffect, useState } from 'react';
export const UserMini = memo(function UserMini({ pubkey }: { pubkey: string }) { export const UserMini = memo(function UserMini({ pubkey }: { pubkey: string }) {
const { db }: any = useContext(DatabaseContext); const { db }: any = useContext(DatabaseContext);
const [profile, setProfile] = useState({ picture: null, name: null }); const [profile, setProfile] = useState(null);
const fetchProfile = useCallback(async (id: string) => { const fetchProfile = useCallback(async (id: string) => {
const res = await fetch(`https://rbr.bio/${id}/metadata.json`, { const res = await fetch(`https://rbr.bio/${id}/metadata.json`, {
@ -26,10 +26,10 @@ export const UserMini = memo(function UserMini({ pubkey }: { pubkey: string }) {
const insertCacheProfile = useCallback( const insertCacheProfile = useCallback(
async (event) => { async (event) => {
// insert to database
await db.execute('INSERT OR IGNORE INTO cache_profiles (id, metadata) VALUES (?, ?);', [pubkey, event.content]);
// update state // update state
setProfile(JSON.parse(event.content)); setProfile(JSON.parse(event.content));
// insert to database
await db.execute('INSERT OR IGNORE INTO cache_profiles (id, metadata) VALUES (?, ?);', [pubkey, event.content]);
}, },
[db, pubkey] [db, pubkey]
); );
@ -51,7 +51,7 @@ export const UserMini = memo(function UserMini({ pubkey }: { pubkey: string }) {
return ( return (
<div className="flex cursor-pointer items-center gap-2.5 rounded-md px-2.5 py-1.5 text-sm font-medium hover:bg-zinc-900"> <div className="flex cursor-pointer items-center gap-2.5 rounded-md px-2.5 py-1.5 text-sm font-medium hover:bg-zinc-900">
<div className="relative h-5 w-5 shrink-0 overflow-hidden rounded"> <div className="relative h-5 w-5 shrink-0 overflow-hidden rounded">
{profile.picture ? ( {profile?.picture ? (
<ImageWithFallback src={profile.picture} alt={pubkey} fill={true} className="rounded object-cover" /> <ImageWithFallback src={profile.picture} alt={pubkey} fill={true} className="rounded object-cover" />
) : ( ) : (
<Avatar <Avatar
@ -64,9 +64,7 @@ export const UserMini = memo(function UserMini({ pubkey }: { pubkey: string }) {
)} )}
</div> </div>
<div className="inline-flex w-full flex-1 flex-col overflow-hidden"> <div className="inline-flex w-full flex-1 flex-col overflow-hidden">
<p className="truncate leading-tight text-zinc-300"> <p className="truncate leading-tight text-zinc-300">{profile?.name || truncate(pubkey, 16, ' .... ')}</p>
{profile.name ? profile.name : truncate(pubkey, 16, ' .... ')}
</p>
</div> </div>
</div> </div>
); );

View File

@ -1,12 +1,45 @@
import BaseLayout from '@layouts/base'; import BaseLayout from '@layouts/base';
import WithSidebarLayout from '@layouts/withSidebar'; import WithSidebarLayout from '@layouts/withSidebar';
import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal } from 'react'; import { Note } from '@components/note';
import { initialNotesAtom } from '@stores/note';
import { useVirtualizer } from '@tanstack/react-virtual';
import { useAtom } from 'jotai';
import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal, useMemo, useRef } from 'react';
export default function Page() { export default function Page() {
const [data]: any = useAtom(initialNotesAtom);
const parentRef = useRef(null);
const count = useMemo(() => data.length, [data]);
const virtualizer = useVirtualizer({
count,
getScrollElement: () => parentRef.current,
getItemKey: (index: number) => data[index].id,
estimateSize: () => 500,
overscan: 5,
});
const items = virtualizer.getVirtualItems();
return ( return (
<div className="h-full w-full"> <div className="h-full w-full">
<p>Circle Newsfeed</p> {items.length > 0 && (
<div ref={parentRef} className="scrollbar-hide h-full w-full overflow-y-auto" style={{ contain: 'strict' }}>
<div className={`relative w-full h-${virtualizer.getTotalSize()}px`}>
<div className="absolute top-0 left-0 w-full" style={{ transform: `translateY(${items[0].start}px)` }}>
{items.map((virtualRow) => (
<div key={virtualRow.key} data-index={virtualRow.index} ref={virtualizer.measureElement}>
<Note event={data[virtualRow.index]} />
</div>
))}
</div>
</div>
</div>
)}
</div> </div>
); );
} }

View File

@ -6,13 +6,13 @@ import { Note } from '@components/note';
import FormBasic from '@components/note/form/basic'; import FormBasic from '@components/note/form/basic';
import { Placeholder } from '@components/note/placeholder'; import { Placeholder } from '@components/note/placeholder';
import { atomHasNewerNote } from '@stores/note'; import { hasNewerNoteAtom } from '@stores/note';
import { dateToUnix } from '@utils/getDate'; import { dateToUnix } from '@utils/getDate';
import { useAtom } from 'jotai'; import { useAtom } from 'jotai';
import { Key, useCallback, useState } from 'react'; import { Key, useCallback, useState } from 'react';
import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal, useContext, useMemo, useRef } from 'react'; import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal, useContext, useEffect, useRef } from 'react';
import { Virtuoso } from 'react-virtuoso'; import { Virtuoso } from 'react-virtuoso';
export default function Page() { export default function Page() {
@ -20,7 +20,7 @@ export default function Page() {
const [data, setData] = useState([]); const [data, setData] = useState([]);
const [reload, setReload] = useState(false); const [reload, setReload] = useState(false);
const [hasNewerNote, setHasNewerNote] = useAtom(atomHasNewerNote); const [hasNewerNote, setHasNewerNote] = useAtom(hasNewerNoteAtom);
const now = useRef(new Date()); const now = useRef(new Date());
const limit = useRef(30); const limit = useRef(30);
@ -70,7 +70,7 @@ export default function Page() {
[data] [data]
); );
useMemo(() => { useEffect(() => {
const getData = async () => { const getData = async () => {
const result = await db.select(`SELECT * FROM cache_notes ORDER BY created_at DESC LIMIT ${limit.current}`); const result = await db.select(`SELECT * FROM cache_notes ORDER BY created_at DESC LIMIT ${limit.current}`);
if (result.length > 0) { if (result.length > 0) {

View File

@ -1,3 +1,14 @@
import { atom } from 'jotai'; import { atom } from 'jotai';
import { atomsWithQuery } from 'jotai-tanstack-query';
import Database from 'tauri-plugin-sql-api';
export const atomHasNewerNote = atom(false); // usecase: notify user that connector has receive newer note
export const hasNewerNoteAtom = atom(false);
// usecase: get all notes (limit 1000)
export const [initialNotesAtom] = atomsWithQuery(() => ({
queryFn: async () => {
const db = await Database.load('sqlite:lume.db');
const result = await db.select(`SELECT * FROM cache_notes ORDER BY created_at DESC LIMIT 1000`);
return result;
},
}));