feat: render nip94 from regular note reference

This commit is contained in:
Kieran 2023-04-21 23:46:22 +01:00
parent a31aa35490
commit 82956f3c69
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
5 changed files with 100 additions and 52 deletions

View File

@ -1,43 +1,31 @@
import { FileExtensionRegex } from "Const";
import { ProxyImg } from "Element/ProxyImg";
export default function MediaLink({ link }: { link: string }) {
const url = new URL(link);
const extension = FileExtensionRegex.test(url.pathname.toLowerCase()) && RegExp.$1;
switch (extension) {
case "gif":
case "jpg":
case "jpeg":
case "jfif":
case "png":
case "bmp":
case "webp": {
return <ProxyImg key={url.toString()} src={url.toString()} />;
}
case "wav":
case "mp3":
case "ogg": {
return <audio key={url.toString()} src={url.toString()} controls />;
}
case "mp4":
case "mov":
case "mkv":
case "avi":
case "m4v":
case "webm": {
return <video key={url.toString()} src={url.toString()} controls />;
}
default:
return (
<a
key={url.toString()}
href={url.toString()}
onClick={e => e.stopPropagation()}
target="_blank"
rel="noreferrer"
className="ext">
{url.toString()}
</a>
);
interface MediaElementProps {
mime: string;
url: string;
magnet?: string;
sha256?: string;
blurHash?: string;
}
export function MediaElement(props: MediaElementProps) {
if (props.mime.startsWith("image/")) {
return <ProxyImg key={props.url} src={props.url} />;
} else if (props.mime.startsWith("audio/")) {
return <audio key={props.url} src={props.url} controls />;
} else if (props.mime.startsWith("video/")) {
return <video key={props.url} src={props.url} controls />;
} else {
return (
<a
key={props.url}
href={props.url}
onClick={e => e.stopPropagation()}
target="_blank"
rel="noreferrer"
className="ext">
{props.url}
</a>
);
}
}

View File

@ -1,24 +1,39 @@
import useEventFeed from "Feed/EventFeed";
import { NostrLink } from "Util";
import HyperText from "Element/HyperText";
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 Reveal from "Element/Reveal";
import { MediaElement } from "Element/MediaLink";
export default function NostrFileHeader({ link }: { link: NostrLink }) {
const ev = useEventFeed(link);
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
// todo: make use of hash
// todo: use magnet or other links if present
const u = ev.data?.tags.find(a => a[0] === "u")?.[1] ?? "";
if (u) {
return <HyperText link={u} creator={ev.data?.pubkey ?? ""} />;
const u = findTag(ev, "url");
const x = findTag(ev, "x");
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 {
return (
<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>
);
}

View File

@ -29,6 +29,7 @@ import Poll from "Element/Poll";
import { EventExt } from "System/EventExt";
import useLogin from "Hooks/useLogin";
import { setBookmarked, setPinned } from "Login";
import { NostrFileElement } from "Element/NostrFileHeader";
import messages from "./messages";
@ -72,8 +73,13 @@ const HiddenNote = ({ children }: { children: React.ReactNode }) => {
};
export default function Note(props: NoteProps) {
const navigate = useNavigate();
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 deletions = useMemo(() => getReactions(related, ev.id, EventKind.Deletion), [related]);
const { isMuted } = useModeration();

View File

@ -1,8 +1,9 @@
import { FormattedMessage } from "react-intl";
import MediaLink from "Element/MediaLink";
import { FileExtensionRegex } from "Const";
import Reveal from "Element/Reveal";
import useLogin from "Hooks/useLogin";
import { MediaElement } from "Element/MediaLink";
interface RevealMediaProps {
creator: string;
@ -18,14 +19,42 @@ export default function RevealMedia(props: RevealMediaProps) {
const hideMedia = pref.autoLoadMedia === "none" || (!isMine && hideNonFollows);
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) {
return (
<Reveal
message={<FormattedMessage defaultMessage="Click to load content from {link}" values={{ link: hostname }} />}>
<MediaLink link={props.link} />
<MediaElement mime={`${type}/${extension}`} url={url.toString()} />
</Reveal>
);
} else {
return <MediaLink link={props.link} />;
return <MediaElement mime={`${type}/${extension}`} url={url.toString()} />;
}
}

View File

@ -5,7 +5,17 @@ import { bytesToHex } from "@noble/hashes/utils";
import { decode as invoiceDecode } from "light-bolt11-decoder";
import { bech32 } from "bech32";
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";
export const sha256 = (str: string | Uint8Array): u256 => {
@ -466,7 +476,7 @@ export function chunks<T>(arr: T[], length: number) {
return result;
}
export function findTag(e: TaggedRawEvent, tag: string) {
export function findTag(e: RawEvent, tag: string) {
const maybeTag = e.tags.find(evTag => {
return evTag[0] === tag;
});