add mobile layout

This commit is contained in:
hzrd149 2023-02-07 17:04:18 -06:00
parent d899250345
commit 84ebe8e77d
13 changed files with 149 additions and 44 deletions

View File

@ -1,12 +1,21 @@
# TODO
# TODO Features
- Adding loading state to `useUserMetadata` so views can show loading state
- Add a debounce to user metadata services so it dose not spam the relay when updating subscription
- user metadata service: remove author from subscription once metadata is returned
- create a stats page showing state of local db and info about app
- create user timeline service that caching events and supports loading older events on request
## Ideas
## MVP
- come up with a clever name
- cache user timelines for performance
- add "Load more" button for user timelines
- build event/thread view
- build relays tab under user
- connect to browser extension for signing
- add simple post form
- add stats page for debugging and cool stats
## Stage 1
- build support for DMs
- linkify posts
- detect LN invoices
- detect LNURL
- add user tip button
- create mobile layout

View File

@ -3,7 +3,11 @@
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, user-scalable=0"
/>
<link rel="stylesheet" href="./src/styles.css" />
<title>personal-nostr-client</title>
</head>
<body>

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm-2.29-2.333A17.9 17.9 0 0 1 8.027 13H4.062a8.008 8.008 0 0 0 5.648 6.667zM10.03 13c.151 2.439.848 4.73 1.97 6.752A15.905 15.905 0 0 0 13.97 13h-3.94zm9.908 0h-3.965a17.9 17.9 0 0 1-1.683 6.667A8.008 8.008 0 0 0 19.938 13zM4.062 11h3.965A17.9 17.9 0 0 1 9.71 4.333 8.008 8.008 0 0 0 4.062 11zm5.969 0h3.938A15.905 15.905 0 0 0 12 4.248 15.905 15.905 0 0 0 10.03 11zm4.259-6.667A17.9 17.9 0 0 1 15.973 11h3.965a8.008 8.008 0 0 0-5.648-6.667z"/></svg>

After

Width:  |  Height:  |  Size: 652 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M21 20a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V9.49a1 1 0 0 1 .386-.79l8-6.222a1 1 0 0 1 1.228 0l8 6.222a1 1 0 0 1 .386.79V20zm-2-1V9.978l-7-5.444-7 5.444V19h14z"/></svg>

After

Width:  |  Height:  |  Size: 290 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M8.686 4l2.607-2.607a1 1 0 0 1 1.414 0L15.314 4H19a1 1 0 0 1 1 1v3.686l2.607 2.607a1 1 0 0 1 0 1.414L20 15.314V19a1 1 0 0 1-1 1h-3.686l-2.607 2.607a1 1 0 0 1-1.414 0L8.686 20H5a1 1 0 0 1-1-1v-3.686l-2.607-2.607a1 1 0 0 1 0-1.414L4 8.686V5a1 1 0 0 1 1-1h3.686zM6 6v3.515L3.515 12 6 14.485V18h3.515L12 20.485 14.485 18H18v-3.515L20.485 12 18 9.515V6h-3.515L12 3.515 9.515 6H6zm6 10a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm0-2a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"/></svg>

After

Width:  |  Height:  |  Size: 580 B

View File

@ -1,28 +1,85 @@
import React from "react";
import { Box, Button, Container, HStack, VStack } from "@chakra-ui/react";
import {
Box,
Button,
Container,
Flex,
IconButton,
VStack,
} from "@chakra-ui/react";
import { useNavigate } from "react-router-dom";
import { ErrorBoundary } from "./error-boundary";
import { ConnectedRelays } from "./connected-relays";
export const Page = ({ children }: { children: React.ReactNode }) => {
import homeIcon from "./icons/home.svg";
import globalIcon from "./icons/global.svg";
import settingsIcon from "./icons/settings.svg";
import { useIsMobile } from "../hooks/use-is-mobile";
const MobileLayout = ({ children }: { children: React.ReactNode }) => {
const navigate = useNavigate();
return (
<Container size="lg" padding={4}>
<HStack alignItems="flex-start" spacing={4} overflow="hidden">
<VStack style={{ width: "15rem" }} alignItems="stretch" flexShrink={0}>
<Button onClick={() => navigate("/")}>Home</Button>
<Button onClick={() => navigate("/global")}>Global</Button>
<Button onClick={() => navigate("/settings")}>Settings</Button>
<ConnectedRelays />
</VStack>
<Box flexGrow={1} overflow="auto">
<ErrorBoundary>{children}</ErrorBoundary>
</Box>
<VStack style={{ width: "15rem" }} alignItems="stretch" flexShrink={0}>
<Button onClick={() => navigate("/")}>Manage Follows</Button>
</VStack>
</HStack>
<Flex direction="column" height="100%">
<Box flexGrow={1} overflow="auto">
{children}
</Box>
<Flex flexShrink={0} gap="2" padding="2">
<IconButton
icon={<img src={homeIcon} />}
aria-label="Home"
onClick={() => navigate("/")}
flexGrow="1"
size="lg"
/>
<IconButton
icon={<img src={globalIcon} />}
aria-label="Global Feed"
onClick={() => navigate("/global")}
flexGrow="1"
size="lg"
/>
<IconButton
icon={<img src={settingsIcon} />}
aria-label="Settings"
onClick={() => navigate("/settings")}
flexGrow="1"
size="lg"
/>
</Flex>
</Flex>
);
};
const DesktopLayout = ({ children }: { children: React.ReactNode }) => {
const navigate = useNavigate();
return (
<Container
size="lg"
display="flex"
gap="4"
height="100vh"
overflow="hidden"
>
<VStack width="15rem" pt="2" alignItems="stretch" flexShrink={0}>
<Button onClick={() => navigate("/")}>Home</Button>
<Button onClick={() => navigate("/global")}>Global Feed</Button>
<Button onClick={() => navigate("/settings")}>Settings</Button>
<ConnectedRelays />
</VStack>
<Box flexGrow={1} overflow="hidden">
<ErrorBoundary>{children}</ErrorBoundary>
</Box>
<VStack width="15rem" pt="2" alignItems="stretch" flexShrink={0}>
<Button onClick={() => navigate("/")}>Manage Follows</Button>
</VStack>
</Container>
);
};
export const Page = ({ children }: { children: React.ReactNode }) => {
const isMobile = useIsMobile();
const Layout = isMobile ? MobileLayout : DesktopLayout;
return <Layout>{children}</Layout>;
};

View File

@ -5,7 +5,6 @@ import {
Card,
CardBody,
CardHeader,
Code,
Flex,
Heading,
HStack,
@ -35,7 +34,7 @@ export const Post = React.memo(({ event }: PostProps) => {
: event.pubkey;
return (
<Card padding="4" variant="outline">
<Card padding="2" variant="outline">
<CardHeader padding="0">
<HStack spacing="4">
<Flex flex="1" gap="2" alignItems="center" flexWrap="wrap">
@ -50,7 +49,7 @@ export const Post = React.memo(({ event }: PostProps) => {
</Flex>
</HStack>
</CardHeader>
<CardBody padding="0" pt={0}>
<CardBody pt="2" pb="0" pr="0" pl="0">
<VStack alignItems="flex-start" justifyContent="stretch">
<Box maxHeight="20rem" overflow="hidden" width="100%">
<ReactMarkdown>
@ -65,7 +64,6 @@ export const Post = React.memo(({ event }: PostProps) => {
<PostModal event={event} isOpen={isOpen} onClose={onClose} />
</>
)}
<Code>{event.id}</Code>
</VStack>
</CardBody>
</Card>

View File

@ -0,0 +1,7 @@
import { useMediaQuery } from "@chakra-ui/react";
export function useIsMobile() {
const [isMobile] = useMediaQuery("(max-width: 1000px)");
return isMobile;
}

View File

@ -25,7 +25,15 @@ const MIGRATIONS: MigrationFunction[] = [
// setup data
const settings = db.createObjectStore("settings");
settings.put(["wss://nostr.rdfriedl.com"], "relays");
settings.put(
[
"wss://nostr.rdfriedl.com",
"wss://relay.damus.io",
"wss://relay.nostr.info",
"wss://nostr.zebedee.cloud",
],
"relays"
);
},
];

8
src/styles.css Normal file
View File

@ -0,0 +1,8 @@
html,
body,
#root {
margin: 0;
height: 100%;
width: 100%;
overflow: hidden;
}

View File

@ -41,7 +41,7 @@ export const GlobalView = () => {
if (timeline.length > 20) timeline.length = 20;
return (
<Flex direction="column" gap="2">
<Flex direction="column" gap="2" overflow="auto" height="100%" padding="2">
{timeline.map((event) => (
<Post key={event.id} event={event} />
))}

View File

@ -8,15 +8,16 @@ import {
TabPanels,
Tabs,
Text,
VStack,
} from "@chakra-ui/react";
import { useParams } from "react-router-dom";
import { UserPostsTab } from "./posts";
import { useUserMetadata } from "../../hooks/use-user-metadata";
import { UserAvatar } from "../../components/user-avatar";
import { getUserFullName } from "../../helpers/user-metadata";
import { useIsMobile } from "../../hooks/use-is-mobile";
export const UserView = () => {
const isMobile = useIsMobile();
const { pubkey } = useParams();
if (!pubkey) {
// TODO: better 404
@ -27,24 +28,34 @@ export const UserView = () => {
const label = metadata ? getUserFullName(metadata) || pubkey : pubkey;
return (
<VStack alignItems="stretch" spacing={4}>
{" "}
<Flex gap="4">
<UserAvatar pubkey={pubkey} size="xl" />
<Flex direction="column" gap="2">
<Heading>{label}</Heading>
<Flex
direction="column"
alignItems="stretch"
gap="2"
overflow="hidden"
height="100%"
>
<Flex gap="4" padding="2">
<UserAvatar pubkey={pubkey} size={isMobile ? "md" : "xl"} />
<Flex direction="column" gap={isMobile ? 0 : 2}>
<Heading size={isMobile ? "md" : "lg"}>{label}</Heading>
{loadingMetadata ? <SkeletonText /> : <Text>{metadata?.about}</Text>}
</Flex>
</Flex>
<Tabs>
<Tabs
display="flex"
flexDirection="column"
flexGrow="1"
overflow="hidden"
>
<TabList>
<Tab>Posts</Tab>
<Tab>Other</Tab>
<Tab>Relays</Tab>
</TabList>
<TabPanels>
<TabPanel>
<TabPanels overflow="auto" height="100%">
<TabPanel pr={0} pl={0}>
<UserPostsTab pubkey={pubkey} />
</TabPanel>
<TabPanel>
@ -55,6 +66,6 @@ export const UserView = () => {
</TabPanel>
</TabPanels>
</Tabs>
</VStack>
</Flex>
);
};

View File

@ -43,7 +43,7 @@ export const UserPostsTab = ({ pubkey }: { pubkey: string }) => {
if (timeline.length > 20) timeline.length = 20;
return (
<Flex direction="column" gap="2">
<Flex direction="column" gap="2" pr="2" pl="2">
{timeline.map((event) => (
<Post key={event.id} event={event} />
))}