feat: parse imeta

This commit is contained in:
Kieran 2023-12-11 11:36:14 +00:00
parent cb95032e7c
commit fce7cc70a3
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
9 changed files with 160 additions and 68 deletions

View File

@ -1,23 +1,40 @@
import { ProxyImg } from "@/Element/ProxyImg"; import { ProxyImg } from "@/Element/ProxyImg";
import useImgProxy from "@/Hooks/useImgProxy"; import useImgProxy from "@/Hooks/useImgProxy";
import React from "react"; import { IMeta } from "@snort/system";
import React, { CSSProperties, useMemo, useRef } from "react";
interface MediaElementProps { interface MediaElementProps {
mime: string; mime: string;
url: string; url: string;
magnet?: string; meta?: IMeta;
sha256?: string;
blurHash?: string;
onMediaClick?: (e: React.MouseEvent<HTMLImageElement>) => void; onMediaClick?: (e: React.MouseEvent<HTMLImageElement>) => void;
} }
export function MediaElement(props: MediaElementProps) { export function MediaElement(props: MediaElementProps) {
const { proxy } = useImgProxy(); const { proxy } = useImgProxy();
const ref = useRef<HTMLImageElement | null>(null);
const autoplay = window.innerWidth >= 768; 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/")) { 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/")) { } else if (props.mime.startsWith("audio/")) {
return <audio key={props.url} src={props.url} controls />; return <audio key={props.url} src={props.url} controls />;
} else if (props.mime.startsWith("video/")) { } else if (props.mime.startsWith("video/")) {
@ -31,6 +48,7 @@ export function MediaElement(props: MediaElementProps) {
controls controls
poster={proxy(props.url)} poster={proxy(props.url)}
className="max-h-[80vh] mx-auto" className="max-h-[80vh] mx-auto"
style={style}
/> />
); );
} else { } else {

View File

@ -30,7 +30,15 @@ export function NostrFileElement({ ev }: { ev: NostrEvent }) {
message={ message={
<FormattedMessage defaultMessage="Click to load content from {link}" id="lsNFM1" values={{ link: u }} /> <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> </Reveal>
); );
} else { } else {

View File

@ -5,11 +5,13 @@ import Reveal from "@/Element/Event/Reveal";
import useLogin from "@/Hooks/useLogin"; import useLogin from "@/Hooks/useLogin";
import { MediaElement } from "@/Element/Embed/MediaElement"; import { MediaElement } from "@/Element/Embed/MediaElement";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { IMeta } from "@snort/system";
interface RevealMediaProps { interface RevealMediaProps {
creator: string; creator: string;
link: string; link: string;
onMediaClick?: (e: React.MouseEvent<HTMLImageElement>) => void; onMediaClick?: (e: React.MouseEvent<HTMLImageElement>) => void;
meta?: IMeta;
} }
export default function RevealMedia(props: RevealMediaProps) { 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> </Reveal>
); );
} else { } 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}
/>
);
} }
} }

View File

@ -1,5 +1,5 @@
import useImgProxy from "@/Hooks/useImgProxy"; 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 { FormattedMessage } from "react-intl";
import { getUrlHostname } from "@/SnortUtils"; import { getUrlHostname } from "@/SnortUtils";
@ -10,46 +10,49 @@ type ProxyImgProps = HTMLProps<HTMLImageElement> & {
missingImageElement?: ReactNode; missingImageElement?: ReactNode;
}; };
export const ProxyImg = ({ size, className, promptToLoadDirectly, missingImageElement, ...props }: ProxyImgProps) => { export const ProxyImg = forwardRef<HTMLImageElement, ProxyImgProps>(
const { proxy } = useImgProxy(); ({ size, className, promptToLoadDirectly, missingImageElement, ...props }: ProxyImgProps, ref) => {
const [loadFailed, setLoadFailed] = useState(false); const { proxy } = useImgProxy();
const [bypass, setBypass] = useState(CONFIG.bypassImgProxyError); 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 ( return (
<div <img
className="note-invoice error" {...props}
onClick={e => { ref={ref}
e.stopPropagation(); src={src}
setBypass(true); width={size}
}}> height={size}
<FormattedMessage className={className}
defaultMessage="Failed to proxy image from {host}, click here to load directly" onError={e => {
id="65BmHb" if (props.onError) {
values={{ props.onError(e);
host: getUrlHostname(props.src), } else {
}} console.error("Failed to proxy image ", props.src);
/> setLoadFailed(true);
</div> }
}}
/>
); );
} },
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);
}
}}
/>
);
};

View File

@ -1,6 +1,6 @@
import "./Text.css"; import "./Text.css";
import { ReactNode, useState } from "react"; import { ReactNode, useState } from "react";
import { HexKey, ParsedFragment } from "@snort/system"; import { HexKey, ParsedFragment, parseIMeta } from "@snort/system";
import classNames from "classnames"; import classNames from "classnames";
import Invoice from "@/Element/Embed/Invoice"; import Invoice from "@/Element/Embed/Invoice";
@ -100,6 +100,7 @@ export default function Text({
const elements = useTextTransformer(id, content, tags); const elements = useTextTransformer(id, content, tags);
const images = elements.filter(a => a.type === "media" && a.mimeType?.startsWith("image")).map(a => a.content); 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) { function renderContentWithHighlightedText(content: string, textToHighlight: string) {
const textToHighlightArray = textToHighlight.trim().toLowerCase().split(" "); const textToHighlightArray = textToHighlight.trim().toLowerCase().split(" ");
@ -136,22 +137,26 @@ export default function Text({
</a> </a>
); );
const RevealMediaInstance = ({ content }: { content: string }) => ( const RevealMediaInstance = ({ content }: { content: string }) => {
<RevealMedia const imeta = iMeta?.[content];
key={content} return (
link={content} <RevealMedia
creator={creator} key={content}
onMediaClick={e => { link={content}
if (!disableMediaSpotlight) { creator={creator}
e.stopPropagation(); meta={imeta}
e.preventDefault(); onMediaClick={e => {
setShowSpotlight(true); if (!disableMediaSpotlight) {
const selected = images.findIndex(b => b === content); e.stopPropagation();
setImageIdx(selected === -1 ? 0 : selected); e.preventDefault();
} setShowSpotlight(true);
}} const selected = images.findIndex(b => b === content);
/> setImageIdx(selected === -1 ? 0 : selected);
); }
}}
/>
);
};
const renderContent = () => { const renderContent = () => {
let lenCtr = 0; let lenCtr = 0;

View File

@ -26,6 +26,7 @@ export default defineConfig({
name: "snort", name: "snort",
ifGitSHA: true, ifGitSHA: true,
command: "git describe --always --tags", command: "git describe --always --tags",
ifMeta: false,
}), }),
], ],
assetsInclude: ["**/*.md", "**/*.wasm"], assetsInclude: ["**/*.md", "**/*.wasm"],

View File

@ -32,6 +32,7 @@ export * from "./pow-util";
export * from "./query-optimizer"; export * from "./query-optimizer";
export * from "./encrypted"; export * from "./encrypted";
export * from "./outbox-model"; export * from "./outbox-model";
export { parseIMeta } from "./utils";
export * from "./impl/nip4"; export * from "./impl/nip4";
export * from "./impl/nip44"; export * from "./impl/nip44";

View File

@ -81,3 +81,13 @@ export interface FullRelaySettings {
} }
export type NotSignedNostrEvent = Omit<NostrEvent, "sig">; export type NotSignedNostrEvent = Omit<NostrEvent, "sig">;
export interface IMeta {
magnet?: string;
sha256?: string;
blurHash?: string;
height?: number;
width?: number;
alt?: string;
fallback?: Array<string>;
}

View File

@ -1,6 +1,6 @@
import { equalProp } from "@snort/shared"; import { equalProp } from "@snort/shared";
import { FlatReqFilter } from "./query-optimizer"; import { FlatReqFilter } from "./query-optimizer";
import { NostrEvent, ReqFilter } from "./nostr"; import { IMeta, NostrEvent, ReqFilter } from "./nostr";
export function findTag(e: NostrEvent, tag: string) { export function findTag(e: NostrEvent, tag: string) {
const maybeTag = e.tags.find(evTag => { const maybeTag = e.tags.find(evTag => {
@ -50,3 +50,35 @@ export function splitByUrl(str: string) {
return str.split(urlRegex); 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;
}