mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-04-10 21:00:17 +02:00
add npub login for fun
This commit is contained in:
parent
62c0581cf2
commit
ae06d3fa1f
@ -29,7 +29,7 @@
|
||||
- [ ] [NIP-04](https://github.com/nostr-protocol/nips/blob/master/04.md): Encrypted Direct Message
|
||||
- [x] [NIP-05](https://github.com/nostr-protocol/nips/blob/master/05.md): Mapping Nostr keys to DNS-based internet identifiers
|
||||
- [ ] [NIP-06](https://github.com/nostr-protocol/nips/blob/master/06.md): Basic key derivation from mnemonic seed phrase
|
||||
- [ ] [NIP-07](https://github.com/nostr-protocol/nips/blob/master/07.md): `window.nostr` capability for web browsers
|
||||
- [x] [NIP-07](https://github.com/nostr-protocol/nips/blob/master/07.md): `window.nostr` capability for web browsers
|
||||
- [ ] [NIP-08](https://github.com/nostr-protocol/nips/blob/master/08.md): Handling Mentions
|
||||
- [ ] [NIP-09](https://github.com/nostr-protocol/nips/blob/master/09.md): Event Deletion
|
||||
- [ ] [NIP-11](https://github.com/nostr-protocol/nips/blob/master/11.md): Relay Information Document
|
||||
@ -51,7 +51,6 @@
|
||||
|
||||
## TODO
|
||||
|
||||
- add relay selection to global feed and allow user to specify custom relay
|
||||
- add `client` tag to published events
|
||||
- add relay selection to global feed
|
||||
- add button for creating lightning invoice via WebLN
|
||||
@ -65,6 +64,7 @@
|
||||
- sort replies by date
|
||||
- filter list of followers by users the user has blocked/reported (stops bots/spammers from showing up at followers)
|
||||
- Add client side relay groups
|
||||
- Add mentions in posts (https://css-tricks.com/so-you-want-to-build-an-mention-autocomplete-feature/)
|
||||
|
||||
## Setup
|
||||
|
||||
|
11
src/app.tsx
11
src/app.tsx
@ -19,6 +19,8 @@ import UserFollowersTab from "./views/user/followers";
|
||||
import UserRelaysTab from "./views/user/relays";
|
||||
import UserFollowingTab from "./views/user/following";
|
||||
import NoteView from "./views/note";
|
||||
import { LoginStartView } from "./views/login/start";
|
||||
import { LoginNpubView } from "./views/login/npub";
|
||||
|
||||
const RequireSetup = ({ children }: { children: JSX.Element }) => {
|
||||
let location = useLocation();
|
||||
@ -38,7 +40,14 @@ const RootPage = () => (
|
||||
);
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{ path: "login", element: <LoginView /> },
|
||||
{
|
||||
path: "login",
|
||||
element: <LoginView />,
|
||||
children: [
|
||||
{ path: "", element: <LoginStartView /> },
|
||||
{ path: "npub", element: <LoginNpubView /> },
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/",
|
||||
element: <RootPage />,
|
||||
|
@ -151,3 +151,9 @@ export const VerificationFailed = createIcon({
|
||||
d: "M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm-1-7v2h2v-2h-2zm0-8v6h2V7h-2z",
|
||||
defaultProps,
|
||||
});
|
||||
|
||||
export const SpyIcon = createIcon({
|
||||
displayName: "SpyIcon",
|
||||
d: "M17 13a4 4 0 1 1-4 4h-2a4 4 0 1 1-.535-2h3.07A3.998 3.998 0 0 1 17 13zM7 15a2 2 0 1 0 0 4 2 2 0 0 0 0-4zm10 0a2 2 0 1 0 0 4 2 2 0 0 0 0-4zM16 3a4 4 0 0 1 4 4v3h2v2H2v-2h2V7a4 4 0 0 1 4-4h8zm0 2H8c-1.054 0-2 .95-2 2v3h12V7c0-1.054-.95-2-2-2z",
|
||||
defaultProps,
|
||||
});
|
||||
|
@ -19,12 +19,14 @@ import { ReplyIcon } from "../icons";
|
||||
import { PostModalContext } from "../../providers/post-modal-provider";
|
||||
import { buildReply } from "../../helpers/nostr-event";
|
||||
import { UserDnsIdentityIcon } from "../user-dns-identity";
|
||||
import { useReadonlyMode } from "../../hooks/use-readonly-mode";
|
||||
|
||||
export type NoteProps = {
|
||||
event: NostrEvent;
|
||||
};
|
||||
export const Note = React.memo(({ event }: NoteProps) => {
|
||||
const isMobile = useIsMobile();
|
||||
const readonly = useReadonlyMode();
|
||||
const { openModal } = useContext(PostModalContext);
|
||||
|
||||
const pubkey = useSubject(identity.pubkey);
|
||||
@ -53,7 +55,14 @@ export const Note = React.memo(({ event }: NoteProps) => {
|
||||
<NoteContents event={event} trusted={following.includes(event.pubkey)} />
|
||||
</CardBody>
|
||||
<CardFooter padding="2" display="flex" gap="2">
|
||||
<IconButton icon={<ReplyIcon />} title="Reply" aria-label="Reply" onClick={reply} size="xs" />
|
||||
<IconButton
|
||||
icon={<ReplyIcon />}
|
||||
title="Reply"
|
||||
aria-label="Reply"
|
||||
onClick={reply}
|
||||
size="xs"
|
||||
isDisabled={readonly}
|
||||
/>
|
||||
<Box flexGrow={1} />
|
||||
<UserTipButton pubkey={event.pubkey} size="xs" />
|
||||
<NoteRelays event={event} size="xs" />
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import { Avatar, Button, Container, Flex, Heading, IconButton, LinkOverlay, VStack } from "@chakra-ui/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, ProfileIcon, SettingsIcon } from "./icons";
|
||||
import { ErrorBoundary } from "./error-boundary";
|
||||
@ -10,10 +10,12 @@ import identity from "../services/identity";
|
||||
import { FollowingList } from "./following-list";
|
||||
import { ReloadPrompt } from "./reload-prompt";
|
||||
import { PostModalProvider } from "../providers/post-modal-provider";
|
||||
import { useReadonlyMode } from "../hooks/use-readonly-mode";
|
||||
|
||||
export const Page = ({ children }: { children: React.ReactNode }) => {
|
||||
const navigate = useNavigate();
|
||||
const isMobile = useIsMobile();
|
||||
const readonly = useReadonlyMode();
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
@ -72,6 +74,11 @@ export const Page = ({ children }: { children: React.ReactNode }) => {
|
||||
<Button onClick={() => identity.logout()} leftIcon={<LogoutIcon />}>
|
||||
Logout
|
||||
</Button>
|
||||
{readonly && (
|
||||
<Text color="yellow.500" textAlign="center">
|
||||
Readonly Mode
|
||||
</Text>
|
||||
)}
|
||||
<ConnectedRelays />
|
||||
</VStack>
|
||||
<Flex flexGrow={1} direction="column" overflow="hidden">
|
||||
|
6
src/hooks/use-readonly-mode.ts
Normal file
6
src/hooks/use-readonly-mode.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import identity from "../services/identity";
|
||||
import useSubject from "./use-subject";
|
||||
|
||||
export function useReadonlyMode() {
|
||||
return useSubject(identity.readonly);
|
||||
}
|
@ -14,6 +14,7 @@ class IdentityService {
|
||||
loading = new BehaviorSubject(true);
|
||||
setup = new BehaviorSubject(false);
|
||||
pubkey = new BehaviorSubject("");
|
||||
readonly = new BehaviorSubject(false);
|
||||
// TODO: remove this when there is a service to manage user relays
|
||||
relays = new BehaviorSubject<PresetRelays>({});
|
||||
private useExtension: boolean = false;
|
||||
@ -25,11 +26,13 @@ class IdentityService {
|
||||
if (savedIdentity) {
|
||||
this.setup.next(true);
|
||||
this.pubkey.next(savedIdentity.pubkey);
|
||||
this.readonly.next(false);
|
||||
this.secKey = savedIdentity.secKey;
|
||||
this.useExtension = savedIdentity.useExtension;
|
||||
} else {
|
||||
this.setup.next(false);
|
||||
this.pubkey.next("");
|
||||
this.readonly.next(false);
|
||||
this.secKey = undefined;
|
||||
this.useExtension = false;
|
||||
}
|
||||
@ -38,6 +41,7 @@ class IdentityService {
|
||||
|
||||
async loginWithExtension() {
|
||||
if (window.nostr) {
|
||||
this.loading.next(true);
|
||||
const pubkey = await window.nostr.getPublicKey();
|
||||
settings.identity.next({
|
||||
pubkey,
|
||||
@ -60,7 +64,14 @@ class IdentityService {
|
||||
// });
|
||||
}
|
||||
|
||||
async logout() {
|
||||
loginWithPubkey(pubkey: string) {
|
||||
this.readonly.next(true);
|
||||
this.pubkey.next(pubkey);
|
||||
this.setup.next(true);
|
||||
this.loading.next(false);
|
||||
}
|
||||
|
||||
logout() {
|
||||
settings.identity.next(null);
|
||||
}
|
||||
}
|
||||
|
@ -11,8 +11,10 @@ import settings from "../../services/settings";
|
||||
import { AddIcon } from "@chakra-ui/icons";
|
||||
import { useContext } from "react";
|
||||
import { PostModalContext } from "../../providers/post-modal-provider";
|
||||
import { useReadonlyMode } from "../../hooks/use-readonly-mode";
|
||||
|
||||
export const FollowingTab = () => {
|
||||
const readonly = useReadonlyMode();
|
||||
const pubkey = useSubject(identity.pubkey);
|
||||
const relays = useSubject(settings.relays);
|
||||
const { openModal } = useContext(PostModalContext);
|
||||
@ -35,7 +37,7 @@ export const FollowingTab = () => {
|
||||
|
||||
return (
|
||||
<Flex direction="column" gap="2">
|
||||
<Button variant="outline" leftIcon={<AddIcon />} onClick={() => openModal()}>
|
||||
<Button variant="outline" leftIcon={<AddIcon />} onClick={() => openModal()} isDisabled={readonly}>
|
||||
New Post
|
||||
</Button>
|
||||
<FormControl display="flex" alignItems="center">
|
||||
|
@ -1,21 +0,0 @@
|
||||
import { Avatar, Button, Flex, Heading, Spinner } from "@chakra-ui/react";
|
||||
import { Navigate, useLocation } from "react-router-dom";
|
||||
import useSubject from "../hooks/use-subject";
|
||||
import identity from "../services/identity";
|
||||
|
||||
export const LoginView = () => {
|
||||
const setup = useSubject(identity.setup);
|
||||
const loading = useSubject(identity.loading);
|
||||
const location = useLocation();
|
||||
|
||||
if (loading) return <Spinner />;
|
||||
if (setup) return <Navigate to={location.state?.from ?? "/"} replace />;
|
||||
|
||||
return (
|
||||
<Flex direction="column" alignItems="center" justifyContent="center" gap="4" height="80%">
|
||||
<Avatar src="/apple-touch-icon.png" size="lg" />
|
||||
<Heading>noStrudel</Heading>
|
||||
<Button onClick={() => identity.loginWithExtension()}>Use browser extension</Button>
|
||||
</Flex>
|
||||
);
|
||||
};
|
19
src/views/login/index.tsx
Normal file
19
src/views/login/index.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import { Avatar, Box, Flex, Heading } from "@chakra-ui/react";
|
||||
import { Navigate, Outlet, useLocation } from "react-router-dom";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import identity from "../../services/identity";
|
||||
|
||||
export const LoginView = () => {
|
||||
const setup = useSubject(identity.setup);
|
||||
const location = useLocation();
|
||||
|
||||
if (setup) return <Navigate to={location.state?.from ?? "/"} replace />;
|
||||
|
||||
return (
|
||||
<Flex direction="column" alignItems="center" justifyContent="center" gap="4" height="80%" px="4">
|
||||
<Avatar src="/apple-touch-icon.png" size="lg" />
|
||||
<Heading>noStrudel</Heading>
|
||||
<Outlet />
|
||||
</Flex>
|
||||
);
|
||||
};
|
60
src/views/login/npub.tsx
Normal file
60
src/views/login/npub.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
import { useState } from "react";
|
||||
import { Button, Flex, FormControl, FormHelperText, FormLabel, Input, Link, useToast } from "@chakra-ui/react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { RelayUrlInput } from "../../components/relay-url-input";
|
||||
import { normalizeToHex } from "../../helpers/nip-19";
|
||||
import identity from "../../services/identity";
|
||||
import settings from "../../services/settings";
|
||||
|
||||
export const LoginNpubView = () => {
|
||||
const navigate = useNavigate();
|
||||
const toast = useToast();
|
||||
const [npub, setNpub] = useState("");
|
||||
const [relay, setRelay] = useState("");
|
||||
|
||||
const handleSubmit: React.FormEventHandler<HTMLDivElement> = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const pubkey = normalizeToHex(npub);
|
||||
if (!pubkey) {
|
||||
return toast({ status: "error", title: "Invalid npub" });
|
||||
}
|
||||
|
||||
identity.loginWithPubkey(pubkey);
|
||||
// TODO: the settings service should not be in charge of the relays
|
||||
if (!settings.relays.value.includes(relay)) {
|
||||
settings.relays.next([...settings.relays.value, relay]);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex as="form" direction="column" gap="4" onSubmit={handleSubmit}>
|
||||
<Button variant="link" onClick={() => navigate("../")}>
|
||||
Back
|
||||
</Button>
|
||||
<FormControl>
|
||||
<FormLabel>Enter user npub</FormLabel>
|
||||
<Input type="text" placeholder="npub1" isRequired value={npub} onChange={(e) => setNpub(e.target.value)} />
|
||||
<FormHelperText>
|
||||
Enter any npub you want.{" "}
|
||||
<Link isExternal href="https://nostr.directory" color="blue.500" target="_blank">
|
||||
nostr.directory
|
||||
</Link>
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<FormLabel>Bootstrap relay</FormLabel>
|
||||
<RelayUrlInput
|
||||
placeholder="wss://nostr.example.com"
|
||||
isRequired
|
||||
value={relay}
|
||||
onChange={(e) => setRelay(e.target.value)}
|
||||
/>
|
||||
<FormHelperText>The first relay to connect to.</FormHelperText>
|
||||
</FormControl>
|
||||
<Button colorScheme="brand" ml="auto" type="submit">
|
||||
Login
|
||||
</Button>
|
||||
</Flex>
|
||||
);
|
||||
};
|
21
src/views/login/start.tsx
Normal file
21
src/views/login/start.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { Button, Spinner } from "@chakra-ui/react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import identity from "../../services/identity";
|
||||
|
||||
export const LoginStartView = () => {
|
||||
const navigate = useNavigate();
|
||||
const loading = useSubject(identity.loading);
|
||||
if (loading) return <Spinner />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={() => identity.loginWithExtension()} colorScheme="brand">
|
||||
Use browser extension
|
||||
</Button>
|
||||
<Button variant="link" onClick={() => navigate("./npub")}>
|
||||
Login with npub
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,17 +1,36 @@
|
||||
import { Avatar, MenuItem } from "@chakra-ui/react";
|
||||
import { MenuIconButton, MenuIconButtonProps } from "../../../components/menu-icon-button";
|
||||
|
||||
import { ClipboardIcon, IMAGE_ICONS } from "../../../components/icons";
|
||||
import { ClipboardIcon, IMAGE_ICONS, SpyIcon } from "../../../components/icons";
|
||||
import { Bech32Prefix, normalizeToBech32 } from "../../../helpers/nip-19";
|
||||
import { useCopyToClipboard } from "react-use";
|
||||
import { truncatedId } from "../../../helpers/nostr-event";
|
||||
import identity from "../../../services/identity";
|
||||
import { useUserMetadata } from "../../../hooks/use-user-metadata";
|
||||
import { getUserDisplayName } from "../../../helpers/user-metadata";
|
||||
|
||||
export const UserProfileMenu = ({ pubkey, ...props }: { pubkey: string } & Omit<MenuIconButtonProps, "children">) => {
|
||||
const [_clipboardState, copyToClipboard] = useCopyToClipboard();
|
||||
const npub = normalizeToBech32(pubkey, Bech32Prefix.Pubkey);
|
||||
const metadata = useUserMetadata(pubkey);
|
||||
|
||||
const loginAsUser = () => {
|
||||
if (confirm(`Do you want to logout and login as ${getUserDisplayName(metadata, pubkey)}?`)) {
|
||||
identity.logout();
|
||||
identity.loginWithPubkey(pubkey);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<MenuIconButton {...props}>
|
||||
<MenuItem icon={<SpyIcon fontSize="1.5em" />} onClick={() => loginAsUser()}>
|
||||
Login as {getUserDisplayName(metadata, pubkey)}
|
||||
</MenuItem>
|
||||
{npub && (
|
||||
<MenuItem onClick={() => copyToClipboard(npub)} icon={<ClipboardIcon fontSize="1.5em" />}>
|
||||
Copy {truncatedId(npub)}
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem
|
||||
as="a"
|
||||
icon={<Avatar src={IMAGE_ICONS.nostrGuruIcon} size="xs" />}
|
||||
@ -44,11 +63,6 @@ export const UserProfileMenu = ({ pubkey, ...props }: { pubkey: string } & Omit<
|
||||
>
|
||||
Open in snort.social
|
||||
</MenuItem>
|
||||
{npub && (
|
||||
<MenuItem onClick={() => copyToClipboard(npub)} icon={<ClipboardIcon />}>
|
||||
Copy {truncatedId(npub)}
|
||||
</MenuItem>
|
||||
)}
|
||||
</MenuIconButton>
|
||||
);
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user