cleanup account switcher

This commit is contained in:
hzrd149 2025-01-15 09:08:21 -06:00
parent fbbe79f194
commit ff68b25a41
15 changed files with 224 additions and 247 deletions

View File

@ -114,7 +114,7 @@
"react-window": "^1.8.11",
"remark-gfm": "^4.0.0",
"remark-wiki-link": "^2.0.1",
"rx-nostr": "^3.4.2",
"rx-nostr": "^3.5.0",
"rxjs": "^7.8.1",
"three": "^0.170.0",
"three-spritetext": "^1.9.3",

12
pnpm-lock.yaml generated
View File

@ -301,8 +301,8 @@ importers:
specifier: ^2.0.1
version: 2.0.1
rx-nostr:
specifier: ^3.4.2
version: 3.4.2
specifier: ^3.5.0
version: 3.5.0
rxjs:
specifier: ^7.8.1
version: 7.8.1
@ -5143,8 +5143,8 @@ packages:
run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
rx-nostr@3.4.2:
resolution: {integrity: sha512-K8SCWyYIE5cd4I8KUfHK207uINyjcU4AY2/G1SlsVEMGbk5IFe28vgqSt87p2JUxzfm4g0/4oWevAlVAERIokw==}
rx-nostr@3.5.0:
resolution: {integrity: sha512-SFk/WTYKW1GAecyxLKNQlkdedrFfGDeT8nz8wTFriIqd2I6jSV5lm7jBkStcnB4ncDK7GOf4QoLcZfxA+OqaQw==}
rxjs@7.8.1:
resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==}
@ -8509,7 +8509,7 @@ snapshots:
applesauce-core: 0.0.0-next-20250114214607(typescript@5.7.3)
nanoid: 5.0.9
nostr-tools: 2.10.4(typescript@5.7.3)
rx-nostr: 3.4.2
rx-nostr: 3.5.0
rxjs: 7.8.1
transitivePeerDependencies:
- supports-color
@ -11950,7 +11950,7 @@ snapshots:
dependencies:
queue-microtask: 1.2.3
rx-nostr@3.4.2:
rx-nostr@3.5.0:
dependencies:
nostr-typedef: 0.9.0
rxjs: 7.8.1

View File

@ -0,0 +1,3 @@
import { createContext } from "react";
export const CollapsedContext = createContext(false);

View File

@ -1,5 +1,5 @@
import { Suspense } from "react";
import { Flex, Spinner } from "@chakra-ui/react";
import { Spinner } from "@chakra-ui/react";
import { Outlet, ScrollRestoration } from "react-router-dom";
import DesktopSideNav from "./side-nav";

View File

@ -1,65 +1,19 @@
import { createContext, useState } from "react";
import {
Flex,
FlexProps,
IconButton,
Menu,
MenuButton,
MenuDivider,
MenuItem,
MenuList,
Spacer,
} from "@chakra-ui/react";
import { useState } from "react";
import { Flex, FlexProps, IconButton } from "@chakra-ui/react";
import UserAvatar from "../../user/user-avatar";
import useCurrentAccount from "../../../hooks/use-current-account";
import accountService from "../../../services/account";
import UserName from "../../user/user-name";
import UserDnsIdentity from "../../user/user-dns-identity";
import { ChevronLeftIcon, ChevronRightIcon, SettingsIcon } from "../../icons";
import Plus from "../../icons/plus";
import NavItem from "../nav-items/nav-item";
import { ChevronLeftIcon, ChevronRightIcon } from "../../icons";
import NavItems from "../nav-items";
import useRootPadding from "../../../hooks/use-root-padding";
export const ExpandedContext = createContext(false);
function UserAccount() {
const account = useCurrentAccount()!;
return (
<Menu placement="right" offset={[32, 16]}>
<MenuButton
as={IconButton}
variant="outline"
w="12"
h="12"
borderRadius="50%"
icon={<UserAvatar pubkey={account.pubkey} />}
/>
<MenuList boxShadow="lg">
<Flex gap="2" px="2" alignItems="center">
<UserAvatar pubkey={account.pubkey} />
<Flex direction="column">
<UserName pubkey={account.pubkey} fontSize="xl" />
<UserDnsIdentity pubkey={account.pubkey} />
</Flex>
</Flex>
<MenuDivider />
<MenuItem onClick={() => accountService.logout()}>Logout</MenuItem>
</MenuList>
</Menu>
);
}
import AccountSwitcher from "../nav-items/account-switcher";
import { CollapsedContext } from "../context";
export default function DesktopSideNav({ ...props }: Omit<FlexProps, "children">) {
const account = useCurrentAccount();
const [expanded, setExpanded] = useState(true);
const [collapsed, setCollapsed] = useState(false);
useRootPadding({ left: expanded ? "var(--chakra-sizes-64)" : "var(--chakra-sizes-16)" });
useRootPadding({ left: collapsed ? "var(--chakra-sizes-16)" : "var(--chakra-sizes-64)" });
return (
<ExpandedContext.Provider value={expanded}>
<CollapsedContext.Provider value={collapsed}>
<Flex
direction="column"
gap="2"
@ -69,7 +23,7 @@ export default function DesktopSideNav({ ...props }: Omit<FlexProps, "children">
borderRightWidth={1}
pt="calc(var(--chakra-space-2) + var(--safe-top))"
pb="calc(var(--chakra-space-2) + var(--safe-bottom))"
w={expanded ? "64" : "16"}
w={collapsed ? "16" : "64"}
position="fixed"
left="0"
bottom="0"
@ -77,27 +31,23 @@ export default function DesktopSideNav({ ...props }: Omit<FlexProps, "children">
zIndex="modal"
overflowY="auto"
overflowX="hidden"
overscroll="none"
{...props}
>
<IconButton
aria-label={expanded ? "Close" : "Open"}
title={expanded ? "Close" : "Open"}
aria-label={collapsed ? "Open" : "Close"}
title={collapsed ? "Open" : "Close"}
size="sm"
variant="ghost"
onClick={() => setExpanded(!expanded)}
icon={expanded ? <ChevronLeftIcon boxSize={5} /> : <ChevronRightIcon boxSize={5} />}
onClick={() => setCollapsed(!collapsed)}
icon={collapsed ? <ChevronRightIcon boxSize={5} /> : <ChevronLeftIcon boxSize={5} />}
position="absolute"
bottom="4"
right="-4"
/>
{account && <UserAccount />}
<NavItem icon={Plus} label="Create new" colorScheme="primary" to="/new" variant="solid" />
<AccountSwitcher />
<NavItems />
<Spacer />
<NavItem label="Settings" icon={SettingsIcon} to="/settings" />
</Flex>
</ExpandedContext.Provider>
</CollapsedContext.Provider>
);
}

View File

@ -1,6 +1,4 @@
import { BehaviorSubject } from "rxjs";
import { Outlet, ScrollRestoration } from "react-router-dom";
import { useObservable } from "applesauce-react/hooks";
import MobileBottomNav from "./bottom-nav";
import { ErrorBoundary } from "../../error-boundary";

View File

@ -1,24 +1,9 @@
import {
Avatar,
Box,
Button,
Drawer,
DrawerBody,
DrawerContent,
DrawerOverlay,
DrawerProps,
Flex,
Text,
} from "@chakra-ui/react";
import { Link as RouterLink } from "react-router-dom";
import { Avatar, Drawer, DrawerBody, DrawerContent, DrawerOverlay, DrawerProps, Flex, Text } from "@chakra-ui/react";
import AccountSwitcher from "../../legacy-layout/account-switcher";
import AccountSwitcher from "../nav-items/account-switcher";
import useCurrentAccount from "../../../hooks/use-current-account";
import NavItems from "../nav-items";
import TaskManagerButtons from "../../legacy-layout/task-manager-buttons";
import { ExpandedContext } from "../desktop/side-nav";
import NavItem from "../nav-items/nav-item";
import { SettingsIcon } from "../../icons";
import { CollapsedContext } from "../context";
export default function NavDrawer({ ...props }: Omit<DrawerProps, "children">) {
const account = useCurrentAccount();
@ -27,7 +12,7 @@ export default function NavDrawer({ ...props }: Omit<DrawerProps, "children">) {
<Drawer placement="left" {...props}>
<DrawerOverlay />
<DrawerContent>
<ExpandedContext.Provider value={true}>
<CollapsedContext.Provider value={false}>
<DrawerBody display="flex" flexDirection="column" px="4" pt="4" overflowY="auto" overflowX="hidden" gap="2">
{account ? (
<AccountSwitcher />
@ -38,16 +23,8 @@ export default function NavDrawer({ ...props }: Omit<DrawerProps, "children">) {
</Flex>
)}
<NavItems />
<Box h="2" />
{!account && (
<Button as={RouterLink} to="/signin" colorScheme="primary" flexShrink={0}>
Sign in
</Button>
)}
<NavItem label="Settings" icon={SettingsIcon} to="/settings" />
<TaskManagerButtons mt="auto" flexShrink={0} />
</DrawerBody>
</ExpandedContext.Provider>
</CollapsedContext.Provider>
</DrawerContent>
</Drawer>
);

View File

@ -0,0 +1,137 @@
import { CloseIcon } from "@chakra-ui/icons";
import { Link as RouterLink } from "react-router-dom";
import {
Box,
Button,
ButtonGroup,
Flex,
IconButton,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalHeader,
ModalOverlay,
Text,
useDisclosure,
} from "@chakra-ui/react";
import { getDisplayName } from "../../../helpers/nostr/profile";
import useUserProfile from "../../../hooks/use-user-profile";
import accountService from "../../../services/account";
import { LogoutIcon } from "../../icons";
import UserAvatar from "../../user/user-avatar";
import AccountTypeBadge from "../../account-info-badge";
import useCurrentAccount from "../../../hooks/use-current-account";
import { Account } from "../../../classes/accounts/account";
import { useObservable } from "applesauce-react/hooks";
import { useContext } from "react";
import UserDnsIdentity from "../../user/user-dns-identity";
import NavItem from "./nav-item";
import LogIn01 from "../../icons/log-in-01";
import { CollapsedContext } from "../context";
function AccountItem({ account, onClick }: { account: Account; onClick?: () => void }) {
const pubkey = account.pubkey;
const metadata = useUserProfile(pubkey, []);
const handleClick = () => {
accountService.switchAccount(pubkey);
if (onClick) onClick();
};
return (
<Box display="flex" gap="2" alignItems="center">
<Flex flex={1} gap="2" overflow="hidden" alignItems="center">
<UserAvatar pubkey={pubkey} size="md" />
<Flex direction="column" overflow="hidden" alignItems="flex-start">
<Text isTruncated>{getDisplayName(metadata, pubkey)}</Text>
<AccountTypeBadge fontSize="0.7em" account={account} />
</Flex>
</Flex>
<ButtonGroup size="sm" variant="ghost">
<Button onClick={handleClick} aria-label="Switch account">
Switch
</Button>
<IconButton
icon={<CloseIcon />}
aria-label="Remove account"
onClick={(e) => {
e.stopPropagation();
if (confirm("Remove this account?")) accountService.removeAccount(pubkey);
}}
colorScheme="red"
/>
</ButtonGroup>
</Box>
);
}
export default function AccountSwitcher() {
const account = useCurrentAccount();
const modal = useDisclosure();
const metadata = useUserProfile(account?.pubkey);
const accounts = useObservable(accountService.accounts);
const otherAccounts = accounts.filter((acc) => acc.pubkey !== account?.pubkey);
const collapsed = useContext(CollapsedContext);
return (
<>
{account ? (
<Flex
as="button"
gap="2"
alignItems="center"
onClick={modal.onToggle}
flexShrink={0}
overflow="hidden"
outline="none"
>
<UserAvatar pubkey={account.pubkey} noProxy size="md" />
{!collapsed && (
<Flex overflow="hidden" direction="column" w="Full" alignItems="flex-start">
<Text whiteSpace="nowrap" fontWeight="bold" fontSize="lg" isTruncated>
{getDisplayName(metadata, account.pubkey)}
</Text>
<UserDnsIdentity pubkey={account.pubkey} />
</Flex>
)}
</Flex>
) : (
<NavItem label="Login" icon={LogIn01} to="/signin" colorScheme="primary" variant="solid" />
)}
<Modal isOpen={modal.isOpen} onClose={modal.onClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader py="2" px="4">
Accounts
</ModalHeader>
<ModalCloseButton />
<ModalBody pb="2" pt="0" px="2" display="flex" flexDirection="column" gap="2">
{otherAccounts.map((account) => (
<AccountItem key={account.pubkey} account={account} onClick={modal.onClose} />
))}
<ButtonGroup w="full">
<Button as={RouterLink} to="/settings/accounts" w="full" onClick={modal.onClose} variant="link">
Manage accounts
</Button>
<Button
leftIcon={<LogoutIcon boxSize={5} />}
aria-label="Logout"
onClick={() => {
accountService.logout(false);
}}
flexShrink={0}
>
Logout
</Button>
</ButtonGroup>
</ModalBody>
</ModalContent>
</Modal>
</>
);
}

View File

@ -1,5 +1,5 @@
import { useMemo } from "react";
import { ButtonProps } from "@chakra-ui/react";
import { ButtonProps, Spacer } from "@chakra-ui/react";
import { useLocation } from "react-router-dom";
import { nip19 } from "nostr-tools";
@ -11,6 +11,7 @@ import {
SearchIcon,
NotesIcon,
LightningIcon,
SettingsIcon,
} from "../../icons";
import useCurrentAccount from "../../../hooks/use-current-account";
import PuzzlePiece01 from "../../icons/puzzle-piece-01";
@ -22,6 +23,8 @@ import { internalApps, internalTools } from "../../../views/other-stuff/apps";
import { App } from "../../../views/other-stuff/component/app-card";
import NavItem from "./nav-item";
import { QuestionIcon } from "@chakra-ui/icons";
import TaskManagerButtons from "../../legacy-layout/task-manager-buttons";
import Plus from "../../icons/plus";
export default function NavItems() {
const location = useLocation();
@ -91,6 +94,9 @@ export default function NavItems() {
return (
<>
{account && account.readonly !== false && (
<NavItem icon={Plus} label="Create new" colorScheme="primary" to="/new" variant="solid" />
)}
<NavItem to="/launchpad" icon={Rocket02} label="Launchpad" />
<NavItem to="/" icon={NotesIcon} colorScheme={location.pathname === "/" ? "primary" : "gray"} label="Notes" />
<NavItem label="Discover" to="/discovery" icon={PuzzlePiece01} />
@ -107,7 +113,10 @@ export default function NavItems() {
<NavItem key={app.id} to={app.to} icon={app.icon || QuestionIcon} label={app.title} />
))}
<NavItem to="/other-stuff" icon={Package} label="More" />
<Spacer />
<NavItem to="/support" icon={LightningIcon} label="Support" />
<NavItem label="Settings" icon={SettingsIcon} to="/settings" />
<TaskManagerButtons mt="auto" flexShrink={0} />
</>
);
}

View File

@ -1,8 +1,8 @@
import { useContext } from "react";
import { Button, ComponentWithAs, IconButton, IconButtonProps, IconProps } from "@chakra-ui/react";
import { To, Link as RouterLink, useLocation } from "react-router-dom";
import { Link as RouterLink, useLocation } from "react-router-dom";
import { ExpandedContext } from "../desktop/side-nav";
import { CollapsedContext } from "../context";
export default function NavItem({
to,
@ -17,27 +17,10 @@ export default function NavItem({
colorScheme?: IconButtonProps["colorScheme"];
variant?: IconButtonProps["variant"];
}) {
const expanded = useContext(ExpandedContext);
const collapsed = useContext(CollapsedContext);
const location = useLocation();
if (expanded)
return (
<Button
as={RouterLink}
aria-label={label}
title={label}
leftIcon={<Icon boxSize={5} />}
variant={variant || "link"}
py="2"
justifyContent="flex-start"
colorScheme={colorScheme || location.pathname.startsWith(to) ? "primary" : undefined}
to={to}
flexShrink={0}
>
{label}
</Button>
);
else
if (collapsed)
return (
<IconButton
as={RouterLink}
@ -51,4 +34,21 @@ export default function NavItem({
colorScheme={colorScheme || location.pathname.startsWith(to) ? "primary" : undefined}
/>
);
else
return (
<Button
as={RouterLink}
aria-label={label}
title={label}
leftIcon={<Icon boxSize={5} />}
variant={variant || "link"}
p="2"
justifyContent="flex-start"
colorScheme={colorScheme || location.pathname.startsWith(to) ? "primary" : undefined}
to={to}
flexShrink={0}
>
{label}
</Button>
);
}

View File

@ -1,101 +0,0 @@
import { CloseIcon } from "@chakra-ui/icons";
import { useNavigate, Link as RouterLink } from "react-router-dom";
import { Box, Button, ButtonGroup, Flex, IconButton, Text, useDisclosure } from "@chakra-ui/react";
import { getDisplayName } from "../../helpers/nostr/profile";
import useUserProfile from "../../hooks/use-user-profile";
import accountService from "../../services/account";
import { AddIcon, ChevronDownIcon, ChevronUpIcon } from "../icons";
import UserAvatar from "../user/user-avatar";
import AccountTypeBadge from "../account-info-badge";
import useCurrentAccount from "../../hooks/use-current-account";
import { Account } from "../../classes/accounts/account";
import { useObservable } from "applesauce-react/hooks";
function AccountItem({ account, onClick }: { account: Account; onClick?: () => void }) {
const pubkey = account.pubkey;
const metadata = useUserProfile(pubkey, []);
const handleClick = () => {
accountService.switchAccount(pubkey);
if (onClick) onClick();
};
return (
<Box display="flex" gap="2" alignItems="center" cursor="pointer">
<Flex as="button" onClick={handleClick} flex={1} gap="2" overflow="hidden" alignItems="center">
<UserAvatar pubkey={pubkey} size="md" />
<Flex direction="column" overflow="hidden" alignItems="flex-start">
<Text isTruncated>{getDisplayName(metadata, pubkey)}</Text>
<AccountTypeBadge fontSize="0.7em" account={account} />
</Flex>
</Flex>
<IconButton
icon={<CloseIcon />}
aria-label="Remove Account"
onClick={(e) => {
e.stopPropagation();
if (confirm("Remove this account?")) accountService.removeAccount(pubkey);
}}
size="sm"
variant="ghost"
/>
</Box>
);
}
export default function AccountSwitcher() {
const navigate = useNavigate();
const account = useCurrentAccount()!;
const { isOpen, onToggle, onClose } = useDisclosure();
const metadata = useUserProfile(account.pubkey);
const accounts = useObservable(accountService.accounts);
const otherAccounts = accounts.filter((acc) => acc.pubkey !== account?.pubkey);
return (
<Flex direction="column" gap="2">
<Box
as="button"
borderRadius="lg"
borderWidth={1}
display="flex"
gap="2"
alignItems="center"
flexGrow={1}
onClick={onToggle}
>
<UserAvatar pubkey={account.pubkey} noProxy size="md" />
<Text whiteSpace="nowrap" fontWeight="bold" fontSize="lg" isTruncated>
{getDisplayName(metadata, account.pubkey)}
</Text>
<Flex ml="auto" alignItems="center" justifyContent="center" aspectRatio={1} h="3rem">
{isOpen ? <ChevronUpIcon fontSize="1.5rem" /> : <ChevronDownIcon fontSize="1.5rem" />}
</Flex>
</Box>
{isOpen && (
<>
{otherAccounts.map((account) => (
<AccountItem key={account.pubkey} account={account} onClick={onClose} />
))}
<ButtonGroup>
<Button as={RouterLink} to="/settings/accounts" w="full">
Manage
</Button>
<IconButton
icon={<AddIcon boxSize={6} />}
aria-label="Add Account"
onClick={() => {
accountService.logout(false);
navigate("/signin", { state: { from: location.pathname } });
}}
colorScheme="primary"
>
Add Account
</IconButton>
</ButtonGroup>
</>
)}
</Flex>
);
}

View File

@ -6,7 +6,7 @@ import { useObservable } from "applesauce-react/hooks";
import Plus from "../icons/plus";
import useCurrentAccount from "../../hooks/use-current-account";
import AccountSwitcher from "./account-switcher";
import AccountSwitcher from "../layout/nav-items/account-switcher";
import NavItems from "../layout/nav-items";
import { PostModalContext } from "../../providers/route/post-modal-provider";
import { offlineMode } from "../../services/offline-mode";

View File

@ -51,7 +51,7 @@ export default function ReactionIconButton({
</Suspense>
);
if (useModal) {
if (useModal)
return (
<>
<IconButton
@ -68,7 +68,7 @@ export default function ReactionIconButton({
</Modal>
</>
);
} else
else
return (
<Popover isLazy isOpen={isOpen} onOpen={open.on} onClose={open.off}>
<PopoverTrigger>

View File

@ -1,27 +1,35 @@
html,
body {
html {
overflow: hidden;
margin: 0;
height: 100%;
width: 100%;
}
#root {
body {
margin: 0;
height: 100%;
width: 100%;
overflow-x: hidden;
overflow-y: auto;
overscroll-behavior: contain;
}
body,
#root {
display: flex;
flex-direction: column;
body {
--safe-top: env(safe-area-inset-top, 0px);
--safe-bottom: env(safe-area-inset-bottom, 0px);
--safe-right: env(safe-area-inset-right, 0px);
--safe-left: env(safe-area-inset-left, 0px);
/* uncomment this to emulate iphone notch and navbar */
/* --safe-top: env(safe-area-inset-top, 30px);
--safe-bottom: env(safe-area-inset-bottom, 30px);
--safe-right: env(safe-area-inset-right);
--safe-left: env(safe-area-inset-left); */
/* --safe-top: 30px;
--safe-bottom: 30px;
--safe-right: env(safe-area-inset-right);
--safe-left: env(safe-area-inset-left); */
}
body,
#root {
width: 100%;
display: flex;
flex-direction: column;
}

View File

@ -92,11 +92,7 @@ export default function AccountSettings() {
<AccountTypeBadge account={account} ml="4" />
<ButtonGroup size="sm" ml="auto">
<Button
onClick={() => accountService.switchAccount(account.pubkey)}
colorScheme="primary"
variant="ghost"
>
<Button onClick={() => accountService.switchAccount(account.pubkey)} variant="ghost">
Switch
</Button>
<Button