chore: monorepo

This commit is contained in:
2023-12-25 14:28:39 +07:00
parent a6da07cd3f
commit 227c2ddefa
374 changed files with 19966 additions and 12758 deletions

View File

@@ -0,0 +1,21 @@
import { useQuery } from '@tanstack/react-query';
import { useArk } from '../provider';
export function useEvent(id: string) {
const ark = useArk();
const { status, isLoading, isError, data } = useQuery({
queryKey: ['event', id],
queryFn: async () => {
const event = await ark.getEventById({ id });
if (!event)
throw new Error(`Cannot get event with ${id}, will be retry after 10 seconds`);
return event;
},
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
retry: 2,
});
return { status, isLoading, isError, data };
}

View File

@@ -0,0 +1,27 @@
import { useQuery } from '@tanstack/react-query';
import { useArk } from '../provider';
export function useProfile(pubkey: string) {
const ark = useArk();
const {
isLoading,
isError,
data: user,
} = useQuery({
queryKey: ['user', pubkey],
queryFn: async () => {
const profile = await ark.getUserProfile({ pubkey });
if (!profile)
throw new Error(
`Cannot get metadata for ${pubkey}, will be retry after 10 seconds`
);
return profile;
},
refetchOnMount: false,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
retry: 2,
});
return { isLoading, isError, user };
}

View File

@@ -0,0 +1,96 @@
import { NDKKind, NDKRelayUrl, NDKTag } from "@nostr-dev-kit/ndk";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useArk, useStorage } from "../provider";
export function useRelay() {
const ark = useArk();
const storage = useStorage();
const queryClient = useQueryClient();
const connectRelay = useMutation({
mutationFn: async (
relay: NDKRelayUrl,
purpose?: "read" | "write" | undefined,
) => {
// Cancel any outgoing refetches
await queryClient.cancelQueries({
queryKey: ["relays", storage.account.pubkey],
});
// Snapshot the previous value
const prevRelays: NDKTag[] = queryClient.getQueryData([
"relays",
storage.account.pubkey,
]);
// create new relay list if not exist
if (!prevRelays) {
await ark.createEvent({
kind: NDKKind.RelayList,
tags: [["r", relay, purpose ?? ""]],
});
}
// add relay to exist list
const index = prevRelays.findIndex((el) => el[1] === relay);
if (index > -1) return;
await ark.createEvent({
kind: NDKKind.RelayList,
tags: [...prevRelays, ["r", relay, purpose ?? ""]],
});
// Optimistically update to the new value
queryClient.setQueryData(
["relays", storage.account.pubkey],
(prev: NDKTag[]) => [...prev, ["r", relay, purpose ?? ""]],
);
// Return a context object with the snapshotted value
return { prevRelays };
},
onSettled: () => {
queryClient.invalidateQueries({
queryKey: ["relays", storage.account.pubkey],
});
},
});
const removeRelay = useMutation({
mutationFn: async (relay: NDKRelayUrl) => {
// Cancel any outgoing refetches
await queryClient.cancelQueries({
queryKey: ["relays", storage.account.pubkey],
});
// Snapshot the previous value
const prevRelays: NDKTag[] = queryClient.getQueryData([
"relays",
storage.account.pubkey,
]);
if (!prevRelays) return;
const index = prevRelays.findIndex((el) => el[1] === relay);
if (index > -1) prevRelays.splice(index, 1);
await ark.createEvent({
kind: NDKKind.RelayList,
tags: prevRelays,
});
// Optimistically update to the new value
queryClient.setQueryData(["relays", storage.account.pubkey], prevRelays);
// Return a context object with the snapshotted value
return { prevRelays };
},
onSettled: () => {
queryClient.invalidateQueries({
queryKey: ["relays", storage.account.pubkey],
});
},
});
return { connectRelay, removeRelay };
}

View File

@@ -0,0 +1,183 @@
import { nanoid } from 'nanoid';
import { nip19 } from 'nostr-tools';
import { ReactNode } from 'react';
import { Link } from 'react-router-dom';
import reactStringReplace from 'react-string-replace';
import {
Hashtag,
ImagePreview,
LinkPreview,
MentionNote,
MentionUser,
VideoPreview,
} from '../components/note';
import { useStorage } from '../provider';
const NOSTR_MENTIONS = [
'@npub1',
'nostr:npub1',
'nostr:nprofile1',
'nostr:naddr1',
'npub1',
'nprofile1',
'naddr1',
'Nostr:npub1',
'Nostr:nprofile1',
'Nostr:naddre1',
];
const NOSTR_EVENTS = [
'nostr:note1',
'note1',
'nostr:nevent1',
'nevent1',
'Nostr:note1',
'Nostr:nevent1',
];
// const BITCOINS = ['lnbc', 'bc1p', 'bc1q'];
const IMAGES = ['.jpg', '.jpeg', '.gif', '.png', '.webp', '.avif', '.tiff'];
const VIDEOS = [
'.mp4',
'.mov',
'.webm',
'.wmv',
'.flv',
'.mts',
'.avi',
'.ogv',
'.mkv',
'.mp3',
'.m3u8',
];
export function useRichContent(content: string, textmode = false) {
const storage = useStorage();
let parsedContent: string | ReactNode[] = content.replace(/\n+/g, '\n');
let linkPreview: string;
let images: string[] = [];
let videos: string[] = [];
let events: string[] = [];
const text = parsedContent;
const words = text.split(/( |\n)/);
if (!textmode) {
if (storage.settings.media) {
images = words.filter((word) => IMAGES.some((el) => word.endsWith(el)));
videos = words.filter((word) => VIDEOS.some((el) => word.endsWith(el)));
}
events = words.filter((word) => NOSTR_EVENTS.some((el) => word.startsWith(el)));
}
const hashtags = words.filter((word) => word.startsWith('#'));
const mentions = words.filter((word) =>
NOSTR_MENTIONS.some((el) => word.startsWith(el))
);
try {
if (images.length) {
for(const image of images) {
parsedContent = reactStringReplace(parsedContent, image, (match, i) => (
<ImagePreview key={match + i} url={match} />
));
}
}
if (videos.length) {
for(const video of videos) {
parsedContent = reactStringReplace(parsedContent, video, (match, i) => (
<VideoPreview key={match + i} url={match} />
));
}
}
if (hashtags.length) {
for(const hashtag of hashtags) {
parsedContent = reactStringReplace(parsedContent, hashtag, (match, i) => {
if (storage.settings.hashtag) return <Hashtag key={match + i} tag={hashtag} />;
return null;
});
}
}
if (events.length) {
for(const event of events) {
const address = event.replace('nostr:', '').replace(/[^a-zA-Z0-9]/g, '');
const decoded = nip19.decode(address);
if (decoded.type === 'note') {
parsedContent = reactStringReplace(parsedContent, event, (match, i) => (
<MentionNote key={match + i} eventId={decoded.data} />
));
}
if (decoded.type === 'nevent') {
parsedContent = reactStringReplace(parsedContent, event, (match, i) => (
<MentionNote key={match + i} eventId={decoded.data.id} />
));
}
}
}
if (mentions.length) {
for(const mention of mentions) {
const address = mention
.replace('nostr:', '')
.replace('@', '')
.replace(/[^a-zA-Z0-9]/g, '');
const decoded = nip19.decode(address);
if (decoded.type === 'npub') {
parsedContent = reactStringReplace(parsedContent, mention, (match, i) => (
<MentionUser key={match + i} pubkey={decoded.data} />
));
}
if (decoded.type === 'nprofile' || decoded.type === 'naddr') {
parsedContent = reactStringReplace(parsedContent, mention, (match, i) => (
<MentionUser key={match + i} pubkey={decoded.data.pubkey} />
));
}
}
}
parsedContent = reactStringReplace(parsedContent, /(https?:\/\/\S+)/g, (match, i) => {
const url = new URL(match);
url.search = '';
if (!linkPreview && !textmode) {
linkPreview = match;
return <LinkPreview key={match + i} url={url.toString()} />;
}
return (
<Link
key={match + i}
to={url.toString()}
target="_blank"
rel="noreferrer"
className="break-all font-normal text-blue-500 hover:text-blue-600"
>
{url.toString()}
</Link>
);
});
parsedContent = reactStringReplace(parsedContent, '\n', () => {
return <div key={nanoid()} className="h-3" />;
});
if (typeof parsedContent[0] === 'string') {
parsedContent[0] = parsedContent[0].trimStart();
}
return { parsedContent };
} catch (e) {
console.warn('[parser] parse failed: ', e);
return { parsedContent };
}
}

View File

@@ -0,0 +1,78 @@
import { MentionOptions } from "@tiptap/extension-mention";
import { ReactRenderer } from "@tiptap/react";
import tippy from "tippy.js";
import { MentionList } from "../components/mentions";
import { useStorage } from "../provider";
export function useSuggestion() {
const storage = useStorage();
const suggestion: MentionOptions["suggestion"] = {
items: async ({ query }) => {
const users = await storage.getAllCacheUsers();
return users
.filter((item) => {
if (item.name)
return item.name.toLowerCase().startsWith(query.toLowerCase());
return item.displayName.toLowerCase().startsWith(query.toLowerCase());
})
.slice(0, 5);
},
render: () => {
let component;
let popup;
return {
onStart: (props) => {
component = new ReactRenderer(MentionList, {
props,
editor: props.editor,
});
if (!props.clientRect) {
return;
}
popup = tippy("body", {
getReferenceClientRect: props.clientRect,
appendTo: () => document.body,
content: component.element,
showOnCreate: true,
interactive: true,
trigger: "manual",
placement: "bottom-start",
});
},
onUpdate(props) {
component.updateProps(props);
if (!props.clientRect) {
return;
}
popup[0].setProps({
getReferenceClientRect: props.clientRect,
});
},
onKeyDown(props) {
if (props.event.key === "Escape") {
popup[0].hide();
return true;
}
return component.ref?.onKeyDown(props);
},
onExit() {
popup[0].destroy();
component.destroy();
},
};
},
};
return { suggestion };
}

View File

@@ -0,0 +1,79 @@
import { type WidgetProps } from '@lume/types';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useStorage } from '../provider';
export function useWidget() {
const storage = useStorage();
const queryClient = useQueryClient();
const addWidget = useMutation({
mutationFn: async (widget: WidgetProps) => {
return await storage.createWidget(widget.kind, widget.title, widget.content);
},
onSuccess: (data) => {
queryClient.setQueryData(['widgets'], (old: WidgetProps[]) => [...old, data]);
},
});
const replaceWidget = useMutation({
mutationFn: async ({
currentId,
widget,
}: {
currentId: string;
widget: WidgetProps;
}) => {
// Cancel any outgoing refetches
await queryClient.cancelQueries({ queryKey: ['widgets'] });
// Snapshot the previous value
const prevWidgets = queryClient.getQueryData(['widgets']);
// create new widget
await storage.removeWidget(currentId);
const newWidget = await storage.createWidget(
widget.kind,
widget.title,
widget.content
);
// Optimistically update to the new value
queryClient.setQueryData(['widgets'], (prev: WidgetProps[]) => [
...prev.filter((t) => t.id !== currentId),
newWidget,
]);
// Return a context object with the snapshotted value
return { prevWidgets };
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['widgets'] });
},
});
const removeWidget = useMutation({
mutationFn: async (id: string) => {
// Cancel any outgoing refetches
await queryClient.cancelQueries({ queryKey: ['widgets'] });
// Snapshot the previous value
const prevWidgets = queryClient.getQueryData(['widgets']);
// Optimistically update to the new value
queryClient.setQueryData(['widgets'], (prev: WidgetProps[]) =>
prev.filter((t) => t.id !== id)
);
// Update in database
await storage.removeWidget(id);
// Return a context object with the snapshotted value
return { prevWidgets };
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['widgets'] });
},
});
return { addWidget, replaceWidget, removeWidget };
}