refactor: replace nip96 with blossom
feat: blossom fallback image loader
This commit is contained in:
@ -1,6 +1,6 @@
|
||||
import { IMeta } from "@snort/system";
|
||||
import classNames from "classnames";
|
||||
import React, { CSSProperties, useEffect, useMemo, useRef } from "react";
|
||||
import React, { CSSProperties, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useInView } from "react-intersection-observer";
|
||||
|
||||
import { ProxyImg } from "@/Components/ProxyImg";
|
||||
@ -45,6 +45,8 @@ const ImageElement = ({ url, meta, onMediaClick, size }: ImageElementProps) => {
|
||||
return style;
|
||||
}, [imageRef?.current, meta]);
|
||||
|
||||
const [alternatives, setAlternatives] = useState<Array<string>>(meta?.fallback ?? []);
|
||||
const [currentUrl, setCurrentUrl] = useState<string>(url);
|
||||
return (
|
||||
<div
|
||||
className={classNames("flex items-center -mx-4 md:mx-0 my-2", {
|
||||
@ -52,8 +54,8 @@ const ImageElement = ({ url, meta, onMediaClick, size }: ImageElementProps) => {
|
||||
"cursor-pointer": onMediaClick,
|
||||
})}>
|
||||
<ProxyImg
|
||||
key={url}
|
||||
src={url}
|
||||
key={currentUrl}
|
||||
src={currentUrl}
|
||||
size={size}
|
||||
sha256={meta?.sha256}
|
||||
onClick={onMediaClick}
|
||||
@ -62,6 +64,14 @@ const ImageElement = ({ url, meta, onMediaClick, size }: ImageElementProps) => {
|
||||
})}
|
||||
style={style}
|
||||
ref={imageRef}
|
||||
onError={() => {
|
||||
const next = alternatives.at(0);
|
||||
if (next) {
|
||||
console.warn("IMG FALLBACK", "Failed to load url, trying next: ", next);
|
||||
setAlternatives(z => z.filter(y => y !== next));
|
||||
setCurrentUrl(next);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,9 +1,9 @@
|
||||
/* eslint-disable max-lines */
|
||||
import { fetchNip05Pubkey, unixNow } from "@snort/shared";
|
||||
import {
|
||||
addExtensionToNip94Url,
|
||||
EventBuilder,
|
||||
EventKind,
|
||||
Nip94Tags,
|
||||
nip94TagsToIMeta,
|
||||
NostrLink,
|
||||
NostrPrefix,
|
||||
@ -158,15 +158,32 @@ export function NoteCreator() {
|
||||
extraTags.push(...note.hashTags.map(a => ["t", a.toLowerCase()]));
|
||||
}
|
||||
|
||||
for (const ex of note.otherEvents ?? []) {
|
||||
const meta = readNip94Tags(ex.tags);
|
||||
if (!meta.url) continue;
|
||||
if (!note.note.endsWith("\n")) {
|
||||
note.note += "\n";
|
||||
}
|
||||
note.note += addExtensionToNip94Url(meta);
|
||||
// attach 1 link and use other duplicates as fallback urls
|
||||
for (const [, v] of Object.entries(note.attachments ?? {})) {
|
||||
const at = v[0];
|
||||
note.note += note.note.length > 0 ? `\n${at.url}` : at.url;
|
||||
console.debug(at);
|
||||
const n94 =
|
||||
(at.nip94?.length ?? 0) > 0
|
||||
? readNip94Tags(at.nip94!)
|
||||
: ({
|
||||
url: at.url,
|
||||
hash: at.sha256,
|
||||
size: at.size,
|
||||
mimeType: at.type,
|
||||
} as Nip94Tags);
|
||||
|
||||
// attach fallbacks
|
||||
n94.fallback ??= [];
|
||||
n94.fallback.push(
|
||||
...v
|
||||
.slice(1)
|
||||
.filter(a => a.url)
|
||||
.map(a => a.url!),
|
||||
);
|
||||
|
||||
extraTags ??= [];
|
||||
extraTags.push(nip94TagsToIMeta(meta));
|
||||
extraTags.push(nip94TagsToIMeta(n94));
|
||||
}
|
||||
|
||||
// add quote repost
|
||||
@ -272,20 +289,12 @@ export function NoteCreator() {
|
||||
async function uploadFile(file: File) {
|
||||
try {
|
||||
if (file && uploader) {
|
||||
const rx = await uploader.upload(file, file.name);
|
||||
const rx = await uploader.upload(file);
|
||||
note.update(v => {
|
||||
if (rx.header) {
|
||||
v.otherEvents ??= [];
|
||||
v.otherEvents.push(rx.header);
|
||||
} else if (rx.url) {
|
||||
v.note = `${v.note ? `${v.note}\n` : ""}${rx.url}`;
|
||||
if (rx.metadata) {
|
||||
v.extraTags ??= [];
|
||||
const imeta = nip94TagsToIMeta(rx.metadata);
|
||||
v.extraTags.push(imeta);
|
||||
}
|
||||
} else if (rx?.error) {
|
||||
v.error = rx.error;
|
||||
if (rx.url) {
|
||||
v.attachments ??= {};
|
||||
v.attachments[rx.sha256] ??= [];
|
||||
v.attachments[rx.sha256].push(rx);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -677,31 +686,25 @@ export function NoteCreator() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{(note.otherEvents?.length ?? 0) > 0 && !note.preview && (
|
||||
{Object.entries(note.attachments ?? {}).length > 0 && !note.preview && (
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{note.otherEvents
|
||||
?.map(a => ({
|
||||
event: a,
|
||||
tags: readNip94Tags(a.tags),
|
||||
}))
|
||||
.filter(a => a.tags.url)
|
||||
.map(a => (
|
||||
<div key={a.tags.url} className="relative">
|
||||
<img
|
||||
className="object-cover w-[80px] h-[80px] !mt-0 rounded-lg"
|
||||
src={addExtensionToNip94Url(a.tags)}
|
||||
/>
|
||||
<Icon
|
||||
name="x"
|
||||
className="absolute -top-[0.25rem] -right-[0.25rem] bg-gray rounded-full cursor-pointer"
|
||||
onClick={() =>
|
||||
note.update(
|
||||
n => (n.otherEvents = n.otherEvents?.filter(b => readNip94Tags(b.tags).url !== a.tags.url)),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{Object.entries(note.attachments ?? {}).map(([k, v]) => (
|
||||
<div key={k} className="relative">
|
||||
<img className="object-cover w-[80px] h-[80px] !mt-0 rounded-lg" src={v[0].url} />
|
||||
<Icon
|
||||
name="x"
|
||||
className="absolute -top-[0.25rem] -right-[0.25rem] bg-gray rounded-full cursor-pointer"
|
||||
onClick={() =>
|
||||
note.update(n => {
|
||||
if (n.attachments?.[k]) {
|
||||
delete n.attachments[k];
|
||||
}
|
||||
return n;
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{noteCreatorFooter()}
|
||||
@ -733,8 +736,11 @@ export function NoteCreator() {
|
||||
<MediaServerFileList
|
||||
onPicked={files => {
|
||||
note.update(n => {
|
||||
n.otherEvents ??= [];
|
||||
n.otherEvents?.push(...files);
|
||||
for (const x of files) {
|
||||
n.attachments ??= {};
|
||||
n.attachments[x.sha256] ??= [];
|
||||
n.attachments[x.sha256].push(x);
|
||||
}
|
||||
n.filePicker = "hidden";
|
||||
});
|
||||
}}
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { NostrEvent } from "@snort/system";
|
||||
import classNames from "classnames";
|
||||
import { useEffect, useState } from "react";
|
||||
import { FormattedMessage, FormattedNumber } from "react-intl";
|
||||
@ -7,8 +6,7 @@ import useEventPublisher from "@/Hooks/useEventPublisher";
|
||||
import useImgProxy from "@/Hooks/useImgProxy";
|
||||
import useLogin from "@/Hooks/useLogin";
|
||||
import { useMediaServerList } from "@/Hooks/useMediaServerList";
|
||||
import { findTag } from "@/Utils";
|
||||
import { Nip96Uploader } from "@/Utils/Upload/Nip96";
|
||||
import { BlobDescriptor, Blossom } from "@/Utils/Upload/blossom";
|
||||
|
||||
import AsyncButton from "../Button/AsyncButton";
|
||||
|
||||
@ -16,12 +14,12 @@ export function MediaServerFileList({
|
||||
onPicked,
|
||||
cols,
|
||||
}: {
|
||||
onPicked: (files: Array<NostrEvent>) => void;
|
||||
onPicked: (files: Array<BlobDescriptor>) => void;
|
||||
cols?: number;
|
||||
}) {
|
||||
const { state } = useLogin(s => ({ v: s.state.version, state: s.state }));
|
||||
const { publisher } = useEventPublisher();
|
||||
const [fileList, setFilesList] = useState<Array<NostrEvent>>([]);
|
||||
const [fileList, setFilesList] = useState<Array<BlobDescriptor>>([]);
|
||||
const [pickedFiles, setPickedFiles] = useState<Array<string>>([]);
|
||||
const servers = useMediaServerList();
|
||||
|
||||
@ -30,11 +28,9 @@ export function MediaServerFileList({
|
||||
if (!publisher) return;
|
||||
for (const s of servers.servers) {
|
||||
try {
|
||||
const sx = new Nip96Uploader(s, publisher);
|
||||
const files = await sx.listFiles();
|
||||
if (files?.files) {
|
||||
res.push(...files.files);
|
||||
}
|
||||
const sx = new Blossom(s, publisher);
|
||||
const files = await sx.list(state.pubkey);
|
||||
res.push(...files);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
@ -42,14 +38,12 @@ export function MediaServerFileList({
|
||||
setFilesList(res);
|
||||
}
|
||||
|
||||
function toggleFile(ev: NostrEvent) {
|
||||
const hash = findTag(ev, "x");
|
||||
if (!hash) return;
|
||||
function toggleFile(b: BlobDescriptor) {
|
||||
setPickedFiles(a => {
|
||||
if (a.includes(hash)) {
|
||||
return a.filter(a => a != hash);
|
||||
if (a.includes(b.sha256)) {
|
||||
return a.filter(a => a != b.sha256);
|
||||
} else {
|
||||
return [...a, hash];
|
||||
return [...a, b.sha256];
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -58,6 +52,17 @@ export function MediaServerFileList({
|
||||
listFiles().catch(console.error);
|
||||
}, [servers.servers.length, state?.version]);
|
||||
|
||||
const finalFileList = fileList
|
||||
.sort((a, b) => (b.uploaded ?? 0) - (a.uploaded ?? 0))
|
||||
.reduce(
|
||||
(acc, v) => {
|
||||
acc[v.sha256] ??= [];
|
||||
acc[v.sha256].push(v);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, Array<BlobDescriptor>>,
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
@ -65,33 +70,25 @@ export function MediaServerFileList({
|
||||
"grid-cols-2": cols === 2 || cols === undefined,
|
||||
"grid-cols-6": cols === 6,
|
||||
})}>
|
||||
{fileList.map(a => (
|
||||
<Nip96File
|
||||
key={a.id}
|
||||
file={a}
|
||||
onClick={() => toggleFile(a)}
|
||||
checked={pickedFiles.includes(findTag(a, "x") ?? "")}
|
||||
/>
|
||||
{Object.entries(finalFileList).map(([k, v]) => (
|
||||
<ServerFile key={k} file={v[0]} onClick={() => toggleFile(v[0])} checked={pickedFiles.includes(k)} />
|
||||
))}
|
||||
</div>
|
||||
<AsyncButton
|
||||
disabled={pickedFiles.length === 0}
|
||||
onClick={() => onPicked(fileList.filter(a => pickedFiles.includes(findTag(a, "x") ?? "")))}>
|
||||
onClick={() => onPicked(fileList.filter(a => pickedFiles.includes(a.sha256)))}>
|
||||
<FormattedMessage defaultMessage="Select" />
|
||||
</AsyncButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Nip96File({ file, checked, onClick }: { file: NostrEvent; checked: boolean; onClick: () => void }) {
|
||||
const mime = findTag(file, "m");
|
||||
const url = findTag(file, "url");
|
||||
const size = findTag(file, "size");
|
||||
function ServerFile({ file, checked, onClick }: { file: BlobDescriptor; checked: boolean; onClick: () => void }) {
|
||||
const { proxy } = useImgProxy();
|
||||
|
||||
function backgroundImage() {
|
||||
if (url && (mime?.startsWith("image/") || mime?.startsWith("video/"))) {
|
||||
return `url(${proxy(url, 512)})`;
|
||||
if (file.url && (file.type?.startsWith("image/") || file.type?.startsWith("video/"))) {
|
||||
return `url(${proxy(file.url, 512)})`;
|
||||
}
|
||||
}
|
||||
|
||||
@ -103,26 +100,25 @@ function Nip96File({ file, checked, onClick }: { file: NostrEvent; checked: bool
|
||||
backgroundImage: backgroundImage(),
|
||||
}}>
|
||||
<div className="absolute w-full h-full opacity-0 bg-black hover:opacity-80 flex flex-col items-center justify-center gap-4">
|
||||
<div>{file.content.length === 0 ? <FormattedMessage defaultMessage="Untitled" /> : file.content}</div>
|
||||
<div>
|
||||
{Number(size) > 1024 * 1024 && (
|
||||
{file.size > 1024 * 1024 && (
|
||||
<FormattedMessage
|
||||
defaultMessage="{n}MiB"
|
||||
values={{
|
||||
n: <FormattedNumber value={Number(size) / 1024 / 1024} />,
|
||||
n: <FormattedNumber value={file.size / 1024 / 1024} />,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{Number(size) < 1024 * 1024 && (
|
||||
{file.size < 1024 * 1024 && (
|
||||
<FormattedMessage
|
||||
defaultMessage="{n}KiB"
|
||||
values={{
|
||||
n: <FormattedNumber value={Number(size) / 1024} />,
|
||||
n: <FormattedNumber value={file.size / 1024} />,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div>{new Date(file.created_at * 1000).toLocaleString()}</div>
|
||||
<div>{file.uploaded && new Date(file.uploaded * 1000).toLocaleString()}</div>
|
||||
</div>
|
||||
<div
|
||||
className={classNames("w-4 h-4 border border-2 rounded-full right-1 top-1 absolute", {
|
||||
|
Reference in New Issue
Block a user