mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-09-18 03:22:28 +02:00
redesign message components
This commit is contained in:
6
.cursor/rules/use-async-hook.mdc
Normal file
6
.cursor/rules/use-async-hook.mdc
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
description:
|
||||||
|
globs:
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
When writing async actions or callbacks in a component use the `useAsyncAction` hook instead of using `try/catch`. the hook is cleaner and handles errors by showing a toast
|
56
src/components/message/message-slack-block.tsx
Normal file
56
src/components/message/message-slack-block.tsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { Box, Flex } from "@chakra-ui/react";
|
||||||
|
import { NostrEvent } from "nostr-tools";
|
||||||
|
|
||||||
|
import UserAvatarLink from "../user/user-avatar-link";
|
||||||
|
import MessageSlack, { MessageSlackProps } from "./message-slack";
|
||||||
|
|
||||||
|
export type MessageSlackBlockProps = {
|
||||||
|
messages: NostrEvent[];
|
||||||
|
reverse?: boolean;
|
||||||
|
renderContent: MessageSlackProps["renderContent"];
|
||||||
|
renderActions?: MessageSlackProps["renderActions"];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function MessageSlackBlock({
|
||||||
|
messages,
|
||||||
|
reverse = false,
|
||||||
|
renderContent,
|
||||||
|
renderActions,
|
||||||
|
}: MessageSlackBlockProps) {
|
||||||
|
const lastEvent = messages[messages.length - 1];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
width="full"
|
||||||
|
_hover={{
|
||||||
|
bg: "var(--chakra-colors-card-hover-overlay)",
|
||||||
|
"& .message-actions": { opacity: 1 },
|
||||||
|
}}
|
||||||
|
transition="background-color 0.1s ease"
|
||||||
|
borderRadius="md"
|
||||||
|
px="3"
|
||||||
|
py="2"
|
||||||
|
position="relative"
|
||||||
|
>
|
||||||
|
<Flex direction="row" gap="2" alignItems="flex-start" width="100%">
|
||||||
|
{/* Avatar - shown only once per message group */}
|
||||||
|
<Box flexShrink={0}>
|
||||||
|
<UserAvatarLink pubkey={lastEvent.pubkey} size="sm" />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Messages container */}
|
||||||
|
<Flex direction={reverse ? "column-reverse" : "column"} gap="2" flex={1}>
|
||||||
|
{messages.map((message, i, arr) => (
|
||||||
|
<MessageSlack
|
||||||
|
key={message.id}
|
||||||
|
message={message}
|
||||||
|
showHeader={reverse ? i === arr.length - 1 : i === 0}
|
||||||
|
renderContent={renderContent}
|
||||||
|
renderActions={renderActions}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
56
src/components/message/message-slack.tsx
Normal file
56
src/components/message/message-slack.tsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { Box, ButtonGroup, Flex } from "@chakra-ui/react";
|
||||||
|
import { NostrEvent } from "nostr-tools";
|
||||||
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
|
import useEventIntersectionRef from "../../hooks/use-event-intersection-ref";
|
||||||
|
import useEventReactions from "../../hooks/use-event-reactions";
|
||||||
|
import EventReactionButtons from "../event-reactions/event-reactions";
|
||||||
|
import Timestamp from "../timestamp";
|
||||||
|
import UserLink from "../user/user-link";
|
||||||
|
|
||||||
|
export type MessageSlackProps = {
|
||||||
|
message: NostrEvent;
|
||||||
|
showHeader?: boolean;
|
||||||
|
renderContent: (message: NostrEvent) => ReactNode;
|
||||||
|
renderActions?: (message: NostrEvent, onReply?: (message: NostrEvent) => void) => ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function MessageSlack({ message, showHeader = true, renderContent, renderActions }: MessageSlackProps) {
|
||||||
|
const reactions = useEventReactions(message) ?? [];
|
||||||
|
const hasReactions = reactions.length > 0;
|
||||||
|
const ref = useEventIntersectionRef(message);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box ref={ref} position="relative" width="full">
|
||||||
|
{/* Quick Actions - float right in message */}
|
||||||
|
{renderActions && (
|
||||||
|
<Box
|
||||||
|
className="message-actions"
|
||||||
|
opacity="0"
|
||||||
|
transition="opacity 0.1s ease"
|
||||||
|
float="right"
|
||||||
|
ml="2"
|
||||||
|
mt="-1"
|
||||||
|
mr="-1"
|
||||||
|
>
|
||||||
|
{renderActions(message)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showHeader && (
|
||||||
|
<Flex align="flex-start" gap="2">
|
||||||
|
<UserLink pubkey={message.pubkey} fontWeight="bold" />
|
||||||
|
<Timestamp timestamp={message.created_at} fontSize="sm" color="GrayText" />
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{renderContent(message)}
|
||||||
|
|
||||||
|
{hasReactions && (
|
||||||
|
<ButtonGroup size="xs" variant="outline" spacing="2">
|
||||||
|
<EventReactionButtons event={message} />
|
||||||
|
</ButtonGroup>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
@@ -59,27 +59,4 @@ export function sortConversationsByLastReceived(conversations: KnownConversation
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Groups messages into bubble sets based on the pubkey and time */
|
export { groupMessageEvents as groupMessages } from "applesauce-core/helpers/messages";
|
||||||
export function groupMessages(messages: NostrEvent[], minutes = 5, ascending = false) {
|
|
||||||
const sorted = messages.sort(sortByDate);
|
|
||||||
|
|
||||||
const groups: { id: string; pubkey: string; events: NostrEvent[] }[] = [];
|
|
||||||
for (const message of sorted) {
|
|
||||||
const last = groups[groups.length - 1];
|
|
||||||
if (last && last.pubkey === message.pubkey) {
|
|
||||||
const lastEvent = last.events[last.events.length - 1];
|
|
||||||
if (lastEvent && dayjs.unix(lastEvent.created_at).diff(dayjs.unix(message.created_at), "minute") < minutes) {
|
|
||||||
last.events.push(message);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const group = { id: message.id, pubkey: message.pubkey, events: [message] };
|
|
||||||
groups.push(group);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ascending) {
|
|
||||||
for (const group of groups) group.events.reverse();
|
|
||||||
return groups.reverse();
|
|
||||||
} else return groups;
|
|
||||||
}
|
|
||||||
|
@@ -22,7 +22,7 @@ import {
|
|||||||
useToast,
|
useToast,
|
||||||
VStack,
|
VStack,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { useObservableState } from "applesauce-react/hooks";
|
import { useObservableEagerState, useObservableState } from "applesauce-react/hooks";
|
||||||
import { useCallback, useRef, useState } from "react";
|
import { useCallback, useRef, useState } from "react";
|
||||||
import { firstValueFrom } from "rxjs";
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
@@ -33,14 +33,14 @@ import localSettings from "../../services/local-settings";
|
|||||||
|
|
||||||
export default function RequireDecryptionCache({ children }: { children: JSX.Element }) {
|
export default function RequireDecryptionCache({ children }: { children: JSX.Element }) {
|
||||||
const stats = useObservableState(decryptionCacheStats$);
|
const stats = useObservableState(decryptionCacheStats$);
|
||||||
const cache = useObservableState(decryptionCache$);
|
const cache = useObservableEagerState(decryptionCache$);
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const disableEncryptionModal = useDisclosure();
|
const disableEncryptionModal = useDisclosure();
|
||||||
const disableCacheModal = useDisclosure();
|
const disableCacheModal = useDisclosure();
|
||||||
const cancelRef = useRef<HTMLButtonElement>(null);
|
const cancelRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
const { loading: isUnlocking, run: unlockCache } = useAsyncAction(async () => {
|
const unlockCache = useAsyncAction(async () => {
|
||||||
if (!password.trim()) {
|
if (!password.trim()) {
|
||||||
toast({
|
toast({
|
||||||
title: "Password required",
|
title: "Password required",
|
||||||
@@ -69,7 +69,7 @@ export default function RequireDecryptionCache({ children }: { children: JSX.Ele
|
|||||||
}
|
}
|
||||||
}, [password, cache, toast]);
|
}, [password, cache, toast]);
|
||||||
|
|
||||||
const { loading: isDisablingCache, run: runDisableMessageCache } = useAsyncAction(async () => {
|
const disableCache = useAsyncAction(async () => {
|
||||||
// Clear the cache first
|
// Clear the cache first
|
||||||
const cache = await firstValueFrom(decryptionCache$);
|
const cache = await firstValueFrom(decryptionCache$);
|
||||||
if (cache) await cache.clear();
|
if (cache) await cache.clear();
|
||||||
@@ -99,12 +99,8 @@ export default function RequireDecryptionCache({ children }: { children: JSX.Ele
|
|||||||
disableEncryptionModal.onClose();
|
disableEncryptionModal.onClose();
|
||||||
}, [toast]);
|
}, [toast]);
|
||||||
|
|
||||||
const disableMessageCache = useCallback(() => {
|
|
||||||
runDisableMessageCache();
|
|
||||||
}, [runDisableMessageCache]);
|
|
||||||
|
|
||||||
// If cache is not encrypted or is already unlocked, render children
|
// If cache is not encrypted or is already unlocked, render children
|
||||||
if (!stats?.isEncrypted || !stats?.isLocked || !cache) {
|
if (stats?.isEncrypted === false || stats?.isLocked === false || cache === null) {
|
||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,7 +129,7 @@ export default function RequireDecryptionCache({ children }: { children: JSX.Ele
|
|||||||
type="password"
|
type="password"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
onKeyPress={(e) => e.key === "Enter" && !isUnlocking && unlockCache()}
|
onKeyPress={(e) => e.key === "Enter" && !unlockCache.loading && unlockCache.run()}
|
||||||
placeholder="Enter current password or new password"
|
placeholder="Enter current password or new password"
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
@@ -153,8 +149,8 @@ export default function RequireDecryptionCache({ children }: { children: JSX.Ele
|
|||||||
<Button
|
<Button
|
||||||
colorScheme="primary"
|
colorScheme="primary"
|
||||||
w="full"
|
w="full"
|
||||||
onClick={unlockCache}
|
onClick={unlockCache.run}
|
||||||
isLoading={isUnlocking}
|
isLoading={unlockCache.loading}
|
||||||
loadingText="Unlocking..."
|
loadingText="Unlocking..."
|
||||||
>
|
>
|
||||||
Unlock Cache
|
Unlock Cache
|
||||||
@@ -228,9 +224,9 @@ export default function RequireDecryptionCache({ children }: { children: JSX.Ele
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
colorScheme="orange"
|
colorScheme="orange"
|
||||||
onClick={disableMessageCache}
|
onClick={disableCache.run}
|
||||||
ml={3}
|
ml={3}
|
||||||
isLoading={isDisablingCache}
|
isLoading={disableCache.loading}
|
||||||
loadingText="Disabling..."
|
loadingText="Disabling..."
|
||||||
>
|
>
|
||||||
Disable Message Cache
|
Disable Message Cache
|
||||||
|
@@ -25,6 +25,7 @@ import IntersectionObserverProvider from "../../providers/local/intersection-obs
|
|||||||
import ThreadsProvider from "../../providers/local/thread-provider";
|
import ThreadsProvider from "../../providers/local/thread-provider";
|
||||||
import localSettings from "../../services/local-settings";
|
import localSettings from "../../services/local-settings";
|
||||||
import DirectMessageBlock from "./components/direct-message-block";
|
import DirectMessageBlock from "./components/direct-message-block";
|
||||||
|
import DirectMessageSlackBlock from "./components/direct-message-slack-block";
|
||||||
import SendMessageForm from "./components/send-message-form";
|
import SendMessageForm from "./components/send-message-form";
|
||||||
import ThreadDrawer from "./components/thread-drawer";
|
import ThreadDrawer from "./components/thread-drawer";
|
||||||
|
|
||||||
@@ -39,7 +40,7 @@ const ChatLog = memo(({ messages }: { messages: NostrEvent[] }) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{grouped.map((group) => (
|
{grouped.map((group) => (
|
||||||
<DirectMessageBlock key={group.id} messages={group.events} reverse />
|
<DirectMessageSlackBlock key={group[0].id} messages={group} reverse />
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
73
src/views/messages/components/decrypt-placeholder-slack.tsx
Normal file
73
src/views/messages/components/decrypt-placeholder-slack.tsx
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { Alert, AlertDescription, AlertIcon, Button } from "@chakra-ui/react";
|
||||||
|
import { useObservableEagerState } from "applesauce-react/hooks";
|
||||||
|
import { NostrEvent } from "nostr-tools";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import DebugEventButton from "../../../components/debug-modal/debug-event-button";
|
||||||
|
import { UnlockIcon } from "../../../components/icons";
|
||||||
|
import { useLegacyMessagePlaintext } from "../../../hooks/use-legacy-message-plaintext";
|
||||||
|
import localSettings from "../../../services/local-settings";
|
||||||
|
|
||||||
|
export default function DecryptPlaceholderSlack({
|
||||||
|
children,
|
||||||
|
message,
|
||||||
|
}: {
|
||||||
|
children: (decrypted: string) => JSX.Element;
|
||||||
|
message: NostrEvent;
|
||||||
|
}): JSX.Element {
|
||||||
|
const autoDecryptMessages = useObservableEagerState(localSettings.autoDecryptMessages);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const { unlock, plaintext, error } = useLegacyMessagePlaintext(message);
|
||||||
|
|
||||||
|
const decrypt = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
await unlock();
|
||||||
|
} catch (e) {}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// auto decrypt
|
||||||
|
useEffect(() => {
|
||||||
|
if (autoDecryptMessages && !plaintext && !error) {
|
||||||
|
setLoading(true);
|
||||||
|
unlock()
|
||||||
|
.catch(() => {})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [autoDecryptMessages, error, plaintext]);
|
||||||
|
|
||||||
|
if (plaintext) {
|
||||||
|
return children(plaintext);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<Alert status="error" borderRadius="md" maxW="lg">
|
||||||
|
<AlertIcon />
|
||||||
|
<AlertDescription flex="1">{error.message}</AlertDescription>
|
||||||
|
<DebugEventButton event={message} size="sm" mr="2" />
|
||||||
|
<Button isLoading={loading} leftIcon={<UnlockIcon />} onClick={decrypt} size="sm">
|
||||||
|
Try again
|
||||||
|
</Button>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
onClick={decrypt}
|
||||||
|
isLoading={loading}
|
||||||
|
leftIcon={<UnlockIcon />}
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
border="1px dashed"
|
||||||
|
w="full"
|
||||||
|
maxW="lg"
|
||||||
|
>
|
||||||
|
Decrypt message
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
@@ -1,7 +1,8 @@
|
|||||||
|
import React from "react";
|
||||||
import { Box, BoxProps } from "@chakra-ui/react";
|
import { Box, BoxProps } from "@chakra-ui/react";
|
||||||
import { useRenderedContent } from "applesauce-react/hooks";
|
import { useRenderedContent } from "applesauce-react/hooks";
|
||||||
|
|
||||||
import { NostrEvent } from "nostr-tools";
|
import { NostrEvent } from "nostr-tools";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
renderAppleMusicUrl,
|
renderAppleMusicUrl,
|
||||||
renderGenericUrl,
|
renderGenericUrl,
|
||||||
@@ -50,7 +51,7 @@ export default function DirectMessageContent({
|
|||||||
text,
|
text,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: { event: NostrEvent; text: string } & BoxProps) {
|
}: { event: NostrEvent; text: string; children?: React.ReactNode } & BoxProps) {
|
||||||
const { plaintext } = useLegacyMessagePlaintext(event);
|
const { plaintext } = useLegacyMessagePlaintext(event);
|
||||||
const content = useRenderedContent(plaintext, components, { linkRenderers, cacheKey: DirectMessageContentSymbol });
|
const content = useRenderedContent(plaintext, components, { linkRenderers, cacheKey: DirectMessageContentSymbol });
|
||||||
|
|
||||||
|
103
src/views/messages/components/direct-message-slack-block.tsx
Normal file
103
src/views/messages/components/direct-message-slack-block.tsx
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import { ButtonGroup, IconButton, Menu, MenuButton, MenuItem, MenuList, useToast } from "@chakra-ui/react";
|
||||||
|
import { useActiveAccount } from "applesauce-react/hooks";
|
||||||
|
import { NostrEvent } from "nostr-tools";
|
||||||
|
import { memo, useCallback } from "react";
|
||||||
|
|
||||||
|
import { ReplyIcon } from "../../../components/icons";
|
||||||
|
import DotsHorizontal from "../../../components/icons/dots-horizontal";
|
||||||
|
import MessageSlackBlock, { MessageSlackBlockProps } from "../../../components/message/message-slack-block";
|
||||||
|
import { useLegacyMessagePlaintext } from "../../../hooks/use-legacy-message-plaintext";
|
||||||
|
import DecryptPlaceholderSlack from "./decrypt-placeholder-slack";
|
||||||
|
import DirectMessageContent from "./direct-message-content";
|
||||||
|
|
||||||
|
function DirectMessageActions({
|
||||||
|
message,
|
||||||
|
onReply,
|
||||||
|
account,
|
||||||
|
toast,
|
||||||
|
}: {
|
||||||
|
message: NostrEvent;
|
||||||
|
onReply?: (message: NostrEvent) => void;
|
||||||
|
account: any;
|
||||||
|
toast: any;
|
||||||
|
}) {
|
||||||
|
const { plaintext } = useLegacyMessagePlaintext(message);
|
||||||
|
const isOwnMessage = message.pubkey === account.pubkey;
|
||||||
|
|
||||||
|
const handleReply = () => {
|
||||||
|
onReply?.(message);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCopyText = async () => {
|
||||||
|
if (plaintext) {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(plaintext);
|
||||||
|
toast({
|
||||||
|
title: "Text copied to clipboard",
|
||||||
|
status: "success",
|
||||||
|
duration: 2000,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
title: "Failed to copy text",
|
||||||
|
status: "error",
|
||||||
|
duration: 2000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = () => {
|
||||||
|
// TODO: Implement delete functionality
|
||||||
|
console.log("Delete message:", message.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ButtonGroup size="xs" variant="ghost" gap="0">
|
||||||
|
<IconButton aria-label="Reply" icon={<ReplyIcon />} onClick={handleReply} size="xs" />
|
||||||
|
{/* <AddReactionButton event={message} size="xs" /> */}
|
||||||
|
{/* <EventZapButton event={message} size="xs" /> */}
|
||||||
|
<Menu>
|
||||||
|
<MenuButton as={IconButton} aria-label="More actions" icon={<DotsHorizontal />} size="xs" />
|
||||||
|
<MenuList fontSize="sm">
|
||||||
|
<MenuItem onClick={handleCopyText}>Copy text</MenuItem>
|
||||||
|
{isOwnMessage && (
|
||||||
|
<MenuItem color="red.500" onClick={handleDelete}>
|
||||||
|
Delete
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</MenuList>
|
||||||
|
</Menu>
|
||||||
|
</ButtonGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DirectMessageSlackBlock({
|
||||||
|
onReply,
|
||||||
|
...props
|
||||||
|
}: Omit<MessageSlackBlockProps, "renderContent"> & {
|
||||||
|
onReply?: (message: NostrEvent) => void;
|
||||||
|
}) {
|
||||||
|
const account = useActiveAccount()!;
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
const renderContent = useCallback(
|
||||||
|
(message: NostrEvent) => (
|
||||||
|
<DecryptPlaceholderSlack message={message}>
|
||||||
|
{(plaintext) => <DirectMessageContent event={message} text={plaintext} />}
|
||||||
|
</DecryptPlaceholderSlack>
|
||||||
|
),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderActions = useCallback(
|
||||||
|
(message: NostrEvent) => {
|
||||||
|
return <DirectMessageActions message={message} onReply={onReply} account={account} toast={toast} />;
|
||||||
|
},
|
||||||
|
[account, toast],
|
||||||
|
);
|
||||||
|
|
||||||
|
return <MessageSlackBlock renderContent={renderContent} renderActions={renderActions} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(DirectMessageSlackBlock);
|
@@ -82,14 +82,14 @@ function ListThreads() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ThreadMessages({ thread, pubkey }: { thread: Thread; pubkey: string }) {
|
function ThreadMessages({ thread, pubkey }: { thread: Thread; pubkey: string }) {
|
||||||
const grouped = groupMessages(thread.messages, 5, true);
|
const grouped = groupMessages(thread.messages, 5);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Flex h="0" flex={1} overflowX="hidden" overflowY="scroll" direction="column" gap="2">
|
<Flex h="0" flex={1} overflowX="hidden" overflowY="scroll" direction="column" gap="2">
|
||||||
{thread.root && <DirectMessageBlock messages={[thread.root]} showThreadButton={false} />}
|
{thread.root && <DirectMessageBlock messages={[thread.root]} showThreadButton={false} />}
|
||||||
{grouped.map((group) => (
|
{grouped.map((group) => (
|
||||||
<DirectMessageBlock key={group.id} messages={group.events} showThreadButton={false} />
|
<DirectMessageBlock key={group[0].id} messages={group} showThreadButton={false} />
|
||||||
))}
|
))}
|
||||||
</Flex>
|
</Flex>
|
||||||
<SendMessageForm flexShrink={0} pubkey={pubkey} rootId={thread.rootId} />
|
<SendMessageForm flexShrink={0} pubkey={pubkey} rootId={thread.rootId} />
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
import { Button, ButtonGroup, Flex, IconButton, LinkBox, LinkOverlay, Text } from "@chakra-ui/react";
|
import { Button, ButtonGroup, Flex, IconButton, LinkBox, LinkOverlay, Text } from "@chakra-ui/react";
|
||||||
import { kinds, nip19 } from "nostr-tools";
|
import { useActiveAccount } from "applesauce-react/hooks";
|
||||||
|
import { NostrEvent, kinds, nip19 } from "nostr-tools";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { Link as RouterLink, useLocation } from "react-router-dom";
|
import { Link as RouterLink, useLocation } from "react-router-dom";
|
||||||
import AutoSizer from "react-virtualized-auto-sizer";
|
import AutoSizer from "react-virtualized-auto-sizer";
|
||||||
import { FixedSizeList, ListChildComponentProps } from "react-window";
|
import { FixedSizeList, ListChildComponentProps } from "react-window";
|
||||||
|
|
||||||
import { useActiveAccount } from "applesauce-react/hooks";
|
|
||||||
import { CheckIcon, SettingsIcon } from "../../components/icons";
|
import { CheckIcon, SettingsIcon } from "../../components/icons";
|
||||||
import SimpleParentView from "../../components/layout/presets/simple-parent-view";
|
import SimpleParentView from "../../components/layout/presets/simple-parent-view";
|
||||||
import RequireActiveAccount from "../../components/router/require-active-account";
|
import RequireActiveAccount from "../../components/router/require-active-account";
|
||||||
@@ -25,7 +25,6 @@ import useUserContacts from "../../hooks/use-user-contacts";
|
|||||||
import useUserMailboxes from "../../hooks/use-user-mailboxes";
|
import useUserMailboxes from "../../hooks/use-user-mailboxes";
|
||||||
import useUserMutes from "../../hooks/use-user-mutes";
|
import useUserMutes from "../../hooks/use-user-mutes";
|
||||||
import IntersectionObserverProvider from "../../providers/local/intersection-observer";
|
import IntersectionObserverProvider from "../../providers/local/intersection-observer";
|
||||||
import { NostrEvent } from "nostr-tools";
|
|
||||||
import RequireDecryptionCache from "../../providers/route/require-decryption-cache";
|
import RequireDecryptionCache from "../../providers/route/require-decryption-cache";
|
||||||
|
|
||||||
export function useDirectMessagesTimeline(pubkey?: string) {
|
export function useDirectMessagesTimeline(pubkey?: string) {
|
||||||
|
Reference in New Issue
Block a user