mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-04-03 01:18:47 +02:00
add new post modal
add reply button cleanup helpers
This commit is contained in:
parent
f69a120c22
commit
33d57ddc71
src
classes
components
helpers
providers
services
views/home
@ -2,7 +2,7 @@ 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 type PostResult = { url: string; message?: string; status: boolean };
|
||||
|
||||
export function nostrPostAction(relays: string[], event: NostrEvent, timeout: number = 5000) {
|
||||
const subject = new Subject<PostResult>();
|
||||
|
@ -133,3 +133,9 @@ export const ShareIcon = createIcon({
|
||||
d: "M13.12 17.023l-4.199-2.29a4 4 0 1 1 0-5.465l4.2-2.29a4 4 0 1 1 .959 1.755l-4.2 2.29a4.008 4.008 0 0 1 0 1.954l4.199 2.29a4 4 0 1 1-.959 1.755zM6 14a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm11-6a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm0 12a2 2 0 1 0 0-4 2 2 0 0 0 0 4z",
|
||||
defaultProps,
|
||||
});
|
||||
|
||||
export const ReplyIcon = createIcon({
|
||||
displayName: "reply-icon",
|
||||
d: "M11 20L1 12l10-8v5c5.523 0 10 4.477 10 10 0 .273-.01.543-.032.81-1.463-2.774-4.33-4.691-7.655-4.805L13 15h-2v5zm-2-7h4.034l.347.007c1.285.043 2.524.31 3.676.766C15.59 12.075 13.42 11 11 11H9V8.161L4.202 12 9 15.839V13z",
|
||||
defaultProps,
|
||||
});
|
||||
|
@ -1,48 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
@ -11,7 +11,7 @@ export const NoteLink = ({ noteId, ...props }: NoteLinkProps) => {
|
||||
const note1 = normalizeToBech32(noteId, Bech32Prefix.Note) ?? noteId;
|
||||
|
||||
return (
|
||||
<Link as={RouterLink} to={`/n/${note1}`} {...props}>
|
||||
<Link as={RouterLink} to={`/n/${note1}`} color="blue.500" {...props}>
|
||||
{truncatedId(note1)}
|
||||
</Link>
|
||||
);
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import React, { useContext } from "react";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import moment from "moment";
|
||||
import { Box, Card, CardBody, CardFooter, CardHeader, Flex, Heading, Link } from "@chakra-ui/react";
|
||||
import { Box, Card, CardBody, CardFooter, CardHeader, Flex, Heading, IconButton, Link } from "@chakra-ui/react";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import { UserAvatarLink } from "../user-avatar-link";
|
||||
import { Bech32Prefix, normalizeToBech32 } from "../../helpers/nip-19";
|
||||
@ -15,20 +15,26 @@ import { UserTipButton } from "../user-tip-button";
|
||||
import { NoteRelays } from "./note-relays";
|
||||
import { useIsMobile } from "../../hooks/use-is-mobile";
|
||||
import { UserLink } from "../user-link";
|
||||
import { ReplyIcon } from "../icons";
|
||||
import { PostModalContext } from "../../providers/post-modal-provider";
|
||||
import { buildReply } from "../../helpers/nostr-event";
|
||||
|
||||
export type NoteProps = {
|
||||
event: NostrEvent;
|
||||
};
|
||||
export const Note = React.memo(({ event }: NoteProps) => {
|
||||
const isMobile = useIsMobile();
|
||||
const { openModal } = useContext(PostModalContext);
|
||||
|
||||
const pubkey = useSubject(identity.pubkey);
|
||||
const contacts = useUserContacts(pubkey);
|
||||
const following = contacts?.contacts || [];
|
||||
|
||||
const reply = () => openModal(buildReply(event));
|
||||
|
||||
return (
|
||||
<Card padding="2" variant="outline">
|
||||
<CardHeader padding="0" mb="2">
|
||||
<Card variant="outline">
|
||||
<CardHeader padding="2" mb="2">
|
||||
<Flex flex="1" gap="2" alignItems="center" wrap="wrap">
|
||||
<UserAvatarLink pubkey={event.pubkey} size={isMobile ? "xs" : "sm"} />
|
||||
|
||||
@ -40,12 +46,12 @@ export const Note = React.memo(({ event }: NoteProps) => {
|
||||
</Link>
|
||||
</Flex>
|
||||
</CardHeader>
|
||||
<CardBody padding="0">
|
||||
<Box overflow="hidden" width="100%">
|
||||
<NoteContents event={event} trusted={following.includes(event.pubkey)} />
|
||||
</Box>
|
||||
<CardBody px="2" py="0">
|
||||
<NoteContents event={event} trusted={following.includes(event.pubkey)} />
|
||||
</CardBody>
|
||||
<CardFooter padding="0" display="flex" gap="2" justifyContent="flex-end">
|
||||
<CardFooter padding="2" display="flex" gap="2">
|
||||
<IconButton icon={<ReplyIcon />} title="Reply" aria-label="Reply" onClick={reply} size="xs" />
|
||||
<Box flexGrow={1} />
|
||||
<UserTipButton pubkey={event.pubkey} size="xs" />
|
||||
<NoteRelays event={event} size="xs" />
|
||||
<NoteMenu event={event} />
|
||||
|
@ -21,7 +21,11 @@ import settings from "../../services/settings";
|
||||
|
||||
const BlurredImage = (props: ImageProps) => {
|
||||
const { isOpen, onToggle } = useDisclosure();
|
||||
return <Image onClick={onToggle} cursor="pointer" filter={isOpen ? "" : "blur(1.5rem)"} {...props} />;
|
||||
return (
|
||||
<Box overflow="hidden">
|
||||
<Image onClick={onToggle} cursor="pointer" filter={isOpen ? "" : "blur(1.5rem)"} {...props} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
type EmbedType = {
|
||||
|
@ -9,6 +9,7 @@ import { useIsMobile } from "../hooks/use-is-mobile";
|
||||
import identity from "../services/identity";
|
||||
import { FollowingList } from "./following-list";
|
||||
import { ReloadPrompt } from "./reload-prompt";
|
||||
import { PostModalProvider } from "../providers/post-modal-provider";
|
||||
|
||||
export const Page = ({ children }: { children: React.ReactNode }) => {
|
||||
const navigate = useNavigate();
|
||||
@ -19,7 +20,9 @@ export const Page = ({ children }: { children: React.ReactNode }) => {
|
||||
<Flex direction="column" height="100%">
|
||||
<ReloadPrompt />
|
||||
<Flex flexGrow={1} direction="column" overflow="hidden">
|
||||
{children}
|
||||
<ErrorBoundary>
|
||||
<PostModalProvider>{children}</PostModalProvider>
|
||||
</ErrorBoundary>
|
||||
</Flex>
|
||||
<Flex flexShrink={0} gap="2" padding="2">
|
||||
<IconButton icon={<FeedIcon />} aria-label="Home" onClick={() => navigate("/")} flexGrow="1" size="lg" />
|
||||
@ -72,7 +75,9 @@ export const Page = ({ children }: { children: React.ReactNode }) => {
|
||||
<ConnectedRelays />
|
||||
</VStack>
|
||||
<Flex flexGrow={1} direction="column" overflow="hidden">
|
||||
<ErrorBoundary>{children}</ErrorBoundary>
|
||||
<ErrorBoundary>
|
||||
<PostModalProvider>{children}</PostModalProvider>
|
||||
</ErrorBoundary>
|
||||
</Flex>
|
||||
<VStack width="15rem" pt="2" alignItems="stretch" flexShrink={0}>
|
||||
<Heading size="md">Following</Heading>
|
||||
|
@ -1,20 +1,111 @@
|
||||
import { Modal, ModalOverlay, ModalContent, ModalHeader, ModalBody, ModalCloseButton } from "@chakra-ui/react";
|
||||
import { PostForm, PostFormProps } from "./post-form";
|
||||
import {
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
Flex,
|
||||
Button,
|
||||
Textarea,
|
||||
Text,
|
||||
} from "@chakra-ui/react";
|
||||
import moment from "moment";
|
||||
import React, { useState } from "react";
|
||||
import { useList } from "react-use";
|
||||
import { nostrPostAction, PostResult } from "../../classes/nostr-post-action";
|
||||
import { getReferences } from "../../helpers/nostr-event";
|
||||
import { useIsMobile } from "../../hooks/use-is-mobile";
|
||||
import settings from "../../services/settings";
|
||||
import { DraftNostrEvent, NostrEvent } from "../../types/nostr-event";
|
||||
import { NoteLink } from "../note-link";
|
||||
import { PostResults } from "./post-results";
|
||||
|
||||
type PostModalProps = PostFormProps & {
|
||||
function emptyDraft(): DraftNostrEvent {
|
||||
return {
|
||||
content: "",
|
||||
kind: 1,
|
||||
tags: [],
|
||||
created_at: moment().unix(),
|
||||
};
|
||||
}
|
||||
|
||||
type PostModalProps = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
initialDraft?: Partial<DraftNostrEvent>;
|
||||
};
|
||||
|
||||
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>
|
||||
);
|
||||
export const PostModal = ({ isOpen, onClose, initialDraft }: PostModalProps) => {
|
||||
const isMobile = useIsMobile();
|
||||
const pad = isMobile ? "2" : "4";
|
||||
|
||||
const [waiting, setWaiting] = useState(false);
|
||||
const [signedEvent, setSignedEvent] = useState<NostrEvent | null>(null);
|
||||
const [results, resultsActions] = useList<PostResult>();
|
||||
const [draft, setDraft] = useState<DraftNostrEvent>(() => Object.assign(emptyDraft(), initialDraft));
|
||||
|
||||
const handleContentChange: React.ChangeEventHandler<HTMLTextAreaElement> = (event) => {
|
||||
setDraft((d) => ({ ...d, content: event.target.value }));
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (window.nostr) {
|
||||
setWaiting(true);
|
||||
const updatedDraft: DraftNostrEvent = { ...draft, created_at: moment().unix() };
|
||||
const event = await window.nostr.signEvent(updatedDraft);
|
||||
setWaiting(false);
|
||||
setSignedEvent(event);
|
||||
|
||||
const postResults = nostrPostAction(settings.relays.value, event);
|
||||
|
||||
postResults.subscribe({
|
||||
next(result) {
|
||||
resultsActions.push(result);
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const refs = getReferences(draft);
|
||||
|
||||
const canSubmit = draft.content.length > 0;
|
||||
|
||||
const renderContent = () => {
|
||||
if (signedEvent) {
|
||||
return (
|
||||
<ModalBody padding="4">
|
||||
<PostResults event={signedEvent} results={results} onClose={onClose} />
|
||||
</ModalBody>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<ModalBody pr={pad} pl={pad}>
|
||||
{refs.replyId && (
|
||||
<Text mb="2">
|
||||
Replying to: <NoteLink noteId={refs.replyId} />
|
||||
</Text>
|
||||
)}
|
||||
<Textarea autoFocus mb="2" value={draft.content} onChange={handleContentChange} rows={5} />
|
||||
</ModalBody>
|
||||
<ModalFooter pr={pad} pl={pad} pb={pad} pt="0">
|
||||
<Flex gap="2" alignItems="center">
|
||||
<Button onClick={onClose} isDisabled={waiting} ml="auto">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button colorScheme="blue" type="submit" isLoading={waiting} onClick={handleSubmit} isDisabled={!canSubmit}>
|
||||
Post
|
||||
</Button>
|
||||
</Flex>
|
||||
</ModalFooter>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} size="4xl">
|
||||
<ModalOverlay />
|
||||
<ModalContent>{renderContent()}</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
@ -1,36 +0,0 @@
|
||||
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")} autoFocus mb="2" />
|
||||
|
||||
<Flex gap="2" justifyContent="flex-end">
|
||||
<Button size="sm" onClick={onCancel} isDisabled={loading}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button colorScheme="brand" size="sm" type="submit" isLoading={loading}>
|
||||
Post
|
||||
</Button>
|
||||
</Flex>
|
||||
</form>
|
||||
);
|
||||
};
|
40
src/components/post-modal/post-results.tsx
Normal file
40
src/components/post-modal/post-results.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import { Alert, AlertDescription, AlertIcon, AlertTitle, Box, Button, Flex, Heading } from "@chakra-ui/react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { PostResult } from "../../classes/nostr-post-action";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
|
||||
export type PostResultsProps = {
|
||||
event: NostrEvent;
|
||||
results: PostResult[];
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export const PostResults = ({ event, results, onClose }: PostResultsProps) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const viewPost = () => {
|
||||
onClose();
|
||||
navigate(`/n/${event.id}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex direction="column" gap="2">
|
||||
<Heading size="md">Posted to relays:</Heading>
|
||||
{results.map((result) => (
|
||||
<Alert key={result.url} status={result.status ? "success" : "warning"}>
|
||||
<AlertIcon />
|
||||
<Box>
|
||||
<AlertTitle>{result.url}</AlertTitle>
|
||||
{result.message && <AlertDescription>{result.message}</AlertDescription>}
|
||||
</Box>
|
||||
</Alert>
|
||||
))}
|
||||
<Flex gap="4" ml="auto">
|
||||
<Button onClick={viewPost} variant="link">
|
||||
View Post
|
||||
</Button>
|
||||
<Button onClick={onClose}>Done</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
@ -1,8 +0,0 @@
|
||||
export function debounce<T>(func: T, timeout = 300) {
|
||||
let timer: number | undefined;
|
||||
return (...args: any[]) => {
|
||||
clearTimeout(timer);
|
||||
// @ts-ignore
|
||||
timer = setTimeout(() => func(args), timeout);
|
||||
};
|
||||
}
|
@ -1,10 +1,12 @@
|
||||
import { isETag, isPTag, NostrEvent } from "../types/nostr-event";
|
||||
import moment from "moment";
|
||||
import { getEventRelays } from "../services/event-relays";
|
||||
import { DraftNostrEvent, isETag, isPTag, NostrEvent } from "../types/nostr-event";
|
||||
|
||||
export function isReply(event: NostrEvent) {
|
||||
export function isReply(event: NostrEvent | DraftNostrEvent) {
|
||||
return !!event.tags.find((tag) => isETag(tag) && tag[3] !== "mention");
|
||||
}
|
||||
|
||||
export function isNote(event: NostrEvent) {
|
||||
export function isNote(event: NostrEvent | DraftNostrEvent) {
|
||||
return !isReply(event);
|
||||
}
|
||||
|
||||
@ -13,7 +15,7 @@ export function truncatedId(id: string) {
|
||||
}
|
||||
|
||||
export type EventReferences = ReturnType<typeof getReferences>;
|
||||
export function getReferences(event: NostrEvent) {
|
||||
export function getReferences(event: NostrEvent | DraftNostrEvent) {
|
||||
const eTags = event.tags.filter(isETag);
|
||||
const pTags = event.tags.filter(isPTag);
|
||||
|
||||
@ -51,3 +53,33 @@ export function getReferences(event: NostrEvent) {
|
||||
replyId,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildReply(event: NostrEvent): DraftNostrEvent {
|
||||
const refs = getReferences(event);
|
||||
const relay = getEventRelays(event.id).getValue()[0];
|
||||
|
||||
const tags: NostrEvent["tags"] = [];
|
||||
|
||||
const rootId = refs.rootId ?? event.id;
|
||||
const replyId = event.id;
|
||||
|
||||
tags.push(["e", rootId, relay, "root"]);
|
||||
if (replyId !== rootId) {
|
||||
tags.push(["e", replyId, relay, "reply"]);
|
||||
}
|
||||
// add all ptags
|
||||
// TODO: omit my own pubkey
|
||||
const ptags = event.tags.filter(isPTag);
|
||||
tags.push(...ptags);
|
||||
if (!ptags.find((t) => t[1] === event.pubkey)) {
|
||||
tags.push(["p", event.pubkey]);
|
||||
}
|
||||
|
||||
return {
|
||||
kind: 1,
|
||||
// TODO: be smarter about picking relay
|
||||
tags,
|
||||
content: "",
|
||||
created_at: moment().unix(),
|
||||
};
|
||||
}
|
||||
|
32
src/providers/post-modal-provider.tsx
Normal file
32
src/providers/post-modal-provider.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import React, { useCallback, useMemo, useState } from "react";
|
||||
import { ErrorBoundary } from "../components/error-boundary";
|
||||
import { PostModal } from "../components/post-modal";
|
||||
import { DraftNostrEvent } from "../types/nostr-event";
|
||||
|
||||
export type PostModalContextType = {
|
||||
openModal: (draft?: Partial<DraftNostrEvent>) => void;
|
||||
};
|
||||
|
||||
export const PostModalContext = React.createContext<PostModalContextType>({
|
||||
openModal: () => {},
|
||||
});
|
||||
|
||||
export const PostModalProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [draft, setDraft] = useState<Partial<DraftNostrEvent> | undefined>(undefined);
|
||||
const openModal = useCallback(
|
||||
(draft?: Partial<DraftNostrEvent>) => {
|
||||
setDraft(draft);
|
||||
setOpen(true);
|
||||
},
|
||||
[setDraft, setOpen]
|
||||
);
|
||||
const context = useMemo(() => ({ openModal }), [openModal]);
|
||||
|
||||
return (
|
||||
<PostModalContext.Provider value={context}>
|
||||
{open && <PostModal isOpen initialDraft={draft} onClose={() => setOpen(false)} />}
|
||||
{children}
|
||||
</PostModalContext.Provider>
|
||||
);
|
||||
};
|
@ -22,28 +22,8 @@ const MIGRATIONS: MigrationFunction[] = [
|
||||
contacts.createIndex("created_at", "created_at");
|
||||
contacts.createIndex("contacts", "contacts", { multiEntry: true });
|
||||
|
||||
const events = db.createObjectStore("text-events", {
|
||||
keyPath: "id",
|
||||
autoIncrement: false,
|
||||
});
|
||||
events.createIndex("pubkey", "pubkey");
|
||||
events.createIndex("created_at", "created_at");
|
||||
events.createIndex("kind", "kind");
|
||||
|
||||
db.createObjectStore("identicon");
|
||||
|
||||
// setup data
|
||||
const settings = db.createObjectStore("settings");
|
||||
settings.put(
|
||||
[
|
||||
"wss://relay.damus.io",
|
||||
"wss://nostr-pub.wellorder.net",
|
||||
"wss://nostr.zebedee.cloud",
|
||||
"wss://satstacker.cloud",
|
||||
"wss://brb.io",
|
||||
],
|
||||
"relays"
|
||||
);
|
||||
},
|
||||
];
|
||||
|
||||
@ -64,8 +44,8 @@ const db = await openDB<CustomSchema>("storage", version, {
|
||||
export async function clearData() {
|
||||
await db.clear("user-metadata");
|
||||
await db.clear("user-contacts");
|
||||
await db.clear("text-events");
|
||||
await db.clear("identicon");
|
||||
await db.clear("settings");
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
|
@ -20,15 +20,6 @@ export interface CustomSchema extends DBSchema {
|
||||
};
|
||||
indexes: { created_at: number; contacts: string };
|
||||
};
|
||||
"text-events": {
|
||||
key: string;
|
||||
value: NostrEvent;
|
||||
indexes: { created_at: number; pubkey: string; kind: number };
|
||||
};
|
||||
identicon: {
|
||||
key: string;
|
||||
value: string;
|
||||
};
|
||||
settings: {
|
||||
key: string;
|
||||
value: any;
|
||||
|
@ -1,13 +1,8 @@
|
||||
import Identicon from "identicon.js";
|
||||
import db from "./db";
|
||||
|
||||
export async function getIdenticon(pubkey: string) {
|
||||
if (pubkey.length < 15) return "";
|
||||
|
||||
// const cached = await db.get("identicon", pubkey);
|
||||
// if (cached) return cached;
|
||||
|
||||
const svg = new Identicon(pubkey, { format: "svg" }).toString();
|
||||
await db.put("identicon", svg, pubkey);
|
||||
return svg;
|
||||
}
|
||||
|
@ -1,6 +1,9 @@
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { unique } from "../helpers/array";
|
||||
import settings from "./settings";
|
||||
|
||||
export type PresetRelays = Record<string, { read: boolean; write: boolean }>;
|
||||
|
||||
export type SavedIdentity = {
|
||||
pubkey: string;
|
||||
secKey?: string;
|
||||
@ -11,6 +14,8 @@ class IdentityService {
|
||||
loading = new BehaviorSubject(true);
|
||||
setup = new BehaviorSubject(false);
|
||||
pubkey = new BehaviorSubject("");
|
||||
// TODO: remove this when there is a service to manage user relays
|
||||
relays = new BehaviorSubject<PresetRelays>({});
|
||||
private useExtension: boolean = false;
|
||||
private secKey: string | undefined = undefined;
|
||||
|
||||
@ -38,6 +43,12 @@ class IdentityService {
|
||||
pubkey,
|
||||
useExtension: true,
|
||||
});
|
||||
|
||||
// disabled because I dont want to load the preset relays yet (ably dose not support changing them)
|
||||
// const relays = await window.nostr.getRelays();
|
||||
// if (Array.isArray(relays)) {
|
||||
// this.relays.next(relays.reduce<PresetRelays>((d, r) => ({ ...d, [r]: { read: true, write: true } }), {}));
|
||||
// } else this.relays.next(relays);
|
||||
}
|
||||
}
|
||||
|
||||
@ -61,4 +72,9 @@ if (import.meta.env.DEV) {
|
||||
window.identity = identity;
|
||||
}
|
||||
|
||||
// TODO: create a service to manage user relays (download latest and handle conflicts)
|
||||
identity.relays.subscribe((presetRelays) => {
|
||||
settings.relays.next(unique([...settings.relays.value, ...Object.keys(presetRelays)]));
|
||||
});
|
||||
|
||||
export default identity;
|
||||
|
@ -8,11 +8,14 @@ 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";
|
||||
import { AddIcon } from "@chakra-ui/icons";
|
||||
import { useContext } from "react";
|
||||
import { PostModalContext } from "../../providers/post-modal-provider";
|
||||
|
||||
export const FollowingTab = () => {
|
||||
const pubkey = useSubject(identity.pubkey);
|
||||
const relays = useSubject(settings.relays);
|
||||
const { openModal } = useContext(PostModalContext);
|
||||
const contacts = useUserContacts(pubkey);
|
||||
const [search, setSearch] = useSearchParams();
|
||||
const showReplies = search.has("replies");
|
||||
@ -32,7 +35,9 @@ export const FollowingTab = () => {
|
||||
|
||||
return (
|
||||
<Flex direction="column" gap="2">
|
||||
<InlineNewPost />
|
||||
<Button variant="outline" leftIcon={<AddIcon />} onClick={() => openModal()}>
|
||||
New Post
|
||||
</Button>
|
||||
<FormControl display="flex" alignItems="center">
|
||||
<FormLabel htmlFor="show-replies" mb="0">
|
||||
Show Replies
|
||||
|
Loading…
x
Reference in New Issue
Block a user