feat: media root tab
This commit is contained in:
@ -1,6 +1,16 @@
|
||||
/* eslint-disable max-lines */
|
||||
import { fetchNip05Pubkey, unixNow } from "@snort/shared";
|
||||
import { EventBuilder, EventKind, NostrLink, NostrPrefix, TaggedNostrEvent, tryParseNostrLink } from "@snort/system";
|
||||
import {
|
||||
addExtensionToNip94Url,
|
||||
EventBuilder,
|
||||
EventKind,
|
||||
nip94TagsToIMeta,
|
||||
NostrLink,
|
||||
NostrPrefix,
|
||||
readNip94Tags,
|
||||
TaggedNostrEvent,
|
||||
tryParseNostrLink,
|
||||
} from "@snort/system";
|
||||
import { useUserProfile } from "@snort/system-react";
|
||||
import { ZapTarget } from "@snort/wallet";
|
||||
import { Menu, MenuItem } from "@szhsin/react-menu";
|
||||
@ -28,7 +38,7 @@ import usePreferences from "@/Hooks/usePreferences";
|
||||
import useRelays from "@/Hooks/useRelays";
|
||||
import { useNoteCreator } from "@/State/NoteCreator";
|
||||
import { openFile, trackEvent } from "@/Utils";
|
||||
import useFileUpload, { addExtensionToNip94Url, nip94TagsToIMeta, readNip94Tags } from "@/Utils/Upload";
|
||||
import useFileUpload from "@/Utils/Upload";
|
||||
import { GetPowWorker } from "@/Utils/wasm";
|
||||
|
||||
import { OkResponseRow } from "./OkResponseRow";
|
||||
|
@ -94,6 +94,17 @@ export function rootTabItems(base: string, pubKey: string | undefined, tags: Arr
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
tab: "media",
|
||||
path: `${base}/media`,
|
||||
show: true,
|
||||
element: (
|
||||
<>
|
||||
<Icon name="camera-plus" />
|
||||
<FormattedMessage defaultMessage="Media" />
|
||||
</>
|
||||
),
|
||||
},
|
||||
] as Array<{
|
||||
tab: RootTabRoutePath;
|
||||
path: string;
|
||||
|
@ -15,12 +15,16 @@ import { AutoLoadMore } from "../Event/LoadMore";
|
||||
import TimelineChunk from "./TimelineChunk";
|
||||
|
||||
export interface TimelineFollowsProps {
|
||||
id?: string;
|
||||
postsOnly: boolean;
|
||||
liveStreams?: boolean;
|
||||
noteFilter?: (ev: NostrEvent) => boolean;
|
||||
noteRenderer?: (ev: NostrEvent) => ReactNode;
|
||||
noteOnClick?: (ev: NostrEvent) => void;
|
||||
displayAs?: DisplayAs;
|
||||
kinds?: Array<EventKind>;
|
||||
showDisplayAsSelector?: boolean;
|
||||
firstChunkSize?: number;
|
||||
windowSize?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -38,12 +42,15 @@ const TimelineFollows = (props: TimelineFollowsProps) => {
|
||||
const { isFollowing, followList } = useFollowsControls();
|
||||
const { chunks, showMore } = useTimelineChunks({
|
||||
now: openedAt,
|
||||
firstChunkSize: Hour * 2,
|
||||
window: props.windowSize,
|
||||
firstChunkSize: props.firstChunkSize ?? Hour * 2,
|
||||
});
|
||||
|
||||
const builder = useCallback(
|
||||
(rb: RequestBuilder) => {
|
||||
rb.withFilter().authors(followList).kinds([EventKind.TextNote, EventKind.Repost, EventKind.Polls]);
|
||||
rb.withFilter()
|
||||
.authors(followList)
|
||||
.kinds(props.kinds ?? [EventKind.TextNote, EventKind.Repost, EventKind.Polls]);
|
||||
},
|
||||
[followList],
|
||||
);
|
||||
@ -58,11 +65,13 @@ const TimelineFollows = (props: TimelineFollowsProps) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<DisplayAsSelector activeSelection={displayAs} onSelect={(displayAs: DisplayAs) => setDisplayAs(displayAs)} />
|
||||
{(props.showDisplayAsSelector ?? true) && (
|
||||
<DisplayAsSelector activeSelection={displayAs} onSelect={(displayAs: DisplayAs) => setDisplayAs(displayAs)} />
|
||||
)}
|
||||
{chunks.map(c => (
|
||||
<TimelineChunk
|
||||
key={c.until}
|
||||
id="follows"
|
||||
id={`follows${props.id ? `:${props.id}` : ""}`}
|
||||
chunk={c}
|
||||
builder={builder}
|
||||
noteFilter={filterEvents}
|
||||
|
@ -59,7 +59,6 @@ export function MediaCol({ setThread }: { setThread: (e: NostrLink) => void }) {
|
||||
</div>
|
||||
<TimelineFollows
|
||||
postsOnly={true}
|
||||
liveStreams={false}
|
||||
noteFilter={e => {
|
||||
const parsed = transformTextCached(e.id, e.content, e.tags);
|
||||
const images = parsed.filter(a => a.type === "media" && a.mimeType?.startsWith("image/"));
|
||||
|
19
packages/app/src/Pages/Root/Media.tsx
Normal file
19
packages/app/src/Pages/Root/Media.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import { EventKind } from "@snort/system";
|
||||
|
||||
import TimelineFollows from "@/Components/Feed/TimelineFollows";
|
||||
import { Day } from "@/Utils/Const";
|
||||
|
||||
export default function MediaPosts() {
|
||||
return (
|
||||
<div className="p">
|
||||
<TimelineFollows
|
||||
id="media"
|
||||
postsOnly={true}
|
||||
kinds={[EventKind.Photo, EventKind.Video, EventKind.ShortVideo]}
|
||||
showDisplayAsSelector={true}
|
||||
firstChunkSize={Day}
|
||||
windowSize={Day}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { lazy } from "react";
|
||||
import { Outlet, RouteObject } from "react-router-dom";
|
||||
import { Outlet, RouteObject, useLocation } from "react-router-dom";
|
||||
|
||||
import { LiveStreams } from "@/Components/LiveStream/LiveStreams";
|
||||
import { RootTabRoutes } from "@/Pages/Root/RootTabRoutes";
|
||||
@ -8,9 +8,10 @@ import { getCurrentRefCode } from "@/Utils";
|
||||
const InviteModal = lazy(() => import("@/Components/Invite"));
|
||||
export default function RootPage() {
|
||||
const code = getCurrentRefCode();
|
||||
const location = useLocation();
|
||||
return (
|
||||
<>
|
||||
<LiveStreams />
|
||||
{(location.pathname === "/" || location.pathname === "/following") && <LiveStreams />}
|
||||
<div className="main-content">
|
||||
<Outlet />
|
||||
</div>
|
||||
|
@ -7,6 +7,7 @@ import { ConversationsTab } from "@/Pages/Root/ConversationsTab";
|
||||
import { DefaultTab } from "@/Pages/Root/DefaultTab";
|
||||
import { FollowedByFriendsTab } from "@/Pages/Root/FollowedByFriendsTab";
|
||||
import { ForYouTab } from "@/Pages/Root/ForYouTab";
|
||||
import MediaPosts from "@/Pages/Root/Media";
|
||||
import { NotesTab } from "@/Pages/Root/NotesTab";
|
||||
import { TagsTab } from "@/Pages/Root/TagsTab";
|
||||
import { TopicsPage } from "@/Pages/TopicsPage";
|
||||
@ -23,7 +24,8 @@ export type RootTabRoutePath =
|
||||
| "trending/hashtags"
|
||||
| "suggested"
|
||||
| "t/:tag"
|
||||
| "topics";
|
||||
| "topics"
|
||||
| "media";
|
||||
|
||||
export type RootTabRoute = {
|
||||
path: RootTabRoutePath;
|
||||
@ -83,4 +85,8 @@ export const RootTabRoutes: RootTabRoute[] = [
|
||||
path: "topics",
|
||||
element: <TopicsPage />,
|
||||
},
|
||||
{
|
||||
path: "media",
|
||||
element: <MediaPosts />,
|
||||
},
|
||||
];
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { base64 } from "@scure/base";
|
||||
import { throwIfOffline } from "@snort/shared";
|
||||
import { EventKind, EventPublisher, NostrEvent } from "@snort/system";
|
||||
import { addExtensionToNip94Url, EventKind, EventPublisher, NostrEvent, readNip94Tags } from "@snort/system";
|
||||
|
||||
import { addExtensionToNip94Url, readNip94Tags, UploadResult } from ".";
|
||||
import { UploadResult } from ".";
|
||||
|
||||
export class Nip96Uploader {
|
||||
#info?: Nip96Info;
|
||||
|
@ -1,28 +1,12 @@
|
||||
import { EventPublisher, NostrEvent } from "@snort/system";
|
||||
import { EventPublisher, Nip94Tags, NostrEvent } from "@snort/system";
|
||||
|
||||
import useEventPublisher from "@/Hooks/useEventPublisher";
|
||||
import { useMediaServerList } from "@/Hooks/useMediaServerList";
|
||||
import { bech32ToHex, randomSample } from "@/Utils";
|
||||
import { FileExtensionRegex, KieranPubKey } from "@/Utils/Const";
|
||||
import { KieranPubKey } from "@/Utils/Const";
|
||||
|
||||
import { Nip96Uploader } from "./Nip96";
|
||||
|
||||
export interface Nip94Tags {
|
||||
url?: string;
|
||||
mimeType?: string;
|
||||
hash?: string;
|
||||
originalHash?: string;
|
||||
size?: number;
|
||||
dimensions?: [number, number];
|
||||
magnet?: string;
|
||||
blurHash?: string;
|
||||
thumb?: string;
|
||||
image?: Array<string>;
|
||||
summary?: string;
|
||||
alt?: string;
|
||||
fallback?: Array<string>;
|
||||
}
|
||||
|
||||
export interface UploadResult {
|
||||
url?: string;
|
||||
error?: string;
|
||||
@ -86,124 +70,3 @@ export default function useFileUpload(privKey?: string) {
|
||||
return new Nip96Uploader("https://nostr.build", pub);
|
||||
}
|
||||
}
|
||||
|
||||
export function addExtensionToNip94Url(meta: Nip94Tags) {
|
||||
if (!meta.url?.match(FileExtensionRegex) && meta.mimeType) {
|
||||
switch (meta.mimeType) {
|
||||
case "image/webp": {
|
||||
return `${meta.url}.webp`;
|
||||
}
|
||||
case "image/jpeg":
|
||||
case "image/jpg": {
|
||||
return `${meta.url}.jpg`;
|
||||
}
|
||||
case "video/mp4": {
|
||||
return `${meta.url}.mp4`;
|
||||
}
|
||||
}
|
||||
}
|
||||
return meta.url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read NIP-94 tags from `imeta` tag
|
||||
*/
|
||||
export function readNip94TagsFromIMeta(tag: Array<string>) {
|
||||
const asTags = tag.slice(1).map(a => a.split(" ", 2));
|
||||
return readNip94Tags(asTags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read NIP-94 tags from event tags
|
||||
*/
|
||||
export function readNip94Tags(tags: Array<Array<string>>) {
|
||||
const res: Nip94Tags = {};
|
||||
for (const tx of tags) {
|
||||
const [k, v] = tx;
|
||||
switch (k) {
|
||||
case "url": {
|
||||
res.url = v;
|
||||
break;
|
||||
}
|
||||
case "m": {
|
||||
res.mimeType = v;
|
||||
break;
|
||||
}
|
||||
case "x": {
|
||||
res.hash = v;
|
||||
break;
|
||||
}
|
||||
case "ox": {
|
||||
res.originalHash = v;
|
||||
break;
|
||||
}
|
||||
case "size": {
|
||||
res.size = Number(v);
|
||||
break;
|
||||
}
|
||||
case "dim": {
|
||||
res.dimensions = v.split("x").map(Number) as [number, number];
|
||||
break;
|
||||
}
|
||||
case "magnet": {
|
||||
res.magnet = v;
|
||||
break;
|
||||
}
|
||||
case "blurhash": {
|
||||
res.blurHash = v;
|
||||
break;
|
||||
}
|
||||
case "thumb": {
|
||||
res.thumb = v;
|
||||
break;
|
||||
}
|
||||
case "image": {
|
||||
res.image ??= [];
|
||||
res.image.push(v);
|
||||
break;
|
||||
}
|
||||
case "summary": {
|
||||
res.summary = v;
|
||||
break;
|
||||
}
|
||||
case "alt": {
|
||||
res.alt = v;
|
||||
break;
|
||||
}
|
||||
case "fallback": {
|
||||
res.fallback ??= [];
|
||||
res.fallback.push(v);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
export function nip94TagsToIMeta(meta: Nip94Tags) {
|
||||
const ret: Array<string> = ["imeta"];
|
||||
const ifPush = (key: string, value?: string | number) => {
|
||||
if (value) {
|
||||
ret.push(`${key} ${value}`);
|
||||
}
|
||||
};
|
||||
ifPush("url", meta.url);
|
||||
ifPush("m", meta.mimeType);
|
||||
ifPush("x", meta.hash);
|
||||
ifPush("ox", meta.originalHash);
|
||||
ifPush("size", meta.size);
|
||||
ifPush("dim", meta.dimensions?.join("x"));
|
||||
ifPush("magnet", meta.magnet);
|
||||
ifPush("blurhash", meta.blurHash);
|
||||
ifPush("thumb", meta.thumb);
|
||||
ifPush("summary", meta.summary);
|
||||
ifPush("alt", meta.alt);
|
||||
if (meta.image) {
|
||||
meta.image.forEach(a => ifPush("image", a));
|
||||
}
|
||||
if (meta.fallback) {
|
||||
meta.fallback.forEach(a => ifPush("fallback", a));
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
@ -1,8 +1,21 @@
|
||||
import { TaggedNostrEvent } from "@snort/system";
|
||||
import { EventKind, ParsedFragment, readNip94TagsFromIMeta, TaggedNostrEvent } from "@snort/system";
|
||||
|
||||
import { transformTextCached } from "@/Hooks/useTextTransformCache";
|
||||
|
||||
export default function getEventMedia(event: TaggedNostrEvent) {
|
||||
// emulate parsed media from imeta kinds
|
||||
const mediaKinds = [EventKind.Photo, EventKind.Video, EventKind.ShortVideo];
|
||||
if (mediaKinds.includes(event.kind)) {
|
||||
const meta = event.tags.filter(a => a[0] === "imeta").map(readNip94TagsFromIMeta);
|
||||
return meta.map(
|
||||
a =>
|
||||
({
|
||||
type: "media",
|
||||
mimeType: a.mimeType,
|
||||
content: a.url,
|
||||
}) as ParsedFragment,
|
||||
);
|
||||
}
|
||||
const parsed = transformTextCached(event.id, event.content, event.tags);
|
||||
return parsed.filter(
|
||||
a => a.type === "media" && (a.mimeType?.startsWith("image/") || a.mimeType?.startsWith("video/")),
|
||||
|
39
packages/system/src/impl/nip92.ts
Normal file
39
packages/system/src/impl/nip92.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { Nip94Tags, readNip94Tags } from "./nip94";
|
||||
|
||||
/**
|
||||
* Read NIP-94 tags from `imeta` tag
|
||||
*/
|
||||
export function readNip94TagsFromIMeta(tag: Array<string>) {
|
||||
const asTags = tag.slice(1).map(a => a.split(" ", 2));
|
||||
return readNip94Tags(asTags);
|
||||
}
|
||||
|
||||
export function nip94TagsToIMeta(meta: Nip94Tags) {
|
||||
const ret: Array<string> = ["imeta"];
|
||||
const ifPush = (key: string, value?: string | number) => {
|
||||
if (value) {
|
||||
ret.push(`${key} ${value}`);
|
||||
}
|
||||
};
|
||||
ifPush("url", meta.url);
|
||||
ifPush("m", meta.mimeType);
|
||||
ifPush("x", meta.hash);
|
||||
ifPush("ox", meta.originalHash);
|
||||
ifPush("size", meta.size);
|
||||
ifPush("dim", meta.dimensions?.join("x"));
|
||||
ifPush("magnet", meta.magnet);
|
||||
ifPush("blurhash", meta.blurHash);
|
||||
ifPush("thumb", meta.thumb);
|
||||
ifPush("summary", meta.summary);
|
||||
ifPush("alt", meta.alt);
|
||||
ifPush("duration", meta.duration);
|
||||
ifPush("bitrate", meta.bitrate);
|
||||
if (meta.image) {
|
||||
meta.image.forEach(a => ifPush("image", a));
|
||||
}
|
||||
if (meta.fallback) {
|
||||
meta.fallback.forEach(a => ifPush("fallback", a));
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
112
packages/system/src/impl/nip94.ts
Normal file
112
packages/system/src/impl/nip94.ts
Normal file
@ -0,0 +1,112 @@
|
||||
import { FileExtensionRegex } from "../const";
|
||||
|
||||
export interface Nip94Tags {
|
||||
url?: string;
|
||||
mimeType?: string;
|
||||
hash?: string;
|
||||
originalHash?: string;
|
||||
size?: number;
|
||||
dimensions?: [number, number];
|
||||
magnet?: string;
|
||||
blurHash?: string;
|
||||
thumb?: string;
|
||||
image?: Array<string>;
|
||||
summary?: string;
|
||||
alt?: string;
|
||||
fallback?: Array<string>;
|
||||
duration?: number;
|
||||
bitrate?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read NIP-94 tags from event tags
|
||||
*/
|
||||
export function readNip94Tags(tags: Array<Array<string>>) {
|
||||
const res: Nip94Tags = {};
|
||||
for (const tx of tags) {
|
||||
const [k, v] = tx;
|
||||
switch (k) {
|
||||
case "url": {
|
||||
res.url = v;
|
||||
break;
|
||||
}
|
||||
case "m": {
|
||||
res.mimeType = v;
|
||||
break;
|
||||
}
|
||||
case "x": {
|
||||
res.hash = v;
|
||||
break;
|
||||
}
|
||||
case "ox": {
|
||||
res.originalHash = v;
|
||||
break;
|
||||
}
|
||||
case "size": {
|
||||
res.size = Number(v);
|
||||
break;
|
||||
}
|
||||
case "dim": {
|
||||
res.dimensions = v.split("x").map(Number) as [number, number];
|
||||
break;
|
||||
}
|
||||
case "magnet": {
|
||||
res.magnet = v;
|
||||
break;
|
||||
}
|
||||
case "blurhash": {
|
||||
res.blurHash = v;
|
||||
break;
|
||||
}
|
||||
case "thumb": {
|
||||
res.thumb = v;
|
||||
break;
|
||||
}
|
||||
case "image": {
|
||||
res.image ??= [];
|
||||
res.image.push(v);
|
||||
break;
|
||||
}
|
||||
case "summary": {
|
||||
res.summary = v;
|
||||
break;
|
||||
}
|
||||
case "alt": {
|
||||
res.alt = v;
|
||||
break;
|
||||
}
|
||||
case "fallback": {
|
||||
res.fallback ??= [];
|
||||
res.fallback.push(v);
|
||||
break;
|
||||
}
|
||||
case "duration": {
|
||||
res.duration = Number(v);
|
||||
break;
|
||||
}
|
||||
case "bitrate": {
|
||||
res.bitrate = Number(v);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
export function addExtensionToNip94Url(meta: Nip94Tags) {
|
||||
if (!meta.url?.match(FileExtensionRegex) && meta.mimeType) {
|
||||
switch (meta.mimeType) {
|
||||
case "image/webp": {
|
||||
return `${meta.url}.webp`;
|
||||
}
|
||||
case "image/jpeg":
|
||||
case "image/jpg": {
|
||||
return `${meta.url}.jpg`;
|
||||
}
|
||||
case "video/mp4": {
|
||||
return `${meta.url}.mp4`;
|
||||
}
|
||||
}
|
||||
}
|
||||
return meta.url;
|
||||
}
|
@ -35,6 +35,8 @@ export * from "./impl/nip44";
|
||||
export * from "./impl/nip46";
|
||||
export * from "./impl/nip57";
|
||||
export * from "./impl/nip55";
|
||||
export * from "./impl/nip94";
|
||||
export * from "./impl/nip92";
|
||||
|
||||
export * from "./cache/index";
|
||||
export * from "./cache/user-relays";
|
||||
|
Reference in New Issue
Block a user