create simple post form

This commit is contained in:
hzrd149 2023-02-07 17:04:19 -06:00
parent 64a68bdb12
commit bba59ccf88
13 changed files with 191 additions and 22 deletions

View File

@ -38,6 +38,7 @@ const RootPage = () => (
);
const router = createBrowserRouter([
{ path: "login", element: <LoginView /> },
{
path: "/",
element: <RootPage />,
@ -78,7 +79,6 @@ const router = createBrowserRouter([
path: "profile",
element: <ProfileView />,
},
{ path: "login", element: <LoginView /> },
{
path: "",
element: <HomeView />,

View File

@ -0,0 +1,43 @@
import { Subject, Subscription } from "rxjs";
import { relayPool } from "../services/relays";
import { NostrEvent } from "../types/nostr-event";
type PostResult = { url: string; message?: string; status: boolean };
export function nostrPostAction(relays: string[], event: NostrEvent, timeout: number = 5000) {
const subject = new Subject<PostResult>();
let remaining = new Set<Subscription>();
for (const url of relays) {
const relay = relayPool.requestRelay(url);
const sub = relay.onCommandResult.subscribe((result) => {
if (result.eventId === event.id) {
subject.next({
url,
status: result.status,
message: result.message,
});
sub.unsubscribe();
remaining.delete(sub);
if (remaining.size === 0) subject.complete();
}
});
remaining.add(sub);
// send event
relay.send(["EVENT", event]);
}
setTimeout(() => {
if (remaining.size > 0) {
for (const sub of remaining) {
sub.unsubscribe();
}
subject.complete();
}
}, timeout);
return subject;
}

View File

@ -0,0 +1,48 @@
import { Button, useDisclosure } from "@chakra-ui/react";
import moment from "moment";
import { useState } from "react";
import { lastValueFrom } from "rxjs";
import { nostrPostAction } from "../classes/nostr-post-action";
import settings from "../services/settings";
import { DraftNostrEvent } from "../types/nostr-event";
import { AddIcon } from "./icons";
import { PostForm, PostFormValues } from "./post-modal/post-form";
export type InlineNewPostProps = {};
export const InlineNewPost = ({}: InlineNewPostProps) => {
const { isOpen, onClose, onOpen } = useDisclosure();
const [loading, setLoading] = useState(false);
const handlePostSubmit = async (values: PostFormValues) => {
setLoading(true);
const draft: DraftNostrEvent = {
content: values.content,
tags: [],
kind: 1,
created_at: moment().unix(),
};
if (window.nostr) {
const event = await window.nostr.signEvent(draft);
const postResults = nostrPostAction(settings.relays.value, event);
postResults.subscribe((result) => {
console.log(`Posted event to ${result.url}: ${result.message}`);
});
await lastValueFrom(postResults);
}
setLoading(false);
};
if (isOpen) {
return <PostForm onSubmit={handlePostSubmit} onCancel={onClose} loading={loading} />;
}
return (
<Button variant="outline" leftIcon={<AddIcon />} onClick={onOpen}>
New Post
</Button>
);
};

View File

@ -1,7 +1,7 @@
import React from "react";
import { Button, Container, Flex, Heading, IconButton, VStack } from "@chakra-ui/react";
import { useNavigate } from "react-router-dom";
import { GlobalIcon, HomeIcon, LogoutIcon, ProfileIcon, SettingsIcon } from "./icons";
import { HomeIcon, LogoutIcon, ProfileIcon, SettingsIcon } from "./icons";
import { ErrorBoundary } from "./error-boundary";
import { ConnectedRelays } from "./connected-relays";
@ -45,7 +45,15 @@ const DesktopLayout = ({ children }: { children: React.ReactNode }) => {
return (
<>
<Container size="lg" display="flex" gap="2" flexDirection="column" height="100vh" overflow="hidden">
<Container
size="lg"
display="flex"
gap="2"
flexDirection="column"
height="100vh"
overflow="hidden"
position="relative"
>
<ReloadPrompt />
<Flex gap="4" grow={1} overflow="hidden">
<VStack width="15rem" pt="2" alignItems="stretch" flexShrink={0}>

View File

@ -0,0 +1,20 @@
import { Modal, ModalOverlay, ModalContent, ModalHeader, ModalBody, ModalCloseButton } from "@chakra-ui/react";
import { PostForm, PostFormProps } from "./post-form";
type PostModalProps = PostFormProps & {
isOpen: boolean;
onClose: () => void;
};
export const PostModal = ({ isOpen, onClose, onSubmit, onCancel }: PostModalProps) => (
<Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader>New Post</ModalHeader>
<ModalCloseButton />
<ModalBody>
<PostForm onSubmit={onSubmit} onCancel={onCancel} />
</ModalBody>
</ModalContent>
</Modal>
);

View File

@ -0,0 +1,36 @@
import { Button, Flex, Textarea } from "@chakra-ui/react";
import { SubmitHandler, useForm } from "react-hook-form";
export type PostFormValues = {
content: string;
};
export type PostFormProps = {
onSubmit: SubmitHandler<PostFormValues>;
onCancel: () => void;
loading?: boolean;
};
export const PostForm = ({ onSubmit, onCancel, loading }: PostFormProps) => {
const {
register,
handleSubmit,
watch,
formState: { errors },
} = useForm<PostFormValues>();
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Textarea {...register("content")} mb="2" />
<Flex gap="2" justifyContent="flex-end">
<Button size="sm" onClick={onCancel} isDisabled={loading}>
Cancel
</Button>
<Button size="sm" type="submit" isLoading={loading}>
Post
</Button>
</Flex>
</form>
);
};

View File

@ -1,5 +1,4 @@
import { BehaviorSubject } from "rxjs";
import db from "./db";
import settings from "./settings";
export type SavedIdentity = {

View File

@ -29,6 +29,9 @@ export class RelayPool {
}
return relay;
}
requestRelays(urls: string[], connect = true) {
return urls.map((url) => this.requestRelay(url, connect));
}
pruneRelays() {
for (const [url, relay] of this.relays.entries()) {

View File

@ -1,5 +1,5 @@
import { Subject } from "rxjs";
import { IncomingNostrEvent, NostrEvent } from "../../types/nostr-event";
import { RawIncomingNostrEvent, NostrEvent } from "../../types/nostr-event";
import { NostrOutgoingMessage } from "../../types/nostr-query";
export type IncomingEvent = {
@ -15,6 +15,13 @@ export type IncomingEOSE = {
type: "EOSE";
subId: string;
};
// NIP-20
export type IncomingCommandResult = {
type: "OK";
eventId: string;
status: boolean;
message?: string;
};
export enum Permission {
NONE = 0,
@ -25,11 +32,12 @@ export enum Permission {
export class Relay {
url: string;
onOpen: Subject<Relay>;
onClose: Subject<Relay>;
onEvent: Subject<IncomingEvent>;
onNotice: Subject<IncomingNotice>;
onEndOfStoredEvents: Subject<IncomingEOSE>;
onOpen = new Subject<Relay>();
onClose = new Subject<Relay>();
onEvent = new Subject<IncomingEvent>();
onNotice = new Subject<IncomingNotice>();
onEndOfStoredEvents = new Subject<IncomingEOSE>();
onCommandResult = new Subject<IncomingCommandResult>();
ws?: WebSocket;
permission: Permission = Permission.ALL;
@ -37,13 +45,6 @@ export class Relay {
constructor(url: string, permission: Permission = Permission.ALL) {
this.url = url;
this.onOpen = new Subject();
this.onClose = new Subject();
this.onEvent = new Subject();
this.onNotice = new Subject();
this.onEndOfStoredEvents = new Subject();
this.permission = permission;
}
@ -118,7 +119,7 @@ export class Relay {
if (!(this.permission & Permission.READ)) return;
try {
const data: IncomingNostrEvent = JSON.parse(event.data);
const data: RawIncomingNostrEvent = JSON.parse(event.data);
const type = data[0];
switch (type) {
@ -131,6 +132,9 @@ export class Relay {
case "EOSE":
this.onEndOfStoredEvents.next({ type, subId: data[1] });
break;
case "OK":
this.onCommandResult.next({ type, eventId: data[1], status: data[2], message: data[3] });
break;
}
} catch (e) {
console.log(`Relay: Failed to parse event from ${this.url}`);

View File

@ -18,7 +18,13 @@ export type NostrEvent = {
sig: string;
};
export type IncomingNostrEvent = ["EVENT", string, NostrEvent] | ["NOTICE", string] | ["EOSE", string];
export type DraftNostrEvent = Omit<NostrEvent, "pubkey" | "id" | "sig">;
export type RawIncomingEvent = ["EVENT", string, NostrEvent];
export type RawIncomingNotice = ["NOTICE", string];
export type RawIncomingEOSE = ["EOSE", string];
export type RawIncomingCommandResult = ["OK", string, boolean, string];
export type RawIncomingNostrEvent = RawIncomingEvent | RawIncomingNotice | RawIncomingEOSE | RawIncomingCommandResult;
export type Kind0ParsedContent = {
name?: string;

View File

@ -1,11 +1,11 @@
import { NostrEvent } from "./nostr-event";
import { DraftNostrEvent, NostrEvent } from "./nostr-event";
declare global {
interface Window {
nostr?: {
enabled: boolean;
getPublicKey: () => Promise<string> | string;
signEvent: (event: NostrEvent) => Promise<NostrEvent> | NostrEvent;
signEvent: (event: DraftNostrEvent) => Promise<NostrEvent> | NostrEvent;
getRelays: () => Record<string, { read: boolean; write: boolean }> | string[];
nip04?: {
encrypt: (pubkey: string, plaintext: string) => Promise<string> | string;

View File

@ -8,6 +8,7 @@ import { useTimelineLoader } from "../../hooks/use-timeline-loader";
import { useUserContacts } from "../../hooks/use-user-contacts";
import identity from "../../services/identity";
import settings from "../../services/settings";
import { InlineNewPost } from "../../components/inline-new-post";
export const FollowingTab = () => {
const pubkey = useSubject(identity.pubkey);
@ -31,6 +32,7 @@ export const FollowingTab = () => {
return (
<Flex direction="column" gap="2">
<InlineNewPost />
<FormControl display="flex" alignItems="center">
<FormLabel htmlFor="show-replies" mb="0">
Show Replies

View File

@ -9,7 +9,7 @@ export const LoginView = () => {
const location = useLocation();
if (loading) return <Spinner />;
if (setup) return <Navigate to={location.state.from} replace />;
if (setup) return <Navigate to={location.state?.from ?? "/"} replace />;
return (
<Flex direction="column" alignItems="center" justifyContent="center">