diff --git a/packages/app/src/Pages/Layout.tsx b/packages/app/src/Pages/Layout.tsx
deleted file mode 100644
index c71fd502..00000000
--- a/packages/app/src/Pages/Layout.tsx
+++ /dev/null
@@ -1,202 +0,0 @@
-import "./Layout.css";
-import { useEffect, useMemo, useState } from "react";
-import { Link, Outlet, useLocation, useNavigate } from "react-router-dom";
-import { FormattedMessage } from "react-intl";
-import { useUserProfile } from "@snort/system-react";
-import { base64 } from "@scure/base";
-import { unwrap } from "@snort/shared";
-
-import Icon from "@/Icons/Icon";
-import useLoginFeed from "@/Feed/LoginFeed";
-import { mapPlanName } from "./subscribe";
-import useLogin from "@/Hooks/useLogin";
-import Avatar from "@/Element/User/Avatar";
-import { isHalloween, isFormElement, isStPatricksDay, isChristmas } from "@/SnortUtils";
-import { getCurrentSubscription } from "@/Subscription";
-import Toaster from "@/Toaster";
-import { useTheme } from "@/Hooks/useTheme";
-import { useLoginRelays } from "@/Hooks/useLoginRelays";
-import { LoginUnlock } from "@/Element/PinPrompt";
-import useKeyboardShortcut from "@/Hooks/useKeyboardShortcut";
-import { LoginStore } from "@/Login";
-import { NoteCreatorButton } from "@/Element/Event/NoteCreatorButton";
-import { ProfileLink } from "@/Element/User/ProfileLink";
-import SearchBox from "../Element/SearchBox";
-import SnortApi from "@/External/SnortApi";
-import useEventPublisher from "@/Hooks/useEventPublisher";
-
-export default function Layout() {
- const location = useLocation();
- const [pageClass, setPageClass] = useState("page");
- const { id, stalker } = useLogin(s => ({ id: s.id, stalker: s.stalker ?? false }));
-
- useLoginFeed();
- useTheme();
- useLoginRelays();
- useKeyboardShortcut(".", event => {
- // if event happened in a form element, do nothing, otherwise focus on search input
- if (event.target && !isFormElement(event.target as HTMLElement)) {
- event.preventDefault();
- window.scrollTo({
- top: 0,
- behavior: "smooth",
- });
- }
- });
-
- const shouldHideHeader = useMemo(() => {
- const hideOn = ["/login", "/new"];
- return hideOn.some(a => location.pathname.startsWith(a));
- }, [location]);
-
- useEffect(() => {
- const widePage = ["/login", "/messages"];
- const noScroll = ["/messages"];
- if (widePage.some(a => location.pathname.startsWith(a))) {
- setPageClass(noScroll.some(a => location.pathname.startsWith(a)) ? "scroll-lock" : "");
- } else {
- setPageClass("page");
- }
- }, [location]);
-
- return (
- <>
-
- {!shouldHideHeader && (
-
- )}
-
-
-
-
-
- {stalker && (
- {
- LoginStore.removeSession(id);
- }}>
-
-
- )}
- >
- );
-}
-
-const AccountHeader = () => {
- const navigate = useNavigate();
-
- useKeyboardShortcut("/", event => {
- // if event happened in a form element, do nothing, otherwise focus on search input
- if (event.target && !isFormElement(event.target as HTMLElement)) {
- event.preventDefault();
- document.querySelector(".search input")?.focus();
- }
- });
-
- const { publicKey, latestNotification, readNotifications, readonly } = useLogin(s => ({
- publicKey: s.publicKey,
- latestNotification: s.latestNotification,
- readNotifications: s.readNotifications,
- readonly: s.readonly,
- }));
- const profile = useUserProfile(publicKey);
- const { publisher } = useEventPublisher();
-
- const hasNotifications = useMemo(
- () => latestNotification > readNotifications,
- [latestNotification, readNotifications],
- );
- const unreadDms = useMemo(() => (publicKey ? 0 : 0), [publicKey]);
-
- async function goToNotifications() {
- // request permissions to send notifications
- if ("Notification" in window) {
- try {
- if (Notification.permission !== "granted") {
- const res = await Notification.requestPermission();
- console.debug(res);
- }
- } catch (e) {
- console.error(e);
- }
- }
- try {
- if ("serviceWorker" in navigator) {
- const reg = await navigator.serviceWorker.ready;
- if (reg && publisher) {
- const api = new SnortApi(undefined, publisher);
- const sub = await reg.pushManager.subscribe({
- userVisibleOnly: true,
- applicationServerKey: (await api.getPushNotificationInfo()).publicKey,
- });
- await api.registerPushNotifications({
- endpoint: sub.endpoint,
- p256dh: base64.encode(new Uint8Array(unwrap(sub.getKey("p256dh")))),
- auth: base64.encode(new Uint8Array(unwrap(sub.getKey("auth")))),
- scope: `${location.protocol}//${location.hostname}`,
- });
- }
- }
- } catch (e) {
- console.error(e);
- }
- }
-
- if (!publicKey) {
- return (
-
- );
- }
- return (
-
- {!location.pathname.startsWith("/search") ?
:
}
- {!readonly && (
-
-
- {unreadDms > 0 &&
}
-
- )}
-
-
- {hasNotifications &&
}
-
-
-
-
-
- );
-};
-
-function LogoHeader() {
- const { subscriptions } = useLogin();
- const currentSubscription = getCurrentSubscription(subscriptions);
-
- const extra = () => {
- if (isHalloween()) return "🎃";
- if (isStPatricksDay()) return "🍀";
- if (isChristmas()) return "🎄";
- };
-
- return (
-
-
- {extra()}
- {CONFIG.appName}
-
- {currentSubscription && (
-
-
- {mapPlanName(currentSubscription.type)}
-
- )}
-
- );
-}
diff --git a/packages/app/src/Pages/Layout/AccountHeader.tsx b/packages/app/src/Pages/Layout/AccountHeader.tsx
new file mode 100644
index 00000000..1b118724
--- /dev/null
+++ b/packages/app/src/Pages/Layout/AccountHeader.tsx
@@ -0,0 +1,104 @@
+import { Link, useNavigate } from "react-router-dom";
+import { useUserProfile } from "@snort/system-react";
+import { useMemo } from "react";
+import { base64 } from "@scure/base";
+import { unwrap } from "@snort/shared";
+import { FormattedMessage } from "react-intl";
+import SearchBox from "../../Element/SearchBox";
+import { ProfileLink } from "../../Element/User/ProfileLink";
+import Avatar from "../../Element/User/Avatar";
+import Icon from "../../Icons/Icon";
+import useKeyboardShortcut from "../../Hooks/useKeyboardShortcut";
+import { isFormElement } from "../../SnortUtils";
+import useLogin from "../../Hooks/useLogin";
+import useEventPublisher from "../../Hooks/useEventPublisher";
+import SnortApi from "../../External/SnortApi";
+
+const AccountHeader = () => {
+ const navigate = useNavigate();
+
+ useKeyboardShortcut("/", event => {
+ // if event happened in a form element, do nothing, otherwise focus on search input
+ if (event.target && !isFormElement(event.target as HTMLElement)) {
+ event.preventDefault();
+ document.querySelector(".search input")?.focus();
+ }
+ });
+
+ const { publicKey, latestNotification, readNotifications, readonly } = useLogin(s => ({
+ publicKey: s.publicKey,
+ latestNotification: s.latestNotification,
+ readNotifications: s.readNotifications,
+ readonly: s.readonly,
+ }));
+ const profile = useUserProfile(publicKey);
+ const { publisher } = useEventPublisher();
+
+ const hasNotifications = useMemo(
+ () => latestNotification > readNotifications,
+ [latestNotification, readNotifications],
+ );
+ const unreadDms = useMemo(() => (publicKey ? 0 : 0), [publicKey]);
+
+ async function goToNotifications() {
+ // request permissions to send notifications
+ if ("Notification" in window) {
+ try {
+ if (Notification.permission !== "granted") {
+ const res = await Notification.requestPermission();
+ console.debug(res);
+ }
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ try {
+ if ("serviceWorker" in navigator) {
+ const reg = await navigator.serviceWorker.ready;
+ if (reg && publisher) {
+ const api = new SnortApi(undefined, publisher);
+ const sub = await reg.pushManager.subscribe({
+ userVisibleOnly: true,
+ applicationServerKey: (await api.getPushNotificationInfo()).publicKey,
+ });
+ await api.registerPushNotifications({
+ endpoint: sub.endpoint,
+ p256dh: base64.encode(new Uint8Array(unwrap(sub.getKey("p256dh")))),
+ auth: base64.encode(new Uint8Array(unwrap(sub.getKey("auth")))),
+ scope: `${location.protocol}//${location.hostname}`,
+ });
+ }
+ }
+ } catch (e) {
+ console.error(e);
+ }
+ }
+
+ if (!publicKey) {
+ return (
+
+ );
+ }
+ return (
+
+ {!location.pathname.startsWith("/search") ?
:
}
+ {!readonly && (
+
+
+ {unreadDms > 0 &&
}
+
+ )}
+
+
+ {hasNotifications &&
}
+
+
+
+
+
+ );
+};
+
+export default AccountHeader;
diff --git a/packages/app/src/Pages/Layout.css b/packages/app/src/Pages/Layout/Layout.css
similarity index 100%
rename from packages/app/src/Pages/Layout.css
rename to packages/app/src/Pages/Layout/Layout.css
diff --git a/packages/app/src/Pages/Layout/LogoHeader.tsx b/packages/app/src/Pages/Layout/LogoHeader.tsx
new file mode 100644
index 00000000..43c6774a
--- /dev/null
+++ b/packages/app/src/Pages/Layout/LogoHeader.tsx
@@ -0,0 +1,30 @@
+import useLogin from "../../Hooks/useLogin";
+import { getCurrentSubscription } from "../../Subscription";
+import { isChristmas, isHalloween, isStPatricksDay } from "../../SnortUtils";
+import { Link } from "react-router-dom";
+import { mapPlanName } from "../subscribe";
+export function LogoHeader() {
+ const { subscriptions } = useLogin();
+ const currentSubscription = getCurrentSubscription(subscriptions);
+
+ const extra = () => {
+ if (isHalloween()) return "🎃";
+ if (isStPatricksDay()) return "🍀";
+ if (isChristmas()) return "🎄";
+ };
+
+ return (
+
+
+ {extra()}
+ {CONFIG.appName}
+
+ {currentSubscription && (
+
+
+ {mapPlanName(currentSubscription.type)}
+
+ )}
+
+ );
+}
diff --git a/packages/app/src/Pages/Layout/NavSidebar.tsx b/packages/app/src/Pages/Layout/NavSidebar.tsx
new file mode 100644
index 00000000..9caf2f14
--- /dev/null
+++ b/packages/app/src/Pages/Layout/NavSidebar.tsx
@@ -0,0 +1,56 @@
+import { LogoHeader } from "./LogoHeader";
+import { Link } from "react-router-dom";
+import Icon from "@/Icons/Icon";
+
+const MENU_ITEMS = [
+ {
+ label: "Home",
+ icon: "home",
+ link: "/",
+ },
+ {
+ label: "Messages",
+ icon: "mail",
+ link: "/messages",
+ },
+ {
+ label: "Notifications",
+ icon: "bell-02",
+ link: "/notifications",
+ },
+ {
+ label: "Settings",
+ icon: "cog",
+ link: "/settings",
+ },
+];
+
+export default function NavSidebar() {
+ return (
+
+
+
+
+ {MENU_ITEMS.map(item => (
+
+
+ {item.label}
+
+ ))}
+
+
+
+ );
+}
+
+function NavLink({ children, ...props }) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/packages/app/src/Pages/Layout/RightColumn.tsx b/packages/app/src/Pages/Layout/RightColumn.tsx
new file mode 100644
index 00000000..a39522ea
--- /dev/null
+++ b/packages/app/src/Pages/Layout/RightColumn.tsx
@@ -0,0 +1,11 @@
+import SearchBox from "../../Element/SearchBox";
+
+export default function RightColumn() {
+ return (
+
+ );
+}
diff --git a/packages/app/src/Pages/Layout/index.tsx b/packages/app/src/Pages/Layout/index.tsx
new file mode 100644
index 00000000..a570e238
--- /dev/null
+++ b/packages/app/src/Pages/Layout/index.tsx
@@ -0,0 +1,99 @@
+import "./Layout.css";
+import { useCallback, useEffect, useMemo, useState } from "react";
+import { Outlet, useLocation } from "react-router-dom";
+
+import Icon from "@/Icons/Icon";
+import useLogin from "@/Hooks/useLogin";
+import { isFormElement } from "@/SnortUtils";
+import Toaster from "@/Toaster";
+import { useTheme } from "@/Hooks/useTheme";
+import { useLoginRelays } from "@/Hooks/useLoginRelays";
+import { LoginUnlock } from "@/Element/PinPrompt";
+import useKeyboardShortcut from "@/Hooks/useKeyboardShortcut";
+import { LoginStore } from "@/Login";
+import { NoteCreatorButton } from "@/Element/Event/NoteCreatorButton";
+import NavSidebar from "./NavSidebar";
+import AccountHeader from "./AccountHeader";
+import RightColumn from "./RightColumn";
+import { LogoHeader } from "./LogoHeader";
+
+export default function Index() {
+ const location = useLocation();
+ const [pageClass, setPageClass] = useState("page");
+ const { id, stalker } = useLogin(s => ({ id: s.id, stalker: s.stalker ?? false }));
+
+ useTheme();
+ useLoginRelays();
+
+ const hideHeaderPaths = ["/login", "/new"];
+ const shouldHideHeader = hideHeaderPaths.some(path => location.pathname.startsWith(path));
+
+ const pageClassPaths = useMemo(
+ () => ({
+ widePage: ["/login", "/messages"],
+ noScroll: ["/messages"],
+ }),
+ [],
+ );
+
+ useEffect(() => {
+ const isWidePage = pageClassPaths.widePage.some(path => location.pathname.startsWith(path));
+ const isNoScroll = pageClassPaths.noScroll.some(path => location.pathname.startsWith(path));
+ setPageClass(isWidePage ? (isNoScroll ? "scroll-lock" : "") : "page");
+ }, [location, pageClassPaths]);
+
+ const handleKeyboardShortcut = useCallback(event => {
+ if (event.target && !isFormElement(event.target as HTMLElement)) {
+ event.preventDefault();
+ window.scrollTo({ top: 0, behavior: "smooth" });
+ }
+ }, []);
+
+ useKeyboardShortcut(".", handleKeyboardShortcut);
+
+ const isStalker = !!stalker;
+
+ return (
+
+ );
+}
+
+function MainContent({ shouldHideHeader }) {
+ return (
+
+ {!shouldHideHeader && }
+
+
+ );
+}
+
+function Header() {
+ return (
+
+ );
+}
+
+function StalkerModal({ id }) {
+ return (
+ LoginStore.removeSession(id)}>
+
+
+ );
+}
diff --git a/packages/app/src/Pages/Profile/ProfilePage.css b/packages/app/src/Pages/Profile/ProfilePage.css
index 6c0851eb..0f0c0f8f 100644
--- a/packages/app/src/Pages/Profile/ProfilePage.css
+++ b/packages/app/src/Pages/Profile/ProfilePage.css
@@ -39,8 +39,6 @@
@media (min-width: 520px) {
.profile .banner {
- width: 100%;
- max-width: 720px;
height: 280px;
}
diff --git a/packages/app/src/SnortUtils/Utils.test.ts b/packages/app/src/SnortUtils/Utils.test.ts
index be354076..16646ed0 100644
--- a/packages/app/src/SnortUtils/Utils.test.ts
+++ b/packages/app/src/SnortUtils/Utils.test.ts
@@ -1,5 +1,5 @@
import { magnetURIDecode, getRelayName } from ".";
-import { describe, it, expect } from 'vitest';
+import { describe, it, expect } from "vitest";
describe("magnet", () => {
it("should parse magnet link", () => {
diff --git a/packages/app/src/index.css b/packages/app/src/index.css
index ff01884f..c3665f7e 100644
--- a/packages/app/src/index.css
+++ b/packages/app/src/index.css
@@ -136,19 +136,7 @@ a.ext {
overflow-x: hidden;
}
-.page {
- width: 100vw;
- margin-left: auto;
- margin-right: auto;
-}
-
@media (min-width: 768px) {
- .page {
- width: 640px;
- margin-left: auto;
- margin-right: auto;
- }
-
.main-content {
border: 1px solid var(--border-color);
}
diff --git a/packages/app/vite.config.ts b/packages/app/vite.config.ts
index 7a3942dd..8c69f69d 100644
--- a/packages/app/vite.config.ts
+++ b/packages/app/vite.config.ts
@@ -47,6 +47,6 @@ export default defineConfig({
},
test: {
globals: true,
- environment: 'jsdom',
+ environment: "jsdom",
},
});