feat: add editor screen
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
export * from "./src/constants";
|
||||
export * from "./src/delay";
|
||||
export * from "./src/formater";
|
||||
export * from "./src/editor";
|
||||
export * from "./src/nip01";
|
||||
export * from "./src/nip94";
|
||||
export * from "./src/notification";
|
||||
export * from "./src/hooks/useNetworkStatus";
|
||||
|
||||
@@ -13,12 +13,16 @@
|
||||
"dayjs": "^1.11.10",
|
||||
"jotai": "^2.6.4",
|
||||
"nostr-tools": "^2.1.9",
|
||||
"react": "^18.2.0"
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"slate": "^0.101.5",
|
||||
"slate-react": "^0.101.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lume/tsconfig": "workspace:^",
|
||||
"@lume/types": "workspace:^",
|
||||
"@types/react": "^18.2.55",
|
||||
"@types/react-dom": "^18.2.19",
|
||||
"tailwind-merge": "^2.2.1",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
|
||||
97
packages/utils/src/editor.ts
Normal file
97
packages/utils/src/editor.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import ReactDOM from "react-dom";
|
||||
import { ReactNode } from "react";
|
||||
import { BaseEditor, Transforms } from "slate";
|
||||
import { ReactEditor } from "slate-react";
|
||||
import { Contact } from "@lume/types";
|
||||
|
||||
export const Portal = ({ children }: { children?: ReactNode }) => {
|
||||
return typeof document === "object"
|
||||
? ReactDOM.createPortal(children, document.body)
|
||||
: null;
|
||||
};
|
||||
|
||||
export const isImagePath = (path: string) => {
|
||||
for (const suffix of ["jpg", "jpeg", "gif", "png", "webp", "avif", "tiff"]) {
|
||||
if (path.endsWith(suffix)) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const isImageUrl = (url: string) => {
|
||||
try {
|
||||
if (!url) return false;
|
||||
const ext = new URL(url).pathname.split(".").pop();
|
||||
return ["jpg", "jpeg", "gif", "png", "webp", "avif", "tiff"].includes(ext);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const insertImage = (editor: ReactEditor | BaseEditor, url: string) => {
|
||||
const text = { text: "" };
|
||||
const image = [
|
||||
{
|
||||
type: "image",
|
||||
url,
|
||||
children: [text],
|
||||
},
|
||||
];
|
||||
const extraText = [
|
||||
{
|
||||
type: "paragraph",
|
||||
children: [text],
|
||||
},
|
||||
];
|
||||
|
||||
// @ts-ignore, idk
|
||||
ReactEditor.focus(editor);
|
||||
Transforms.insertNodes(editor, image);
|
||||
Transforms.insertNodes(editor, extraText);
|
||||
};
|
||||
|
||||
export const insertMention = (
|
||||
editor: ReactEditor | BaseEditor,
|
||||
contact: Contact,
|
||||
) => {
|
||||
const text = { text: "" };
|
||||
const mention = {
|
||||
type: "mention",
|
||||
npub: `nostr:${contact.pubkey}`,
|
||||
name: contact.profile.name || contact.profile.display_name || "anon",
|
||||
children: [text],
|
||||
};
|
||||
const extraText = [
|
||||
{
|
||||
type: "paragraph",
|
||||
children: [text],
|
||||
},
|
||||
];
|
||||
|
||||
// @ts-ignore, idk
|
||||
ReactEditor.focus(editor);
|
||||
Transforms.insertNodes(editor, mention);
|
||||
Transforms.insertNodes(editor, extraText);
|
||||
};
|
||||
|
||||
export const insertNostrEvent = (
|
||||
editor: ReactEditor | BaseEditor,
|
||||
eventId: string,
|
||||
) => {
|
||||
const text = { text: "" };
|
||||
const event = [
|
||||
{
|
||||
type: "event",
|
||||
eventId: `nostr:${eventId}`,
|
||||
children: [text],
|
||||
},
|
||||
];
|
||||
const extraText = [
|
||||
{
|
||||
type: "paragraph",
|
||||
children: [text],
|
||||
},
|
||||
];
|
||||
|
||||
Transforms.insertNodes(editor, event);
|
||||
Transforms.insertNodes(editor, extraText);
|
||||
};
|
||||
96
packages/utils/src/nip01.ts
Normal file
96
packages/utils/src/nip01.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { nip19 } from "nostr-tools";
|
||||
import { EventPointer, ProfilePointer } from "nostr-tools/lib/types/nip19";
|
||||
|
||||
// Borrow from NDK
|
||||
// https://github.com/nostr-dev-kit/ndk/blob/master/ndk/src/events/content-tagger.ts
|
||||
export async function generateContentTags(content: string) {
|
||||
let promises: Promise<void>[] = [];
|
||||
let tags: string[][] = [];
|
||||
|
||||
const tagRegex = /(@|nostr:)(npub|nprofile|note|nevent|naddr)[a-zA-Z0-9]+/g;
|
||||
const hashtagRegex = /#(\w+)/g;
|
||||
|
||||
const addTagIfNew = (t: string[]) => {
|
||||
if (!tags.find((t2) => t2[0] === t[0] && t2[1] === t[1])) {
|
||||
tags.push(t);
|
||||
}
|
||||
};
|
||||
|
||||
content = content.replace(tagRegex, (tag) => {
|
||||
try {
|
||||
const entity = tag.split(/(@|nostr:)/)[2];
|
||||
const { type, data } = nip19.decode(entity);
|
||||
let t: string[] | undefined;
|
||||
|
||||
switch (type) {
|
||||
case "npub":
|
||||
t = ["p", data as string];
|
||||
break;
|
||||
case "nprofile":
|
||||
t = ["p", (data as ProfilePointer).pubkey as string];
|
||||
break;
|
||||
case "note":
|
||||
promises.push(
|
||||
new Promise(async (resolve) => {
|
||||
addTagIfNew(["e", data, "", "mention"]);
|
||||
resolve();
|
||||
}),
|
||||
);
|
||||
break;
|
||||
case "nevent":
|
||||
promises.push(
|
||||
new Promise(async (resolve) => {
|
||||
let { id, relays, author } = data as EventPointer;
|
||||
|
||||
// If the nevent doesn't have a relay specified, try to get one
|
||||
if (!relays || relays.length === 0) {
|
||||
relays = [""];
|
||||
}
|
||||
|
||||
addTagIfNew(["e", id, relays[0], "mention"]);
|
||||
if (author) addTagIfNew(["p", author]);
|
||||
resolve();
|
||||
}),
|
||||
);
|
||||
break;
|
||||
case "naddr":
|
||||
promises.push(
|
||||
new Promise(async (resolve) => {
|
||||
const id = [data.kind, data.pubkey, data.identifier].join(":");
|
||||
let relays = data.relays ?? [];
|
||||
|
||||
// If the naddr doesn't have a relay specified, try to get one
|
||||
if (relays.length === 0) {
|
||||
relays = [""];
|
||||
}
|
||||
|
||||
addTagIfNew(["a", id, relays[0], "mention"]);
|
||||
addTagIfNew(["p", data.pubkey]);
|
||||
resolve();
|
||||
}),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
return tag;
|
||||
}
|
||||
|
||||
if (t) addTagIfNew(t);
|
||||
|
||||
return `nostr:${entity}`;
|
||||
} catch (error) {
|
||||
return tag;
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
content = content.replace(hashtagRegex, (tag, word) => {
|
||||
const t: string[] = ["t", word];
|
||||
if (!tags.find((t2) => t2[0] === t[0] && t2[1] === t[1])) {
|
||||
tags.push(t);
|
||||
}
|
||||
return tag; // keep the original tag in the content
|
||||
});
|
||||
|
||||
return { content, tags };
|
||||
}
|
||||
Reference in New Issue
Block a user