forked from Kieran/snort
feat: parse imeta
This commit is contained in:
parent
cb95032e7c
commit
fce7cc70a3
@ -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 {
|
||||||
|
@ -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 {
|
||||||
|
@ -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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
@ -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;
|
||||||
|
@ -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"],
|
||||||
|
@ -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";
|
||||||
|
@ -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>;
|
||||||
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user