mirror of
https://github.com/luminous-devs/lume.git
synced 2024-09-29 16:30:55 +00:00
feat: polish note component
This commit is contained in:
parent
e0d4c53098
commit
a9d10ff93b
@ -29,8 +29,6 @@
|
||||
"@tauri-apps/plugin-sql": "2.0.0-alpha.5",
|
||||
"@tauri-apps/plugin-updater": "2.0.0-alpha.5",
|
||||
"@tauri-apps/plugin-upload": "2.0.0-alpha.5",
|
||||
"@tiptap/extension-mention": "^2.1.13",
|
||||
"@tiptap/react": "^2.1.13",
|
||||
"@vidstack/react": "^1.9.8",
|
||||
"get-urls": "^12.1.0",
|
||||
"jotai": "^2.6.1",
|
||||
|
@ -267,10 +267,16 @@ export class Ark {
|
||||
return event;
|
||||
}
|
||||
|
||||
public getEventThread({ tags }: { tags: NDKTag[] }) {
|
||||
public getEventThread({
|
||||
content,
|
||||
tags,
|
||||
}: { content: string; tags: NDKTag[] }) {
|
||||
let rootEventId: string = null;
|
||||
let replyEventId: string = null;
|
||||
|
||||
if (content.includes("nostr:note1") || content.includes("nostr:nevent1"))
|
||||
return null;
|
||||
|
||||
const events = tags.filter((el) => el[0] === "e");
|
||||
|
||||
if (!events.length) return null;
|
||||
|
@ -1,38 +1,129 @@
|
||||
import { useEvent } from '../../hooks/useEvent';
|
||||
import { NoteChildUser } from './childUser';
|
||||
import { NOSTR_MENTIONS } from "@lume/utils";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import { ReactNode, useMemo } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import reactStringReplace from "react-string-replace";
|
||||
import { useEvent } from "../../hooks/useEvent";
|
||||
import { NoteChildUser } from "./childUser";
|
||||
import { Hashtag } from "./mentions/hashtag";
|
||||
import { MentionUser } from "./mentions/user";
|
||||
|
||||
export function NoteChild({ eventId, isRoot }: { eventId: string; isRoot?: boolean }) {
|
||||
const { isLoading, isError, data } = useEvent(eventId);
|
||||
export function NoteChild({
|
||||
eventId,
|
||||
isRoot,
|
||||
}: { eventId: string; isRoot?: boolean }) {
|
||||
const { isLoading, isError, data } = useEvent(eventId);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="relative flex gap-3">
|
||||
<div className="relative flex-1 rounded-md bg-neutral-200 px-2 py-2 dark:bg-neutral-800">
|
||||
<div className="h-4 w-full animate-pulse bg-neutral-300 dark:bg-neutral-700" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const richContent = useMemo(() => {
|
||||
if (!data) return "";
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<div className="relative flex gap-3">
|
||||
<div className="relative flex-1 rounded-md bg-neutral-200 px-2 py-2 dark:bg-neutral-800">
|
||||
Failed to fetch event
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
let parsedContent: string | ReactNode[] = data.content.replace(
|
||||
/\n+/g,
|
||||
"\n",
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="relative flex gap-3">
|
||||
<div className="relative flex-1 rounded-md bg-neutral-200 px-2 py-2 dark:bg-neutral-800">
|
||||
<div className="absolute right-0 top-[18px] h-3 w-3 -translate-y-1/2 translate-x-1/2 rotate-45 transform bg-neutral-200 dark:bg-neutral-800" />
|
||||
<div className="break-p mt-6 line-clamp-3 select-text leading-normal text-neutral-900 dark:text-neutral-100">
|
||||
{data.content}
|
||||
</div>
|
||||
</div>
|
||||
<NoteChildUser pubkey={data.pubkey} subtext={isRoot ? 'posted' : 'replied'} />
|
||||
</div>
|
||||
);
|
||||
const text = parsedContent;
|
||||
const words = text.split(/( |\n)/);
|
||||
|
||||
const hashtags = words.filter((word) => word.startsWith("#"));
|
||||
const mentions = words.filter((word) =>
|
||||
NOSTR_MENTIONS.some((el) => word.startsWith(el)),
|
||||
);
|
||||
|
||||
if (hashtags.length) {
|
||||
for (const hashtag of hashtags) {
|
||||
parsedContent = reactStringReplace(
|
||||
parsedContent,
|
||||
hashtag,
|
||||
(match, i) => {
|
||||
return <Hashtag key={match + i} tag={hashtag} />;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
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>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
return parsedContent;
|
||||
}, [data]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="relative flex gap-3">
|
||||
<div className="relative flex-1 rounded-md bg-neutral-200 px-2 py-2 dark:bg-neutral-800">
|
||||
<div className="h-4 w-full animate-pulse bg-neutral-300 dark:bg-neutral-700" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<div className="relative flex gap-3">
|
||||
<div className="relative flex-1 rounded-md bg-neutral-200 px-2 py-2 dark:bg-neutral-800">
|
||||
Failed to fetch event
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative flex gap-3">
|
||||
<div className="relative flex-1 rounded-md bg-neutral-200 px-2 py-2 dark:bg-neutral-800">
|
||||
<div className="absolute right-0 top-[18px] h-3 w-3 -translate-y-1/2 translate-x-1/2 rotate-45 transform bg-neutral-200 dark:bg-neutral-800" />
|
||||
<div className="break-p mt-6 line-clamp-3 select-text leading-normal text-neutral-900 dark:text-neutral-100">
|
||||
{richContent}
|
||||
</div>
|
||||
</div>
|
||||
<NoteChildUser
|
||||
pubkey={data.pubkey}
|
||||
subtext={isRoot ? "posted" : "replied"}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,4 +1,12 @@
|
||||
import { cn } from "@lume/utils";
|
||||
import {
|
||||
AUDIOS,
|
||||
IMAGES,
|
||||
NOSTR_EVENTS,
|
||||
NOSTR_MENTIONS,
|
||||
VIDEOS,
|
||||
cn,
|
||||
regionNames,
|
||||
} from "@lume/utils";
|
||||
import { NDKKind } from "@nostr-dev-kit/ndk";
|
||||
import { fetch } from "@tauri-apps/plugin-http";
|
||||
import getUrls from "get-urls";
|
||||
@ -19,55 +27,12 @@ import {
|
||||
} from "../..";
|
||||
import { NIP89 } from "./nip89";
|
||||
|
||||
const NOSTR_MENTIONS = [
|
||||
"@npub1",
|
||||
"nostr:npub1",
|
||||
"nostr:nprofile1",
|
||||
"nostr:naddr1",
|
||||
"npub1",
|
||||
"nprofile1",
|
||||
"naddr1",
|
||||
"Nostr:npub1",
|
||||
"Nostr:nprofile1",
|
||||
"Nostr:naddre1",
|
||||
];
|
||||
|
||||
const NOSTR_EVENTS = [
|
||||
"@nevent1",
|
||||
"@note1",
|
||||
"@nostr:note1",
|
||||
"@nostr:nevent1",
|
||||
"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",
|
||||
"m3u8",
|
||||
];
|
||||
|
||||
const AUDIOS = ["mp3", "ogg", "wav"];
|
||||
|
||||
export function NoteContent({
|
||||
className,
|
||||
isTranslatable = false,
|
||||
}: {
|
||||
className?: string;
|
||||
isTranslatable?: boolean;
|
||||
}) {
|
||||
const storage = useStorage();
|
||||
const event = useNoteContext();
|
||||
@ -79,7 +44,7 @@ export function NoteContent({
|
||||
if (event.kind !== NDKKind.Text) return content;
|
||||
|
||||
let parsedContent: string | ReactNode[] = content.replace(/\n+/g, "\n");
|
||||
let linkPreview: string;
|
||||
let linkPreview: string = undefined;
|
||||
let images: string[] = [];
|
||||
let videos: string[] = [];
|
||||
let audios: string[] = [];
|
||||
@ -299,7 +264,7 @@ export function NoteContent({
|
||||
<div className="break-p select-text whitespace-pre-line text-balance leading-normal">
|
||||
{richContent}
|
||||
</div>
|
||||
{storage.settings.translation ? (
|
||||
{isTranslatable && storage.settings.translation ? (
|
||||
translated ? (
|
||||
<button
|
||||
type="button"
|
||||
@ -307,7 +272,7 @@ export function NoteContent({
|
||||
setTranslated(false);
|
||||
setContent(event.content);
|
||||
}}
|
||||
className="mt-2 text-sm text-blue-500 hover:text-blue-600 border-none shadow-none focus:outline-none"
|
||||
className="mt-3 text-sm text-blue-500 hover:text-blue-600 border-none shadow-none focus:outline-none"
|
||||
>
|
||||
Show original content
|
||||
</button>
|
||||
@ -315,9 +280,9 @@ export function NoteContent({
|
||||
<button
|
||||
type="button"
|
||||
onClick={translate}
|
||||
className="mt-2 text-sm text-blue-500 hover:text-blue-600 border-none shadow-none focus:outline-none"
|
||||
className="mt-3 text-sm text-blue-500 hover:text-blue-600 border-none shadow-none focus:outline-none"
|
||||
>
|
||||
Translate to Vietnamese
|
||||
Translate to {regionNames.of(storage.locale)}
|
||||
</button>
|
||||
)
|
||||
) : null}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { CheckCircleIcon, DownloadIcon } from "@lume/icons";
|
||||
import { getImageMeta } from "@lume/utils";
|
||||
import { downloadDir } from "@tauri-apps/api/path";
|
||||
import { Window } from "@tauri-apps/api/window";
|
||||
import { download } from "@tauri-apps/plugin-upload";
|
||||
@ -24,12 +23,9 @@ export function ImagePreview({ url }: { url: string }) {
|
||||
|
||||
const open = async () => {
|
||||
const name = new URL(url).pathname.split("/").pop();
|
||||
const image = await getImageMeta(url);
|
||||
return new Window("image-viewer", {
|
||||
url,
|
||||
title: name,
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -9,7 +9,6 @@ export function VideoPreview({ url }: { url: string }) {
|
||||
<MediaPlayer
|
||||
src={url}
|
||||
className="w-full my-1 overflow-hidden rounded-lg"
|
||||
aspectRatio="16/9"
|
||||
load="visible"
|
||||
>
|
||||
<MediaProvider />
|
||||
|
@ -1,14 +1,10 @@
|
||||
import { NDKEvent } from "@nostr-dev-kit/ndk";
|
||||
import { Note } from "..";
|
||||
import { useArk } from "../../../provider";
|
||||
|
||||
export function TextNote({
|
||||
event,
|
||||
className,
|
||||
}: { event: NDKEvent; className?: string }) {
|
||||
const ark = useArk();
|
||||
const thread = ark.getEventThread({ tags: event.tags });
|
||||
|
||||
return (
|
||||
<Note.Provider event={event}>
|
||||
<Note.Root className={className}>
|
||||
@ -16,7 +12,7 @@ export function TextNote({
|
||||
<Note.User className="flex-1 pr-1" />
|
||||
<Note.Menu />
|
||||
</div>
|
||||
<Note.Thread thread={thread} className="mb-2" />
|
||||
<Note.Thread className="mb-2" />
|
||||
<Note.Content className="min-w-0 px-3" />
|
||||
<div className="flex items-center justify-between px-3 h-14">
|
||||
<Note.Pin />
|
||||
|
@ -1,32 +1,9 @@
|
||||
import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
|
||||
import { Note } from "..";
|
||||
import { useEvent } from "../../../hooks/useEvent";
|
||||
import { useArk } from "../../../provider";
|
||||
|
||||
export function ThreadNote({ eventId }: { eventId: string }) {
|
||||
const ark = useArk();
|
||||
const { isLoading, data } = useEvent(eventId);
|
||||
|
||||
const renderEventKind = (event: NDKEvent) => {
|
||||
const thread = ark.getEventThread({ tags: data.tags });
|
||||
switch (event.kind) {
|
||||
case NDKKind.Text:
|
||||
return (
|
||||
<>
|
||||
<Note.Thread thread={thread} className="mb-2" />
|
||||
<Note.Content className="min-w-0 px-3" />
|
||||
</>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<>
|
||||
<Note.Thread thread={thread} className="mb-2" />
|
||||
<Note.Content className="min-w-0 px-3" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
@ -38,7 +15,8 @@ export function ThreadNote({ eventId }: { eventId: string }) {
|
||||
<Note.User className="flex-1 pr-1" />
|
||||
<Note.Menu />
|
||||
</div>
|
||||
{renderEventKind(data)}
|
||||
<Note.Thread className="mb-2" />
|
||||
<Note.Content className="min-w-0 px-3" isTranslatable />
|
||||
<div className="flex items-center justify-between px-3 h-14">
|
||||
<Note.Pin />
|
||||
<div className="inline-flex items-center gap-10">
|
||||
|
@ -2,16 +2,22 @@ import { PinIcon } from "@lume/icons";
|
||||
import { COL_TYPES } from "@lume/utils";
|
||||
import { Link } from "react-router-dom";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { Note } from ".";
|
||||
import { Note, useNoteContext } from ".";
|
||||
import { useArk } from "../..";
|
||||
import { useColumnContext } from "../column";
|
||||
|
||||
export function NoteThread({
|
||||
thread,
|
||||
className,
|
||||
}: {
|
||||
thread: { rootEventId: string; replyEventId: string };
|
||||
className?: string;
|
||||
}) {
|
||||
const ark = useArk();
|
||||
const event = useNoteContext();
|
||||
const thread = ark.getEventThread({
|
||||
content: event.content,
|
||||
tags: event.tags,
|
||||
});
|
||||
|
||||
const { addColumn } = useColumnContext();
|
||||
|
||||
if (!thread) return null;
|
||||
|
@ -1,12 +1,7 @@
|
||||
import { LoaderIcon } from "@lume/icons";
|
||||
import { NDKCacheAdapterTauri } from "@lume/ndk-cache-tauri";
|
||||
import { LumeStorage } from "@lume/storage";
|
||||
import {
|
||||
FETCH_LIMIT,
|
||||
QUOTES,
|
||||
delay,
|
||||
sendNativeNotification,
|
||||
} from "@lume/utils";
|
||||
import { QUOTES, delay, sendNativeNotification } from "@lume/utils";
|
||||
import NDK, {
|
||||
NDKEvent,
|
||||
NDKKind,
|
||||
@ -18,7 +13,7 @@ import NDK, {
|
||||
import { ndkAdapter } from "@nostr-fetch/adapter-ndk";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { fetch } from "@tauri-apps/plugin-http";
|
||||
import { platform } from "@tauri-apps/plugin-os";
|
||||
import { locale, platform } from "@tauri-apps/plugin-os";
|
||||
import { relaunch } from "@tauri-apps/plugin-process";
|
||||
import Database from "@tauri-apps/plugin-sql";
|
||||
import { check } from "@tauri-apps/plugin-updater";
|
||||
@ -101,9 +96,10 @@ const LumeProvider = ({ children }: PropsWithChildren<object>) => {
|
||||
|
||||
async function init() {
|
||||
const platformName = await platform();
|
||||
const osLocale = await locale();
|
||||
const sqliteAdapter = await Database.load("sqlite:lume_v3.db");
|
||||
|
||||
const storage = new LumeStorage(sqliteAdapter, platformName);
|
||||
const storage = new LumeStorage(sqliteAdapter, platformName, osLocale);
|
||||
await storage.init();
|
||||
|
||||
// check for new update
|
||||
@ -145,9 +141,9 @@ const LumeProvider = ({ children }: PropsWithChildren<object>) => {
|
||||
explicitRelayUrls,
|
||||
outboxRelayUrls,
|
||||
blacklistRelayUrls,
|
||||
enableOutboxModel: !storage.settings.lowPowerMode,
|
||||
autoConnectUserRelays: !storage.settings.lowPowerMode,
|
||||
autoFetchUserMutelist: !storage.settings.lowPowerMode,
|
||||
enableOutboxModel: !storage.settings.lowPower,
|
||||
autoConnectUserRelays: !storage.settings.lowPower,
|
||||
autoFetchUserMutelist: !storage.settings.lowPower,
|
||||
// clientName: 'Lume',
|
||||
// clientNip89: '',
|
||||
});
|
||||
|
@ -12,6 +12,7 @@ export function HomeRoute({ colKey }: { colKey: string }) {
|
||||
const storage = useStorage();
|
||||
const ref = useRef<VListHandle>();
|
||||
const cacheKey = `${colKey}-vlist`;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [offset, cache] = useMemo(() => {
|
||||
const serialized = sessionStorage.getItem(cacheKey);
|
||||
@ -48,7 +49,6 @@ export function HomeRoute({ colKey }: { colKey: string }) {
|
||||
return lastEvent.created_at - 1;
|
||||
},
|
||||
initialData: () => {
|
||||
const queryClient = useQueryClient();
|
||||
const queryCacheData = queryClient.getQueryState([colKey])
|
||||
?.data as NDKEvent[];
|
||||
if (queryCacheData) {
|
||||
|
@ -17,6 +17,7 @@ export class LumeStorage {
|
||||
#db: Database;
|
||||
#depot: Child;
|
||||
readonly platform: Platform;
|
||||
readonly locale: string;
|
||||
public account: Account;
|
||||
public settings: {
|
||||
autoupdate: boolean;
|
||||
@ -30,8 +31,9 @@ export class LumeStorage {
|
||||
translateApiKey: string;
|
||||
};
|
||||
|
||||
constructor(db: Database, platform: Platform) {
|
||||
constructor(db: Database, platform: Platform, locale: string) {
|
||||
this.#db = db;
|
||||
this.locale = locale;
|
||||
this.platform = platform;
|
||||
this.settings = {
|
||||
autoupdate: false,
|
||||
|
@ -5,7 +5,12 @@ import {
|
||||
useProfile,
|
||||
useStorage,
|
||||
} from "@lume/ark";
|
||||
import { ArrowLeftIcon, ArrowRightCircleIcon, LoaderIcon } from "@lume/icons";
|
||||
import {
|
||||
ArrowLeftIcon,
|
||||
ArrowRightCircleIcon,
|
||||
ArrowRightIcon,
|
||||
LoaderIcon,
|
||||
} from "@lume/icons";
|
||||
import { FETCH_LIMIT, displayNpub } from "@lume/utils";
|
||||
import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
|
||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||
@ -104,14 +109,20 @@ export function UserRoute() {
|
||||
return (
|
||||
<div className="pb-5 overflow-y-auto">
|
||||
<WindowVirtualizer>
|
||||
<div className="h-11 bg-neutral-50 dark:bg-neutral-950 border-b flex items-center px-3 border-neutral-100 dark:border-neutral-900 mb-3">
|
||||
<div className="h-11 bg-neutral-50 dark:bg-neutral-950 border-b flex items-center justify-start gap-2 px-3 border-neutral-100 dark:border-neutral-900 mb-3">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-2.5 text-sm font-medium"
|
||||
className="size-9 hover:bg-neutral-100 hover:text-blue-500 dark:hover:bg-neutral-900 rounded-lg inline-flex items-center justify-center"
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
<ArrowLeftIcon className="size-4" />
|
||||
Back
|
||||
<ArrowLeftIcon className="size-5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="size-9 hover:bg-neutral-100 hover:text-blue-500 dark:hover:bg-neutral-900 rounded-lg inline-flex items-center justify-center"
|
||||
onClick={() => navigate(1)}
|
||||
>
|
||||
<ArrowRightIcon className="size-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="px-3">
|
||||
|
@ -1,5 +1,50 @@
|
||||
export const FETCH_LIMIT = 20;
|
||||
|
||||
export const NOSTR_MENTIONS = [
|
||||
"@npub1",
|
||||
"nostr:npub1",
|
||||
"nostr:nprofile1",
|
||||
"nostr:naddr1",
|
||||
"npub1",
|
||||
"nprofile1",
|
||||
"naddr1",
|
||||
"Nostr:npub1",
|
||||
"Nostr:nprofile1",
|
||||
"Nostr:naddre1",
|
||||
];
|
||||
|
||||
export const NOSTR_EVENTS = [
|
||||
"@nevent1",
|
||||
"@note1",
|
||||
"@nostr:note1",
|
||||
"@nostr:nevent1",
|
||||
"nostr:note1",
|
||||
"note1",
|
||||
"nostr:nevent1",
|
||||
"nevent1",
|
||||
"Nostr:note1",
|
||||
"Nostr:nevent1",
|
||||
];
|
||||
|
||||
// const BITCOINS = ['lnbc', 'bc1p', 'bc1q'];
|
||||
|
||||
export const IMAGES = ["jpg", "jpeg", "gif", "png", "webp", "avif", "tiff"];
|
||||
|
||||
export const VIDEOS = [
|
||||
"mp4",
|
||||
"mov",
|
||||
"webm",
|
||||
"wmv",
|
||||
"flv",
|
||||
"mts",
|
||||
"avi",
|
||||
"ogv",
|
||||
"mkv",
|
||||
"m3u8",
|
||||
];
|
||||
|
||||
export const AUDIOS = ["mp3", "ogg", "wav"];
|
||||
|
||||
export const HASHTAGS = [
|
||||
{ hashtag: "#food" },
|
||||
{ hashtag: "#gaming" },
|
||||
|
@ -65,3 +65,6 @@ export function displayNpub(pubkey: string, len: number) {
|
||||
|
||||
// convert number to K, M, B, T, etc.
|
||||
export const compactNumber = Intl.NumberFormat("en", { notation: "compact" });
|
||||
|
||||
// country name
|
||||
export const regionNames = new Intl.DisplayNames(["en"], { type: "language" });
|
||||
|
886
pnpm-lock.yaml
886
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user