feat: render nip94 from regular note reference
This commit is contained in:
parent
a31aa35490
commit
82956f3c69
|
@ -1,43 +1,31 @@
|
||||||
import { FileExtensionRegex } from "Const";
|
|
||||||
import { ProxyImg } from "Element/ProxyImg";
|
import { ProxyImg } from "Element/ProxyImg";
|
||||||
|
|
||||||
export default function MediaLink({ link }: { link: string }) {
|
interface MediaElementProps {
|
||||||
const url = new URL(link);
|
mime: string;
|
||||||
const extension = FileExtensionRegex.test(url.pathname.toLowerCase()) && RegExp.$1;
|
url: string;
|
||||||
switch (extension) {
|
magnet?: string;
|
||||||
case "gif":
|
sha256?: string;
|
||||||
case "jpg":
|
blurHash?: string;
|
||||||
case "jpeg":
|
}
|
||||||
case "jfif":
|
|
||||||
case "png":
|
export function MediaElement(props: MediaElementProps) {
|
||||||
case "bmp":
|
if (props.mime.startsWith("image/")) {
|
||||||
case "webp": {
|
return <ProxyImg key={props.url} src={props.url} />;
|
||||||
return <ProxyImg key={url.toString()} src={url.toString()} />;
|
} else if (props.mime.startsWith("audio/")) {
|
||||||
}
|
return <audio key={props.url} src={props.url} controls />;
|
||||||
case "wav":
|
} else if (props.mime.startsWith("video/")) {
|
||||||
case "mp3":
|
return <video key={props.url} src={props.url} controls />;
|
||||||
case "ogg": {
|
} else {
|
||||||
return <audio key={url.toString()} src={url.toString()} controls />;
|
return (
|
||||||
}
|
<a
|
||||||
case "mp4":
|
key={props.url}
|
||||||
case "mov":
|
href={props.url}
|
||||||
case "mkv":
|
onClick={e => e.stopPropagation()}
|
||||||
case "avi":
|
target="_blank"
|
||||||
case "m4v":
|
rel="noreferrer"
|
||||||
case "webm": {
|
className="ext">
|
||||||
return <video key={url.toString()} src={url.toString()} controls />;
|
{props.url}
|
||||||
}
|
</a>
|
||||||
default:
|
);
|
||||||
return (
|
|
||||||
<a
|
|
||||||
key={url.toString()}
|
|
||||||
href={url.toString()}
|
|
||||||
onClick={e => e.stopPropagation()}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
className="ext">
|
|
||||||
{url.toString()}
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +1,39 @@
|
||||||
import useEventFeed from "Feed/EventFeed";
|
|
||||||
import { NostrLink } from "Util";
|
|
||||||
import HyperText from "Element/HyperText";
|
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
|
import { RawEvent } from "@snort/nostr";
|
||||||
|
|
||||||
|
import { findTag, NostrLink } from "Util";
|
||||||
|
import useEventFeed from "Feed/EventFeed";
|
||||||
import PageSpinner from "Element/PageSpinner";
|
import PageSpinner from "Element/PageSpinner";
|
||||||
|
import Reveal from "Element/Reveal";
|
||||||
|
import { MediaElement } from "Element/MediaLink";
|
||||||
|
|
||||||
export default function NostrFileHeader({ link }: { link: NostrLink }) {
|
export default function NostrFileHeader({ link }: { link: NostrLink }) {
|
||||||
const ev = useEventFeed(link);
|
const ev = useEventFeed(link);
|
||||||
|
|
||||||
if (!ev.data) return <PageSpinner />;
|
if (!ev.data) return <PageSpinner />;
|
||||||
|
return <NostrFileElement ev={ev.data} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NostrFileElement({ ev }: { ev: RawEvent }) {
|
||||||
// assume image or embed which can be rendered by the hypertext kind
|
// assume image or embed which can be rendered by the hypertext kind
|
||||||
// todo: make use of hash
|
// todo: make use of hash
|
||||||
// todo: use magnet or other links if present
|
// todo: use magnet or other links if present
|
||||||
const u = ev.data?.tags.find(a => a[0] === "u")?.[1] ?? "";
|
const u = findTag(ev, "url");
|
||||||
if (u) {
|
const x = findTag(ev, "x");
|
||||||
return <HyperText link={u} creator={ev.data?.pubkey ?? ""} />;
|
const m = findTag(ev, "m");
|
||||||
|
const blurHash = findTag(ev, "blurhash");
|
||||||
|
const magnet = findTag(ev, "magnet");
|
||||||
|
|
||||||
|
if (u && m) {
|
||||||
|
return (
|
||||||
|
<Reveal message={<FormattedMessage defaultMessage="Click to load content from {link}" values={{ link: u }} />}>
|
||||||
|
<MediaElement mime={m} url={u} sha256={x} magnet={magnet} blurHash={blurHash} />
|
||||||
|
</Reveal>
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<b className="error">
|
<b className="error">
|
||||||
<FormattedMessage defaultMessage="Unknown file header: {name}" values={{ name: ev.data?.content }} />
|
<FormattedMessage defaultMessage="Unknown file header: {name}" values={{ name: ev.content }} />
|
||||||
</b>
|
</b>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,7 @@ import Poll from "Element/Poll";
|
||||||
import { EventExt } from "System/EventExt";
|
import { EventExt } from "System/EventExt";
|
||||||
import useLogin from "Hooks/useLogin";
|
import useLogin from "Hooks/useLogin";
|
||||||
import { setBookmarked, setPinned } from "Login";
|
import { setBookmarked, setPinned } from "Login";
|
||||||
|
import { NostrFileElement } from "Element/NostrFileHeader";
|
||||||
|
|
||||||
import messages from "./messages";
|
import messages from "./messages";
|
||||||
|
|
||||||
|
@ -72,8 +73,13 @@ const HiddenNote = ({ children }: { children: React.ReactNode }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Note(props: NoteProps) {
|
export default function Note(props: NoteProps) {
|
||||||
const navigate = useNavigate();
|
|
||||||
const { data: ev, related, highlight, options: opt, ignoreModeration = false } = props;
|
const { data: ev, related, highlight, options: opt, ignoreModeration = false } = props;
|
||||||
|
|
||||||
|
if (ev.kind === EventKind.FileHeader) {
|
||||||
|
return <NostrFileElement ev={ev} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
const [showReactions, setShowReactions] = useState(false);
|
const [showReactions, setShowReactions] = useState(false);
|
||||||
const deletions = useMemo(() => getReactions(related, ev.id, EventKind.Deletion), [related]);
|
const deletions = useMemo(() => getReactions(related, ev.id, EventKind.Deletion), [related]);
|
||||||
const { isMuted } = useModeration();
|
const { isMuted } = useModeration();
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
import MediaLink from "Element/MediaLink";
|
import { FileExtensionRegex } from "Const";
|
||||||
import Reveal from "Element/Reveal";
|
import Reveal from "Element/Reveal";
|
||||||
import useLogin from "Hooks/useLogin";
|
import useLogin from "Hooks/useLogin";
|
||||||
|
import { MediaElement } from "Element/MediaLink";
|
||||||
|
|
||||||
interface RevealMediaProps {
|
interface RevealMediaProps {
|
||||||
creator: string;
|
creator: string;
|
||||||
|
@ -18,14 +19,42 @@ export default function RevealMedia(props: RevealMediaProps) {
|
||||||
const hideMedia = pref.autoLoadMedia === "none" || (!isMine && hideNonFollows);
|
const hideMedia = pref.autoLoadMedia === "none" || (!isMine && hideNonFollows);
|
||||||
const hostname = new URL(props.link).hostname;
|
const hostname = new URL(props.link).hostname;
|
||||||
|
|
||||||
|
const url = new URL(props.link);
|
||||||
|
const extension = FileExtensionRegex.test(url.pathname.toLowerCase()) && RegExp.$1;
|
||||||
|
const type = (() => {
|
||||||
|
switch (extension) {
|
||||||
|
case "gif":
|
||||||
|
case "jpg":
|
||||||
|
case "jpeg":
|
||||||
|
case "jfif":
|
||||||
|
case "png":
|
||||||
|
case "bmp":
|
||||||
|
case "webp":
|
||||||
|
return "image";
|
||||||
|
case "wav":
|
||||||
|
case "mp3":
|
||||||
|
case "ogg":
|
||||||
|
return "audio";
|
||||||
|
case "mp4":
|
||||||
|
case "mov":
|
||||||
|
case "mkv":
|
||||||
|
case "avi":
|
||||||
|
case "m4v":
|
||||||
|
case "webm":
|
||||||
|
return "video";
|
||||||
|
default:
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
if (hideMedia) {
|
if (hideMedia) {
|
||||||
return (
|
return (
|
||||||
<Reveal
|
<Reveal
|
||||||
message={<FormattedMessage defaultMessage="Click to load content from {link}" values={{ link: hostname }} />}>
|
message={<FormattedMessage defaultMessage="Click to load content from {link}" values={{ link: hostname }} />}>
|
||||||
<MediaLink link={props.link} />
|
<MediaElement mime={`${type}/${extension}`} url={url.toString()} />
|
||||||
</Reveal>
|
</Reveal>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return <MediaLink link={props.link} />;
|
return <MediaElement mime={`${type}/${extension}`} url={url.toString()} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,17 @@ import { bytesToHex } from "@noble/hashes/utils";
|
||||||
import { decode as invoiceDecode } from "light-bolt11-decoder";
|
import { decode as invoiceDecode } from "light-bolt11-decoder";
|
||||||
import { bech32 } from "bech32";
|
import { bech32 } from "bech32";
|
||||||
import base32Decode from "base32-decode";
|
import base32Decode from "base32-decode";
|
||||||
import { HexKey, TaggedRawEvent, u256, EventKind, encodeTLV, NostrPrefix, decodeTLV, TLVEntryType } from "@snort/nostr";
|
import {
|
||||||
|
HexKey,
|
||||||
|
TaggedRawEvent,
|
||||||
|
u256,
|
||||||
|
EventKind,
|
||||||
|
encodeTLV,
|
||||||
|
NostrPrefix,
|
||||||
|
decodeTLV,
|
||||||
|
TLVEntryType,
|
||||||
|
RawEvent,
|
||||||
|
} from "@snort/nostr";
|
||||||
import { MetadataCache } from "Cache";
|
import { MetadataCache } from "Cache";
|
||||||
|
|
||||||
export const sha256 = (str: string | Uint8Array): u256 => {
|
export const sha256 = (str: string | Uint8Array): u256 => {
|
||||||
|
@ -466,7 +476,7 @@ export function chunks<T>(arr: T[], length: number) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function findTag(e: TaggedRawEvent, tag: string) {
|
export function findTag(e: RawEvent, tag: string) {
|
||||||
const maybeTag = e.tags.find(evTag => {
|
const maybeTag = e.tags.find(evTag => {
|
||||||
return evTag[0] === tag;
|
return evTag[0] === tag;
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue