mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-03-17 21:31:43 +01:00
create simple post form
This commit is contained in:
parent
64a68bdb12
commit
bba59ccf88
@ -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 />,
|
||||
|
43
src/classes/nostr-post-action.ts
Normal file
43
src/classes/nostr-post-action.ts
Normal 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;
|
||||
}
|
48
src/components/inline-new-post.tsx
Normal file
48
src/components/inline-new-post.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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}>
|
||||
|
20
src/components/post-modal/index.tsx
Normal file
20
src/components/post-modal/index.tsx
Normal 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>
|
||||
);
|
36
src/components/post-modal/post-form.tsx
Normal file
36
src/components/post-modal/post-form.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -1,5 +1,4 @@
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import db from "./db";
|
||||
import settings from "./settings";
|
||||
|
||||
export type SavedIdentity = {
|
||||
|
@ -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()) {
|
||||
|
@ -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}`);
|
||||
|
@ -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;
|
||||
|
4
src/types/nostr-extensions.d.ts
vendored
4
src/types/nostr-extensions.d.ts
vendored
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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">
|
||||
|
Loading…
x
Reference in New Issue
Block a user