Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions desktop/src/features/channels/lib/dmHuddleMembers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import type { Channel } from "@/shared/api/types";
import { normalizePubkey } from "@/shared/lib/pubkey";

export function getDmHuddleMemberPubkeys(
channel: Channel | null,
agentPubkeys: ReadonlySet<string> | undefined,
currentPubkey: string | undefined,
) {
if (channel?.channelType !== "dm" || !agentPubkeys) {
return [];
}

const normalizedCurrentPubkey = currentPubkey
? normalizePubkey(currentPubkey)
: null;
const seen = new Set<string>();

return channel.participantPubkeys.filter((pubkey) => {
const normalizedPubkey = normalizePubkey(pubkey);
if (
normalizedCurrentPubkey &&
normalizedPubkey === normalizedCurrentPubkey
) {
return false;
}

if (!agentPubkeys.has(normalizedPubkey) || seen.has(normalizedPubkey)) {
return false;
}

seen.add(normalizedPubkey);
return true;
});
}

export function hasOtherDmParticipant(
channel: Channel | null,
currentPubkey: string | undefined,
) {
if (channel?.channelType !== "dm") {
return false;
}

const normalizedCurrentPubkey = currentPubkey
? normalizePubkey(currentPubkey)
: null;

return channel.participantPubkeys.some((pubkey) => {
const normalizedPubkey = normalizePubkey(pubkey);
return (
!normalizedCurrentPubkey || normalizedPubkey !== normalizedCurrentPubkey
);
});
}
22 changes: 1 addition & 21 deletions desktop/src/features/channels/ui/ChannelHeaderStatusBadge.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,18 @@
import type { EphemeralChannelDisplay } from "@/features/channels/lib/ephemeralChannel";
import { EphemeralChannelBadge } from "@/features/channels/ui/EphemeralChannelBadge";
import { PresenceBadge } from "@/features/presence/ui/PresenceBadge";
import type { Channel, PresenceStatus } from "@/shared/api/types";

type ChannelHeaderStatusBadgeProps = {
channelType?: Channel["channelType"];
ephemeralDisplay: EphemeralChannelDisplay | null;
presenceStatus: PresenceStatus | null;
};

export function ChannelHeaderStatusBadge({
channelType,
ephemeralDisplay,
presenceStatus,
}: ChannelHeaderStatusBadgeProps) {
const ephemeralBadge = ephemeralDisplay ? (
return ephemeralDisplay ? (
<EphemeralChannelBadge
display={ephemeralDisplay}
testId="chat-ephemeral-badge"
variant="header"
/>
) : null;

if (channelType === "dm" && presenceStatus) {
return (
<>
<PresenceBadge
data-testid="chat-presence-badge"
status={presenceStatus}
/>
{ephemeralBadge}
</>
);
}

return ephemeralBadge;
}
16 changes: 16 additions & 0 deletions desktop/src/features/channels/ui/ChannelPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ import {
} from "@/features/messages/ui/MessageTimeline";
import type { ImetaMedia } from "@/features/messages/lib/imetaMediaMarkdown";
import { buildDirectMessageIntro } from "@/features/channels/lib/dmParticipantDisplay";
import {
getDmHuddleMemberPubkeys,
hasOtherDmParticipant,
} from "@/features/channels/lib/dmHuddleMembers";
import {
buildVideoReviewCommentsByRootId,
buildVideoReviewContextForMessage,
Expand Down Expand Up @@ -69,6 +73,7 @@ type ChannelPaneProps = {
activeChannel: Channel | null;
activityAgents?: BotActivityAgent[];
agentPubkeys?: ReadonlySet<string>;
agentPubkeysPending?: boolean;
agentSessionAgents: ChannelAgentSessionAgent[];
botTypingEntries: TypingIndicatorEntry[];
channelFind: ReturnType<typeof useChannelFind>;
Expand Down Expand Up @@ -180,6 +185,7 @@ type ChannelPaneProps = {
export const ChannelPane = React.memo(function ChannelPane({
activeChannel,
agentPubkeys,
agentPubkeysPending = false,
agentSessionAgents,
activityAgents = agentSessionAgents,
botTypingEntries,
Expand Down Expand Up @@ -274,6 +280,12 @@ export const ChannelPane = React.memo(function ChannelPane({
!activeChannel.archivedAt;
const hasMainComposerOverlay = !isNonMemberView;
const activeChannelId = activeChannel?.id ?? null;
const huddleMemberPubkeys = React.useMemo(
() => getDmHuddleMemberPubkeys(activeChannel, agentPubkeys, currentPubkey),
[activeChannel, agentPubkeys, currentPubkey],
);
const huddleMemberPubkeysPending =
agentPubkeysPending && hasOtherDmParticipant(activeChannel, currentPubkey);
const isActiveWelcomeChannel =
activeChannel !== null && isWelcomeChannel(activeChannel);
useComposerHeightPadding(
Expand Down Expand Up @@ -677,6 +689,8 @@ export const ChannelPane = React.memo(function ChannelPane({
followThreadById={followThreadById}
hasComposerOverlay={hasMainComposerOverlay}
hasOlderMessages={hasOlderMessages}
huddleMemberPubkeys={huddleMemberPubkeys}
huddleMemberPubkeysPending={huddleMemberPubkeysPending}
isFetchingOlder={isFetchingOlder}
isFollowingThreadById={isFollowingThreadById}
isMessageUnreadById={isMessageUnreadById}
Expand Down Expand Up @@ -842,6 +856,8 @@ export const ChannelPane = React.memo(function ChannelPane({
disabled={isComposerDisabled}
editTarget={threadEditTarget}
firstUnreadReplyId={threadFirstUnreadReplyId}
huddleMemberPubkeys={huddleMemberPubkeys}
huddleMemberPubkeysPending={huddleMemberPubkeysPending}
isFollowingThread={isFollowingThread}
isMessageUnreadById={isMessageUnreadById}
isSending={isSending}
Expand Down
16 changes: 15 additions & 1 deletion desktop/src/features/channels/ui/ChannelScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -311,8 +311,21 @@ export function ChannelScreen({
for (const agent of relayAgents) {
pubkeys.add(normalizePubkey(agent.pubkey));
}
for (const [pubkey, profile] of Object.entries(
messageProfilesQuery.data?.profiles ?? {},
)) {
if (profile.isAgent) {
pubkeys.add(normalizePubkey(pubkey));
}
}
return pubkeys;
}, [channelMembers, managedAgents, relayAgents]);
}, [channelMembers, managedAgents, messageProfilesQuery.data, relayAgents]);
const agentPubkeysPending =
activeChannel?.channelType === "dm" &&
(channelMembersQuery.isPending ||
managedAgentsQuery.isPending ||
relayAgentsQuery.isPending ||
(messageProfilePubkeys.length > 0 && messageProfilesQuery.isPending));
Comment thread
klopez4212 marked this conversation as resolved.
const {
agentSessionCandidates,
botTypingEntries,
Expand Down Expand Up @@ -758,6 +771,7 @@ export function ChannelScreen({
activeChannel={activeChannel}
activityAgents={channelAgentSessionAgents}
agentPubkeys={agentPubkeys}
agentPubkeysPending={agentPubkeysPending}
agentSessionAgents={agentSessionAgents}
botTypingEntries={botTypingEntries}
channelFind={channelFind}
Expand Down
35 changes: 24 additions & 11 deletions desktop/src/features/channels/ui/ChannelScreenHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,21 @@ import { getChannelDescription } from "@/features/channels/lib/channelDescriptio
import { getDmParticipantPreview } from "@/features/channels/lib/dmParticipantDisplay";
import { ChannelHeaderStatusBadge } from "@/features/channels/ui/ChannelHeaderStatusBadge";
import { ChannelMembersBar } from "@/features/channels/ui/ChannelMembersBar";
import { ProfileAvatar } from "@/features/profile/ui/ProfileAvatar";
import {
DEFAULT_HOVER_PROFILE_STATUS_GEOMETRY,
ProfileAvatarWithStatus,
scaleProfileAvatarStatusGeometry,
} from "@/features/profile/ui/ProfileAvatarWithStatus";
import { Button } from "@/shared/ui/button";
import type { Channel, PresenceStatus } from "@/shared/api/types";
import { UserAvatar } from "@/shared/ui/UserAvatar";

const DM_HEADER_AVATAR_SIZE = 32;
const DM_HEADER_AVATAR_STATUS_GEOMETRY = scaleProfileAvatarStatusGeometry(
DEFAULT_HOVER_PROFILE_STATUS_GEOMETRY,
DM_HEADER_AVATAR_SIZE,
);

type ChannelScreenHeaderProps = {
activeChannel: Channel | null;
activeChannelEphemeralDisplay: EphemeralChannelDisplay | null;
Expand Down Expand Up @@ -102,21 +112,24 @@ export function ChannelScreenHeader({
participants={activeDmHeaderParticipants}
/>
) : (
<ProfileAvatar
<ProfileAvatarWithStatus
avatarClassName="text-xs"
avatarUrl={activeDmAvatarUrl}
className="h-6 w-6 rounded-full text-2xs"
iconClassName="h-3.5 w-3.5"
className="mr-1.5 h-8 w-8"
geometry={DM_HEADER_AVATAR_STATUS_GEOMETRY}
iconClassName="h-4 w-4"
label={activeChannelTitle}
size={DM_HEADER_AVATAR_SIZE}
status={activeDmPresenceStatus ?? "offline"}
statusTestId="chat-presence-badge"
testId="chat-header-dm-avatar"
/>
)
) : undefined
}
statusBadge={
<ChannelHeaderStatusBadge
channelType={activeChannel?.channelType}
ephemeralDisplay={activeChannelEphemeralDisplay}
presenceStatus={isGroupDm ? null : activeDmPresenceStatus}
/>
}
title={activeChannelTitle}
Expand All @@ -137,7 +150,7 @@ function DmHeaderParticipantStack({
return (
<div
aria-hidden="true"
className="mr-1 flex shrink-0 items-center"
className="mr-1.5 flex shrink-0 items-center"
data-testid="chat-header-dm-avatar-stack"
>
{visibleParticipants.map((participant, index) => (
Expand All @@ -148,15 +161,15 @@ function DmHeaderParticipantStack({
style={{
zIndex: index + 1,
...(index < stackItemCount - 1 && {
mask: "radial-gradient(circle 16px at calc(100% + 4px) 50%, transparent 99%, #fff 100%)",
mask: "radial-gradient(circle 18px at calc(100% + 4px) 50%, transparent 99%, #fff 100%)",
WebkitMask:
"radial-gradient(circle 16px at calc(100% + 4px) 50%, transparent 99%, #fff 100%)",
"radial-gradient(circle 18px at calc(100% + 4px) 50%, transparent 99%, #fff 100%)",
}),
}}
>
<UserAvatar
avatarUrl={participant.avatarUrl}
className="h-7 w-7 text-2xs"
className="h-8 w-8 text-xs"
displayName={participant.displayName}
size="sm"
/>
Expand All @@ -168,7 +181,7 @@ function DmHeaderParticipantStack({
data-testid="chat-header-dm-avatar-stack-more"
style={{ zIndex: stackItemCount }}
>
<span className="flex h-7 w-7 items-center justify-center rounded-full bg-secondary font-semibold text-secondary-foreground shadow-xs">
<span className="flex h-8 w-8 items-center justify-center rounded-full bg-secondary font-semibold text-secondary-foreground shadow-xs">
<span className="text-2xs leading-none">+{hiddenCount}</span>
</span>
</div>
Expand Down
7 changes: 5 additions & 2 deletions desktop/src/features/chat/ui/ChatHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export function ChatHeader({
<div className="flex h-9 min-w-0 items-center gap-2.5">
<div className="min-w-0 flex-1">
<div className="group/title flex min-w-0 items-center gap-[4px] overflow-hidden">
<div className="shrink-0">
<div className="flex shrink-0 items-center">
{leadingContent ?? (
<ChannelIcon
channelType={channelType}
Expand All @@ -130,7 +130,10 @@ export function ChatHeader({
)}
</div>
<h1
className="min-w-0 translate-y-px truncate text-base font-semibold leading-6 tracking-tight"
className={cn(
"min-w-0 truncate text-base font-semibold leading-6 tracking-tight",
channelType !== "dm" && "translate-y-px",
)}
data-testid="chat-title"
title={trimmedDescription || undefined}
>
Expand Down
1 change: 1 addition & 0 deletions desktop/src/features/home/lib/inbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export type InboxTypeLabel = {

export type InboxReply = {
authorLabel: string;
authorPubkey: string;
avatarUrl: string | null;
content: string;
depth?: number;
Expand Down
22 changes: 22 additions & 0 deletions desktop/src/features/home/ui/HomeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import type { HomeFeedResponse } from "@/shared/api/types";
import { KIND_REACTION } from "@/shared/constants/kinds";
import { topChromeInset } from "@/shared/layout/chromeLayout";
import { cn } from "@/shared/lib/cn";
import { normalizePubkey } from "@/shared/lib/pubkey";
import { resolveMentionNames } from "@/shared/lib/resolveMentionNames";
import { useElementWidth } from "@/shared/hooks/use-mobile";
import {
Expand Down Expand Up @@ -212,6 +213,21 @@ export function HomeView({
enabled: feedProfilePubkeys.length > 0,
});
const feedProfiles = feedProfilesQuery.data?.profiles;
const inboxAgentPubkeys = React.useMemo(() => {
const pubkeys = new Set<string>();

for (const item of feed?.feed.agentActivity ?? []) {
pubkeys.add(normalizePubkey(item.pubkey));
}

for (const [pubkey, profile] of Object.entries(feedProfiles ?? {})) {
if (profile.isAgent) {
pubkeys.add(normalizePubkey(pubkey));
}
}

return pubkeys;
}, [feed?.feed.agentActivity, feedProfiles]);
const inboxItems = React.useMemo(
() =>
buildInboxItems({
Expand Down Expand Up @@ -279,9 +295,12 @@ export function HomeView({

return timelineMessages.map((message) => {
const event = eventById.get(message.id);
const authorPubkey =
message.pubkey ?? event?.pubkey ?? selectedItem.item.pubkey;
return {
id: message.id,
authorLabel: message.author,
authorPubkey,
avatarUrl: message.avatarUrl ?? null,
content: message.body,
depth: event ? getContextMessageDepth(event, eventById) : message.depth,
Expand Down Expand Up @@ -457,6 +476,7 @@ export function HomeView({
{showListPane ? (
<InboxListPane
activeReminderEventIds={activeReminderEventIds}
agentPubkeys={inboxAgentPubkeys}
doneSet={effectiveDoneSet}
dueReminderCount={dueReminderCount}
filter={filter}
Expand Down Expand Up @@ -524,6 +544,7 @@ export function HomeView({

{showDetailPane ? (
<InboxDetailPane
agentPubkeys={inboxAgentPubkeys}
canDelete={canDelete}
canOpenChannel={Boolean(
selectedItem?.item.channelId &&
Expand Down Expand Up @@ -604,6 +625,7 @@ export function HomeView({
pubkey: authorPubkey,
})
: "You",
authorPubkey,
avatarUrl:
currentPubkey && feedProfiles
? (feedProfiles[currentPubkey.trim().toLowerCase()]
Expand Down
Loading
Loading