Files
snort/packages/app/src/Pages/DeckLayout.tsx
2023-11-26 11:28:42 +02:00

213 lines
6.7 KiB
TypeScript

import "./Deck.css";
import { CSSProperties, createContext, useContext, useEffect, useState } from "react";
import { Outlet, useNavigate } from "react-router-dom";
import { FormattedMessage } from "react-intl";
import { NostrLink, TaggedNostrEvent } from "@snort/system";
import useLoginFeed from "@/Feed/LoginFeed";
import { useLoginRelays } from "@/Hooks/useLoginRelays";
import { useTheme } from "@/Hooks/useTheme";
import Articles from "@/Element/Articles";
import TimelineFollows from "@/Element/Feed/TimelineFollows";
import { transformTextCached } from "@/Hooks/useTextTransformCache";
import Icon from "@/Icons/Icon";
import NotificationsPage from "./Notifications";
import useImgProxy from "@/Hooks/useImgProxy";
import Modal from "@/Element/Modal";
import { Thread } from "@/Element/Event/Thread";
import { RootTabs } from "@/Element/RootTabs";
import { SpotlightMedia } from "@/Element/SpotlightMedia";
import { ThreadContext, ThreadContextWrapper } from "@/Hooks/useThreadContext";
import Toaster from "@/Toaster";
import useLogin from "@/Hooks/useLogin";
import { LongFormText } from "@/Element/Event/LongFormText";
import NavSidebar from "@/Pages/Layout/NavSidebar";
import ErrorBoundary from "@/Element/ErrorBoundary";
type Cols = "notes" | "articles" | "media" | "streams" | "notifications";
interface DeckState {
thread?: NostrLink;
article?: TaggedNostrEvent;
}
interface DeckScope {
setThread: (e?: NostrLink) => void;
setArticle: (e?: TaggedNostrEvent) => void;
reset: () => void;
}
export const DeckContext = createContext<DeckScope | undefined>(undefined);
export function SnortDeckLayout() {
const login = useLogin();
const navigate = useNavigate();
const [deckState, setDeckState] = useState<DeckState>({
thread: undefined,
article: undefined,
});
useLoginFeed();
useTheme();
useLoginRelays();
useEffect(() => {
if (!login.publicKey) {
navigate("/");
}
}, [login]);
if (!login.publicKey) return null;
const cols = ["notes", "media", "notifications", "articles"] as Array<Cols>;
return (
<div className="deck-layout">
<DeckContext.Provider
value={{
...deckState,
setThread: (e?: NostrLink) => setDeckState({ thread: e }),
setArticle: (e?: TaggedNostrEvent) => setDeckState({ article: e }),
reset: () => setDeckState({}),
}}>
<NavSidebar narrow={true} />
<ErrorBoundary>
<div className="deck-cols">
{cols.map(c => {
switch (c) {
case "notes":
return <NotesCol />;
case "media":
return <MediaCol setThread={t => setDeckState({ thread: t })} />;
case "articles":
return <ArticlesCol />;
case "notifications":
return <NotificationsCol setThread={t => setDeckState({ thread: t })} />;
}
})}
</div>
{deckState.thread && (
<>
<Modal id="thread-overlay" onClose={() => setDeckState({})} className="thread-overlay thread">
<ThreadContextWrapper link={deckState.thread}>
<SpotlightFromThread onClose={() => setDeckState({})} />
<div>
<Thread onBack={() => setDeckState({})} disableSpotlight={true} />
</div>
</ThreadContextWrapper>
</Modal>
</>
)}
{deckState.article && (
<>
<Modal
id="thread-overlay-article"
onClose={() => setDeckState({})}
className="thread-overlay long-form"
onClick={() => setDeckState({})}>
<div onClick={e => e.stopPropagation()}>
<LongFormText ev={deckState.article} isPreview={false} related={[]} />
</div>
</Modal>
</>
)}
<Toaster />
</ErrorBoundary>
</DeckContext.Provider>
</div>
);
}
function SpotlightFromThread({ onClose }: { onClose: () => void }) {
const thread = useContext(ThreadContext);
const parsed = thread.root ? transformTextCached(thread.root.id, thread.root.content, thread.root.tags) : [];
const images = parsed.filter(a => a.type === "media" && a.mimeType?.startsWith("image/"));
if (images.length === 0) return;
return <SpotlightMedia images={images.map(a => a.content)} idx={0} onClose={onClose} />;
}
function NotesCol() {
return (
<div>
<div className="deck-col-header flex">
<div className="flex f-1 g8">
<Icon name="rows-01" size={24} />
<FormattedMessage defaultMessage="Notes" id="7+Domh" />
</div>
<div className="f-1">
<RootTabs base="/deck" />
</div>
</div>
<div>
<Outlet />
</div>
</div>
);
}
function ArticlesCol() {
return (
<div>
<div className="deck-col-header flex g8">
<Icon name="file-06" size={24} />
<FormattedMessage defaultMessage="Articles" id="3KNMbJ" />
</div>
<div>
<Articles />
</div>
</div>
);
}
function MediaCol({ setThread }: { setThread: (e: NostrLink) => void }) {
const { proxy } = useImgProxy();
return (
<div>
<div className="deck-col-header flex g8">
<Icon name="camera-lens" size={24} />
<FormattedMessage defaultMessage="Media" id="hmZ3Bz" />
</div>
<div className="image-grid p">
<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/"));
return images.length > 0;
}}
noteRenderer={e => {
const parsed = transformTextCached(e.id, e.content, e.tags);
const images = parsed.filter(a => a.type === "media" && a.mimeType?.startsWith("image/"));
return (
<div
className="media-note"
key={e.id}
style={
{
"--img": `url(${proxy(images[0].content)})`,
} as CSSProperties
}
onClick={() => setThread(NostrLink.fromEvent(e))}></div>
);
}}
/>
</div>
</div>
);
}
function NotificationsCol({ setThread }: { setThread: (e: NostrLink) => void }) {
return (
<div>
<div className="deck-col-header flex g8">
<Icon name="bell-02" size={24} />
<FormattedMessage defaultMessage="Notifications" id="NAidKb" />
</div>
<div>
<NotificationsPage onClick={setThread} />
</div>
</div>
);
}