- {root && renderRoot(root)}
- {root && renderChain(root.id)}
+ {thread.root && renderRoot(thread.root)}
+ {thread.root && renderChain(thread.root.id)}
{brokenChains.length > 0 &&
Other replies
}
{brokenChains.map(a => {
diff --git a/packages/app/src/Element/TimelineFollows.tsx b/packages/app/src/Element/TimelineFollows.tsx
index c9649968..1980dd51 100644
--- a/packages/app/src/Element/TimelineFollows.tsx
+++ b/packages/app/src/Element/TimelineFollows.tsx
@@ -1,5 +1,5 @@
import "./Timeline.css";
-import { useCallback, useContext, useMemo, useState, useSyncExternalStore } from "react";
+import { ReactNode, useCallback, useContext, useMemo, useState, useSyncExternalStore } from "react";
import { FormattedMessage } from "react-intl";
import { TaggedNostrEvent, EventKind, u256, NostrEvent } from "@snort/system";
import { unixNow } from "@snort/shared";
@@ -19,6 +19,9 @@ import Icon from "Icons/Icon";
export interface TimelineFollowsProps {
postsOnly: boolean;
+ liveStreams?: boolean;
+ noteFilter?: (ev: NostrEvent) => boolean;
+ noteRenderer?: (ev: NostrEvent) => ReactNode;
}
/**
@@ -46,7 +49,7 @@ const TimelineFollows = (props: TimelineFollowsProps) => {
const a = nts.filter(a => a.kind !== EventKind.LiveEvent);
return a
?.filter(a => (props.postsOnly ? !a.tags.some(b => b[0] === "e") : true))
- .filter(a => !isMuted(a.pubkey) && login.follows.item.includes(a.pubkey));
+ .filter(a => !isMuted(a.pubkey) && login.follows.item.includes(a.pubkey) && (props.noteFilter?.(a) ?? true));
},
[props.postsOnly, muted, login.follows.timestamp],
);
@@ -83,7 +86,7 @@ const TimelineFollows = (props: TimelineFollowsProps) => {
return (
<>
-
+ {(props.liveStreams ?? true) &&
}
{latestFeed.length > 0 && (
<>
onShowLatest()} ref={ref}>
@@ -110,7 +113,7 @@ const TimelineFollows = (props: TimelineFollowsProps) => {
)}
>
)}
- {mainFeed.map(a => (
+ {mainFeed.map(a => props.noteRenderer?.(a) ?? (
))}
diff --git a/packages/app/src/Element/TrendingUsers.tsx b/packages/app/src/Element/TrendingUsers.tsx
index 0fd51c90..af5cd25c 100644
--- a/packages/app/src/Element/TrendingUsers.tsx
+++ b/packages/app/src/Element/TrendingUsers.tsx
@@ -22,8 +22,8 @@ export default function TrendingUsers() {
if (!userList) return
;
return (
- <>
+
- >
+
);
}
diff --git a/packages/app/src/Feed/ArticlesFeed.ts b/packages/app/src/Feed/ArticlesFeed.ts
new file mode 100644
index 00000000..39aa81c2
--- /dev/null
+++ b/packages/app/src/Feed/ArticlesFeed.ts
@@ -0,0 +1,21 @@
+import { EventKind, NoteCollection, RequestBuilder } from "@snort/system";
+import { useRequestBuilder } from "@snort/system-react";
+import useLogin from "Hooks/useLogin";
+import { useMemo } from "react";
+
+export function useArticles() {
+ const {publicKey, follows} = useLogin();
+
+ const sub = useMemo(() => {
+ if(!publicKey) return null;
+ const rb = new RequestBuilder(`articles:${publicKey}`);
+ rb.withFilter()
+ .kinds([EventKind.LongFormTextNote])
+ .authors(follows.item)
+ .limit(20);
+
+ return rb;
+ }, [follows.timestamp]);
+
+ return useRequestBuilder(NoteCollection, sub);
+}
\ No newline at end of file
diff --git a/packages/app/src/Hooks/useLoginRelays.tsx b/packages/app/src/Hooks/useLoginRelays.tsx
new file mode 100644
index 00000000..8c961266
--- /dev/null
+++ b/packages/app/src/Hooks/useLoginRelays.tsx
@@ -0,0 +1,22 @@
+import { System } from "index";
+import { useEffect } from "react";
+import useLogin from "./useLogin";
+
+export function useLoginRelays() {
+ const { relays } = useLogin();
+
+ useEffect(() => {
+ if (relays) {
+ (async () => {
+ for (const [k, v] of Object.entries(relays.item)) {
+ await System.ConnectToRelay(k, v);
+ }
+ for (const v of System.Sockets) {
+ if (!relays.item[v.address] && !v.ephemeral) {
+ System.DisconnectRelay(v.address);
+ }
+ }
+ })();
+ }
+ }, [relays]);
+}
\ No newline at end of file
diff --git a/packages/app/src/Hooks/useTextTransformCache.tsx b/packages/app/src/Hooks/useTextTransformCache.tsx
new file mode 100644
index 00000000..f767aa39
--- /dev/null
+++ b/packages/app/src/Hooks/useTextTransformCache.tsx
@@ -0,0 +1,15 @@
+import { ParsedFragment, transformText } from "@snort/system";
+
+const TextCache = new Map
>();
+
+export function transformTextCached(id: string, content: string, tags: Array>) {
+ const cached = TextCache.get(id);
+ if (cached) return cached;
+ const newCache = transformText(content, tags);
+ TextCache.set(id, newCache);
+ return newCache;
+}
+
+export function useTextTransformer(id: string, content: string, tags: Array>) {
+ return transformTextCached(id, content, tags);
+}
\ No newline at end of file
diff --git a/packages/app/src/Hooks/useTheme.tsx b/packages/app/src/Hooks/useTheme.tsx
new file mode 100644
index 00000000..2739387d
--- /dev/null
+++ b/packages/app/src/Hooks/useTheme.tsx
@@ -0,0 +1,32 @@
+import { useEffect } from "react";
+import useLogin from "./useLogin";
+
+export function useTheme() {
+ const { preferences } = useLogin();
+
+ function setTheme(theme: "light" | "dark") {
+ const elm = document.documentElement;
+ if (theme === "light" && !elm.classList.contains("light")) {
+ elm.classList.add("light");
+ } else if (theme === "dark" && elm.classList.contains("light")) {
+ elm.classList.remove("light");
+ }
+ }
+
+ useEffect(() => {
+ const osTheme = window.matchMedia("(prefers-color-scheme: light)");
+ setTheme(
+ preferences.theme === "system" && osTheme.matches ? "light" : preferences.theme === "light" ? "light" : "dark"
+ );
+
+ osTheme.onchange = e => {
+ if (preferences.theme === "system") {
+ setTheme(e.matches ? "light" : "dark");
+ }
+ };
+ return () => {
+ osTheme.onchange = null;
+ };
+ }, [preferences.theme]);
+
+}
\ No newline at end of file
diff --git a/packages/app/src/Hooks/useThreadContext.tsx b/packages/app/src/Hooks/useThreadContext.tsx
new file mode 100644
index 00000000..841b6ab2
--- /dev/null
+++ b/packages/app/src/Hooks/useThreadContext.tsx
@@ -0,0 +1,110 @@
+import { unwrap } from "@snort/shared";
+import { EventExt, EventKind, NostrLink, NostrPrefix, TaggedNostrEvent, u256, Thread as ThreadInfo, } from "@snort/system";
+import useThreadFeed from "Feed/ThreadFeed";
+import { findTag } from "SnortUtils";
+import { ReactNode, createContext, useMemo, useState } from "react";
+import { useLocation } from "react-router-dom";
+
+export interface ThreadContext {
+ current: string,
+ root?: TaggedNostrEvent,
+ chains: Map>,
+ data: Array,
+ setCurrent: (i: string) => void;
+}
+
+export const ThreadContext = createContext({} as ThreadContext)
+
+export function ThreadContextWrapper({ link, children }: { link: NostrLink, children?: ReactNode }) {
+ const location = useLocation();
+ const [currentId, setCurrentId] = useState(link.id);
+ const thread = useThreadFeed(link);
+
+ const chains = useMemo(() => {
+ const chains = new Map>();
+ if (thread.data) {
+ thread.data
+ ?.filter(a => a.kind === EventKind.TextNote)
+ .sort((a, b) => b.created_at - a.created_at)
+ .forEach(v => {
+ const t = EventExt.extractThread(v);
+ let replyTo = t?.replyTo?.value ?? t?.root?.value;
+ if (t?.root?.key === "a" && t?.root?.value) {
+ const parsed = t.root.value.split(":");
+ replyTo = thread.data?.find(
+ a => a.kind === Number(parsed[0]) && a.pubkey === parsed[1] && findTag(a, "d") === parsed[2]
+ )?.id;
+ }
+ if (replyTo) {
+ if (!chains.has(replyTo)) {
+ chains.set(replyTo, [v]);
+ } else {
+ unwrap(chains.get(replyTo)).push(v);
+ }
+ }
+ });
+ }
+ return chains;
+ }, [thread.data]);
+
+ // Root is the parent of the current note or the current note if its a root note or the root of the thread
+ const root = useMemo(() => {
+ const currentNote =
+ thread.data?.find(
+ ne =>
+ ne.id === currentId ||
+ (link.type === NostrPrefix.Address && findTag(ne, "d") === currentId && ne.pubkey === link.author)
+ ) ?? (location.state && "sig" in location.state ? (location.state as TaggedNostrEvent) : undefined);
+ if (currentNote) {
+ const currentThread = EventExt.extractThread(currentNote);
+ const isRoot = (ne?: ThreadInfo) => ne === undefined;
+
+ if (isRoot(currentThread)) {
+ return currentNote;
+ }
+ const replyTo = currentThread?.replyTo ?? currentThread?.root;
+
+ // sometimes the root event ID is missing, and we can only take the happy path if the root event ID exists
+ if (replyTo) {
+ if (replyTo.key === "a" && replyTo.value) {
+ const parsed = replyTo.value.split(":");
+ return thread.data?.find(
+ a => a.kind === Number(parsed[0]) && a.pubkey === parsed[1] && findTag(a, "d") === parsed[2]
+ );
+ }
+ if (replyTo.value) {
+ return thread.data?.find(a => a.id === replyTo.value);
+ }
+ }
+
+ const possibleRoots = thread.data?.filter(a => {
+ const thread = EventExt.extractThread(a);
+ return isRoot(thread);
+ });
+ if (possibleRoots) {
+ // worst case we need to check every possible root to see which one contains the current note as a child
+ for (const ne of possibleRoots) {
+ const children = chains.get(ne.id) ?? [];
+
+ if (children.find(ne => ne.id === currentId)) {
+ return ne;
+ }
+ }
+ }
+ }
+ }, [thread.data, currentId, location]);
+
+ const ctxValue = useMemo(() => {
+ return {
+ current: currentId,
+ root,
+ chains,
+ data: thread.data,
+ setCurrent: v => setCurrentId(v)
+ } as ThreadContext
+ }, [root, chains]);
+
+ return
+ {children}
+
+}
\ No newline at end of file
diff --git a/packages/app/src/Pages/Deck.css b/packages/app/src/Pages/Deck.css
new file mode 100644
index 00000000..49fb1a86
--- /dev/null
+++ b/packages/app/src/Pages/Deck.css
@@ -0,0 +1,97 @@
+.deck-layout {
+ display: flex;
+ height: 100vh;
+ overflow-y: hidden;
+}
+
+.deck-layout .deck-cols {
+ display: flex;
+ height: 100vh;
+ overflow-y: hidden;
+ overflow-x: auto;
+}
+
+.deck-layout .deck-cols .deck-col-header {
+ padding: 8px 16px;
+ border: 1px solid var(--border-color);
+ border-collapse: collapse;
+ font-size: 20px;
+ font-weight: 700;
+ min-height: 40px;
+ max-height: 40px;
+}
+
+.deck-layout .deck-cols .deck-col-header:not(:last-of-type) {
+ border-right: 0;
+}
+
+.deck-layout .deck-cols > div {
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+ width: 550px;
+ min-width: 550px;
+}
+
+.deck-layout .deck-cols > div > div:not(:first-of-type) {
+ overflow-y: scroll;
+}
+
+.image-grid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 4px;
+}
+
+.image-grid > .media-note {
+ border: 1px solid var(--border-color);
+ background-image: var(--img);
+ background-position: center;
+ background-size: cover;
+ aspect-ratio: 1;
+ cursor: pointer;
+}
+
+.thread-overlay .modal-body {
+ background-color: unset;
+ padding: 0;
+ width: 100vw;
+ height: 100vh;
+ display: flex;
+ flex-direction: row;
+ border-radius: unset;
+ gap: 16px;
+ --border-color: #3A3A3A;
+}
+
+.thread-overlay .modal-body > div:last-of-type {
+ width: 550px;
+ min-width: 550px;
+ height: 100vh;
+ overflow-y: auto;
+ background-color: var(--gray-superdark);
+}
+
+.thread-overlay .spotlight {
+ flex-grow: 1;
+ margin: auto;
+ text-align: center;
+}
+
+.thread-overlay .spotlight .details {
+ right: calc(28px + 550px + 16px);
+}
+
+.thread-overlay .spotlight .right {
+ right: calc(24px + 550px + 16px);
+}
+
+.thread-overlay .spotlight img,
+.thread-overlay .spotlight video {
+ max-width: calc(100vw - 550px - 16px);
+}
+
+.thread-overlay .main-content {
+ border: 0;
+ border-bottom: 1px solid var(--border-color);
+}
\ No newline at end of file
diff --git a/packages/app/src/Pages/DeckLayout.tsx b/packages/app/src/Pages/DeckLayout.tsx
new file mode 100644
index 00000000..05407512
--- /dev/null
+++ b/packages/app/src/Pages/DeckLayout.tsx
@@ -0,0 +1,151 @@
+import "./Deck.css";
+import { CSSProperties, useContext, useEffect, useState } from "react";
+import { Outlet, useNavigate } from "react-router-dom";
+import { FormattedMessage } from "react-intl";
+import { NostrPrefix, createNostrLink } from "@snort/system";
+
+import { DeckNav } from "Element/Deck/Nav";
+import useLoginFeed from "Feed/LoginFeed";
+import { useLoginRelays } from "Hooks/useLoginRelays";
+import { useTheme } from "Hooks/useTheme";
+import Articles from "Element/Deck/Articles";
+import TimelineFollows from "Element/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/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";
+
+type Cols = "notes" | "articles" | "media" | "streams" | "notifications";
+
+export function SnortDeckLayout() {
+ const login = useLogin();
+ const navigate = useNavigate();
+ const [thread, setThread] = useState();
+
+ useLoginFeed();
+ useTheme();
+ useLoginRelays();
+
+ useEffect(() => {
+ if (!login.publicKey) {
+ navigate("/");
+ }
+ }, [login]);
+
+ if (!login.publicKey) return null;
+ const cols = ["notes", "media", "notifications", "articles"] as Array;
+ return
+
+
+ {cols.map(c => {
+ switch (c) {
+ case "notes": return
+ case "media": return
+ case "articles": return
+ case "notifications": return
+ }
+ })}
+
+ {thread && <>
+
setThread(undefined)} className="thread-overlay">
+
+ setThread(undefined)} />
+
+
+
+
+
+ >}
+
+
+}
+
+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/"));
+
+ return a.content)} idx={0} onClose={onClose} />
+}
+
+function NotesCol() {
+ return (
+
+ );
+}
+
+function ArticlesCol() {
+ return (
+
+ );
+}
+
+function MediaCol({ setThread }: { setThread: (e: string) => void }) {
+ const { proxy } = useImgProxy();
+ return (
+
+
+
+
+
+
+
{
+ 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 setThread(e.id)}>
+ }} />
+
+
+ );
+}
+
+function NotificationsCol() {
+
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/packages/app/src/Pages/Layout.tsx b/packages/app/src/Pages/Layout.tsx
index a428f941..10864cde 100644
--- a/packages/app/src/Pages/Layout.tsx
+++ b/packages/app/src/Pages/Layout.tsx
@@ -4,13 +4,13 @@ import { useDispatch, useSelector } from "react-redux";
import { Link, Outlet, useLocation, useNavigate } from "react-router-dom";
import { FormattedMessage, useIntl } from "react-intl";
import { useUserProfile } from "@snort/system-react";
+import { NostrPrefix, createNostrLink, tryParseNostrLink } from "@snort/system";
import messages from "./messages";
import Icon from "Icons/Icon";
import { RootState } from "State/Store";
import { setShow, reset } from "State/NoteCreator";
-import { System } from "index";
import useLoginFeed from "Feed/LoginFeed";
import { NoteCreator } from "Element/NoteCreator";
import { mapPlanName } from "./subscribe";
@@ -20,8 +20,9 @@ import { profileLink } from "SnortUtils";
import { getCurrentSubscription } from "Subscription";
import Toaster from "Toaster";
import Spinner from "Icons/Spinner";
-import { NostrPrefix, createNostrLink, tryParseNostrLink } from "@snort/system";
import { fetchNip05Pubkey } from "Nip05/Verifier";
+import { useTheme } from "Hooks/useTheme";
+import { useLoginRelays } from "Hooks/useLoginRelays";
export default function Layout() {
const location = useLocation();
@@ -30,10 +31,13 @@ export default function Layout() {
const isReplyNoteCreatorShowing = replyTo && isNoteCreatorShowing;
const dispatch = useDispatch();
const navigate = useNavigate();
- const { publicKey, relays, preferences, subscriptions } = useLogin();
+ const { publicKey, subscriptions } = useLogin();
const currentSubscription = getCurrentSubscription(subscriptions);
const [pageClass, setPageClass] = useState("page");
+
useLoginFeed();
+ useTheme();
+ useLoginRelays();
const handleNoteCreatorButtonClick = () => {
if (replyTo) {
@@ -62,46 +66,6 @@ export default function Layout() {
}
}, [location]);
- useEffect(() => {
- if (relays) {
- (async () => {
- for (const [k, v] of Object.entries(relays.item)) {
- await System.ConnectToRelay(k, v);
- }
- for (const v of System.Sockets) {
- if (!relays.item[v.address] && !v.ephemeral) {
- System.DisconnectRelay(v.address);
- }
- }
- })();
- }
- }, [relays]);
-
- function setTheme(theme: "light" | "dark") {
- const elm = document.documentElement;
- if (theme === "light" && !elm.classList.contains("light")) {
- elm.classList.add("light");
- } else if (theme === "dark" && elm.classList.contains("light")) {
- elm.classList.remove("light");
- }
- }
-
- useEffect(() => {
- const osTheme = window.matchMedia("(prefers-color-scheme: light)");
- setTheme(
- preferences.theme === "system" && osTheme.matches ? "light" : preferences.theme === "light" ? "light" : "dark",
- );
-
- osTheme.onchange = e => {
- if (preferences.theme === "system") {
- setTheme(e.matches ? "light" : "dark");
- }
- };
- return () => {
- osTheme.onchange = null;
- };
- }, [preferences.theme]);
-
return (
{!shouldHideHeader && (
@@ -220,7 +184,7 @@ const AccountHeader = () => {
{unreadDms > 0 &&
}
-
+
{hasNotifications &&
}
button {
- background: white;
- color: black;
- font-size: 16px;
- padding: 10px 16px;
- border-radius: 1000px;
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 12px;
-}
diff --git a/packages/app/src/Pages/Root.tsx b/packages/app/src/Pages/Root.tsx
index 8f03f52d..40f12a0d 100644
--- a/packages/app/src/Pages/Root.tsx
+++ b/packages/app/src/Pages/Root.tsx
@@ -1,8 +1,7 @@
-import { ReactNode, useEffect, useState } from "react";
-import { Link, Outlet, RouteObject, useLocation, useNavigate, useParams } from "react-router-dom";
+import { useEffect, useState } from "react";
+import { Link, Outlet, RouteObject, useParams } from "react-router-dom";
import { FormattedMessage } from "react-intl";
-import { Menu, MenuItem } from "@szhsin/react-menu";
-import "./Root.css";
+import { unixNow } from "@snort/shared";
import Timeline from "Element/Timeline";
import { System } from "index";
@@ -10,165 +9,26 @@ import { TimelineSubject } from "Feed/TimelineFeed";
import { debounce, getRelayName, sha256 } from "SnortUtils";
import useLogin from "Hooks/useLogin";
import Discover from "Pages/Discover";
-import Icon from "Icons/Icon";
import TrendingUsers from "Element/TrendingUsers";
import TrendingNotes from "Element/TrendingPosts";
import HashTagsPage from "Pages/HashTagsPage";
import SuggestedProfiles from "Element/SuggestedProfiles";
import { TaskList } from "Tasks/TaskList";
import TimelineFollows from "Element/TimelineFollows";
+import { RootTabs } from "Element/RootTabs";
import messages from "./messages";
-import { unixNow } from "@snort/shared";
interface RelayOption {
url: string;
paid: boolean;
}
-type RootPage = "following" | "conversations" | "trending-notes" | "trending-people" | "suggested" | "tags" | "global";
-
export default function RootPage() {
- const navigate = useNavigate();
- const location = useLocation();
- const { publicKey: pubKey, tags, preferences } = useLogin();
- const [rootType, setRootType] = useState("following");
-
- const menuItems = [
- {
- tab: "following",
- path: "/notes",
- show: Boolean(pubKey),
- element: (
- <>
-
-
- >
- ),
- },
- {
- tab: "trending-notes",
- path: "/trending/notes",
- show: true,
- element: (
- <>
-
-
- >
- ),
- },
- {
- tab: "conversations",
- path: "/conversations",
- show: Boolean(pubKey),
- element: (
- <>
-
-
- >
- ),
- },
- {
- tab: "trending-people",
- path: "/trending/people",
- show: true,
- element: (
- <>
-
-
- >
- ),
- },
- {
- tab: "suggested",
- path: "/suggested",
- show: Boolean(pubKey),
- element: (
- <>
-
-
- >
- ),
- },
- {
- tab: "global",
- path: "/global",
- show: true,
- element: (
- <>
-
-
- >
- ),
- },
- ] as Array<{
- tab: RootPage;
- path: string;
- show: boolean;
- element: ReactNode;
- }>;
-
- useEffect(() => {
- if (location.pathname === "/") {
- const t = pubKey ? preferences.defaultRootTab ?? "/notes" : "/trending/notes";
- navigate(t);
- } else {
- const currentTab = menuItems.find(a => a.path === location.pathname)?.tab;
- if (currentTab) {
- setRootType(currentTab);
- }
- }
- }, [location]);
-
- function currentMenuItem() {
- if (location.pathname.startsWith("/t/")) {
- return (
- <>
-
- {location.pathname.split("/").slice(-1)}
- >
- );
- }
- return menuItems.find(a => a.tab === rootType)?.element;
- }
-
return (
<>
-
-
+
+
@@ -196,7 +56,7 @@ const FollowsHint = () => {
return null;
};
-const GlobalTab = () => {
+export const GlobalTab = () => {
const { relays } = useLogin();
const [relay, setRelay] = useState
();
const [allRelays, setAllRelays] = useState();
@@ -272,7 +132,7 @@ const GlobalTab = () => {
);
};
-const NotesTab = () => {
+export const NotesTab = () => {
return (
<>
@@ -282,71 +142,81 @@ const NotesTab = () => {
);
};
-const ConversationsTab = () => {
+export const ConversationsTab = () => {
return ;
};
-const TagsTab = () => {
+export const TagsTab = (params: { tag?: string }) => {
const { tag } = useParams();
+ const t = params.tag ?? tag ?? "";
const subject: TimelineSubject = {
type: "hashtag",
- items: [tag ?? ""],
- discriminator: `tags-${tag}`,
+ items: [t],
+ discriminator: `tags-${t}`,
streams: true,
};
return ;
};
+const DefaultTab = () => {
+ const { preferences, publicKey } = useLogin();
+ const tab = publicKey ? preferences.defaultRootTab ?? `notes` : `trending/notes`;
+ const elm = RootTabRoutes.find(a => a.path === tab)?.element;
+ return elm;
+}
+
+export const RootTabRoutes = [
+ {
+ path: "",
+ element:
+ },
+ {
+ path: "global",
+ element: ,
+ },
+ {
+ path: "notes",
+ element: ,
+ },
+ {
+ path: "conversations",
+ element: ,
+ },
+ {
+ path: "discover",
+ element: ,
+ },
+ {
+ path: "tag/:tag",
+ element: ,
+ },
+ {
+ path: "trending/notes",
+ element: ,
+ },
+ {
+ path: "trending/people",
+ element: ,
+ },
+ {
+ path: "suggested",
+ element: (
+
+
+
+ ),
+ },
+ {
+ path: "t/:tag",
+ element: ,
+ },
+];
+
export const RootRoutes = [
{
path: "/",
element: ,
- children: [
- {
- path: "global",
- element: ,
- },
- {
- path: "notes",
- element: ,
- },
- {
- path: "conversations",
- element: ,
- },
- {
- path: "discover",
- element: ,
- },
- {
- path: "tag/:tag",
- element: ,
- },
- {
- path: "trending/notes",
- element: ,
- },
- {
- path: "trending/people",
- element: (
-
-
-
- ),
- },
- {
- path: "suggested",
- element: (
-
-
-
- ),
- },
- {
- path: "/t/:tag",
- element: ,
- },
- ],
+ children: RootTabRoutes,
},
] as RouteObject[];
diff --git a/packages/app/src/index.css b/packages/app/src/index.css
index e759b129..8584a12a 100644
--- a/packages/app/src/index.css
+++ b/packages/app/src/index.css
@@ -261,6 +261,10 @@ button.icon:hover {
display: inline-flex;
}
+.light .btn {
+ color: #64748B;
+}
+
.btn-warn {
border-color: var(--error);
}
@@ -353,6 +357,7 @@ input:disabled {
.f-center {
justify-content: center;
+ align-items: center;
}
.f-1 {
diff --git a/packages/app/src/index.tsx b/packages/app/src/index.tsx
index 781a190d..6d6d4410 100644
--- a/packages/app/src/index.tsx
+++ b/packages/app/src/index.tsx
@@ -28,7 +28,7 @@ import Store from "State/Store";
import Layout from "Pages/Layout";
import LoginPage from "Pages/LoginPage";
import ProfilePage from "Pages/ProfilePage";
-import { RootRoutes } from "Pages/Root";
+import { RootRoutes, RootTabRoutes } from "Pages/Root";
import NotificationsPage from "Pages/Notifications";
import SettingsPage, { SettingsRoutes } from "Pages/SettingsPage";
import ErrorPage from "Pages/ErrorPage";
@@ -40,13 +40,14 @@ import HelpPage from "Pages/HelpPage";
import { NewUserRoutes } from "Pages/new";
import { WalletRoutes } from "Pages/WalletPage";
import NostrLinkHandler from "Pages/NostrLinkHandler";
-import Thread from "Element/Thread";
+import { ThreadRoute } from "Element/Thread";
import { SubscribeRoutes } from "Pages/subscribe";
import ZapPoolPage from "Pages/ZapPool";
import DebugPage from "Pages/Debug";
import { db } from "Db";
import { preload, RelayMetrics, UserCache, UserRelays } from "Cache";
import { LoginStore } from "Login";
+import { SnortDeckLayout } from "Pages/DeckLayout";
const WasmQueryOptimizer = {
expandFilter: (f: ReqFilter) => {
@@ -152,7 +153,7 @@ export const router = createBrowserRouter([
},
{
path: "/e/:id",
- element: ,
+ element: ,
},
{
path: "/p/:id",
@@ -200,6 +201,18 @@ export const router = createBrowserRouter([
},
],
},
+ {
+ path: "/deck",
+ element: ,
+ loader: async () => {
+ if (!didInit) {
+ didInit = true;
+ return await initSite();
+ }
+ return null;
+ },
+ children: RootTabRoutes
+ }
]);
const root = ReactDOM.createRoot(unwrap(document.getElementById("root")));
diff --git a/packages/system/src/event-kind.ts b/packages/system/src/event-kind.ts
index 72a7e03d..ab9af77f 100644
--- a/packages/system/src/event-kind.ts
+++ b/packages/system/src/event-kind.ts
@@ -24,6 +24,7 @@ enum EventKind {
TagLists = 30002, // NIP-51c
Badge = 30009, // NIP-58
ProfileBadges = 30008, // NIP-58
+ LongFormTextNote = 30023, // NIP-23
LiveEvent = 30311, // NIP-102
ZapstrTrack = 31337,
SimpleChatMetadata = 39_000, // NIP-29
diff --git a/packages/system/src/nostr-link.ts b/packages/system/src/nostr-link.ts
index 2aeedbba..2ab44fa2 100644
--- a/packages/system/src/nostr-link.ts
+++ b/packages/system/src/nostr-link.ts
@@ -22,6 +22,22 @@ export function linkToEventTag(link: NostrLink) {
}
}
+export function tagToNostrLink(tag: Array) {
+ switch(tag[0]) {
+ case "e": {
+ return createNostrLink(NostrPrefix.Event, tag[1], tag.slice(2));
+ }
+ case "p": {
+ return createNostrLink(NostrPrefix.Profile, tag[1], tag.slice(2));
+ }
+ case "a": {
+ const [kind, author, dTag] = tag[1].split(":");
+ return createNostrLink(NostrPrefix.Address, dTag, tag.slice(2), Number(kind), author);
+ }
+ }
+ throw new Error(`Unknown tag kind ${tag[0]}`);
+}
+
export function createNostrLinkToEvent(ev: TaggedNostrEvent | NostrEvent) {
const relays = "relays" in ev ? ev.relays : undefined;