mirror of
https://github.com/luminous-devs/lume.git
synced 2024-10-01 01:10:48 +00:00
chore: remove storage layer
This commit is contained in:
parent
9127d5c5ea
commit
e9ce932646
@ -15,10 +15,15 @@ function Home() {
|
|||||||
const ark = useArk();
|
const ark = useArk();
|
||||||
const { data, hasNextPage, isLoading, isFetchingNextPage, fetchNextPage } =
|
const { data, hasNextPage, isLoading, isFetchingNextPage, fetchNextPage } =
|
||||||
useInfiniteQuery({
|
useInfiniteQuery({
|
||||||
queryKey: ["timeline"],
|
queryKey: ["local_timeline"],
|
||||||
initialPageParam: 0,
|
initialPageParam: 0,
|
||||||
queryFn: async ({ pageParam }: { pageParam: number }) => {
|
queryFn: async ({ pageParam }: { pageParam: number }) => {
|
||||||
const events = await ark.get_text_events(FETCH_LIMIT, pageParam, true);
|
const events = await ark.get_events(
|
||||||
|
"local",
|
||||||
|
FETCH_LIMIT,
|
||||||
|
pageParam,
|
||||||
|
true,
|
||||||
|
);
|
||||||
return events;
|
return events;
|
||||||
},
|
},
|
||||||
getNextPageParam: (lastPage) => {
|
getNextPageParam: (lastPage) => {
|
||||||
|
@ -106,7 +106,8 @@ export class Ark {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async get_text_events(
|
public async get_events(
|
||||||
|
type: "local" | "global",
|
||||||
limit: number,
|
limit: number,
|
||||||
asOf?: number,
|
asOf?: number,
|
||||||
dedup?: boolean,
|
dedup?: boolean,
|
||||||
@ -118,7 +119,7 @@ export class Ark {
|
|||||||
const seenIds = new Set<string>();
|
const seenIds = new Set<string>();
|
||||||
const dedupQueue = new Set<string>();
|
const dedupQueue = new Set<string>();
|
||||||
|
|
||||||
const nostrEvents: Event[] = await invoke("get_local_events", {
|
const nostrEvents: Event[] = await invoke(`get_${type}_events`, {
|
||||||
limit,
|
limit,
|
||||||
until,
|
until,
|
||||||
});
|
});
|
||||||
@ -287,6 +288,36 @@ export class Ark {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async get_contact_list() {
|
||||||
|
try {
|
||||||
|
const cmd: string[] = await invoke("get_contact_list");
|
||||||
|
return cmd;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async follow(id: string, alias?: string) {
|
||||||
|
try {
|
||||||
|
const cmd: string = await invoke("follow", { id, alias });
|
||||||
|
return cmd;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async unfollow(id: string) {
|
||||||
|
try {
|
||||||
|
const cmd: string = await invoke("unfollow", { id });
|
||||||
|
return cmd;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async user_to_bech32(key: string, relays: string[]) {
|
public async user_to_bech32(key: string, relays: string[]) {
|
||||||
try {
|
try {
|
||||||
const cmd: string = await invoke("user_to_bech32", {
|
const cmd: string = await invoke("user_to_bech32", {
|
||||||
|
@ -9,15 +9,28 @@ export function NoteReaction() {
|
|||||||
const event = useNoteContext();
|
const event = useNoteContext();
|
||||||
|
|
||||||
const [reaction, setReaction] = useState<"+" | "-">(null);
|
const [reaction, setReaction] = useState<"+" | "-">(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const up = async () => {
|
const up = async () => {
|
||||||
|
// start loading
|
||||||
|
setLoading(false);
|
||||||
|
|
||||||
const res = await ark.upvote(event.id, event.pubkey);
|
const res = await ark.upvote(event.id, event.pubkey);
|
||||||
if (res) setReaction("+");
|
if (res) setReaction("+");
|
||||||
|
|
||||||
|
// stop loading
|
||||||
|
setLoading(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const down = async () => {
|
const down = async () => {
|
||||||
|
// start loading
|
||||||
|
setLoading(false);
|
||||||
|
|
||||||
const res = await ark.downvote(event.id, event.pubkey);
|
const res = await ark.downvote(event.id, event.pubkey);
|
||||||
if (res) setReaction("-");
|
if (res) setReaction("-");
|
||||||
|
|
||||||
|
// stop loading
|
||||||
|
setLoading(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -25,7 +38,7 @@ export function NoteReaction() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={up}
|
onClick={up}
|
||||||
disabled={!!reaction}
|
disabled={!!reaction || loading}
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex size-7 items-center justify-center rounded-full",
|
"inline-flex size-7 items-center justify-center rounded-full",
|
||||||
reaction === "+"
|
reaction === "+"
|
||||||
@ -38,7 +51,7 @@ export function NoteReaction() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={down}
|
onClick={down}
|
||||||
disabled={!!reaction}
|
disabled={!!reaction || loading}
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex size-7 items-center justify-center rounded-full",
|
"inline-flex size-7 items-center justify-center rounded-full",
|
||||||
reaction === "-"
|
reaction === "-"
|
||||||
|
@ -1,260 +1,12 @@
|
|||||||
import { webln } from "@getalby/sdk";
|
import { ZapIcon } from "@lume/icons";
|
||||||
import { type SendPaymentResponse } from "@getalby/sdk/dist/types";
|
|
||||||
import { CancelIcon, LoaderIcon, ZapIcon } from "@lume/icons";
|
|
||||||
import { useStorage } from "@lume/storage";
|
|
||||||
import { cn, compactNumber, displayNpub } from "@lume/utils";
|
|
||||||
import * as Dialog from "@radix-ui/react-dialog";
|
|
||||||
import * as Tooltip from "@radix-ui/react-tooltip";
|
|
||||||
import { QRCodeSVG } from "qrcode.react";
|
|
||||||
import { useState } from "react";
|
|
||||||
import CurrencyInput from "react-currency-input-field";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { useNoteContext } from "../provider";
|
|
||||||
import { useProfile } from "@lume/ark";
|
|
||||||
|
|
||||||
export function NoteZap() {
|
export function NoteZap() {
|
||||||
const storage = useStorage();
|
|
||||||
const event = useNoteContext();
|
|
||||||
|
|
||||||
const [amount, setAmount] = useState<string>("21");
|
|
||||||
const [zapMessage, setZapMessage] = useState<string>("");
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const [isCompleted, setIsCompleted] = useState(false);
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const [invoice, setInvoice] = useState<string>(null);
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { user } = useProfile(event.pubkey);
|
|
||||||
|
|
||||||
const createZapRequest = async (instant?: boolean) => {
|
|
||||||
if (instant && !storage.nwc) return;
|
|
||||||
|
|
||||||
let nwc: webln.NostrWebLNProvider = undefined;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// start loading
|
|
||||||
setIsLoading(true);
|
|
||||||
|
|
||||||
const zapAmount = parseInt(amount) * 1000;
|
|
||||||
const res = await event.zap(zapAmount, zapMessage);
|
|
||||||
|
|
||||||
if (!storage.nwc) return setInvoice(res);
|
|
||||||
|
|
||||||
// user connect nwc
|
|
||||||
nwc = new webln.NostrWebLNProvider({
|
|
||||||
nostrWalletConnectUrl: storage.nwc,
|
|
||||||
});
|
|
||||||
await nwc.enable();
|
|
||||||
|
|
||||||
// send payment via nwc
|
|
||||||
const send: SendPaymentResponse = await nwc.sendPayment(res);
|
|
||||||
|
|
||||||
if (send) {
|
|
||||||
toast.success(
|
|
||||||
`You've zapped ${compactNumber.format(send.amount)} sats to ${
|
|
||||||
user?.name || user?.displayName || "anon"
|
|
||||||
}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// reset after 1.5 secs
|
|
||||||
if (!instant) {
|
|
||||||
const timeout = setTimeout(() => setIsCompleted(false), 1500);
|
|
||||||
clearTimeout(timeout);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// eose
|
|
||||||
nwc.close();
|
|
||||||
|
|
||||||
// update state
|
|
||||||
setIsCompleted(true);
|
|
||||||
setIsLoading(false);
|
|
||||||
} catch (e) {
|
|
||||||
nwc?.close();
|
|
||||||
setIsLoading(false);
|
|
||||||
toast.error(String(e));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (storage.settings.instantZap) {
|
|
||||||
return (
|
|
||||||
<Tooltip.Provider>
|
|
||||||
<Tooltip.Root delayDuration={150}>
|
|
||||||
<Tooltip.Trigger asChild>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => createZapRequest(true)}
|
|
||||||
className="group inline-flex h-7 w-7 items-center justify-center text-neutral-800 dark:text-neutral-200"
|
|
||||||
>
|
|
||||||
{isLoading ? (
|
|
||||||
<LoaderIcon className="size-4 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<ZapIcon
|
|
||||||
className={cn(
|
|
||||||
"size-5 group-hover:text-blue-500",
|
|
||||||
isCompleted ? "text-blue-500" : "",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</Tooltip.Trigger>
|
|
||||||
<Tooltip.Portal>
|
|
||||||
<Tooltip.Content className="data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade inline-flex h-7 select-none items-center justify-center rounded-md bg-neutral-950 px-3.5 text-sm text-neutral-50 will-change-[transform,opacity] dark:bg-neutral-50 dark:text-neutral-950">
|
|
||||||
{t("note.zap.tooltip")}
|
|
||||||
<Tooltip.Arrow className="fill-neutral-950 dark:fill-neutral-50" />
|
|
||||||
</Tooltip.Content>
|
|
||||||
</Tooltip.Portal>
|
|
||||||
</Tooltip.Root>
|
|
||||||
</Tooltip.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog.Root open={isOpen} onOpenChange={setIsOpen}>
|
<button
|
||||||
<Tooltip.Provider>
|
type="button"
|
||||||
<Tooltip.Root delayDuration={150}>
|
className="group inline-flex h-7 w-7 items-center justify-center text-neutral-800 dark:text-neutral-200"
|
||||||
<Dialog.Trigger asChild>
|
>
|
||||||
<Tooltip.Trigger asChild>
|
<ZapIcon className="size-5 group-hover:text-blue-500" />
|
||||||
<button
|
</button>
|
||||||
type="button"
|
|
||||||
className="group inline-flex h-7 w-7 items-center justify-center text-neutral-800 dark:text-neutral-200"
|
|
||||||
>
|
|
||||||
<ZapIcon className="size-5 group-hover:text-blue-500" />
|
|
||||||
</button>
|
|
||||||
</Tooltip.Trigger>
|
|
||||||
</Dialog.Trigger>
|
|
||||||
<Tooltip.Portal>
|
|
||||||
<Tooltip.Content className="data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade inline-flex h-7 select-none items-center justify-center rounded-md bg-neutral-950 px-3.5 text-sm text-neutral-50 will-change-[transform,opacity] dark:bg-neutral-50 dark:text-neutral-950">
|
|
||||||
{t("note.zap.tooltip")}
|
|
||||||
<Tooltip.Arrow className="fill-neutral-950 dark:fill-neutral-50" />
|
|
||||||
</Tooltip.Content>
|
|
||||||
</Tooltip.Portal>
|
|
||||||
</Tooltip.Root>
|
|
||||||
</Tooltip.Provider>
|
|
||||||
<Dialog.Portal>
|
|
||||||
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/20 backdrop-blur-sm dark:bg-white/20" />
|
|
||||||
<Dialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
|
|
||||||
<Dialog.Close className="absolute right-5 top-5 z-50">
|
|
||||||
<div className="flex flex-col gap-1.5">
|
|
||||||
<div className="inline-flex size-10 items-center justify-center rounded-lg bg-white dark:bg-black">
|
|
||||||
<CancelIcon className="size-5" />
|
|
||||||
</div>
|
|
||||||
<span className="text-sm font-medium">Esc</span>
|
|
||||||
</div>
|
|
||||||
</Dialog.Close>
|
|
||||||
<div className="relative h-min w-full max-w-xl rounded-xl bg-white dark:bg-black">
|
|
||||||
<div className="inline-flex w-full shrink-0 items-center justify-center px-5 py-3">
|
|
||||||
<div className="w-6" />
|
|
||||||
<Dialog.Title className="text-center font-semibold">
|
|
||||||
{t("note.zap.modalTitle")}{" "}
|
|
||||||
{user?.name ||
|
|
||||||
user?.displayName ||
|
|
||||||
displayNpub(event.pubkey, 16)}
|
|
||||||
</Dialog.Title>
|
|
||||||
</div>
|
|
||||||
{!invoice ? (
|
|
||||||
<div className="overflow-y-auto overflow-x-hidden px-5 pb-5">
|
|
||||||
<div className="relative flex h-36 flex-col">
|
|
||||||
<div className="inline-flex h-full flex-1 items-center justify-center gap-1">
|
|
||||||
<CurrencyInput
|
|
||||||
placeholder="0"
|
|
||||||
defaultValue={"21"}
|
|
||||||
value={amount}
|
|
||||||
decimalsLimit={2}
|
|
||||||
min={0} // 0 sats
|
|
||||||
max={10000} // 1M sats
|
|
||||||
maxLength={10000} // 1M sats
|
|
||||||
onValueChange={(value) => setAmount(value)}
|
|
||||||
className="w-full flex-1 border-none bg-transparent text-right text-4xl font-semibold placeholder:text-neutral-600 focus:outline-none focus:ring-0 dark:text-neutral-400"
|
|
||||||
/>
|
|
||||||
<span className="w-full flex-1 text-left text-4xl font-semibold text-neutral-500 dark:text-neutral-400">
|
|
||||||
sats
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="inline-flex items-center justify-center gap-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setAmount("69")}
|
|
||||||
className="w-max rounded-full bg-neutral-100 px-2.5 py-1 text-sm font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
|
|
||||||
>
|
|
||||||
69 sats
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setAmount("100")}
|
|
||||||
className="w-max rounded-full bg-neutral-100 px-2.5 py-1 text-sm font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
|
|
||||||
>
|
|
||||||
100 sats
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setAmount("200")}
|
|
||||||
className="w-max rounded-full bg-neutral-100 px-2.5 py-1 text-sm font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
|
|
||||||
>
|
|
||||||
200 sats
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setAmount("500")}
|
|
||||||
className="w-max rounded-full bg-neutral-100 px-2.5 py-1 text-sm font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
|
|
||||||
>
|
|
||||||
500 sats
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setAmount("1000")}
|
|
||||||
className="w-max rounded-full bg-neutral-100 px-2.5 py-1 text-sm font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
|
|
||||||
>
|
|
||||||
1K sats
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-4 flex w-full flex-col gap-2">
|
|
||||||
<input
|
|
||||||
name="zapMessage"
|
|
||||||
value={zapMessage}
|
|
||||||
onChange={(e) => setZapMessage(e.target.value)}
|
|
||||||
spellCheck={false}
|
|
||||||
autoComplete="off"
|
|
||||||
autoCorrect="off"
|
|
||||||
autoCapitalize="off"
|
|
||||||
placeholder={t("note.zap.messagePlaceholder")}
|
|
||||||
className="w-full resize-none rounded-lg border-transparent bg-neutral-100 px-3 py-3 !outline-none placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-950 dark:text-neutral-400"
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => createZapRequest()}
|
|
||||||
className="inline-flex h-9 w-full items-center justify-center rounded-lg border-t border-neutral-900 bg-neutral-950 pb-[2px] font-semibold text-neutral-50 hover:bg-neutral-900 dark:border-neutral-800 dark:bg-neutral-900 dark:hover:bg-neutral-800"
|
|
||||||
>
|
|
||||||
{isCompleted
|
|
||||||
? t("note.zap.buttonFinish")
|
|
||||||
: isLoading
|
|
||||||
? t("note.zap.buttonLoading")
|
|
||||||
: t("note.zap.zap")}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex flex-col items-center justify-center gap-4 px-5 pb-5">
|
|
||||||
<div className="rounded-lg bg-neutral-100 p-3 dark:bg-neutral-900">
|
|
||||||
<QRCodeSVG value={invoice} size={256} />
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col items-center gap-1">
|
|
||||||
<h3 className="text-lg font-medium">
|
|
||||||
{t("note.zap.invoiceButton")}
|
|
||||||
</h3>
|
|
||||||
<span className="text-center text-sm text-neutral-600 dark:text-neutral-400">
|
|
||||||
{t("note.zap.invoiceFooter")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Dialog.Content>
|
|
||||||
</Dialog.Portal>
|
|
||||||
</Dialog.Root>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { useStorage } from "@lume/storage";
|
|
||||||
import { Kind } from "@lume/types";
|
import { Kind } from "@lume/types";
|
||||||
import {
|
import {
|
||||||
AUDIOS,
|
AUDIOS,
|
||||||
@ -8,14 +7,11 @@ import {
|
|||||||
VIDEOS,
|
VIDEOS,
|
||||||
canPreview,
|
canPreview,
|
||||||
cn,
|
cn,
|
||||||
regionNames,
|
|
||||||
} from "@lume/utils";
|
} from "@lume/utils";
|
||||||
import { fetch } from "@tauri-apps/plugin-http";
|
|
||||||
import getUrls from "get-urls";
|
import getUrls from "get-urls";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { ReactNode, useMemo, useState } from "react";
|
import { ReactNode, useMemo } from "react";
|
||||||
import reactStringReplace from "react-string-replace";
|
import reactStringReplace from "react-string-replace";
|
||||||
import { toast } from "sonner";
|
|
||||||
import { stripHtml } from "string-strip-html";
|
import { stripHtml } from "string-strip-html";
|
||||||
import { Hashtag } from "./mentions/hashtag";
|
import { Hashtag } from "./mentions/hashtag";
|
||||||
import { MentionNote } from "./mentions/note";
|
import { MentionNote } from "./mentions/note";
|
||||||
@ -27,19 +23,12 @@ import { VideoPreview } from "./preview/video";
|
|||||||
import { useNoteContext } from "./provider";
|
import { useNoteContext } from "./provider";
|
||||||
|
|
||||||
export function NoteContent({ className }: { className?: string }) {
|
export function NoteContent({ className }: { className?: string }) {
|
||||||
const storage = useStorage();
|
|
||||||
const event = useNoteContext();
|
const event = useNoteContext();
|
||||||
|
|
||||||
const [content, setContent] = useState(event.content);
|
|
||||||
const [translate, setTranslate] = useState({
|
|
||||||
translatable: false,
|
|
||||||
translated: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const richContent = useMemo(() => {
|
const richContent = useMemo(() => {
|
||||||
if (event.kind !== Kind.Text) return content;
|
if (event.kind !== Kind.Text) return event.content;
|
||||||
|
|
||||||
let parsedContent: string | ReactNode[] = stripHtml(content).result;
|
let parsedContent: string | ReactNode[] = stripHtml(event.content).result;
|
||||||
let linkPreview: string = undefined;
|
let linkPreview: string = undefined;
|
||||||
let images: string[] = [];
|
let images: string[] = [];
|
||||||
let videos: string[] = [];
|
let videos: string[] = [];
|
||||||
@ -50,32 +39,32 @@ export function NoteContent({ className }: { className?: string }) {
|
|||||||
const words = text.split(/( |\n)/);
|
const words = text.split(/( |\n)/);
|
||||||
const urls = [...getUrls(text)];
|
const urls = [...getUrls(text)];
|
||||||
|
|
||||||
if (storage.settings.media && !storage.settings.lowPower) {
|
images = urls.filter((word) =>
|
||||||
images = urls.filter((word) =>
|
IMAGES.some((el) => {
|
||||||
IMAGES.some((el) => {
|
const url = new URL(word);
|
||||||
const url = new URL(word);
|
const extension = url.pathname.split(".")[1];
|
||||||
const extension = url.pathname.split(".")[1];
|
if (extension === el) return true;
|
||||||
if (extension === el) return true;
|
return false;
|
||||||
return false;
|
}),
|
||||||
}),
|
);
|
||||||
);
|
|
||||||
videos = urls.filter((word) =>
|
videos = urls.filter((word) =>
|
||||||
VIDEOS.some((el) => {
|
VIDEOS.some((el) => {
|
||||||
const url = new URL(word);
|
const url = new URL(word);
|
||||||
const extension = url.pathname.split(".")[1];
|
const extension = url.pathname.split(".")[1];
|
||||||
if (extension === el) return true;
|
if (extension === el) return true;
|
||||||
return false;
|
return false;
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
audios = urls.filter((word) =>
|
|
||||||
AUDIOS.some((el) => {
|
audios = urls.filter((word) =>
|
||||||
const url = new URL(word);
|
AUDIOS.some((el) => {
|
||||||
const extension = url.pathname.split(".")[1];
|
const url = new URL(word);
|
||||||
if (extension === el) return true;
|
const extension = url.pathname.split(".")[1];
|
||||||
return false;
|
if (extension === el) return true;
|
||||||
}),
|
return false;
|
||||||
);
|
}),
|
||||||
}
|
);
|
||||||
|
|
||||||
events = words.filter((word) =>
|
events = words.filter((word) =>
|
||||||
NOSTR_EVENTS.some((el) => word.startsWith(el)),
|
NOSTR_EVENTS.some((el) => word.startsWith(el)),
|
||||||
@ -121,9 +110,7 @@ export function NoteContent({ className }: { className?: string }) {
|
|||||||
for (const hashtag of hashtags) {
|
for (const hashtag of hashtags) {
|
||||||
const regex = new RegExp(`(|^)${hashtag}\\b`, "g");
|
const regex = new RegExp(`(|^)${hashtag}\\b`, "g");
|
||||||
parsedContent = reactStringReplace(parsedContent, regex, () => {
|
parsedContent = reactStringReplace(parsedContent, regex, () => {
|
||||||
if (storage.settings.hashtag)
|
return <Hashtag key={nanoid()} tag={hashtag} />;
|
||||||
return <Hashtag key={nanoid()} tag={hashtag} />;
|
|
||||||
return null;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -174,7 +161,7 @@ export function NoteContent({ className }: { className?: string }) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
parsedContent = reactStringReplace(parsedContent, "\n", () => {
|
parsedContent = reactStringReplace(parsedContent, "\n", () => {
|
||||||
return <div key={nanoid()} />;
|
return <br key={nanoid()} />;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (typeof parsedContent[0] === "string") {
|
if (typeof parsedContent[0] === "string") {
|
||||||
@ -186,35 +173,7 @@ export function NoteContent({ className }: { className?: string }) {
|
|||||||
console.warn(event.id, `[parser] parse failed: ${e}`);
|
console.warn(event.id, `[parser] parse failed: ${e}`);
|
||||||
return parsedContent;
|
return parsedContent;
|
||||||
}
|
}
|
||||||
}, [content]);
|
}, []);
|
||||||
|
|
||||||
const translateContent = async () => {
|
|
||||||
try {
|
|
||||||
if (!translate.translatable) return;
|
|
||||||
|
|
||||||
const res = await fetch("https://translate.nostr.wine/translate", {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({
|
|
||||||
q: event.content,
|
|
||||||
target: storage.locale.slice(0, 2),
|
|
||||||
api_key: storage.settings.translateApiKey,
|
|
||||||
}),
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok)
|
|
||||||
toast.error(
|
|
||||||
"Cannot connect to translate service, please try again later.",
|
|
||||||
);
|
|
||||||
|
|
||||||
const data = await res.json();
|
|
||||||
|
|
||||||
setContent(data.translatedText);
|
|
||||||
setTranslate((state) => ({ ...state, translated: true }));
|
|
||||||
} catch (e) {
|
|
||||||
console.error("translate api: ", String(e));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (event.kind !== Kind.Text) {
|
if (event.kind !== Kind.Text) {
|
||||||
return <NIP89 className={className} />;
|
return <NIP89 className={className} />;
|
||||||
@ -225,25 +184,6 @@ export function NoteContent({ className }: { className?: string }) {
|
|||||||
<div className="content-break select-text whitespace-pre-line text-balance leading-normal">
|
<div className="content-break select-text whitespace-pre-line text-balance leading-normal">
|
||||||
{richContent}
|
{richContent}
|
||||||
</div>
|
</div>
|
||||||
{storage.settings.translation && translate.translatable ? (
|
|
||||||
translate.translated ? (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setContent(event.content)}
|
|
||||||
className="mt-3 border-none text-sm text-blue-500 shadow-none hover:text-blue-600 focus:outline-none"
|
|
||||||
>
|
|
||||||
Show original content
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={translateContent}
|
|
||||||
className="mt-3 border-none text-sm text-blue-500 shadow-none hover:text-blue-600 focus:outline-none"
|
|
||||||
>
|
|
||||||
Translate to {regionNames.of(storage.locale)}
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { useStorage } from "@lume/storage";
|
|
||||||
import { cn } from "@lume/utils";
|
import { cn } from "@lume/utils";
|
||||||
import * as Avatar from "@radix-ui/react-avatar";
|
import * as Avatar from "@radix-ui/react-avatar";
|
||||||
import { minidenticon } from "minidenticons";
|
import { minidenticon } from "minidenticons";
|
||||||
@ -7,59 +6,45 @@ import { useMemo } from "react";
|
|||||||
import { useUserContext } from "./provider";
|
import { useUserContext } from "./provider";
|
||||||
|
|
||||||
export function UserAvatar({ className }: { className?: string }) {
|
export function UserAvatar({ className }: { className?: string }) {
|
||||||
const user = useUserContext();
|
const user = useUserContext();
|
||||||
const storage = useStorage();
|
|
||||||
|
|
||||||
const fallbackAvatar = useMemo(
|
const fallbackAvatar = useMemo(
|
||||||
() =>
|
() =>
|
||||||
`data:image/svg+xml;utf8,${encodeURIComponent(
|
`data:image/svg+xml;utf8,${encodeURIComponent(
|
||||||
minidenticon(user?.pubkey || nanoid(), 90, 50),
|
minidenticon(user?.pubkey || nanoid(), 90, 50),
|
||||||
)}`,
|
)}`,
|
||||||
[user],
|
[user],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!user.profile) {
|
if (!user.profile) {
|
||||||
return (
|
return (
|
||||||
<div className="shrink-0">
|
<div className="shrink-0">
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-black/20 dark:bg-white/20 rounded animate-pulse",
|
"animate-pulse rounded bg-black/20 dark:bg-white/20",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Avatar.Root className="shrink-0">
|
<Avatar.Root className="shrink-0">
|
||||||
{storage.settings.lowPower ? (
|
<Avatar.Image
|
||||||
<Avatar.Image
|
src={user.profile.picture}
|
||||||
src={fallbackAvatar}
|
alt={user.pubkey}
|
||||||
alt={user.pubkey}
|
loading="eager"
|
||||||
loading="eager"
|
decoding="async"
|
||||||
decoding="async"
|
className={cn("ring-1 ring-black/5 dark:ring-white/5", className)}
|
||||||
className={cn(
|
/>
|
||||||
"bg-black dark:bg-white ring-1 ring-black/5 dark:ring-white/5",
|
<Avatar.Fallback delayMs={120}>
|
||||||
className,
|
<img
|
||||||
)}
|
src={fallbackAvatar}
|
||||||
/>
|
alt={user.pubkey}
|
||||||
) : (
|
className={cn("bg-black dark:bg-white", className)}
|
||||||
<Avatar.Image
|
/>
|
||||||
src={user.profile.picture}
|
</Avatar.Fallback>
|
||||||
alt={user.pubkey}
|
</Avatar.Root>
|
||||||
loading="eager"
|
);
|
||||||
decoding="async"
|
|
||||||
className={cn("ring-1 ring-black/5 dark:ring-white/5", className)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Avatar.Fallback delayMs={120}>
|
|
||||||
<img
|
|
||||||
src={fallbackAvatar}
|
|
||||||
alt={user.pubkey}
|
|
||||||
className={cn("bg-black dark:bg-white", className)}
|
|
||||||
/>
|
|
||||||
</Avatar.Fallback>
|
|
||||||
</Avatar.Root>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@ -3,15 +3,11 @@ import { LoaderIcon } from "@lume/icons";
|
|||||||
import { cn } from "@lume/utils";
|
import { cn } from "@lume/utils";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useUserContext } from "./provider";
|
||||||
|
|
||||||
export function UserFollowButton({
|
export function UserFollowButton({ className }: { className?: string }) {
|
||||||
target,
|
|
||||||
className,
|
|
||||||
}: {
|
|
||||||
target: string;
|
|
||||||
className?: string;
|
|
||||||
}) {
|
|
||||||
const ark = useArk();
|
const ark = useArk();
|
||||||
|
const user = useUserContext();
|
||||||
|
|
||||||
const [t] = useTranslation();
|
const [t] = useTranslation();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@ -20,10 +16,10 @@ export function UserFollowButton({
|
|||||||
const toggleFollow = async () => {
|
const toggleFollow = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
if (!followed) {
|
if (!followed) {
|
||||||
const add = await ark.createContact(target);
|
const add = await ark.follow(user.pubkey);
|
||||||
if (add) setFollowed(true);
|
if (add) setFollowed(true);
|
||||||
} else {
|
} else {
|
||||||
const remove = await ark.deleteContact(target);
|
const remove = await ark.unfollow(user.pubkey);
|
||||||
if (remove) setFollowed(false);
|
if (remove) setFollowed(false);
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@ -33,8 +29,8 @@ export function UserFollowButton({
|
|||||||
async function status() {
|
async function status() {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const contacts = await ark.getUserContacts();
|
const contacts = await ark.get_contact_list();
|
||||||
if (contacts?.includes(target)) {
|
if (contacts?.includes(user.pubkey)) {
|
||||||
setFollowed(true);
|
setFollowed(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
|
import { useArk } from "@lume/ark";
|
||||||
import { Metadata } from "@lume/types";
|
import { Metadata } from "@lume/types";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
|
||||||
import { ReactNode, createContext, useContext } from "react";
|
import { ReactNode, createContext, useContext } from "react";
|
||||||
|
|
||||||
const UserContext = createContext<{ pubkey: string; profile: Metadata }>(null);
|
const UserContext = createContext<{ pubkey: string; profile: Metadata }>(null);
|
||||||
@ -14,12 +14,13 @@ export function UserProvider({
|
|||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
embed?: string;
|
embed?: string;
|
||||||
}) {
|
}) {
|
||||||
|
const ark = useArk();
|
||||||
const { data: profile } = useQuery({
|
const { data: profile } = useQuery({
|
||||||
queryKey: ["user", pubkey],
|
queryKey: ["user", pubkey],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (embed) return JSON.parse(embed) as Metadata;
|
if (embed) return JSON.parse(embed) as Metadata;
|
||||||
try {
|
try {
|
||||||
const profile: Metadata = await invoke("get_profile", { id: pubkey });
|
const profile: Metadata = await ark.get_profile(pubkey);
|
||||||
return profile;
|
return profile;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(e);
|
throw new Error(e);
|
||||||
|
@ -47,15 +47,19 @@ fn main() {
|
|||||||
client
|
client
|
||||||
.add_relay("wss://nostr.mutinywallet.com")
|
.add_relay("wss://nostr.mutinywallet.com")
|
||||||
.await
|
.await
|
||||||
.expect("Failed to add bootstrap relay.");
|
.unwrap_or_default();
|
||||||
client
|
client
|
||||||
.add_relay("wss://bostr.yonle.lecturify.net")
|
.add_relay("wss://relay.nostr.band")
|
||||||
.await
|
.await
|
||||||
.expect("Failed to add bootstrap relay.");
|
.unwrap_or_default();
|
||||||
|
client
|
||||||
|
.add_relay("wss://relay.damus.io")
|
||||||
|
.await
|
||||||
|
.unwrap_or_default();
|
||||||
client
|
client
|
||||||
.add_relay("wss://purplepag.es")
|
.add_relay("wss://purplepag.es")
|
||||||
.await
|
.await
|
||||||
.expect("Failed to add bootstrap relay.");
|
.unwrap_or_default();
|
||||||
|
|
||||||
// Connect
|
// Connect
|
||||||
client.connect().await;
|
client.connect().await;
|
||||||
|
@ -48,7 +48,6 @@ pub async fn save_key(
|
|||||||
.get_contact_list_public_keys(Some(Duration::from_secs(10)))
|
.get_contact_list_public_keys(Some(Duration::from_secs(10)))
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
println!("total contacts: {}", list.len());
|
|
||||||
*contact_list = Some(list);
|
*contact_list = Some(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -161,7 +160,6 @@ pub async fn load_selected_account(
|
|||||||
.get_contact_list_public_keys(Some(Duration::from_secs(10)))
|
.get_contact_list_public_keys(Some(Duration::from_secs(10)))
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
println!("total contacts: {}", list.len());
|
|
||||||
*contact_list = Some(list);
|
*contact_list = Some(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user