chore: monorepo
This commit is contained in:
21
packages/ark/src/hooks/useEvent.ts
Normal file
21
packages/ark/src/hooks/useEvent.ts
Normal 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 };
|
||||
}
|
||||
27
packages/ark/src/hooks/useProfile.ts
Normal file
27
packages/ark/src/hooks/useProfile.ts
Normal 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 };
|
||||
}
|
||||
96
packages/ark/src/hooks/useRelay.ts
Normal file
96
packages/ark/src/hooks/useRelay.ts
Normal 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 };
|
||||
}
|
||||
183
packages/ark/src/hooks/useRichContent.tsx
Normal file
183
packages/ark/src/hooks/useRichContent.tsx
Normal 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 };
|
||||
}
|
||||
}
|
||||
78
packages/ark/src/hooks/useSuggestion.ts
Normal file
78
packages/ark/src/hooks/useSuggestion.ts
Normal 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 };
|
||||
}
|
||||
79
packages/ark/src/hooks/useWidget.ts
Normal file
79
packages/ark/src/hooks/useWidget.ts
Normal 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 };
|
||||
}
|
||||
Reference in New Issue
Block a user