fix mention in composer and improve error handling

This commit is contained in:
Ren Amamiya 2023-09-01 15:57:31 +07:00
parent cc315a190a
commit e6d35bc635
10 changed files with 156 additions and 124 deletions

View File

@ -1,4 +1,5 @@
import { invoke } from '@tauri-apps/api/tauri'; import { invoke } from '@tauri-apps/api/tauri';
import { message } from '@tauri-apps/plugin-dialog';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useNDK } from '@libs/ndk/provider'; import { useNDK } from '@libs/ndk/provider';
@ -40,7 +41,10 @@ export function SplashScreen() {
} catch (e) { } catch (e) {
setIsLoading(false); setIsLoading(false);
setErrorMessage(e); setErrorMessage(e);
console.log('prefetch failed, error: ', e); await message(`Something wrong: ${e}`, {
title: 'Lume',
type: 'error',
});
} }
}; };

View File

@ -1,5 +1,6 @@
// inspire by: https://github.com/nostr-dev-kit/ndk-react/ // inspire by: https://github.com/nostr-dev-kit/ndk-react/
import NDK from '@nostr-dev-kit/ndk'; import NDK from '@nostr-dev-kit/ndk';
import { message } from '@tauri-apps/plugin-dialog';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import TauriAdapter from '@libs/ndk/cache'; import TauriAdapter from '@libs/ndk/cache';
@ -28,7 +29,7 @@ export const NDKInstance = () => {
}); });
const controller = new AbortController(); const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort('timeout'), 5000); const timeoutId = setTimeout(() => controller.abort('timeout'), 10000);
const requests = urls.map((url) => const requests = urls.map((url) =>
fetch(url, { fetch(url, {
@ -58,14 +59,16 @@ export const NDKInstance = () => {
// return all validate relays // return all validate relays
return verifiedRelays; return verifiedRelays;
} catch (e) { } catch (e) {
console.error('ndk instance error: ', e); console.error('verify relay failed with error: ', e);
} }
} }
async function initNDK() { async function initNDK() {
let explicitRelayUrls: string[]; let explicitRelayUrls: string[];
const explicitRelayUrlsFromDB = await db.getExplicitRelayUrls(); const explicitRelayUrlsFromDB = await db.getExplicitRelayUrls();
console.log('relays in db: ', explicitRelayUrlsFromDB); console.log('relays in db: ', explicitRelayUrlsFromDB);
console.log('ndk cache adapter: ', cacheAdapter);
if (explicitRelayUrlsFromDB) { if (explicitRelayUrlsFromDB) {
explicitRelayUrls = await verifyRelays(explicitRelayUrlsFromDB); explicitRelayUrls = await verifyRelays(explicitRelayUrlsFromDB);
@ -73,13 +76,21 @@ export const NDKInstance = () => {
explicitRelayUrls = await verifyRelays(FULL_RELAYS); explicitRelayUrls = await verifyRelays(FULL_RELAYS);
} }
console.log('ndk cache adapter: ', cacheAdapter); if (explicitRelayUrls.length < 1) {
const instance = new NDK({ explicitRelayUrls, cacheAdapter }); await message('Something is wrong. No relays have been found.', {
title: 'Lume',
type: 'error',
});
}
const instance = new NDK({ explicitRelayUrls, cacheAdapter });
try { try {
await instance.connect(10000); await instance.connect(10000);
} catch (error) { } catch (error) {
throw new Error('NDK instance init failed: ', error); await message(`NDK instance init failed: ${error}`, {
title: 'Lume',
type: 'error',
});
} }
setNDK(instance); setNDK(instance);

View File

@ -1,3 +1,4 @@
import { message } from '@tauri-apps/plugin-dialog';
import Database from '@tauri-apps/plugin-sql'; import Database from '@tauri-apps/plugin-sql';
import { PropsWithChildren, createContext, useContext, useEffect, useState } from 'react'; import { PropsWithChildren, createContext, useContext, useEffect, useState } from 'react';
@ -15,11 +16,18 @@ const StorageProvider = ({ children }: PropsWithChildren<object>) => {
const [db, setDB] = useState<LumeStorage>(undefined); const [db, setDB] = useState<LumeStorage>(undefined);
async function initLumeStorage() { async function initLumeStorage() {
const sqlite = await Database.load('sqlite:lume.db'); try {
const lumeStorage = new LumeStorage(sqlite); const sqlite = await Database.load('sqlite:lume.db');
const lumeStorage = new LumeStorage(sqlite);
if (!lumeStorage.account) await lumeStorage.getActiveAccount(); if (!lumeStorage.account) await lumeStorage.getActiveAccount();
setDB(lumeStorage); setDB(lumeStorage);
} catch (e) {
await message(`Cannot initialize database: ${e}`, {
title: 'Lume',
type: 'error',
});
}
} }
useEffect(() => { useEffect(() => {

View File

@ -1,75 +1,84 @@
import { NDKUserProfile } from '@nostr-dev-kit/ndk'; import { NDKUserProfile } from '@nostr-dev-kit/ndk';
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react'; import { type SuggestionProps } from '@tiptap/suggestion';
import {
ForwardedRef,
forwardRef,
useEffect,
useImperativeHandle,
useState,
} from 'react';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
import { MentionItem } from '@shared/composer'; import { MentionItem } from '@shared/composer';
export const MentionList = forwardRef((props: any, ref: any) => { export const MentionList = forwardRef(
const [selectedIndex, setSelectedIndex] = useState(0); (props: SuggestionProps, ref: ForwardedRef<unknown>) => {
const [selectedIndex, setSelectedIndex] = useState(0);
const selectItem = (index) => { const selectItem = (index) => {
const item = props.items[index]; const item = props.items[index];
if (item) { if (item) {
props.command({ id: item }); props.command({ id: item });
}
};
const upHandler = () => {
setSelectedIndex((selectedIndex + props.items.length - 1) % props.items.length);
};
const downHandler = () => {
setSelectedIndex((selectedIndex + 1) % props.items.length);
};
const enterHandler = () => {
selectItem(selectedIndex);
};
useEffect(() => setSelectedIndex(0), [props.items]);
useImperativeHandle(ref, () => ({
onKeyDown: ({ event }) => {
if (event.key === 'ArrowUp') {
upHandler();
return true;
} }
};
if (event.key === 'ArrowDown') { const upHandler = () => {
downHandler(); setSelectedIndex((selectedIndex + props.items.length - 1) % props.items.length);
return true; };
}
if (event.key === 'Enter') { const downHandler = () => {
enterHandler(); setSelectedIndex((selectedIndex + 1) % props.items.length);
return true; };
}
return false; const enterHandler = () => {
}, selectItem(selectedIndex);
})); };
return ( useEffect(() => setSelectedIndex(0), [props.items]);
<div className="flex w-[250px] flex-col rounded-xl border-t border-zinc-700/50 bg-zinc-800 px-3 py-3">
{props.items.length ? ( useImperativeHandle(ref, () => ({
props.items.map((item: NDKUserProfile, index: number) => ( onKeyDown: ({ event }) => {
<button if (event.key === 'ArrowUp') {
className={twMerge( upHandler();
'h-11 w-full rounded-lg px-2 text-start text-sm font-medium hover:bg-zinc-700', return true;
`${index === selectedIndex ? 'is-selected' : ''}` }
)}
key={index} if (event.key === 'ArrowDown') {
onClick={() => selectItem(index)} downHandler();
> return true;
<MentionItem profile={item} /> }
</button>
)) if (event.key === 'Enter') {
) : ( enterHandler();
<div>No result</div> return true;
)} }
</div>
); return false;
}); },
}));
return (
<div className="flex w-[250px] flex-col rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
{props.items.length ? (
props.items.map((item: NDKUserProfile, index: number) => (
<button
className={twMerge(
'h-11 w-full rounded-lg px-2 text-start text-sm font-medium hover:bg-white/10',
`${index === selectedIndex ? 'is-selected' : ''}`
)}
key={index}
onClick={() => selectItem(index)}
>
<MentionItem profile={item} />
</button>
))
) : (
<div>No result</div>
)}
</div>
);
}
);
MentionList.displayName = 'MentionList'; MentionList.displayName = 'MentionList';

View File

@ -19,13 +19,13 @@ export function NIP05({
className?: string; className?: string;
}) { }) {
const { status, data } = useQuery( const { status, data } = useQuery(
[nip05], ['nip05', nip05],
async () => { async () => {
try { try {
const username = nip05.split('@')[0]; const localPath = nip05.split('@')[0];
const service = nip05.split('@')[1]; const service = nip05.split('@')[1];
// #TODO: use tauri native fetch to avoid CORS // #TODO: use tauri native fetch to avoid CORS
const verifyURL = `https://${service}/.well-known/nostr.json?name=${username}`; const verifyURL = `https://${service}/.well-known/nostr.json?name=${localPath}`;
const res = await fetch(verifyURL, { const res = await fetch(verifyURL, {
method: 'GET', method: 'GET',
@ -37,11 +37,11 @@ export function NIP05({
if (!res.ok) throw new Error(`Failed to fetch NIP-05 service: ${nip05}`); if (!res.ok) throw new Error(`Failed to fetch NIP-05 service: ${nip05}`);
const data: NIP05 = await res.json(); const data: NIP05 = await res.json();
if (data.names) { if (data.names) {
if (data.names.username !== pubkey) return false; if (data.names.username !== pubkey) return false;
return true; return true;
} }
return false;
} catch (e) { } catch (e) {
throw new Error(`Failed to verify NIP-05, error: ${e}`); throw new Error(`Failed to verify NIP-05, error: ${e}`);
} }

View File

@ -1,4 +1,5 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk'; import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import { useCallback } from 'react';
import { import {
ArticleNote, ArticleNote,
@ -15,9 +16,25 @@ import { User } from '@shared/user';
import { useEvent } from '@utils/hooks/useEvent'; import { useEvent } from '@utils/hooks/useEvent';
export function Repost({ event }: { event: NDKEvent }) { export function Repost({ event }: { event: NDKEvent }) {
const repostID = event.tags.find((el) => el[0] === 'e')?.[1]; const repostID = event.tags.find((el) => el[0] === 'e')[1] ?? '';
const { status, data } = useEvent(repostID, event.content as unknown as string); const { status, data } = useEvent(repostID, event.content as unknown as string);
const renderKind = useCallback(
(event: NDKEvent) => {
switch (event.kind) {
case NDKKind.Text:
return <TextNote event={event} />;
case NDKKind.Article:
return <ArticleNote event={event} />;
case 1063:
return <FileNote event={event} />;
default:
return <UnknownNote event={event} />;
}
},
[event]
);
if (status === 'loading') { if (status === 'loading') {
return ( return (
<div className="h-min w-full px-3 pb-3"> <div className="h-min w-full px-3 pb-3">
@ -43,19 +60,6 @@ export function Repost({ event }: { event: NDKEvent }) {
); );
} }
const renderKind = (event: NDKEvent) => {
switch (event.kind) {
case NDKKind.Text:
return <TextNote event={event} />;
case NDKKind.Article:
return <ArticleNote event={event} />;
case 1063:
return <FileNote event={event} />;
default:
return <UnknownNote event={event} />;
}
};
return ( return (
<div className="h-min w-full px-3 pb-3"> <div className="h-min w-full px-3 pb-3">
<div className="relative overflow-hidden rounded-xl bg-white/10 px-3 pt-3 backdrop-blur-xl"> <div className="relative overflow-hidden rounded-xl bg-white/10 px-3 pt-3 backdrop-blur-xl">
@ -64,7 +68,7 @@ export function Repost({ event }: { event: NDKEvent }) {
<RepostUser pubkey={event.pubkey} /> <RepostUser pubkey={event.pubkey} />
<User pubkey={data.pubkey} time={data.created_at} isRepost={true} /> <User pubkey={data.pubkey} time={data.created_at} isRepost={true} />
</div> </div>
<div className="flex items-start gap-3"> <div className="-mt-2 flex items-start gap-3">
<div className="w-11 shrink-0" /> <div className="w-11 shrink-0" />
<div className="relative z-20 flex-1"> <div className="relative z-20 flex-1">
{renderKind(data)} {renderKind(data)}

View File

@ -2,7 +2,7 @@ import { NDKEvent } from '@nostr-dev-kit/ndk';
export function UnknownNote({ event }: { event: NDKEvent }) { export function UnknownNote({ event }: { event: NDKEvent }) {
return ( return (
<div className="mb-3 mt-3 flex w-full flex-col gap-2"> <div className="flex w-full flex-col gap-2">
<div className="inline-flex flex-col gap-1 rounded-md bg-white/10 px-2 py-2 backdrop-blur-xl"> <div className="inline-flex flex-col gap-1 rounded-md bg-white/10 px-2 py-2 backdrop-blur-xl">
<span className="text-sm font-medium leading-none text-white/50"> <span className="text-sm font-medium leading-none text-white/50">
Unknown kind: {event.kind} Unknown kind: {event.kind}

View File

@ -23,8 +23,8 @@ export function User({
isChat?: boolean; isChat?: boolean;
}) { }) {
const { status, user } = useProfile(pubkey); const { status, user } = useProfile(pubkey);
const createdAt = formatCreatedAt(time, isChat);
const createdAt = formatCreatedAt(time, isChat);
const avatarWidth = size === 'small' ? 'w-6' : 'w-11'; const avatarWidth = size === 'small' ? 'w-6' : 'w-11';
const avatarHeight = size === 'small' ? 'h-6' : 'h-11'; const avatarHeight = size === 'small' ? 'h-6' : 'h-11';
@ -102,10 +102,10 @@ export function User({
<h5 className="text-sm font-semibold leading-none"> <h5 className="text-sm font-semibold leading-none">
{user?.display_name || user?.name || user?.username} {user?.display_name || user?.name || user?.username}
</h5> </h5>
{user?.nip05 ? ( {user.nip05 ? (
<NIP05 <NIP05
pubkey={pubkey} pubkey={pubkey}
nip05={user?.nip05} nip05={user.nip05}
className="max-w-[15rem] truncate text-sm leading-none text-white/50" className="max-w-[15rem] truncate text-sm leading-none text-white/50"
/> />
) : ( ) : (

View File

@ -19,36 +19,34 @@ export function useEvent(id: string, embed?: string) {
} }
// get event from db // get event from db
const dbEvent = await db.getEventByID(id); const dbEvent = await db.getEventByID(id);
if (dbEvent) { if (dbEvent) return dbEvent;
return dbEvent;
// get event from relay if event in db not present
const event = await ndk.fetchEvent(id);
if (!event) throw new Error(`Event not found: ${id}`);
let root: string;
let reply: string;
if (event.tags?.[0]?.[0] === 'e' && !event.tags?.[0]?.[3]) {
root = event.tags[0][1];
} else { } else {
// get event from relay if event in db not present root = event.tags.find((el) => el[3] === 'root')?.[1];
const event = await ndk.fetchEvent(id); reply = event.tags.find((el) => el[3] === 'reply')?.[1];
if (!event) throw new Error(`Event not found: ${id}`);
let root: string;
let reply: string;
if (event.tags?.[0]?.[0] === 'e' && !event.tags?.[0]?.[3]) {
root = event.tags[0][1];
} else {
root = event.tags.find((el) => el[3] === 'root')?.[1];
reply = event.tags.find((el) => el[3] === 'reply')?.[1];
}
const rawEvent = toRawEvent(event);
await db.createEvent(
event.id,
JSON.stringify(rawEvent),
event.pubkey,
event.kind,
root,
reply,
event.created_at
);
return event;
} }
const rawEvent = toRawEvent(event);
await db.createEvent(
event.id,
JSON.stringify(rawEvent),
event.pubkey,
event.kind,
root,
reply,
event.created_at
);
return event;
}, },
{ {
enabled: !!ndk, enabled: !!ndk,

View File

@ -5,8 +5,6 @@ import { Event, parseReferences } from 'nostr-tools';
import { RichContent } from '@utils/types'; import { RichContent } from '@utils/types';
export function parser(event: NDKEvent) { export function parser(event: NDKEvent) {
if (event.kind !== 1) return;
const references = parseReferences(event as unknown as Event); const references = parseReferences(event as unknown as Event);
const urls = getUrls(event.content as unknown as string); const urls = getUrls(event.content as unknown as string);