From dd941ae70ecc0e51a11bed532eee20777fda4cb5 Mon Sep 17 00:00:00 2001 From: Martti Malmi Date: Tue, 21 Nov 2023 11:57:25 +0200 Subject: [PATCH] wip 3-col layout --- packages/app/src/Pages/Layout.tsx | 202 ------------------ .../app/src/Pages/Layout/AccountHeader.tsx | 104 +++++++++ .../app/src/Pages/{ => Layout}/Layout.css | 0 packages/app/src/Pages/Layout/LogoHeader.tsx | 30 +++ packages/app/src/Pages/Layout/NavSidebar.tsx | 56 +++++ packages/app/src/Pages/Layout/RightColumn.tsx | 11 + packages/app/src/Pages/Layout/index.tsx | 99 +++++++++ .../app/src/Pages/Profile/ProfilePage.css | 2 - packages/app/src/SnortUtils/Utils.test.ts | 2 +- packages/app/src/index.css | 12 -- packages/app/vite.config.ts | 2 +- 11 files changed, 302 insertions(+), 218 deletions(-) delete mode 100644 packages/app/src/Pages/Layout.tsx create mode 100644 packages/app/src/Pages/Layout/AccountHeader.tsx rename packages/app/src/Pages/{ => Layout}/Layout.css (100%) create mode 100644 packages/app/src/Pages/Layout/LogoHeader.tsx create mode 100644 packages/app/src/Pages/Layout/NavSidebar.tsx create mode 100644 packages/app/src/Pages/Layout/RightColumn.tsx create mode 100644 packages/app/src/Pages/Layout/index.tsx 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 ( +
+
+
+ + + +
+ + +
+ + {isStalker && } +
+ ); +} + +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", }, });