add account switcher

This commit is contained in:
hzrd149 2023-02-14 14:33:50 -06:00
parent 7f446bb201
commit a0bea43552
10 changed files with 367 additions and 202 deletions

View File

@ -1,147 +0,0 @@
import React from "react";
import { Avatar, Button, Container, Flex, Heading, IconButton, LinkOverlay, Text, VStack } from "@chakra-ui/react";
import { Link, useNavigate } from "react-router-dom";
import { FeedIcon, LogoutIcon, NotificationIcon, ProfileIcon, RelayIcon, SettingsIcon } from "./icons";
import { ErrorBoundary } from "./error-boundary";
import { ConnectedRelays } from "./connected-relays";
import { useIsMobile } from "../hooks/use-is-mobile";
import accountService from "../services/account";
import { FollowingList } from "./following-list";
import { ReloadPrompt } from "./reload-prompt";
import { PostModalProvider } from "../providers/post-modal-provider";
import { useReadonlyMode } from "../hooks/use-readonly-mode";
import { ProfileButton } from "./profile-button";
import { UserAvatarLink } from "./user-avatar-link";
import { useCurrentAccount } from "../hooks/use-current-account";
const MobileProfileHeader = () => {
const account = useCurrentAccount();
return (
<Flex justifyContent="space-between" padding="2" alignItems="center">
<UserAvatarLink pubkey={account.pubkey} size="sm" />
{account.readonly && (
<Button
colorScheme="red"
textAlign="center"
variant="link"
onClick={() => confirm("Exit readonly mode?") && accountService.logout()}
>
Readonly Mode
</Button>
)}
<Flex gap="2">
<ConnectedRelays />
<IconButton
as={Link}
variant="ghost"
icon={<NotificationIcon />}
aria-label="Notifications"
title="Notifications"
size="sm"
to="/notifications"
/>
</Flex>
</Flex>
);
};
const MobileBottomNav = () => {
const navigate = useNavigate();
return (
<Flex flexShrink={0} gap="2" padding="2">
<IconButton icon={<FeedIcon />} aria-label="Home" onClick={() => navigate("/")} flexGrow="1" size="lg" />
<IconButton
icon={<ProfileIcon />}
aria-label="Profile"
onClick={() => navigate(`/profile`)}
flexGrow="1"
size="lg"
/>
<IconButton
icon={<SettingsIcon />}
aria-label="Settings"
onClick={() => navigate("/settings")}
flexGrow="1"
size="lg"
/>
</Flex>
);
};
const DesktopSideNav = () => {
const navigate = useNavigate();
const readonly = useReadonlyMode();
return (
<VStack width="15rem" pt="2" alignItems="stretch" flexShrink={0}>
<Flex gap="2" alignItems="center" position="relative">
<LinkOverlay as={Link} to="/" />
<Avatar src="/apple-touch-icon.png" size="sm" />
<Heading size="md">noStrudel</Heading>
</Flex>
<ProfileButton />
<Button onClick={() => navigate("/")} leftIcon={<FeedIcon />}>
Home
</Button>
<Button onClick={() => navigate("/notifications")} leftIcon={<NotificationIcon />}>
Notifications
</Button>
<Button onClick={() => navigate("/relays")} leftIcon={<RelayIcon />}>
Relays
</Button>
<Button onClick={() => navigate("/settings")} leftIcon={<SettingsIcon />}>
Settings
</Button>
<Button onClick={() => accountService.logout()} leftIcon={<LogoutIcon />}>
Logout
</Button>
{readonly && (
<Text color="red.200" textAlign="center">
Readonly Mode
</Text>
)}
<ConnectedRelays />
</VStack>
);
};
const FollowingSideNav = () => {
return (
<VStack width="15rem" pt="2" alignItems="stretch" flexShrink={0}>
<Heading size="md">Following</Heading>
<FollowingList />
</VStack>
);
};
export const Page = ({ children }: { children: React.ReactNode }) => {
const isMobile = useIsMobile();
return (
<Container
size="lg"
display="flex"
flexDirection="column"
height="100vh"
overflow="hidden"
position="relative"
padding="0"
>
<ReloadPrompt />
{isMobile && <MobileProfileHeader />}
<Flex gap="4" grow={1} overflow="hidden">
{!isMobile && <DesktopSideNav />}
<Flex flexGrow={1} direction="column" overflow="hidden">
<ErrorBoundary>
<PostModalProvider>{children}</PostModalProvider>
</ErrorBoundary>
</Flex>
{!isMobile && <FollowingSideNav />}
</Flex>
{isMobile && <MobileBottomNav />}
</Container>
);
};

View File

@ -0,0 +1,95 @@
import { CloseIcon } from "@chakra-ui/icons";
import {
Accordion,
AccordionButton,
AccordionIcon,
AccordionItem,
AccordionPanel,
Box,
Button,
Flex,
IconButton,
Text,
useAccordionContext,
} from "@chakra-ui/react";
import { getUserDisplayName } from "../../helpers/user-metadata";
import useSubject from "../../hooks/use-subject";
import { useUserMetadata } from "../../hooks/use-user-metadata";
import accountService from "../../services/account";
import { AddIcon } from "../icons";
import { UserAvatar } from "../user-avatar";
function AccountItem({ pubkey }: { pubkey: string }) {
const metadata = useUserMetadata(pubkey, []);
const accord = useAccordionContext();
const handleClick = () => {
if (accord) accord.setIndex(-1);
accountService.switchAccount(pubkey);
};
return (
<Box
display="flex"
gap="4"
alignItems="center"
overflow="hidden"
padding="2"
cursor="pointer"
onClick={handleClick}
>
<UserAvatar pubkey={pubkey} size="sm" />
<Text flex={1} mr="4" overflow="hidden">
{getUserDisplayName(metadata, pubkey)}
</Text>
<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 function AccountSwitcherList() {
const accounts = useSubject(accountService.accounts);
const current = useSubject(accountService.current);
const otherAccounts = accounts.filter((acc) => acc.pubkey !== current?.pubkey);
return (
<Flex gap="2" direction="column">
{otherAccounts.map((account) => (
<AccountItem key={account.pubkey} pubkey={account.pubkey} />
))}
<Button size="sm" mx="2" mb="2" leftIcon={<AddIcon />}>
Add Account
</Button>
</Flex>
);
}
export default function AccountSwitcher() {
return (
<Accordion allowToggle>
<AccordionItem>
<h2>
<AccordionButton>
<Box as="span" flex="1" textAlign="left">
Accounts
</Box>
<AccordionIcon />
</AccordionButton>
</h2>
<AccordionPanel padding={0}>
<AccountSwitcherList />
</AccordionPanel>
</AccordionItem>
</Accordion>
);
}

View File

@ -0,0 +1,47 @@
import { SettingsIcon } from "@chakra-ui/icons";
import { Avatar, Button, Flex, Heading, LinkOverlay, Text, VStack } from "@chakra-ui/react";
import { Link, useNavigate } from "react-router-dom";
import { useCurrentAccount } from "../../hooks/use-current-account";
import accountService from "../../services/account";
import { ConnectedRelays } from "../connected-relays";
import { FeedIcon, LogoutIcon, NotificationIcon, RelayIcon } from "../icons";
import { ProfileButton } from "../profile-button";
import AccountSwitcher from "./account-switcher";
export default function DesktopSideNav() {
const navigate = useNavigate();
const account = useCurrentAccount();
return (
<VStack width="15rem" pt="2" alignItems="stretch" flexShrink={0}>
<Flex gap="2" alignItems="center" position="relative">
<LinkOverlay as={Link} to="/" />
<Avatar src="/apple-touch-icon.png" size="sm" />
<Heading size="md">noStrudel</Heading>
</Flex>
<ProfileButton />
<AccountSwitcher />
<Button onClick={() => navigate("/")} leftIcon={<FeedIcon />}>
Home
</Button>
<Button onClick={() => navigate("/notifications")} leftIcon={<NotificationIcon />}>
Notifications
</Button>
<Button onClick={() => navigate("/relays")} leftIcon={<RelayIcon />}>
Relays
</Button>
<Button onClick={() => navigate("/settings")} leftIcon={<SettingsIcon />}>
Settings
</Button>
<Button onClick={() => accountService.logout()} leftIcon={<LogoutIcon />}>
Logout
</Button>
{account.readonly && (
<Text color="red.200" textAlign="center">
Readonly Mode
</Text>
)}
<ConnectedRelays />
</VStack>
);
}

View File

@ -0,0 +1,49 @@
import React from "react";
import { Container, Flex, Heading, VStack } from "@chakra-ui/react";
import { ErrorBoundary } from "../error-boundary";
import { useIsMobile } from "../../hooks/use-is-mobile";
import { FollowingList } from "../following-list";
import { ReloadPrompt } from "../reload-prompt";
import { PostModalProvider } from "../../providers/post-modal-provider";
import MobileHeader from "./mobile-header";
import DesktopSideNav from "./desktop-side-nav";
import MobileBottomNav from "./mobile-bottom-nav";
const FollowingSideNav = () => {
return (
<VStack width="15rem" pt="2" alignItems="stretch" flexShrink={0}>
<Heading size="md">Following</Heading>
<FollowingList />
</VStack>
);
};
export const Page = ({ children }: { children: React.ReactNode }) => {
const isMobile = useIsMobile();
return (
<Container
size="lg"
display="flex"
flexDirection="column"
height="100vh"
overflow="hidden"
position="relative"
padding="0"
>
<ReloadPrompt />
{isMobile && <MobileHeader />}
<Flex gap="4" grow={1} overflow="hidden">
{!isMobile && <DesktopSideNav />}
<Flex flexGrow={1} direction="column" overflow="hidden">
<ErrorBoundary>
<PostModalProvider>{children}</PostModalProvider>
</ErrorBoundary>
</Flex>
{!isMobile && <FollowingSideNav />}
</Flex>
{isMobile && <MobileBottomNav />}
</Container>
);
};

View File

@ -0,0 +1,28 @@
import { SettingsIcon } from "@chakra-ui/icons";
import { Flex, IconButton } from "@chakra-ui/react";
import { useNavigate } from "react-router-dom";
import { FeedIcon, ProfileIcon } from "../icons";
export default function MobileBottomNav() {
const navigate = useNavigate();
return (
<Flex flexShrink={0} gap="2" padding="2">
<IconButton icon={<FeedIcon />} aria-label="Home" onClick={() => navigate("/")} flexGrow="1" size="lg" />
<IconButton
icon={<ProfileIcon />}
aria-label="Profile"
onClick={() => navigate(`/profile`)}
flexGrow="1"
size="lg"
/>
<IconButton
icon={<SettingsIcon />}
aria-label="Settings"
onClick={() => navigate("/settings")}
flexGrow="1"
size="lg"
/>
</Flex>
);
}

View File

@ -0,0 +1,43 @@
import { Button, Flex, IconButton, Text, useDisclosure } from "@chakra-ui/react";
import { useEffect } from "react";
import { Link, useLocation } from "react-router-dom";
import { useCurrentAccount } from "../../hooks/use-current-account";
import accountService from "../../services/account";
import { ConnectedRelays } from "../connected-relays";
import { NotificationIcon } from "../icons";
import { UserAvatar } from "../user-avatar";
import MobileSideDrawer from "./mobile-side-drawer";
export default function MobileHeader() {
const { isOpen, onOpen, onClose } = useDisclosure();
const account = useCurrentAccount();
const location = useLocation();
useEffect(() => onClose(), [location.key, account]);
return (
<>
<Flex justifyContent="space-between" padding="2" alignItems="center">
<UserAvatar pubkey={account.pubkey} size="sm" onClick={onOpen} />
{account.readonly && (
<Text color="red.200" textAlign="center">
Readonly Mode
</Text>
)}
<Flex gap="2">
<ConnectedRelays />
<IconButton
as={Link}
variant="ghost"
icon={<NotificationIcon />}
aria-label="Notifications"
title="Notifications"
size="sm"
to="/notifications"
/>
</Flex>
</Flex>
{isOpen && <MobileSideDrawer isOpen={isOpen} onClose={onClose} />}
</>
);
}

View File

@ -0,0 +1,58 @@
import {
Button,
Drawer,
DrawerBody,
DrawerCloseButton,
DrawerContent,
DrawerHeader,
DrawerOverlay,
DrawerProps,
Flex,
Text,
} from "@chakra-ui/react";
import { useNavigate } from "react-router-dom";
import { getUserDisplayName } from "../../helpers/user-metadata";
import { useCurrentAccount } from "../../hooks/use-current-account";
import { useUserMetadata } from "../../hooks/use-user-metadata";
import accountService from "../../services/account";
import { LogoutIcon, ProfileIcon, RelayIcon, SettingsIcon } from "../icons";
import { UserAvatar } from "../user-avatar";
import AccountSwitcher from "./account-switcher";
export default function MobileSideDrawer({ ...props }: Omit<DrawerProps, "children">) {
const navigate = useNavigate();
const account = useCurrentAccount();
const metadata = useUserMetadata(account.pubkey);
return (
<Drawer placement="left" {...props}>
<DrawerOverlay />
<DrawerContent>
<DrawerCloseButton />
<DrawerHeader>
<Flex gap="2">
<UserAvatar pubkey={account.pubkey} size="sm" />
<Text>{getUserDisplayName(metadata, account.pubkey)}</Text>
</Flex>
</DrawerHeader>
<DrawerBody padding={0} overflowY="auto" overflowX="hidden">
<AccountSwitcher />
<Flex direction="column" gap="2" padding="2">
<Button onClick={() => navigate(`/u/${account.pubkey}`)} leftIcon={<ProfileIcon />}>
Profile
</Button>
<Button onClick={() => navigate("/relays")} leftIcon={<RelayIcon />}>
Relays
</Button>
<Button onClick={() => navigate("/settings")} leftIcon={<SettingsIcon />}>
Settings
</Button>
<Button onClick={() => accountService.logout()} leftIcon={<LogoutIcon />}>
Logout
</Button>
</Flex>
</DrawerBody>
</DrawerContent>
</Drawer>
);
}

View File

@ -0,0 +1,40 @@
import { CloseIcon } from "@chakra-ui/icons";
import { Box, IconButton, Text } from "@chakra-ui/react";
import { getUserDisplayName } from "../../../helpers/user-metadata";
import { useUserMetadata } from "../../../hooks/use-user-metadata";
import accountService from "../../../services/account";
import { UserAvatar } from "../../../components/user-avatar";
export default function AccountCard({ pubkey }: { pubkey: string }) {
// this wont load unless the data is cached since there are no relay connections yet
const metadata = useUserMetadata(pubkey, []);
return (
<Box
display="flex"
gap="4"
alignItems="center"
borderWidth="1px"
borderRadius="lg"
overflow="hidden"
padding="2"
cursor="pointer"
onClick={() => accountService.switchAccount(pubkey)}
>
<UserAvatar pubkey={pubkey} size="sm" />
<Text flex={1} mr="4" overflow="hidden">
{getUserDisplayName(metadata, pubkey)}
</Text>
<IconButton
icon={<CloseIcon />}
aria-label="Remove Account"
onClick={(e) => {
e.stopPropagation();
accountService.removeAccount(pubkey);
}}
size="sm"
variant="ghost"
/>
</Box>
);
}

View File

@ -1,59 +1,10 @@
import { CloseIcon } from "@chakra-ui/icons";
import {
Alert,
AlertDescription,
AlertIcon,
AlertTitle,
Box,
Button,
Flex,
Heading,
IconButton,
Spinner,
Text,
} from "@chakra-ui/react";
import { Alert, AlertDescription, AlertIcon, AlertTitle, Box, Button, Flex, Heading, Spinner } from "@chakra-ui/react";
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { UserAvatar } from "../../components/user-avatar";
import { getUserDisplayName } from "../../helpers/user-metadata";
import AccountCard from "./components/account-card";
import useSubject from "../../hooks/use-subject";
import { useUserMetadata } from "../../hooks/use-user-metadata";
import accountService from "../../services/account";
const AvailableAccount = ({ pubkey }: { pubkey: string }) => {
// this wont load unless the data is cached since there are no relay connections yet
const metadata = useUserMetadata(pubkey, []);
return (
<Box
display="flex"
gap="4"
alignItems="center"
borderWidth="1px"
borderRadius="lg"
overflow="hidden"
padding="2"
cursor="pointer"
onClick={() => accountService.switchAccount(pubkey)}
>
<UserAvatar pubkey={pubkey} size="sm" />
<Text flex={1} mr="4" overflow="hidden">
{getUserDisplayName(metadata, pubkey)}
</Text>
<IconButton
icon={<CloseIcon />}
aria-label="Remove Account"
onClick={(e) => {
e.stopPropagation();
accountService.removeAccount(pubkey);
}}
size="sm"
variant="ghost"
/>
</Box>
);
};
export const LoginStartView = () => {
const navigate = useNavigate();
const [loading, setLoading] = useState(false);
@ -105,9 +56,9 @@ export const LoginStartView = () => {
<Heading size="md" mt="4">
Accounts:
</Heading>
<Flex gap="2" direction="column">
<Flex gap="2" direction="column" minW={300}>
{accounts.map((account) => (
<AvailableAccount key={account.pubkey} pubkey={account.pubkey} />
<AccountCard key={account.pubkey} pubkey={account.pubkey} />
))}
</Flex>
</>

View File

@ -12,9 +12,10 @@ export const UserProfileMenu = ({ pubkey, ...props }: { pubkey: string } & Omit<
const metadata = useUserMetadata(pubkey);
const loginAsUser = () => {
if (confirm(`Do you want to logout and login as ${getUserDisplayName(metadata, pubkey)}?`)) {
accountService.switchToTemporary({ pubkey, readonly: true });
if (!accountService.hasAccount(pubkey)) {
accountService.addAccount(pubkey, [], true);
}
accountService.switchAccount(pubkey);
};
return (