mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-03-17 21:31:43 +01:00
add mobile layout
This commit is contained in:
parent
d899250345
commit
84ebe8e77d
25
README.md
25
README.md
@ -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
|
||||
|
@ -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>
|
||||
|
1
src/components/icons/global.svg
Normal file
1
src/components/icons/global.svg
Normal 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 |
1
src/components/icons/home.svg
Normal file
1
src/components/icons/home.svg
Normal 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 |
1
src/components/icons/settings.svg
Normal file
1
src/components/icons/settings.svg
Normal 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 |
@ -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>;
|
||||
};
|
||||
|
@ -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>
|
||||
|
7
src/hooks/use-is-mobile.ts
Normal file
7
src/hooks/use-is-mobile.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { useMediaQuery } from "@chakra-ui/react";
|
||||
|
||||
export function useIsMobile() {
|
||||
const [isMobile] = useMediaQuery("(max-width: 1000px)");
|
||||
|
||||
return isMobile;
|
||||
}
|
@ -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
8
src/styles.css
Normal file
@ -0,0 +1,8 @@
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
@ -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} />
|
||||
))}
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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} />
|
||||
))}
|
||||
|
Loading…
x
Reference in New Issue
Block a user