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 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 {

View File

@ -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 {

View File

@ -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}
/>
);
}
}

View File

@ -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);
}
}}
/>
);
};
},
);

View File

@ -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;

View File

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

View File

@ -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";

View File

@ -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>;
}

View File

@ -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;
}