feat: parse imeta
This commit is contained in:
parent
cb95032e7c
commit
fce7cc70a3
@ -1,23 +1,40 @@
|
||||
import { ProxyImg } from "@/Element/ProxyImg";
|
||||
import useImgProxy from "@/Hooks/useImgProxy";
|
||||
import React from "react";
|
||||
import { IMeta } from "@snort/system";
|
||||
import React, { CSSProperties, useMemo, useRef } from "react";
|
||||
|
||||
interface MediaElementProps {
|
||||
mime: string;
|
||||
url: string;
|
||||
magnet?: string;
|
||||
sha256?: string;
|
||||
blurHash?: string;
|
||||
meta?: IMeta;
|
||||
onMediaClick?: (e: React.MouseEvent<HTMLImageElement>) => void;
|
||||
}
|
||||
|
||||
export function MediaElement(props: MediaElementProps) {
|
||||
const { proxy } = useImgProxy();
|
||||
|
||||
const ref = useRef<HTMLImageElement | null>(null);
|
||||
const autoplay = window.innerWidth >= 768;
|
||||
|
||||
const style = useMemo(() => {
|
||||
const style = {} as CSSProperties;
|
||||
if (props.meta?.height && props.meta.width && ref.current) {
|
||||
const scale = ref.current.offsetWidth / props.meta.width;
|
||||
style.height = `${props.meta.height * scale}px`;
|
||||
}
|
||||
return style;
|
||||
}, [ref.current]);
|
||||
|
||||
if (props.mime.startsWith("image/")) {
|
||||
return <ProxyImg key={props.url} src={props.url} onClick={props.onMediaClick} className="max-h-[80vh] mx-auto" />;
|
||||
return (
|
||||
<ProxyImg
|
||||
key={props.url}
|
||||
src={props.url}
|
||||
onClick={props.onMediaClick}
|
||||
className="max-h-[80vh] mx-auto"
|
||||
style={style}
|
||||
ref={ref}
|
||||
/>
|
||||
);
|
||||
} else if (props.mime.startsWith("audio/")) {
|
||||
return <audio key={props.url} src={props.url} controls />;
|
||||
} else if (props.mime.startsWith("video/")) {
|
||||
@ -31,6 +48,7 @@ export function MediaElement(props: MediaElementProps) {
|
||||
controls
|
||||
poster={proxy(props.url)}
|
||||
className="max-h-[80vh] mx-auto"
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
|
@ -30,7 +30,15 @@ export function NostrFileElement({ ev }: { ev: NostrEvent }) {
|
||||
message={
|
||||
<FormattedMessage defaultMessage="Click to load content from {link}" id="lsNFM1" values={{ link: u }} />
|
||||
}>
|
||||
<MediaElement mime={m} url={u} sha256={x} magnet={magnet} blurHash={blurHash} />
|
||||
<MediaElement
|
||||
mime={m}
|
||||
url={u}
|
||||
meta={{
|
||||
sha256: x,
|
||||
magnet: magnet,
|
||||
blurHash: blurHash,
|
||||
}}
|
||||
/>
|
||||
</Reveal>
|
||||
);
|
||||
} else {
|
||||
|
@ -5,11 +5,13 @@ import Reveal from "@/Element/Event/Reveal";
|
||||
import useLogin from "@/Hooks/useLogin";
|
||||
import { MediaElement } from "@/Element/Embed/MediaElement";
|
||||
import { Link } from "react-router-dom";
|
||||
import { IMeta } from "@snort/system";
|
||||
|
||||
interface RevealMediaProps {
|
||||
creator: string;
|
||||
link: string;
|
||||
onMediaClick?: (e: React.MouseEvent<HTMLImageElement>) => void;
|
||||
meta?: IMeta;
|
||||
}
|
||||
|
||||
export default function RevealMedia(props: RevealMediaProps) {
|
||||
@ -66,10 +68,22 @@ export default function RevealMedia(props: RevealMediaProps) {
|
||||
}}
|
||||
/>
|
||||
}>
|
||||
<MediaElement mime={`${type}/${extension}`} url={url.toString()} onMediaClick={props.onMediaClick} />
|
||||
<MediaElement
|
||||
mime={`${type}/${extension}`}
|
||||
url={url.toString()}
|
||||
onMediaClick={props.onMediaClick}
|
||||
meta={props.meta}
|
||||
/>
|
||||
</Reveal>
|
||||
);
|
||||
} else {
|
||||
return <MediaElement mime={`${type}/${extension}`} url={url.toString()} onMediaClick={props.onMediaClick} />;
|
||||
return (
|
||||
<MediaElement
|
||||
mime={`${type}/${extension}`}
|
||||
url={url.toString()}
|
||||
onMediaClick={props.onMediaClick}
|
||||
meta={props.meta}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import useImgProxy from "@/Hooks/useImgProxy";
|
||||
import React, { HTMLProps, ReactNode, useState } from "react";
|
||||
import React, { HTMLProps, ReactNode, forwardRef, useState } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { getUrlHostname } from "@/SnortUtils";
|
||||
|
||||
@ -10,46 +10,49 @@ type ProxyImgProps = HTMLProps<HTMLImageElement> & {
|
||||
missingImageElement?: ReactNode;
|
||||
};
|
||||
|
||||
export const ProxyImg = ({ size, className, promptToLoadDirectly, missingImageElement, ...props }: ProxyImgProps) => {
|
||||
const { proxy } = useImgProxy();
|
||||
const [loadFailed, setLoadFailed] = useState(false);
|
||||
const [bypass, setBypass] = useState(CONFIG.bypassImgProxyError);
|
||||
export const ProxyImg = forwardRef<HTMLImageElement, ProxyImgProps>(
|
||||
({ size, className, promptToLoadDirectly, missingImageElement, ...props }: ProxyImgProps, ref) => {
|
||||
const { proxy } = useImgProxy();
|
||||
const [loadFailed, setLoadFailed] = useState(false);
|
||||
const [bypass, setBypass] = useState(CONFIG.bypassImgProxyError);
|
||||
|
||||
if (loadFailed && !bypass && (promptToLoadDirectly ?? true)) {
|
||||
if (loadFailed && !bypass && (promptToLoadDirectly ?? true)) {
|
||||
return (
|
||||
<div
|
||||
className="note-invoice error"
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
setBypass(true);
|
||||
}}>
|
||||
<FormattedMessage
|
||||
defaultMessage="Failed to proxy image from {host}, click here to load directly"
|
||||
id="65BmHb"
|
||||
values={{
|
||||
host: getUrlHostname(props.src),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const src = loadFailed && bypass ? props.src : proxy(props.src ?? "", size);
|
||||
if (!src || (loadFailed && !bypass)) return missingImageElement;
|
||||
return (
|
||||
<div
|
||||
className="note-invoice error"
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
setBypass(true);
|
||||
}}>
|
||||
<FormattedMessage
|
||||
defaultMessage="Failed to proxy image from {host}, click here to load directly"
|
||||
id="65BmHb"
|
||||
values={{
|
||||
host: getUrlHostname(props.src),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<img
|
||||
{...props}
|
||||
ref={ref}
|
||||
src={src}
|
||||
width={size}
|
||||
height={size}
|
||||
className={className}
|
||||
onError={e => {
|
||||
if (props.onError) {
|
||||
props.onError(e);
|
||||
} else {
|
||||
console.error("Failed to proxy image ", props.src);
|
||||
setLoadFailed(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const src = loadFailed && bypass ? props.src : proxy(props.src ?? "", size);
|
||||
if (!src || (loadFailed && !bypass)) return missingImageElement;
|
||||
return (
|
||||
<img
|
||||
{...props}
|
||||
src={src}
|
||||
width={size}
|
||||
height={size}
|
||||
className={className}
|
||||
onError={e => {
|
||||
if (props.onError) {
|
||||
props.onError(e);
|
||||
} else {
|
||||
console.error("Failed to proxy image ", props.src);
|
||||
setLoadFailed(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
},
|
||||
);
|
||||
|
@ -1,6 +1,6 @@
|
||||
import "./Text.css";
|
||||
import { ReactNode, useState } from "react";
|
||||
import { HexKey, ParsedFragment } from "@snort/system";
|
||||
import { HexKey, ParsedFragment, parseIMeta } from "@snort/system";
|
||||
import classNames from "classnames";
|
||||
|
||||
import Invoice from "@/Element/Embed/Invoice";
|
||||
@ -100,6 +100,7 @@ export default function Text({
|
||||
const elements = useTextTransformer(id, content, tags);
|
||||
|
||||
const images = elements.filter(a => a.type === "media" && a.mimeType?.startsWith("image")).map(a => a.content);
|
||||
const iMeta = parseIMeta(tags);
|
||||
|
||||
function renderContentWithHighlightedText(content: string, textToHighlight: string) {
|
||||
const textToHighlightArray = textToHighlight.trim().toLowerCase().split(" ");
|
||||
@ -136,22 +137,26 @@ export default function Text({
|
||||
</a>
|
||||
);
|
||||
|
||||
const RevealMediaInstance = ({ content }: { content: string }) => (
|
||||
<RevealMedia
|
||||
key={content}
|
||||
link={content}
|
||||
creator={creator}
|
||||
onMediaClick={e => {
|
||||
if (!disableMediaSpotlight) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setShowSpotlight(true);
|
||||
const selected = images.findIndex(b => b === content);
|
||||
setImageIdx(selected === -1 ? 0 : selected);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
const RevealMediaInstance = ({ content }: { content: string }) => {
|
||||
const imeta = iMeta?.[content];
|
||||
return (
|
||||
<RevealMedia
|
||||
key={content}
|
||||
link={content}
|
||||
creator={creator}
|
||||
meta={imeta}
|
||||
onMediaClick={e => {
|
||||
if (!disableMediaSpotlight) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setShowSpotlight(true);
|
||||
const selected = images.findIndex(b => b === content);
|
||||
setImageIdx(selected === -1 ? 0 : selected);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderContent = () => {
|
||||
let lenCtr = 0;
|
||||
|
@ -26,6 +26,7 @@ export default defineConfig({
|
||||
name: "snort",
|
||||
ifGitSHA: true,
|
||||
command: "git describe --always --tags",
|
||||
ifMeta: false,
|
||||
}),
|
||||
],
|
||||
assetsInclude: ["**/*.md", "**/*.wasm"],
|
||||
|
@ -32,6 +32,7 @@ export * from "./pow-util";
|
||||
export * from "./query-optimizer";
|
||||
export * from "./encrypted";
|
||||
export * from "./outbox-model";
|
||||
export { parseIMeta } from "./utils";
|
||||
|
||||
export * from "./impl/nip4";
|
||||
export * from "./impl/nip44";
|
||||
|
@ -81,3 +81,13 @@ export interface FullRelaySettings {
|
||||
}
|
||||
|
||||
export type NotSignedNostrEvent = Omit<NostrEvent, "sig">;
|
||||
|
||||
export interface IMeta {
|
||||
magnet?: string;
|
||||
sha256?: string;
|
||||
blurHash?: string;
|
||||
height?: number;
|
||||
width?: number;
|
||||
alt?: string;
|
||||
fallback?: Array<string>;
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { equalProp } from "@snort/shared";
|
||||
import { FlatReqFilter } from "./query-optimizer";
|
||||
import { NostrEvent, ReqFilter } from "./nostr";
|
||||
import { IMeta, NostrEvent, ReqFilter } from "./nostr";
|
||||
|
||||
export function findTag(e: NostrEvent, tag: string) {
|
||||
const maybeTag = e.tags.find(evTag => {
|
||||
@ -50,3 +50,35 @@ export function splitByUrl(str: string) {
|
||||
|
||||
return str.split(urlRegex);
|
||||
}
|
||||
|
||||
export function parseIMeta(tags: Array<Array<string>>) {
|
||||
let ret: Record<string, IMeta> | undefined;
|
||||
const imetaTags = tags.filter(a => a[0] === "imeta");
|
||||
for (const imetaTag of imetaTags) {
|
||||
ret ??= {};
|
||||
let imeta: IMeta = {};
|
||||
let url = "";
|
||||
for (const t of imetaTag.slice(1)) {
|
||||
const [k, v] = t.split(" ");
|
||||
if (k === "url") {
|
||||
url = v;
|
||||
}
|
||||
if (k === "dim") {
|
||||
const [w, h] = v.split("x");
|
||||
imeta.height = Number(h);
|
||||
imeta.width = Number(w);
|
||||
}
|
||||
if (k === "blurhash") {
|
||||
imeta.blurHash = v;
|
||||
}
|
||||
if (k === "x") {
|
||||
imeta.sha256 = v;
|
||||
}
|
||||
if (k === "alt") {
|
||||
imeta.alt = v;
|
||||
}
|
||||
}
|
||||
ret[url] = imeta;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user