mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-07-14 14:22:25 +02:00
Dont require login for profile and note views
This commit is contained in:
5
.changeset/nervous-bats-teach.md
Normal file
5
.changeset/nervous-bats-teach.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"nostrudel": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Dont require login for profile and note views
|
35
src/app.tsx
35
src/app.tsx
@ -37,37 +37,12 @@ import UserMediaTab from "./views/user/media";
|
|||||||
// code split search view because QrScanner library is 400kB
|
// code split search view because QrScanner library is 400kB
|
||||||
const SearchView = React.lazy(() => import("./views/search"));
|
const SearchView = React.lazy(() => import("./views/search"));
|
||||||
|
|
||||||
const RequireCurrentAccount = ({ children }: { children: JSX.Element }) => {
|
|
||||||
let location = useLocation();
|
|
||||||
const loading = useSubject(accountService.loading);
|
|
||||||
const account = useSubject(accountService.current);
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<Flex alignItems="center" height="100%" gap="4" direction="column">
|
|
||||||
<Flex gap="4" grow="1" alignItems="center">
|
|
||||||
<Spinner />
|
|
||||||
<Text>Loading Accounts</Text>
|
|
||||||
</Flex>
|
|
||||||
<Button variant="link" margin="4" onClick={() => deleteDatabase()}>
|
|
||||||
Stuck loading? clear cache
|
|
||||||
</Button>
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (!account) return <Navigate to="/login" state={{ from: location.pathname }} replace />;
|
|
||||||
|
|
||||||
return children;
|
|
||||||
};
|
|
||||||
|
|
||||||
const RootPage = () => (
|
const RootPage = () => (
|
||||||
<RequireCurrentAccount>
|
<Page>
|
||||||
<Page>
|
<Suspense fallback={<Spinner />}>
|
||||||
<Suspense fallback={<Spinner />}>
|
<Outlet />
|
||||||
<Outlet />
|
</Suspense>
|
||||||
</Suspense>
|
</Page>
|
||||||
</Page>
|
|
||||||
</RequireCurrentAccount>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
|
@ -18,7 +18,7 @@ export function QuoteRepostButton({ event }: { event: NostrEvent }) {
|
|||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
aria-label="Quote repost"
|
aria-label="Quote repost"
|
||||||
title="Quote repost"
|
title="Quote repost"
|
||||||
isDisabled={account.readonly}
|
isDisabled={account?.readonly ?? true}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -45,7 +45,7 @@ export default function ReactionButton({ note, ...props }: { note: NostrEvent }
|
|||||||
handleClick(input);
|
handleClick(input);
|
||||||
};
|
};
|
||||||
|
|
||||||
const isLiked = reactions.some((event) => event.pubkey === account.pubkey);
|
const isLiked = !!account && reactions.some((event) => event.pubkey === account.pubkey);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// <Popover placement="bottom" trigger="hover" openDelay={500}>
|
// <Popover placement="bottom" trigger="hover" openDelay={500}>
|
||||||
|
@ -13,6 +13,12 @@ export function ReplyButton({ event }: { event: NostrEvent }) {
|
|||||||
const reply = () => openModal(buildReply(event));
|
const reply = () => openModal(buildReply(event));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IconButton icon={<ReplyIcon />} title="Reply" aria-label="Reply" onClick={reply} isDisabled={account.readonly} />
|
<IconButton
|
||||||
|
icon={<ReplyIcon />}
|
||||||
|
title="Reply"
|
||||||
|
aria-label="Reply"
|
||||||
|
onClick={reply}
|
||||||
|
isDisabled={account?.readonly ?? true}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -30,6 +30,7 @@ export function RepostButton({ event }: { event: NostrEvent }) {
|
|||||||
|
|
||||||
const handleClick = async () => {
|
const handleClick = async () => {
|
||||||
try {
|
try {
|
||||||
|
if (!account) throw new Error("not logged in");
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const draftRepost = buildRepost(event);
|
const draftRepost = buildRepost(event);
|
||||||
const repost = await signingService.requestSignature(draftRepost, account);
|
const repost = await signingService.requestSignature(draftRepost, account);
|
||||||
@ -50,7 +51,7 @@ export function RepostButton({ event }: { event: NostrEvent }) {
|
|||||||
onClick={onOpen}
|
onClick={onOpen}
|
||||||
aria-label="Repost Note"
|
aria-label="Repost Note"
|
||||||
title="Repost Note"
|
title="Repost Note"
|
||||||
isDisabled={account.readonly}
|
isDisabled={account?.readonly ?? true}
|
||||||
isLoading={loading}
|
isLoading={loading}
|
||||||
/>
|
/>
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
|
@ -43,6 +43,7 @@ export const NoteMenu = ({ event, ...props }: { event: NostrEvent } & Omit<MenuI
|
|||||||
|
|
||||||
const deleteNote = useCallback(async () => {
|
const deleteNote = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
|
if (!account) throw new Error("not logged in");
|
||||||
setDeleting(true);
|
setDeleting(true);
|
||||||
const deleteEvent = buildDeleteEvent([event.id], reason);
|
const deleteEvent = buildDeleteEvent([event.id], reason);
|
||||||
const signed = await signingService.requestSignature(deleteEvent, account);
|
const signed = await signingService.requestSignature(deleteEvent, account);
|
||||||
@ -75,7 +76,7 @@ export const NoteMenu = ({ event, ...props }: { event: NostrEvent } & Omit<MenuI
|
|||||||
Copy Note ID
|
Copy Note ID
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
{account.pubkey === event.pubkey && (
|
{account?.pubkey === event.pubkey && (
|
||||||
<MenuItem icon={<TrashIcon />} color="red.500" onClick={deleteModal.onOpen}>
|
<MenuItem icon={<TrashIcon />} color="red.500" onClick={deleteModal.onOpen}>
|
||||||
Delete Note
|
Delete Note
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
@ -26,7 +26,7 @@ export default function NoteZapButton({ note, ...props }: { note: NostrEvent } &
|
|||||||
}, [zaps]);
|
}, [zaps]);
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
|
|
||||||
const hasZapped = parsedZaps.some((zapRequest) => zapRequest.request.pubkey === account.pubkey);
|
const hasZapped = !!account && parsedZaps.some((zapRequest) => zapRequest.request.pubkey === account.pubkey);
|
||||||
const tipAddress = metadata?.lud06 || metadata?.lud16;
|
const tipAddress = metadata?.lud06 || metadata?.lud16;
|
||||||
|
|
||||||
const invoicePaid = () => eventZapsService.requestZaps(note.id, clientRelaysService.getReadUrls(), true);
|
const invoicePaid = () => eventZapsService.requestZaps(note.id, clientRelaysService.getReadUrls(), true);
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import React, { PropsWithChildren, useContext } from "react";
|
import React, { PropsWithChildren, useContext } from "react";
|
||||||
import { NostrEvent } from "../../types/nostr-event";
|
import { NostrEvent } from "../../types/nostr-event";
|
||||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
|
||||||
import { useUserContacts } from "../../hooks/use-user-contacts";
|
|
||||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||||
|
import clientFollowingService from "../../services/client-following";
|
||||||
|
import useSubject from "../../hooks/use-subject";
|
||||||
|
|
||||||
const TrustContext = React.createContext<boolean>(false);
|
const TrustContext = React.createContext<boolean>(false);
|
||||||
|
|
||||||
@ -18,11 +18,9 @@ export function TrustProvider({
|
|||||||
const parentTrust = useContext(TrustContext);
|
const parentTrust = useContext(TrustContext);
|
||||||
|
|
||||||
const account = useCurrentAccount();
|
const account = useCurrentAccount();
|
||||||
const readRelays = useReadRelayUrls();
|
const following = useSubject(clientFollowingService.following).map((p) => p[1]);
|
||||||
const contacts = useUserContacts(account.pubkey, readRelays);
|
|
||||||
const following = contacts?.contacts || [];
|
|
||||||
|
|
||||||
const isEventTrusted = trust || (!!event && (event.pubkey === account.pubkey || following.includes(event.pubkey)));
|
const isEventTrusted = trust || (!!event && (event.pubkey === account?.pubkey || following.includes(event.pubkey)));
|
||||||
|
|
||||||
return <TrustContext.Provider value={parentTrust || isEventTrusted}>{children}</TrustContext.Provider>;
|
return <TrustContext.Provider value={parentTrust || isEventTrusted}>{children}</TrustContext.Provider>;
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ import { useCurrentAccount } from "../../hooks/use-current-account";
|
|||||||
import accountService from "../../services/account";
|
import accountService from "../../services/account";
|
||||||
import { ConnectedRelays } from "../connected-relays";
|
import { ConnectedRelays } from "../connected-relays";
|
||||||
import { ChatIcon, FeedIcon, LogoutIcon, NotificationIcon, ProfileIcon, RelayIcon, SearchIcon } from "../icons";
|
import { ChatIcon, FeedIcon, LogoutIcon, NotificationIcon, ProfileIcon, RelayIcon, SearchIcon } from "../icons";
|
||||||
import { ProfileButton } from "../profile-button";
|
import ProfileLink from "./profile-link";
|
||||||
import AccountSwitcher from "./account-switcher";
|
import AccountSwitcher from "./account-switcher";
|
||||||
|
|
||||||
export default function DesktopSideNav() {
|
export default function DesktopSideNav() {
|
||||||
@ -19,7 +19,7 @@ export default function DesktopSideNav() {
|
|||||||
<Avatar src="/apple-touch-icon.png" size="sm" />
|
<Avatar src="/apple-touch-icon.png" size="sm" />
|
||||||
<Heading size="md">noStrudel</Heading>
|
<Heading size="md">noStrudel</Heading>
|
||||||
</Flex>
|
</Flex>
|
||||||
<ProfileButton />
|
<ProfileLink />
|
||||||
<AccountSwitcher />
|
<AccountSwitcher />
|
||||||
<Button onClick={() => navigate("/")} leftIcon={<FeedIcon />}>
|
<Button onClick={() => navigate("/")} leftIcon={<FeedIcon />}>
|
||||||
Home
|
Home
|
||||||
@ -42,10 +42,12 @@ export default function DesktopSideNav() {
|
|||||||
<Button onClick={() => navigate("/settings")} leftIcon={<SettingsIcon />}>
|
<Button onClick={() => navigate("/settings")} leftIcon={<SettingsIcon />}>
|
||||||
Settings
|
Settings
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={() => accountService.logout()} leftIcon={<LogoutIcon />}>
|
{account && (
|
||||||
Logout
|
<Button onClick={() => accountService.logout()} leftIcon={<LogoutIcon />}>
|
||||||
</Button>
|
Logout
|
||||||
{account.readonly && (
|
</Button>
|
||||||
|
)}
|
||||||
|
{account?.readonly && (
|
||||||
<Text color="red.200" textAlign="center">
|
<Text color="red.200" textAlign="center">
|
||||||
Readonly Mode
|
Readonly Mode
|
||||||
</Text>
|
</Text>
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { Flex, IconButton, useDisclosure } from "@chakra-ui/react";
|
import { Avatar, Flex, IconButton, useDisclosure } from "@chakra-ui/react";
|
||||||
import { useContext, useEffect } from "react";
|
import { useContext, useEffect } from "react";
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||||
import { PostModalContext } from "../../providers/post-modal-provider";
|
import { PostModalContext } from "../../providers/post-modal-provider";
|
||||||
import { ChatIcon, HomeIcon, NotificationIcon, PlusCircleIcon, SearchIcon } from "../icons";
|
import { ChatIcon, FeedIcon, HomeIcon, NotificationIcon, PlusCircleIcon, SearchIcon } from "../icons";
|
||||||
import { UserAvatar } from "../user-avatar";
|
import { UserAvatar } from "../user-avatar";
|
||||||
import MobileSideDrawer from "./mobile-side-drawer";
|
import MobileSideDrawer from "./mobile-side-drawer";
|
||||||
|
|
||||||
@ -19,7 +19,11 @@ export default function MobileBottomNav() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Flex flexShrink={0} gap="2" padding="2" alignItems="center">
|
<Flex flexShrink={0} gap="2" padding="2" alignItems="center">
|
||||||
<UserAvatar pubkey={account.pubkey} size="sm" onClick={onOpen} />
|
{account ? (
|
||||||
|
<UserAvatar pubkey={account.pubkey} size="sm" onClick={onOpen} noProxy />
|
||||||
|
) : (
|
||||||
|
<Avatar size="sm" src="/apple-touch-icon.png" onClick={onOpen} cursor="pointer" />
|
||||||
|
)}
|
||||||
<IconButton icon={<HomeIcon />} aria-label="Home" onClick={() => navigate("/")} flexGrow="1" size="md" />
|
<IconButton icon={<HomeIcon />} aria-label="Home" onClick={() => navigate("/")} flexGrow="1" size="md" />
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<SearchIcon />}
|
icon={<SearchIcon />}
|
||||||
@ -36,7 +40,7 @@ export default function MobileBottomNav() {
|
|||||||
}}
|
}}
|
||||||
variant="solid"
|
variant="solid"
|
||||||
colorScheme="brand"
|
colorScheme="brand"
|
||||||
isDisabled={account.readonly}
|
isDisabled={account?.readonly ?? true}
|
||||||
/>
|
/>
|
||||||
<IconButton icon={<ChatIcon />} aria-label="Messages" onClick={() => navigate(`/dm`)} flexGrow="1" size="md" />
|
<IconButton icon={<ChatIcon />} aria-label="Messages" onClick={() => navigate(`/dm`)} flexGrow="1" size="md" />
|
||||||
<IconButton
|
<IconButton
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
Avatar,
|
||||||
Button,
|
Button,
|
||||||
Drawer,
|
Drawer,
|
||||||
DrawerBody,
|
DrawerBody,
|
||||||
@ -10,32 +11,36 @@ import {
|
|||||||
Flex,
|
Flex,
|
||||||
Text,
|
Text,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { Link as RouterLink, 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 { ConnectedRelays } from "../connected-relays";
|
import { ConnectedRelays } from "../connected-relays";
|
||||||
import { HomeIcon, LogoutIcon, ProfileIcon, RelayIcon, SearchIcon, SettingsIcon } from "../icons";
|
import { HomeIcon, LogoutIcon, ProfileIcon, RelayIcon, SearchIcon, SettingsIcon } from "../icons";
|
||||||
import { UserAvatar } from "../user-avatar";
|
import { UserAvatar } from "../user-avatar";
|
||||||
import { UserLink } from "../user-link";
|
import { UserLink } from "../user-link";
|
||||||
import AccountSwitcher from "./account-switcher";
|
import AccountSwitcher from "./account-switcher";
|
||||||
|
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||||
|
import accountService from "../../services/account";
|
||||||
|
|
||||||
export default function MobileSideDrawer({ ...props }: Omit<DrawerProps, "children">) {
|
export default function MobileSideDrawer({ ...props }: Omit<DrawerProps, "children">) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const account = useCurrentAccount();
|
const account = useCurrentAccount();
|
||||||
const metadata = useUserMetadata(account.pubkey);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer placement="left" {...props}>
|
<Drawer placement="left" {...props}>
|
||||||
<DrawerOverlay />
|
<DrawerOverlay />
|
||||||
<DrawerContent>
|
<DrawerContent>
|
||||||
<DrawerCloseButton />
|
<DrawerCloseButton />
|
||||||
<DrawerHeader>
|
<DrawerHeader px="4" py="4">
|
||||||
<Flex gap="2">
|
{account ? (
|
||||||
<UserAvatar pubkey={account.pubkey} size="sm" />
|
<Flex gap="2">
|
||||||
<UserLink pubkey={account.pubkey} />
|
<UserAvatar pubkey={account.pubkey} size="sm" noProxy />
|
||||||
</Flex>
|
<UserLink pubkey={account.pubkey} />
|
||||||
|
</Flex>
|
||||||
|
) : (
|
||||||
|
<Flex gap="2">
|
||||||
|
<Avatar src="/apple-touch-icon.png" size="sm" />
|
||||||
|
<Text m={0}>Nostrudel</Text>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
</DrawerHeader>
|
</DrawerHeader>
|
||||||
<DrawerBody padding={0} overflowY="auto" overflowX="hidden">
|
<DrawerBody padding={0} overflowY="auto" overflowX="hidden">
|
||||||
<AccountSwitcher />
|
<AccountSwitcher />
|
||||||
@ -55,9 +60,15 @@ export default function MobileSideDrawer({ ...props }: Omit<DrawerProps, "childr
|
|||||||
<Button onClick={() => navigate("/settings")} leftIcon={<SettingsIcon />}>
|
<Button onClick={() => navigate("/settings")} leftIcon={<SettingsIcon />}>
|
||||||
Settings
|
Settings
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={() => accountService.logout()} leftIcon={<LogoutIcon />}>
|
{account ? (
|
||||||
Logout
|
<Button onClick={() => accountService.logout()} leftIcon={<LogoutIcon />}>
|
||||||
</Button>
|
Logout
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button as={RouterLink} to="/login" colorScheme="brand">
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<ConnectedRelays />
|
<ConnectedRelays />
|
||||||
</Flex>
|
</Flex>
|
||||||
</DrawerBody>
|
</DrawerBody>
|
||||||
|
40
src/components/page/profile-link.tsx
Normal file
40
src/components/page/profile-link.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { Box, Button, LinkBox, Text } from "@chakra-ui/react";
|
||||||
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
|
import { UserAvatar } from "../user-avatar";
|
||||||
|
import { useUserMetadata } from "../../hooks/use-user-metadata";
|
||||||
|
import { Bech32Prefix, normalizeToBech32 } from "../../helpers/nip19";
|
||||||
|
import { truncatedId } from "../../helpers/nostr-event";
|
||||||
|
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||||
|
|
||||||
|
function ProfileButton() {
|
||||||
|
const account = useCurrentAccount()!;
|
||||||
|
const metadata = useUserMetadata(account.pubkey);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LinkBox
|
||||||
|
as={RouterLink}
|
||||||
|
to={`/u/${normalizeToBech32(account.pubkey, Bech32Prefix.Pubkey)}`}
|
||||||
|
display="flex"
|
||||||
|
gap="2"
|
||||||
|
overflow="hidden"
|
||||||
|
>
|
||||||
|
<UserAvatar pubkey={account.pubkey} noProxy />
|
||||||
|
<Box>
|
||||||
|
<Text fontWeight="bold">{metadata?.name}</Text>
|
||||||
|
<Text>{truncatedId(normalizeToBech32(account.pubkey) ?? "")}</Text>
|
||||||
|
</Box>
|
||||||
|
</LinkBox>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ProfileLink() {
|
||||||
|
const account = useCurrentAccount();
|
||||||
|
|
||||||
|
if (account) return <ProfileButton />;
|
||||||
|
else
|
||||||
|
return (
|
||||||
|
<Button as={RouterLink} to="/login" state={{ from: location.pathname }} colorScheme="brand">
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
@ -1,28 +0,0 @@
|
|||||||
import { Box, LinkBox, Text } from "@chakra-ui/react";
|
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
import { UserAvatar } from "./user-avatar";
|
|
||||||
import { useUserMetadata } from "../hooks/use-user-metadata";
|
|
||||||
import { Bech32Prefix, normalizeToBech32 } from "../helpers/nip19";
|
|
||||||
import { truncatedId } from "../helpers/nostr-event";
|
|
||||||
import { useCurrentAccount } from "../hooks/use-current-account";
|
|
||||||
|
|
||||||
export const ProfileButton = () => {
|
|
||||||
const { pubkey } = useCurrentAccount();
|
|
||||||
const metadata = useUserMetadata(pubkey);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<LinkBox
|
|
||||||
as={Link}
|
|
||||||
to={`/u/${normalizeToBech32(pubkey, Bech32Prefix.Pubkey)}`}
|
|
||||||
display="flex"
|
|
||||||
gap="2"
|
|
||||||
overflow="hidden"
|
|
||||||
>
|
|
||||||
<UserAvatar pubkey={pubkey} noProxy />
|
|
||||||
<Box>
|
|
||||||
<Text fontWeight="bold">{metadata?.name}</Text>
|
|
||||||
<Text>{truncatedId(normalizeToBech32(pubkey) ?? "")}</Text>
|
|
||||||
</Box>
|
|
||||||
</LinkBox>
|
|
||||||
);
|
|
||||||
};
|
|
@ -2,8 +2,6 @@ import { Button, ButtonProps } from "@chakra-ui/react";
|
|||||||
import { useCurrentAccount } from "../hooks/use-current-account";
|
import { useCurrentAccount } from "../hooks/use-current-account";
|
||||||
import useSubject from "../hooks/use-subject";
|
import useSubject from "../hooks/use-subject";
|
||||||
import clientFollowingService from "../services/client-following";
|
import clientFollowingService from "../services/client-following";
|
||||||
import { useAsync } from "react-use";
|
|
||||||
import { NostrRequest } from "../classes/nostr-request";
|
|
||||||
import clientRelaysService from "../services/client-relays";
|
import clientRelaysService from "../services/client-relays";
|
||||||
import { useUserContacts } from "../hooks/use-user-contacts";
|
import { useUserContacts } from "../hooks/use-user-contacts";
|
||||||
|
|
||||||
@ -30,8 +28,14 @@ export const UserFollowButton = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button colorScheme="brand" {...props} isLoading={savingDraft} onClick={toggleFollow} isDisabled={account.readonly}>
|
<Button
|
||||||
{isFollowing ? "Unfollow" : userContacts?.contacts.includes(account.pubkey) ? "Follow Back" : "Follow"}
|
colorScheme="brand"
|
||||||
|
{...props}
|
||||||
|
isLoading={savingDraft}
|
||||||
|
onClick={toggleFollow}
|
||||||
|
isDisabled={account?.readonly ?? true}
|
||||||
|
>
|
||||||
|
{isFollowing ? "Unfollow" : account && userContacts?.contacts.includes(account.pubkey) ? "Follow Back" : "Follow"}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -2,7 +2,5 @@ import accountService from "../services/account";
|
|||||||
import useSubject from "./use-subject";
|
import useSubject from "./use-subject";
|
||||||
|
|
||||||
export function useCurrentAccount() {
|
export function useCurrentAccount() {
|
||||||
const account = useSubject(accountService.current);
|
return useSubject(accountService.current);
|
||||||
if (!account) throw Error("no account");
|
|
||||||
return account;
|
|
||||||
}
|
}
|
||||||
|
44
src/providers/require-current-account.tsx
Normal file
44
src/providers/require-current-account.tsx
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { Link, useLocation } from "react-router-dom";
|
||||||
|
import useSubject from "../hooks/use-subject";
|
||||||
|
import accountService from "../services/account";
|
||||||
|
import { Button, Flex, Heading, Spinner, Text } from "@chakra-ui/react";
|
||||||
|
import { deleteDatabase } from "../services/db";
|
||||||
|
import { ExternalLinkIcon } from "../components/icons";
|
||||||
|
|
||||||
|
export default function RequireCurrentAccount({ children }: { children: JSX.Element }) {
|
||||||
|
let location = useLocation();
|
||||||
|
const loading = useSubject(accountService.loading);
|
||||||
|
const account = useSubject(accountService.current);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<Flex alignItems="center" height="100%" gap="4" direction="column">
|
||||||
|
<Flex gap="4" grow="1" alignItems="center">
|
||||||
|
<Spinner />
|
||||||
|
<Text>Loading Accounts</Text>
|
||||||
|
</Flex>
|
||||||
|
<Button variant="link" margin="4" onClick={() => deleteDatabase()}>
|
||||||
|
Stuck loading? clear cache
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!account)
|
||||||
|
return (
|
||||||
|
<Flex direction="column" w="full" h="full" alignItems="center" justifyContent="center" gap="4">
|
||||||
|
<Heading size="md">You must be logged in to use this view</Heading>
|
||||||
|
<Button
|
||||||
|
as={Link}
|
||||||
|
to="/login"
|
||||||
|
state={{ from: location.pathname }}
|
||||||
|
colorScheme="brand"
|
||||||
|
rightIcon={<ExternalLinkIcon />}
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
|
||||||
|
return children;
|
||||||
|
}
|
@ -4,7 +4,7 @@ import { AppSettings } from "./user-app-settings";
|
|||||||
|
|
||||||
export type Account = {
|
export type Account = {
|
||||||
pubkey: string;
|
pubkey: string;
|
||||||
readonly?: boolean;
|
readonly: boolean;
|
||||||
relays?: string[];
|
relays?: string[];
|
||||||
secKey?: ArrayBuffer;
|
secKey?: ArrayBuffer;
|
||||||
iv?: Uint8Array;
|
iv?: Uint8Array;
|
||||||
|
@ -10,6 +10,14 @@ import signingService from "./signing";
|
|||||||
|
|
||||||
export type RelayDirectory = Record<string, { read: boolean; write: boolean }>;
|
export type RelayDirectory = Record<string, { read: boolean; write: boolean }>;
|
||||||
|
|
||||||
|
const DEFAULT_RELAYS = [
|
||||||
|
{ url: "wss://relay.damus.io", mode: RelayMode.READ },
|
||||||
|
{ url: "wss://nostr.wine", mode: RelayMode.READ },
|
||||||
|
{ url: "wss://relay.snort.social", mode: RelayMode.READ },
|
||||||
|
{ url: "wss://eden.nostr.land", mode: RelayMode.READ },
|
||||||
|
{ url: "wss://nos.lol", mode: RelayMode.READ },
|
||||||
|
];
|
||||||
|
|
||||||
class ClientRelayService {
|
class ClientRelayService {
|
||||||
bootstrapRelays = new Set<string>();
|
bootstrapRelays = new Set<string>();
|
||||||
relays = new PersistentSubject<RelayConfig[]>([]);
|
relays = new PersistentSubject<RelayConfig[]>([]);
|
||||||
@ -19,9 +27,10 @@ class ClientRelayService {
|
|||||||
constructor() {
|
constructor() {
|
||||||
let lastSubject: Subject<ParsedUserRelays> | undefined;
|
let lastSubject: Subject<ParsedUserRelays> | undefined;
|
||||||
accountService.current.subscribe((account) => {
|
accountService.current.subscribe((account) => {
|
||||||
this.relays.next([]);
|
if (!account) {
|
||||||
|
this.relays.next(DEFAULT_RELAYS);
|
||||||
if (!account) return;
|
return;
|
||||||
|
} else this.relays.next([]);
|
||||||
|
|
||||||
if (account.relays) {
|
if (account.relays) {
|
||||||
this.bootstrapRelays.clear();
|
this.bootstrapRelays.clear();
|
||||||
|
@ -19,6 +19,7 @@ import { DraftNostrEvent, NostrEvent } from "../../types/nostr-event";
|
|||||||
import DecryptPlaceholder from "./decrypt-placeholder";
|
import DecryptPlaceholder from "./decrypt-placeholder";
|
||||||
import { EmbedableContent } from "../../helpers/embeds";
|
import { EmbedableContent } from "../../helpers/embeds";
|
||||||
import { embedImages, embedLinks, embedNostrLinks, embedVideos } from "../../components/embed-types";
|
import { embedImages, embedLinks, embedNostrLinks, embedVideos } from "../../components/embed-types";
|
||||||
|
import RequireCurrentAccount from "../../providers/require-current-account";
|
||||||
|
|
||||||
function MessageContent({ event, text }: { event: NostrEvent; text: string }) {
|
function MessageContent({ event, text }: { event: NostrEvent; text: string }) {
|
||||||
let content: EmbedableContent = [text];
|
let content: EmbedableContent = [text];
|
||||||
@ -33,7 +34,7 @@ function MessageContent({ event, text }: { event: NostrEvent; text: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Message({ event }: { event: NostrEvent } & Omit<CardProps, "children">) {
|
function Message({ event }: { event: NostrEvent } & Omit<CardProps, "children">) {
|
||||||
const account = useCurrentAccount();
|
const account = useCurrentAccount()!;
|
||||||
const isOwnMessage = account.pubkey === event.pubkey;
|
const isOwnMessage = account.pubkey === event.pubkey;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -55,7 +56,7 @@ function Message({ event }: { event: NostrEvent } & Omit<CardProps, "children">)
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function DirectMessageChatView() {
|
function DirectMessageChatPage() {
|
||||||
const { key } = useParams();
|
const { key } = useParams();
|
||||||
if (!key) return <Navigate to="/" />;
|
if (!key) return <Navigate to="/" />;
|
||||||
const pubkey = normalizeToHex(key);
|
const pubkey = normalizeToHex(key);
|
||||||
@ -131,3 +132,10 @@ export default function DirectMessageChatView() {
|
|||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
export default function DirectMessageChatView() {
|
||||||
|
return (
|
||||||
|
<RequireCurrentAccount>
|
||||||
|
<DirectMessageChatPage />
|
||||||
|
</RequireCurrentAccount>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -25,6 +25,7 @@ import { useUserMetadata } from "../../hooks/use-user-metadata";
|
|||||||
import directMessagesService from "../../services/direct-messages";
|
import directMessagesService from "../../services/direct-messages";
|
||||||
import { ExternalLinkIcon } from "../../components/icons";
|
import { ExternalLinkIcon } from "../../components/icons";
|
||||||
import { useIsMobile } from "../../hooks/use-is-mobile";
|
import { useIsMobile } from "../../hooks/use-is-mobile";
|
||||||
|
import RequireCurrentAccount from "../../providers/require-current-account";
|
||||||
|
|
||||||
function ContactCard({ pubkey }: { pubkey: string }) {
|
function ContactCard({ pubkey }: { pubkey: string }) {
|
||||||
const subject = useMemo(() => directMessagesService.getUserMessages(pubkey), [pubkey]);
|
const subject = useMemo(() => directMessagesService.getUserMessages(pubkey), [pubkey]);
|
||||||
@ -48,7 +49,7 @@ function ContactCard({ pubkey }: { pubkey: string }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DirectMessagesView() {
|
function DirectMessagesPage() {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const [from, setFrom] = useState(moment().subtract(2, "days"));
|
const [from, setFrom] = useState(moment().subtract(2, "days"));
|
||||||
const conversations = useSubject(directMessagesService.conversations);
|
const conversations = useSubject(directMessagesService.conversations);
|
||||||
@ -121,4 +122,10 @@ function DirectMessagesView() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DirectMessagesView;
|
export default function DirectMessagesView() {
|
||||||
|
return (
|
||||||
|
<RequireCurrentAccount>
|
||||||
|
<DirectMessagesPage />
|
||||||
|
</RequireCurrentAccount>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { Button, Flex, Spinner } from "@chakra-ui/react";
|
import { Button, Flex, Spinner } from "@chakra-ui/react";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { Note } from "../../components/note";
|
import { Note } from "../../components/note";
|
||||||
@ -11,6 +11,7 @@ import userContactsService, { UserContacts } from "../../services/user-contacts"
|
|||||||
import { PersistentSubject } from "../../classes/subject";
|
import { PersistentSubject } from "../../classes/subject";
|
||||||
import useSubject from "../../hooks/use-subject";
|
import useSubject from "../../hooks/use-subject";
|
||||||
import { useThrottle } from "react-use";
|
import { useThrottle } from "react-use";
|
||||||
|
import RequireCurrentAccount from "../../providers/require-current-account";
|
||||||
|
|
||||||
class DiscoverContacts {
|
class DiscoverContacts {
|
||||||
pubkey: string;
|
pubkey: string;
|
||||||
@ -59,9 +60,9 @@ class DiscoverContacts {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function DiscoverTab() {
|
function DiscoverTabBody() {
|
||||||
useAppTitle("discover");
|
useAppTitle("discover");
|
||||||
const account = useCurrentAccount();
|
const account = useCurrentAccount()!;
|
||||||
const relays = useReadRelayUrls();
|
const relays = useReadRelayUrls();
|
||||||
|
|
||||||
const discover = useMemo(() => new DiscoverContacts(account.pubkey, relays), [account.pubkey, relays.join("|")]);
|
const discover = useMemo(() => new DiscoverContacts(account.pubkey, relays), [account.pubkey, relays.join("|")]);
|
||||||
@ -86,3 +87,11 @@ export default function DiscoverTab() {
|
|||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default function DiscoverTab() {
|
||||||
|
return (
|
||||||
|
<RequireCurrentAccount>
|
||||||
|
<DiscoverTabBody />
|
||||||
|
</RequireCurrentAccount>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -11,9 +11,10 @@ import { PostModalContext } from "../../providers/post-modal-provider";
|
|||||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||||
import RepostNote from "../../components/note/repost-note";
|
import RepostNote from "../../components/note/repost-note";
|
||||||
|
import RequireCurrentAccount from "../../providers/require-current-account";
|
||||||
|
|
||||||
export default function FollowingTab() {
|
function FollowingTabBody() {
|
||||||
const account = useCurrentAccount();
|
const account = useCurrentAccount()!;
|
||||||
const readRelays = useReadRelayUrls();
|
const readRelays = useReadRelayUrls();
|
||||||
const { openModal } = useContext(PostModalContext);
|
const { openModal } = useContext(PostModalContext);
|
||||||
const contacts = useUserContacts(account.pubkey, readRelays);
|
const contacts = useUserContacts(account.pubkey, readRelays);
|
||||||
@ -55,3 +56,11 @@ export default function FollowingTab() {
|
|||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default function FollowingTab() {
|
||||||
|
return (
|
||||||
|
<RequireCurrentAccount>
|
||||||
|
<FollowingTabBody />
|
||||||
|
</RequireCurrentAccount>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -72,7 +72,7 @@ export default function LoginNsecView() {
|
|||||||
const pubkey = getPublicKey(hexKey);
|
const pubkey = getPublicKey(hexKey);
|
||||||
|
|
||||||
const encrypted = await signingService.encryptSecKey(hexKey);
|
const encrypted = await signingService.encryptSecKey(hexKey);
|
||||||
accountService.addAccount({ pubkey, relays: [relayUrl], ...encrypted });
|
accountService.addAccount({ pubkey, relays: [relayUrl], ...encrypted, readonly: false });
|
||||||
clientRelaysService.bootstrapRelays.add(relayUrl);
|
clientRelaysService.bootstrapRelays.add(relayUrl);
|
||||||
accountService.switchAccount(pubkey);
|
accountService.switchAccount(pubkey);
|
||||||
};
|
};
|
||||||
|
@ -42,7 +42,7 @@ export default function LoginStartView() {
|
|||||||
relays = ["wss://relay.damus.io", "wss://relay.snort.social", "wss://nostr.wine"];
|
relays = ["wss://relay.damus.io", "wss://relay.snort.social", "wss://nostr.wine"];
|
||||||
}
|
}
|
||||||
|
|
||||||
accountService.addAccount({ pubkey, relays, useExtension: true });
|
accountService.addAccount({ pubkey, relays, useExtension: true, readonly: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
accountService.switchAccount(pubkey);
|
accountService.switchAccount(pubkey);
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { Button, Card, CardBody, CardHeader, Flex, Spinner, Text } from "@chakra-ui/react";
|
import { Button, Card, CardBody, CardHeader, Flex, Spinner, Text } from "@chakra-ui/react";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { memo } from "react";
|
import { memo } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
import { UserAvatar } from "../../components/user-avatar";
|
import { UserAvatar } from "../../components/user-avatar";
|
||||||
import { UserLink } from "../../components/user-link";
|
import { UserLink } from "../../components/user-link";
|
||||||
import { convertTimestampToDate } from "../../helpers/date";
|
import { convertTimestampToDate } from "../../helpers/date";
|
||||||
@ -10,6 +9,7 @@ import { useCurrentAccount } from "../../hooks/use-current-account";
|
|||||||
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
||||||
import { NostrEvent } from "../../types/nostr-event";
|
import { NostrEvent } from "../../types/nostr-event";
|
||||||
import { NoteLink } from "../../components/note-link";
|
import { NoteLink } from "../../components/note-link";
|
||||||
|
import RequireCurrentAccount from "../../providers/require-current-account";
|
||||||
|
|
||||||
const Kind1Notification = ({ event }: { event: NostrEvent }) => (
|
const Kind1Notification = ({ event }: { event: NostrEvent }) => (
|
||||||
<Card size="sm" variant="outline">
|
<Card size="sm" variant="outline">
|
||||||
@ -35,9 +35,9 @@ const NotificationItem = memo(({ event }: { event: NostrEvent }) => {
|
|||||||
return <>Unknown event type {event.kind}</>;
|
return <>Unknown event type {event.kind}</>;
|
||||||
});
|
});
|
||||||
|
|
||||||
const NotificationsView = () => {
|
function NotificationsPage() {
|
||||||
const readRelays = useReadRelayUrls();
|
const readRelays = useReadRelayUrls();
|
||||||
const account = useCurrentAccount();
|
const account = useCurrentAccount()!;
|
||||||
const { events, loading, loadMore } = useTimelineLoader(
|
const { events, loading, loadMore } = useTimelineLoader(
|
||||||
"notifications",
|
"notifications",
|
||||||
readRelays,
|
readRelays,
|
||||||
@ -60,6 +60,12 @@ const NotificationsView = () => {
|
|||||||
{loading ? <Spinner ml="auto" mr="auto" mt="8" mb="8" /> : <Button onClick={() => loadMore()}>Load More</Button>}
|
{loading ? <Spinner ml="auto" mr="auto" mt="8" mb="8" /> : <Button onClick={() => loadMore()}>Load More</Button>}
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
export default NotificationsView;
|
export default function NotificationsView() {
|
||||||
|
return (
|
||||||
|
<RequireCurrentAccount>
|
||||||
|
<NotificationsPage />
|
||||||
|
</RequireCurrentAccount>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -49,7 +49,7 @@ type MetadataFormProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const MetadataForm = ({ defaultValues, onSubmit }: MetadataFormProps) => {
|
const MetadataForm = ({ defaultValues, onSubmit }: MetadataFormProps) => {
|
||||||
const account = useCurrentAccount();
|
const account = useCurrentAccount()!;
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
@ -189,7 +189,7 @@ export const ProfileEditView = () => {
|
|||||||
const writeRelays = useWriteRelayUrls();
|
const writeRelays = useWriteRelayUrls();
|
||||||
const readRelays = useReadRelayUrls();
|
const readRelays = useReadRelayUrls();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const account = useCurrentAccount();
|
const account = useCurrentAccount()!;
|
||||||
const metadata = useUserMetadata(account.pubkey, readRelays, true);
|
const metadata = useUserMetadata(account.pubkey, readRelays, true);
|
||||||
|
|
||||||
const defaultValues = useMemo<FormData>(
|
const defaultValues = useMemo<FormData>(
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
|
import RequireCurrentAccount from "../../providers/require-current-account";
|
||||||
import { ProfileEditView } from "./edit";
|
import { ProfileEditView } from "./edit";
|
||||||
|
|
||||||
export default function ProfileView() {
|
export default function ProfileView() {
|
||||||
return <ProfileEditView />;
|
return (
|
||||||
|
<RequireCurrentAccount>
|
||||||
|
<ProfileEditView />
|
||||||
|
</RequireCurrentAccount>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -26,8 +26,9 @@ import useSubject from "../../hooks/use-subject";
|
|||||||
import { RelayStatus } from "../../components/relay-status";
|
import { RelayStatus } from "../../components/relay-status";
|
||||||
import { normalizeRelayUrl } from "../../helpers/url";
|
import { normalizeRelayUrl } from "../../helpers/url";
|
||||||
import { RelayScoreBreakdown } from "../../components/relay-score-breakdown";
|
import { RelayScoreBreakdown } from "../../components/relay-score-breakdown";
|
||||||
|
import RequireCurrentAccount from "../../providers/require-current-account";
|
||||||
|
|
||||||
export default function RelaysView() {
|
function RelaysPage() {
|
||||||
const relays = useSubject(clientRelaysService.relays);
|
const relays = useSubject(clientRelaysService.relays);
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
@ -152,3 +153,11 @@ export default function RelaysView() {
|
|||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default function RelaysView() {
|
||||||
|
return (
|
||||||
|
<RequireCurrentAccount>
|
||||||
|
<RelaysPage />
|
||||||
|
</RequireCurrentAccount>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -29,7 +29,7 @@ export default function Header({
|
|||||||
const npub = normalizeToBech32(pubkey, Bech32Prefix.Pubkey);
|
const npub = normalizeToBech32(pubkey, Bech32Prefix.Pubkey);
|
||||||
|
|
||||||
const account = useCurrentAccount();
|
const account = useCurrentAccount();
|
||||||
const isSelf = pubkey === account.pubkey;
|
const isSelf = pubkey === account?.pubkey;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex direction="column" gap="2" px="2" pt="2">
|
<Flex direction="column" gap="2" px="2" pt="2">
|
||||||
|
Reference in New Issue
Block a user