Skip to content

Commit

Permalink
Merge branch 'next'
Browse files Browse the repository at this point in the history
  • Loading branch information
hzrd149 committed Nov 14, 2023
2 parents e740b6f + 127b3d1 commit 61fee4e
Show file tree
Hide file tree
Showing 127 changed files with 2,117 additions and 1,278 deletions.
5 changes: 5 additions & 0 deletions .changeset/brave-squids-exercise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"nostrudel": minor
---

Add "DM Feed" tool
5 changes: 5 additions & 0 deletions .changeset/cold-seals-rhyme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"nostrudel": minor
---

Thread view improvements
5 changes: 5 additions & 0 deletions .changeset/new-cobras-draw.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"nostrudel": minor
---

Add option to search communities in search view
5 changes: 5 additions & 0 deletions .changeset/ninety-ligers-move.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"nostrudel": minor
---

Add "create $prism" link to lists
5 changes: 5 additions & 0 deletions .changeset/smart-turkeys-tickle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"nostrudel": minor
---

Add people list to search and hashtag views
5 changes: 5 additions & 0 deletions .changeset/smooth-chairs-bow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"nostrudel": patch
---

Fix link cards breaking lines
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
18
20
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
The MIT License (MIT)

Copyright (c) 2023 Talha Buğra Bulut
Copyright (c) 2023 hzrd149

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
2 changes: 1 addition & 1 deletion dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# syntax=docker/dockerfile:1
FROM node:18
FROM node:20
WORKDIR /app
COPY . /app/
ENV VITE_COMMIT_HASH=""
Expand Down
2 changes: 2 additions & 0 deletions src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ import RelayView from "./views/relays/relay";
import RelayReviewsView from "./views/relays/reviews";
import PopularRelaysView from "./views/relays/popular";
import UserDMsTab from "./views/user/dms";
import DMFeedView from "./views/tools/dm-feed";
const UserTracksTab = lazy(() => import("./views/user/tracks"));

const ToolsHomeView = lazy(() => import("./views/tools"));
Expand Down Expand Up @@ -230,6 +231,7 @@ const router = createHashRouter([
{ path: "network", element: <NetworkView /> },
{ path: "network-mute-graph", element: <NetworkMuteGraphView /> },
{ path: "network-dm-graph", element: <NetworkDMGraphView /> },
{ path: "dm-feed", element: <DMFeedView /> },
],
},
{
Expand Down
121 changes: 121 additions & 0 deletions src/components/chat-windows/chat-window.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { useState } from "react";
import { Button, Card, CardBody, CardHeader, CloseButton, Flex, Heading, IconButton, useToast } from "@chakra-ui/react";
import { Kind } from "nostr-tools";
import dayjs from "dayjs";
import { useForm } from "react-hook-form";

import { ChevronDownIcon, ChevronUpIcon } from "../icons";
import UserName from "../user-name";
import MagicTextArea from "../magic-textarea";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import useCurrentAccount from "../../hooks/use-current-account";
import { useReadRelayUrls, useWriteRelayUrls } from "../../hooks/use-client-relays";
import { useUserRelays } from "../../hooks/use-user-relays";
import { RelayMode } from "../../classes/relay";
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
import IntersectionObserverProvider from "../../providers/intersection-observer";
import useSubject from "../../hooks/use-subject";
import Message from "../../views/messages/message";
import { LightboxProvider } from "../lightbox-provider";
import { useSigningContext } from "../../providers/signing-provider";
import { DraftNostrEvent } from "../../types/nostr-event";
import NostrPublishAction from "../../classes/nostr-publish-action";
import { correctContentMentions, createEmojiTags } from "../../helpers/nostr/post";
import { useContextEmojis } from "../../providers/emoji-provider";

export default function ChatWindow({ pubkey, onClose }: { pubkey: string; onClose: () => void }) {
const toast = useToast();
const account = useCurrentAccount()!;
const emojis = useContextEmojis();
const [expanded, setExpanded] = useState(true);

const usersRelays = useUserRelays(pubkey);
const readRelays = useReadRelayUrls(usersRelays.filter((c) => c.mode & RelayMode.WRITE).map((c) => c.url));
const writeRelays = useWriteRelayUrls(usersRelays.filter((c) => c.mode & RelayMode.WRITE).map((c) => c.url));

const timeline = useTimelineLoader(`${pubkey}-${account.pubkey}-messages`, readRelays, [
{ authors: [account.pubkey], kinds: [Kind.EncryptedDirectMessage], "#p": [pubkey] },
{ authors: [pubkey], kinds: [Kind.EncryptedDirectMessage], "#p": [account.pubkey] },
]);

const { handleSubmit, getValues, setValue, formState, watch, reset } = useForm({ defaultValues: { content: "" } });
watch("content");
const { requestSignature, requestEncrypt } = useSigningContext();
const submit = handleSubmit(async (values) => {
try {
if (!values.content) return;
let draft: DraftNostrEvent = {
kind: Kind.EncryptedDirectMessage,
content: values.content,
tags: [["p", pubkey]],
created_at: dayjs().unix(),
};

draft = createEmojiTags(draft, emojis);
draft.content = correctContentMentions(draft.content);

// encrypt content
draft.content = await requestEncrypt(draft.content, pubkey);

const signed = await requestSignature(draft);
const pub = new NostrPublishAction("Send DM", writeRelays, signed);

reset();
} catch (e) {
if (e instanceof Error) toast({ status: "error", description: e.message });
}
});

const messages = useSubject(timeline.timeline);
const callback = useTimelineCurserIntersectionCallback(timeline);

return (
<Card size="sm" borderRadius="md" w={expanded ? "md" : "xs"} variant="outline">
<CardHeader display="flex" gap="2" alignItems="center">
<Heading size="md" mr="8">
<UserName pubkey={pubkey} />
</Heading>
<IconButton
aria-label="Toggle Window"
onClick={() => setExpanded((v) => !v)}
variant="ghost"
icon={expanded ? <ChevronDownIcon /> : <ChevronUpIcon />}
ml="auto"
size="sm"
/>
<CloseButton onClick={onClose} />
</CardHeader>
{expanded && (
<>
<CardBody
maxH="lg"
overflowX="hidden"
overflowY="auto"
pt="0"
display="flex"
flexDirection="column-reverse"
gap="2"
>
<LightboxProvider>
<IntersectionObserverProvider callback={callback}>
{messages.map((event) => (
<Message key={event.id} event={event} />
))}
</IntersectionObserverProvider>
</LightboxProvider>
</CardBody>
<Flex as="form" onSubmit={submit} gap="2">
<MagicTextArea
isRequired
value={getValues().content}
onChange={(e) => setValue("content", e.target.value, { shouldDirty: true })}
/>
<Button type="submit" isLoading={formState.isSubmitting}>
Send
</Button>
</Flex>
</>
)}
</Card>
);
}
90 changes: 90 additions & 0 deletions src/components/chat-windows/contacts-window.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { useEffect, useMemo, useState } from "react";
import {
Alert,
AlertIcon,
Button,
Card,
CardBody,
CardHeader,
CloseButton,
Heading,
IconButton,
Input,
InputGroup,
InputLeftElement,
} from "@chakra-ui/react";
import dayjs from "dayjs";

import { ChevronDownIcon, ChevronUpIcon, SearchIcon } from "../icons";
import useSubject from "../../hooks/use-subject";
import directMessagesService from "../../services/direct-messages";
import UserAvatar from "../user-avatar";
import UserName from "../user-name";

export default function ContactsWindow({
onClose,
onSelectPubkey,
}: {
onClose: () => void;
onSelectPubkey: (pubkey: string) => void;
}) {
const [expanded, setExpanded] = useState(true);

// TODO: find a better way to load recent contacts
const [from, setFrom] = useState(() => dayjs().subtract(2, "days"));
const conversations = useSubject(directMessagesService.conversations);
useEffect(() => directMessagesService.loadDateRange(from), [from]);
const sortedConversations = useMemo(() => {
return Array.from(conversations).sort((a, b) => {
const latestA = directMessagesService.getUserMessages(a).value[0]?.created_at ?? 0;
const latestB = directMessagesService.getUserMessages(b).value[0]?.created_at ?? 0;

return latestB - latestA;
});
}, [conversations]);

return (
<Card size="sm" borderRadius="md" minW={expanded ? "sm" : 0}>
<CardHeader display="flex" gap="2" alignItems="center">
<Heading size="md" mr="8">
Contacts
</Heading>
<IconButton
aria-label="Toggle Window"
onClick={() => setExpanded((v) => !v)}
variant="ghost"
icon={expanded ? <ChevronDownIcon /> : <ChevronUpIcon />}
ml="auto"
size="sm"
/>
<CloseButton onClick={onClose} />
</CardHeader>
{expanded && (
<CardBody maxH="lg" overflowX="hidden" overflowY="auto" pt="0" display="flex" flexDirection="column" gap="2">
<Alert status="warning">
<AlertIcon />
Work in progress!
</Alert>
{/* <InputGroup>
<InputLeftElement pointerEvents="none">
<SearchIcon />
</InputLeftElement>
<Input autoFocus />
</InputGroup> */}
{sortedConversations.map((pubkey) => (
<Button
key={pubkey}
leftIcon={<UserAvatar pubkey={pubkey} size="sm" />}
justifyContent="flex-start"
p="2"
variant="ghost"
onClick={() => onSelectPubkey(pubkey)}
>
<UserName pubkey={pubkey} />
</Button>
))}
</CardBody>
)}
</Card>
);
}
56 changes: 56 additions & 0 deletions src/components/chat-windows/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Flex, IconButton } from "@chakra-ui/react";
import { useCallback, useState } from "react";
import { useLocalStorage } from "react-use";

import ContactsWindow from "./contacts-window";
import { DirectMessagesIcon } from "../icons";
import ChatWindow from "./chat-window";
import useCurrentAccount from "../../hooks/use-current-account";

export default function ChatWindows() {
const account = useCurrentAccount();
const [pubkeys, setPubkeys] = useState<string[]>([]);
const [show, setShow] = useLocalStorage("show-chat-windows", false);

const openPubkey = useCallback(
(pubkey: string) => {
setPubkeys((keys) => (keys.includes(pubkey) ? keys : keys.concat(pubkey)));
},
[setPubkeys],
);

const closePubkey = useCallback(
(pubkey: string) => {
setPubkeys((keys) => keys.filter((key) => key !== pubkey));
},
[setPubkeys],
);

if (!account) {
return null;
}

if (!show) {
return (
<IconButton
icon={<DirectMessagesIcon boxSize={6} />}
aria-label="Show Contacts"
onClick={() => setShow(true)}
position="fixed"
bottom="0"
right="0"
size="lg"
zIndex={1}
/>
);
}

return (
<Flex direction="row-reverse" position="fixed" bottom="0" right="0" gap="4" alignItems="flex-end" zIndex={1}>
<ContactsWindow onClose={() => setShow(false)} onSelectPubkey={openPubkey} />
{pubkeys.map((pubkey) => (
<ChatWindow key={pubkey} pubkey={pubkey} onClose={() => closePubkey(pubkey)} />
))}
</Flex>
);
}
41 changes: 41 additions & 0 deletions src/components/embed-event/event-types/embedded-dm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Card, CardBody, CardHeader, CardProps, LinkBox, Spacer, Text } from "@chakra-ui/react";

import { NostrEvent } from "../../../types/nostr-event";
import { TrustProvider } from "../../../providers/trust";
import UserAvatarLink from "../../user-avatar-link";
import { UserLink } from "../../user-link";
import Timestamp from "../../timestamp";
import DecryptPlaceholder from "../../../views/messages/decrypt-placeholder";
import { MessageContent } from "../../../views/messages/message";
import { getMessageRecipient } from "../../../services/direct-messages";
import useCurrentAccount from "../../../hooks/use-current-account";

export default function EmbeddedDM({ dm, ...props }: Omit<CardProps, "children"> & { dm: NostrEvent }) {
const account = useCurrentAccount();
const isOwnMessage = account?.pubkey === dm.pubkey;

const sender = dm.pubkey;
const receiver = getMessageRecipient(dm);

if (!receiver) return "Broken DM";

return (
<TrustProvider event={dm}>
<Card as={LinkBox} variant="outline" {...props}>
<CardHeader display="flex" gap="2" p="2" alignItems="center">
<UserAvatarLink pubkey={sender} size="xs" />
<UserLink pubkey={sender} fontWeight="bold" isTruncated fontSize="lg" />
<Text mx="2">Messaged</Text>
<UserAvatarLink pubkey={receiver} size="xs" />
<UserLink pubkey={receiver} fontWeight="bold" isTruncated fontSize="lg" />
<Timestamp timestamp={dm.created_at} />
</CardHeader>
<CardBody px="2" pt="0" pb="2">
<DecryptPlaceholder data={dm.content} pubkey={isOwnMessage ? getMessageRecipient(dm) ?? "" : dm.pubkey}>
{(text) => <MessageContent event={dm} text={text} />}
</DecryptPlaceholder>
</CardBody>
</Card>
</TrustProvider>
);
}
Loading

0 comments on commit 61fee4e

Please sign in to comment.