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

View File

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

View File

@ -1,75 +1,84 @@
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 { MentionItem } from '@shared/composer';
export const MentionList = forwardRef((props: any, ref: any) => {
const [selectedIndex, setSelectedIndex] = useState(0);
export const MentionList = forwardRef(
(props: SuggestionProps, ref: ForwardedRef<unknown>) => {
const [selectedIndex, setSelectedIndex] = useState(0);
const selectItem = (index) => {
const item = props.items[index];
const selectItem = (index) => {
const item = props.items[index];
if (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 (item) {
props.command({ id: item });
}
};
if (event.key === 'ArrowDown') {
downHandler();
return true;
}
const upHandler = () => {
setSelectedIndex((selectedIndex + props.items.length - 1) % props.items.length);
};
if (event.key === 'Enter') {
enterHandler();
return true;
}
const downHandler = () => {
setSelectedIndex((selectedIndex + 1) % props.items.length);
};
return false;
},
}));
const enterHandler = () => {
selectItem(selectedIndex);
};
return (
<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 ? (
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-zinc-700',
`${index === selectedIndex ? 'is-selected' : ''}`
)}
key={index}
onClick={() => selectItem(index)}
>
<MentionItem profile={item} />
</button>
))
) : (
<div>No result</div>
)}
</div>
);
});
useEffect(() => setSelectedIndex(0), [props.items]);
useImperativeHandle(ref, () => ({
onKeyDown: ({ event }) => {
if (event.key === 'ArrowUp') {
upHandler();
return true;
}
if (event.key === 'ArrowDown') {
downHandler();
return true;
}
if (event.key === 'Enter') {
enterHandler();
return true;
}
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';

View File

@ -19,13 +19,13 @@ export function NIP05({
className?: string;
}) {
const { status, data } = useQuery(
[nip05],
['nip05', nip05],
async () => {
try {
const username = nip05.split('@')[0];
const localPath = nip05.split('@')[0];
const service = nip05.split('@')[1];
// #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, {
method: 'GET',
@ -37,11 +37,11 @@ export function NIP05({
if (!res.ok) throw new Error(`Failed to fetch NIP-05 service: ${nip05}`);
const data: NIP05 = await res.json();
if (data.names) {
if (data.names.username !== pubkey) return false;
return true;
}
return false;
} catch (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 { useCallback } from 'react';
import {
ArticleNote,
@ -15,9 +16,25 @@ import { User } from '@shared/user';
import { useEvent } from '@utils/hooks/useEvent';
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 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') {
return (
<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 (
<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">
@ -64,7 +68,7 @@ export function Repost({ event }: { event: NDKEvent }) {
<RepostUser pubkey={event.pubkey} />
<User pubkey={data.pubkey} time={data.created_at} isRepost={true} />
</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="relative z-20 flex-1">
{renderKind(data)}

View File

@ -2,7 +2,7 @@ import { NDKEvent } from '@nostr-dev-kit/ndk';
export function UnknownNote({ event }: { event: NDKEvent }) {
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">
<span className="text-sm font-medium leading-none text-white/50">
Unknown kind: {event.kind}

View File

@ -23,8 +23,8 @@ export function User({
isChat?: boolean;
}) {
const { status, user } = useProfile(pubkey);
const createdAt = formatCreatedAt(time, isChat);
const createdAt = formatCreatedAt(time, isChat);
const avatarWidth = size === 'small' ? 'w-6' : 'w-11';
const avatarHeight = size === 'small' ? 'h-6' : 'h-11';
@ -102,10 +102,10 @@ export function User({
<h5 className="text-sm font-semibold leading-none">
{user?.display_name || user?.name || user?.username}
</h5>
{user?.nip05 ? (
{user.nip05 ? (
<NIP05
pubkey={pubkey}
nip05={user?.nip05}
nip05={user.nip05}
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
const dbEvent = await db.getEventByID(id);
if (dbEvent) {
return dbEvent;
if (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 {
// 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 {
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;
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;
},
{
enabled: !!ndk,

View File

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