diff --git a/desktop/src/app/AppShell.tsx b/desktop/src/app/AppShell.tsx index a9acbe0b2..97f3610a8 100644 --- a/desktop/src/app/AppShell.tsx +++ b/desktop/src/app/AppShell.tsx @@ -26,6 +26,7 @@ import { collectMessageAuthorPubkeys, formatTimelineMessages, } from "@/features/messages/lib/formatTimelineMessages"; +import { useChannelTyping } from "@/features/messages/useChannelTyping"; import { getChannelIdFromTags, getThreadReference, @@ -151,8 +152,21 @@ export function AppShell() { () => collectMessageAuthorPubkeys(resolvedMessages), [resolvedMessages], ); - const messageProfilesQuery = useUsersBatchQuery(messageAuthorPubkeys, { - enabled: resolvedMessages.length > 0, + const latestMessageEvent = React.useMemo( + () => resolvedMessages[resolvedMessages.length - 1] ?? null, + [resolvedMessages], + ); + const typingPubkeys = useChannelTyping( + activeChannel, + identityQuery.data?.pubkey, + latestMessageEvent, + ); + const messageProfilePubkeys = React.useMemo( + () => [...new Set([...messageAuthorPubkeys, ...typingPubkeys])], + [messageAuthorPubkeys, typingPubkeys], + ); + const messageProfilesQuery = useUsersBatchQuery(messageProfilePubkeys, { + enabled: messageProfilePubkeys.length > 0, }); const timelineMessages = React.useMemo( @@ -638,6 +652,7 @@ export function AppShell() { ? searchAnchor.eventId : null } + typingPubkeys={typingPubkeys} /> )} diff --git a/desktop/src/app/ChannelPane.tsx b/desktop/src/app/ChannelPane.tsx index f389cfc8a..fd1e656ba 100644 --- a/desktop/src/app/ChannelPane.tsx +++ b/desktop/src/app/ChannelPane.tsx @@ -2,6 +2,7 @@ import * as React from "react"; import { MessageComposer } from "@/features/messages/ui/MessageComposer"; import { MessageTimeline } from "@/features/messages/ui/MessageTimeline"; +import { TypingIndicatorRow } from "@/features/messages/ui/TypingIndicatorRow"; import type { TimelineMessage } from "@/features/messages/types"; import type { UserProfileLookup } from "@/features/profile/lib/identity"; import type { Channel } from "@/shared/api/types"; @@ -29,6 +30,7 @@ type ChannelPaneProps = { replyTargetId: string | null; replyTargetMessage: TimelineMessage | null; targetMessageId: string | null; + typingPubkeys: string[]; }; export function ChannelPane({ @@ -46,6 +48,7 @@ export function ChannelPane({ replyTargetId, replyTargetMessage, targetMessageId, + typingPubkeys, }: ChannelPaneProps) { return ( @@ -72,6 +75,12 @@ export function ChannelPane({ onToggleReaction={onToggleReaction} targetMessageId={targetMessageId} /> +
-
+
-
-
-
-

- {agent.name} -

- {isManagedLocally ? ( - - Local - - ) : null} -
-

- {truncatePubkey(agent.pubkey)} - {agent.agentType ? ` · ${agent.agentType}` : ""} -

-
- -
- -
- {visibleCapabilities.map((capability) => ( - - {capability} - - ))} - {hiddenCapabilityCount > 0 ? ( - - +{hiddenCapabilityCount} - - ) : null} -
- -

- {agent.channels.length > 0 - ? `Visible in ${agent.channels.join(", ")}` - : "No visible channel memberships yet."} -

- - ); -} diff --git a/desktop/src/features/agents/ui/RelayDirectorySection.tsx b/desktop/src/features/agents/ui/RelayDirectorySection.tsx index 6efc9629d..6f9eb22a6 100644 --- a/desktop/src/features/agents/ui/RelayDirectorySection.tsx +++ b/desktop/src/features/agents/ui/RelayDirectorySection.tsx @@ -1,6 +1,7 @@ import type { RelayAgent } from "@/shared/api/types"; +import { PresenceBadge } from "@/features/presence/ui/PresenceBadge"; import { Skeleton } from "@/shared/ui/skeleton"; -import { RelayAgentCard } from "./RelayAgentCard"; +import { truncatePubkey } from "./agentUi"; export function RelayDirectorySection({ error, @@ -13,6 +14,17 @@ export function RelayDirectorySection({ managedPubkeys: Set; relayAgents: RelayAgent[]; }) { + const sortedAgents = [...relayAgents].sort((left, right) => { + const leftManaged = managedPubkeys.has(left.pubkey); + const rightManaged = managedPubkeys.has(right.pubkey); + + if (leftManaged !== rightManaged) { + return leftManaged ? -1 : 1; + } + + return left.name.localeCompare(right.name); + }); + return (
@@ -25,17 +37,24 @@ export function RelayDirectorySection({
{isLoading ? ( -
- {["directory-1", "directory-2"].map((key) => ( -
- - - -
- ))} +
+
+ {["directory-1", "directory-2", "directory-3"].map((key) => ( +
+
+ + +
+ + + + +
+ ))} +
) : null} @@ -51,13 +70,70 @@ export function RelayDirectorySection({
) : null} - {relayAgents.map((agent) => ( - - ))} + {!isLoading && relayAgents.length > 0 ? ( +
+
+ + + + + + + + + + + + {sortedAgents.map((agent) => { + const isManagedLocally = managedPubkeys.has(agent.pubkey); + + return ( + + + + + + + + ); + })} + +
AgentStatusTypeChannelsSource
+
+

+ {agent.name} +

+

+ {truncatePubkey(agent.pubkey)} +

+
+
+ + + {agent.agentType || "Unknown"} + + + {agent.channels.length > 0 + ? agent.channels.join(", ") + : "No visible channel memberships"} + + + + {isManagedLocally ? "Local" : "Relay"} + +
+
+
+ ) : null} {error ? (

diff --git a/desktop/src/features/messages/ui/MessageRow.tsx b/desktop/src/features/messages/ui/MessageRow.tsx index cfed645a5..e5c2147ab 100644 --- a/desktop/src/features/messages/ui/MessageRow.tsx +++ b/desktop/src/features/messages/ui/MessageRow.tsx @@ -231,7 +231,7 @@ export function MessageRow({

{renderBody()} {reactions.length > 0 ? ( -
+
{reactions.map((reaction: TimelineReaction) => (