Skip to content
Open
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
6 changes: 2 additions & 4 deletions desktop/scripts/check-pubkey-truncation.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,10 @@ const rules = [
// Non-display uses: array windows over pubkey lists, color/initials
// derivation where the value is never presented as an identity.
const overrides = new Set([
// ProfileAvatar fallback label — decorative glyphs inside an avatar disc.
"src/features/huddle/components/ParticipantList.tsx:92",
// HexAvatar: 6-char badge + hue derivation inside a color-coded disc,
// clearly decorative (paired with a full truncatePubkey aria-label).
"src/features/huddle/components/ParticipantList.tsx:143",
"src/features/huddle/components/ParticipantList.tsx:144",
"src/features/huddle/components/ParticipantList.tsx:150",
"src/features/huddle/components/ParticipantList.tsx:151",
// clientId (not a pubkey) sliced in a debug log next to the real thing.
"src/features/channels/readState/readStateManager.ts:338",
// Array windows (first N pubkeys), not string truncation.
Expand Down
25 changes: 25 additions & 0 deletions desktop/src/app/AppHuddleBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type * as React from "react";

import { HuddleBar } from "@/features/huddle";

import { AppProfilePanelProvider } from "@/app/AppProfilePanelProvider";

type AppHuddleBarProps = Pick<
React.ComponentProps<typeof HuddleBar>,
"onOpenThread" | "onVisibilityChange"
>;

export function AppHuddleBar({
onOpenThread,
onVisibilityChange,
}: AppHuddleBarProps) {
return (
<AppProfilePanelProvider>
<HuddleBar
className="h-full"
onOpenThread={onOpenThread}
onVisibilityChange={onVisibilityChange}
/>
</AppProfilePanelProvider>
);
}
22 changes: 22 additions & 0 deletions desktop/src/app/AppProfilePanelProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as React from "react";

import { useAppNavigation } from "@/app/navigation/useAppNavigation";
import { ProfilePanelProvider } from "@/shared/context/ProfilePanelContext";

export function AppProfilePanelProvider({
children,
}: Readonly<{ children: React.ReactNode }>) {
const { goProfile } = useAppNavigation();
const handleOpenProfilePanel = React.useCallback(
(pubkey: string) => {
void goProfile(pubkey);
},
[goProfile],
);

return (
<ProfilePanelProvider onOpenProfilePanel={handleOpenProfilePanel}>
{children}
</ProfilePanelProvider>
);
}
412 changes: 206 additions & 206 deletions desktop/src/app/AppShell.tsx

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions desktop/src/app/navigation/useAppNavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,18 @@ export function useAppNavigation() {
[commitNavigation],
);

const goProfile = React.useCallback(
(pubkey: string, behavior?: NavigationBehavior) =>
commitNavigation(
{
to: "/pulse",
search: { profile: pubkey },
},
behavior,
),
[commitNavigation],
);

const goProjects = React.useCallback(
(behavior?: NavigationBehavior) =>
commitNavigation(
Expand Down Expand Up @@ -303,6 +315,7 @@ export function useAppNavigation() {
goProject,
goProjects,
goPulse,
goProfile,
goSettings,
goWorkflow,
goWorkflows,
Expand Down
64 changes: 45 additions & 19 deletions desktop/src/features/channels/ui/ChannelScreenHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
ProfileAvatarWithStatus,
scaleProfileAvatarStatusGeometry,
} from "@/features/profile/ui/ProfileAvatarWithStatus";
import { UserProfilePopover } from "@/features/profile/ui/UserProfilePopover";
import { Button } from "@/shared/ui/button";
import type { Channel, PresenceStatus } from "@/shared/api/types";
import { UserAvatar } from "@/shared/ui/UserAvatar";
Expand Down Expand Up @@ -65,6 +66,7 @@ export function ChannelScreenHeader({
const isGroupDm =
activeChannel?.channelType === "dm" &&
activeDmHeaderParticipants.length > 1;
const activeDmParticipant = activeDmHeaderParticipants[0] ?? null;
const showJoinButton =
activeChannel !== null &&
!activeChannel.isMember &&
Expand Down Expand Up @@ -113,6 +115,25 @@ export function ChannelScreenHeader({
<DmHeaderParticipantStack
participants={activeDmHeaderParticipants}
/>
) : activeDmParticipant ? (
<UserProfilePopover
pubkey={activeDmParticipant.pubkey}
triggerAriaLabel={`Open profile for ${activeChannelTitle}`}
triggerElement="span"
>
<ProfileAvatarWithStatus
avatarClassName="text-xs"
avatarUrl={activeDmAvatarUrl}
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"
/>
</UserProfilePopover>
) : (
<ProfileAvatarWithStatus
avatarClassName="text-xs"
Expand Down Expand Up @@ -152,31 +173,36 @@ function DmHeaderParticipantStack({

return (
<div
aria-hidden="true"
className="mr-1.5 flex shrink-0 items-center"
data-testid="chat-header-dm-avatar-stack"
>
{visibleParticipants.map((participant, index) => (
<div
className={index > 0 ? "-ml-2" : ""}
data-testid="chat-header-dm-avatar-stack-participant"
<UserProfilePopover
key={participant.pubkey}
style={{
zIndex: index + 1,
...(index < stackItemCount - 1 && {
mask: "radial-gradient(circle 18px at calc(100% + 4px) 50%, transparent 99%, #fff 100%)",
WebkitMask:
"radial-gradient(circle 18px at calc(100% + 4px) 50%, transparent 99%, #fff 100%)",
}),
}}
pubkey={participant.pubkey}
triggerAriaLabel={`Open profile for ${participant.displayName}`}
triggerElement="span"
>
<UserAvatar
avatarUrl={participant.avatarUrl}
className="h-8 w-8 text-xs"
displayName={participant.displayName}
size="sm"
/>
</div>
<span
className={index > 0 ? "-ml-2" : ""}
data-testid="chat-header-dm-avatar-stack-participant"
style={{
zIndex: index + 1,
...(index < stackItemCount - 1 && {
mask: "radial-gradient(circle 18px at calc(100% + 4px) 50%, transparent 99%, #fff 100%)",
WebkitMask:
"radial-gradient(circle 18px at calc(100% + 4px) 50%, transparent 99%, #fff 100%)",
}),
}}
>
<UserAvatar
avatarUrl={participant.avatarUrl}
className="h-8 w-8 text-xs"
displayName={participant.displayName}
size="sm"
/>
</span>
</UserProfilePopover>
))}
{hiddenCount > 0 ? (
<div
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from "@/features/community-members/hooks";
import { useUsersBatchQuery } from "@/features/profile/hooks";
import { ProfileAvatar } from "@/features/profile/ui/ProfileAvatar";
import { UserProfilePopover } from "@/features/profile/ui/UserProfilePopover";
import { SettingsSectionHeader } from "@/features/settings/ui/SettingsSectionHeader";
import type {
RelayMember,
Expand Down Expand Up @@ -129,11 +130,17 @@ function RelayMemberRow({
className="group/member flex min-h-14 items-center gap-3 px-1 py-2.5"
data-testid={`relay-member-row-${member.pubkey}`}
>
<ProfileAvatar
avatarUrl={profile?.avatarUrl ?? null}
className="h-9 w-9 text-xs shadow-none"
label={displayName}
/>
<UserProfilePopover
pubkey={member.pubkey}
triggerAriaLabel={`Open profile for ${displayName}`}
triggerElement="span"
>
<ProfileAvatar
avatarUrl={profile?.avatarUrl ?? null}
className="h-9 w-9 text-xs shadow-none"
label={displayName}
/>
</UserProfilePopover>
<div className="min-w-0 flex-1">
<div className="flex min-w-0 items-center gap-1.5 text-sm font-medium">
<HoverMemberIdentity
Expand Down
33 changes: 20 additions & 13 deletions desktop/src/features/huddle/components/ParticipantList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as React from "react";

import { useUsersBatchQuery } from "@/features/profile/hooks";
import { ProfileAvatar } from "@/features/profile/ui/ProfileAvatar";
import { UserProfilePopover } from "@/features/profile/ui/UserProfilePopover";
import { cn } from "@/shared/lib/cn";
import { Button } from "@/shared/ui/button";
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover";
Expand Down Expand Up @@ -86,19 +87,25 @@ export function HuddleParticipantsControl({
className="flex min-w-0 items-center gap-2 rounded-md px-2 py-1.5"
key={pubkey}
>
{profile?.displayName || profile?.avatarUrl ? (
<ProfileAvatar
avatarUrl={profile.avatarUrl ?? null}
label={profile.displayName || pubkey.slice(0, 6)}
className={cn(
"h-8 w-8 rounded-full text-2xs",
isActive &&
"ring-2 ring-green-500 ring-offset-1 ring-offset-background",
)}
/>
) : (
<HexAvatar pubkey={pubkey} isActive={isActive} size="lg" />
)}
<UserProfilePopover
pubkey={pubkey}
triggerAriaLabel={`Open profile for ${displayName}`}
triggerElement="span"
>
{profile?.displayName || profile?.avatarUrl ? (
<ProfileAvatar
avatarUrl={profile.avatarUrl ?? null}
label={profile.displayName || truncatePubkey(pubkey)}
className={cn(
"h-8 w-8 rounded-full text-2xs",
isActive &&
"ring-2 ring-green-500 ring-offset-1 ring-offset-background",
)}
/>
) : (
<HexAvatar pubkey={pubkey} isActive={isActive} size="lg" />
)}
</UserProfilePopover>

<div className="min-w-0 flex-1">
<div className="truncate text-sm font-medium">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { getDmParticipantPreview } from "@/features/channels/lib/dmParticipantDisplay";
import { UserProfilePopover } from "@/features/profile/ui/UserProfilePopover";
import { UserAvatar } from "@/shared/ui/UserAvatar";

export type DirectMessageIntroParticipant = {
Expand All @@ -18,31 +19,36 @@ export function DirectMessageIntroAvatarStack({

return (
<div
aria-hidden="true"
className="flex shrink-0 items-center"
data-testid="message-dm-intro-avatar-stack"
>
{visibleParticipants.map((participant, index) => (
<div
className={index > 0 ? "-ml-5" : ""}
data-testid="message-dm-intro-avatar-stack-participant"
<UserProfilePopover
key={participant.pubkey}
style={{
zIndex: index + 1,
...(index < stackItemCount - 1 && {
mask: "radial-gradient(circle 34px at calc(100% + 10px) 50%, transparent 99%, #fff 100%)",
WebkitMask:
"radial-gradient(circle 34px at calc(100% + 10px) 50%, transparent 99%, #fff 100%)",
}),
}}
pubkey={participant.pubkey}
triggerAriaLabel={`Open profile for ${participant.displayName}`}
triggerElement="span"
>
<UserAvatar
avatarUrl={participant.avatarUrl}
className="h-[60px] w-[60px] text-base"
displayName={participant.displayName}
size="md"
/>
</div>
<span
className={index > 0 ? "-ml-5" : ""}
data-testid="message-dm-intro-avatar-stack-participant"
style={{
zIndex: index + 1,
...(index < stackItemCount - 1 && {
mask: "radial-gradient(circle 34px at calc(100% + 10px) 50%, transparent 99%, #fff 100%)",
WebkitMask:
"radial-gradient(circle 34px at calc(100% + 10px) 50%, transparent 99%, #fff 100%)",
}),
}}
>
<UserAvatar
avatarUrl={participant.avatarUrl}
className="h-[60px] w-[60px] text-base"
displayName={participant.displayName}
size="md"
/>
</span>
</UserProfilePopover>
))}
{hiddenCount > 0 ? (
<div
Expand Down
11 changes: 10 additions & 1 deletion desktop/tests/e2e/invites-settings-screenshots.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { expect, test } from "@playwright/test";

import { waitForAnimations } from "../helpers/animations";
import { installMockBridge } from "../helpers/bridge";
import { TEST_IDENTITIES, installMockBridge } from "../helpers/bridge";
import { openSettings } from "../helpers/settings";

const OUTDIR = "test-results/invites-settings";
Expand Down Expand Up @@ -36,6 +36,15 @@ test.beforeEach(async ({ page }, testInfo) => {
await openSettings(page, "community-members");
});

test("opens a profile from a community member avatar", async ({ page }) => {
await page.getByRole("button", { name: "Open profile for alice" }).click();

await expect(page).toHaveURL(
new RegExp(`/pulse\\?profile=${TEST_IDENTITIES.alice.pubkey}$`),
);
await expect(page.getByTestId("user-profile-panel")).toBeVisible();
});

test("capture: consolidated invites settings", async ({ page }) => {
const panel = page.getByTestId("settings-panel-community-members");

Expand Down
Loading