mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(desktop): open profiles from avatars (#3751)
## Summary - show profile descriptions in hover cards as a single truncated line - open the profile panel when avatars are clicked across desktop surfaces - make the direct-message intro avatar clickable ## Validation - Desktop static checks - 3,807 desktop tests via pre-push --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
+214
-214
@@ -63,7 +63,8 @@ import {
|
||||
type SettingsSection,
|
||||
isSettingsSection,
|
||||
} from "@/features/settings/ui/SettingsPanels";
|
||||
import { HuddleBar, HuddleProvider } from "@/features/huddle";
|
||||
import { HuddleProvider } from "@/features/huddle";
|
||||
import { AppHuddleBar } from "@/app/AppHuddleBar";
|
||||
import { useDueReminderBadgeCount } from "@/features/reminders/hooks";
|
||||
import { RemindMeLaterProvider } from "@/features/reminders/ui/RemindMeLaterProvider";
|
||||
import { useReminderNotifications } from "@/features/reminders/useReminderNotifications";
|
||||
@@ -97,7 +98,7 @@ import { SidebarInset, SidebarProvider } from "@/shared/ui/sidebar";
|
||||
import { RelayConnectionOverlay } from "@/app/RelayConnectionOverlay";
|
||||
import { useSidebarRelayConnectionCard } from "@/features/sidebar/ui/useSidebarRelayConnectionCard";
|
||||
import { AppShellTrayMenu } from "@/app/useAppShellTrayMenu";
|
||||
|
||||
import { AppProfilePanelProvider } from "@/app/AppProfilePanelProvider";
|
||||
const LazySettingsScreen = React.lazy(async () => {
|
||||
const module = await import("@/features/settings/ui/SettingsScreen");
|
||||
return { default: module.SettingsScreen };
|
||||
@@ -160,7 +161,6 @@ export function AppShell() {
|
||||
? locationSearchSection
|
||||
: DEFAULT_SETTINGS_SECTION;
|
||||
const startupReady = useDeferredStartup();
|
||||
|
||||
const identityQuery = useIdentityQuery();
|
||||
const { mutedChannelIds, muteChannel, unmuteChannel } = useChannelMutes(
|
||||
identityQuery.data?.pubkey,
|
||||
@@ -303,7 +303,6 @@ export function AppShell() {
|
||||
? (channels.find((channel) => channel.id === targetChannelId) ?? null)
|
||||
: null;
|
||||
}, [channels, managedChannelId, selectedChannelId]);
|
||||
|
||||
const {
|
||||
handleChannelNotification,
|
||||
handleDmNotification,
|
||||
@@ -518,7 +517,6 @@ export function AppShell() {
|
||||
},
|
||||
[applyAgents, applyCanvas, createChannelMutation, goChannel],
|
||||
);
|
||||
|
||||
const handleCreateForum = React.useCallback(
|
||||
async ({
|
||||
description,
|
||||
@@ -586,7 +584,6 @@ export function AppShell() {
|
||||
},
|
||||
[goHome, hideDmMutation, selectedChannelId],
|
||||
);
|
||||
|
||||
const handleOpenSettings = React.useCallback(
|
||||
(section: SettingsSection = DEFAULT_SETTINGS_SECTION) => {
|
||||
setIsChannelManagementOpen(false);
|
||||
@@ -594,12 +591,10 @@ export function AppShell() {
|
||||
},
|
||||
[goSettings],
|
||||
);
|
||||
|
||||
const handleCloseSettings = React.useCallback(
|
||||
() => closeSettings(),
|
||||
[closeSettings],
|
||||
);
|
||||
|
||||
// Section switches rewrite the settings entry rather than stacking one
|
||||
// history entry per section, so back always exits settings in one step.
|
||||
const handleSettingsSectionChange = React.useCallback(
|
||||
@@ -620,11 +615,8 @@ export function AppShell() {
|
||||
unreadChannelIds,
|
||||
unreadChannelNotificationCount,
|
||||
});
|
||||
// Dispatch `buzz://message` deep links into the router.
|
||||
useMessageDeepLinks();
|
||||
const handleOpenNewDm = React.useCallback(
|
||||
() => void goNewMessage(),
|
||||
[goNewMessage],
|
||||
);
|
||||
const handleOpenCreateChannel = React.useCallback(
|
||||
() => setIsCreateChannelOpen(true),
|
||||
[],
|
||||
@@ -657,7 +649,7 @@ export function AppShell() {
|
||||
|
||||
if (key === "k" && event.shiftKey) {
|
||||
event.preventDefault();
|
||||
handleOpenNewDm();
|
||||
void goNewMessage();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -686,9 +678,9 @@ export function AppShell() {
|
||||
};
|
||||
}, [
|
||||
handleOpenBrowseChannels,
|
||||
handleOpenNewDm,
|
||||
handleOpenCreateChannel,
|
||||
handleOpenSearch,
|
||||
goNewMessage,
|
||||
goHome,
|
||||
settingsOpen,
|
||||
]);
|
||||
@@ -770,216 +762,224 @@ export function AppShell() {
|
||||
/>
|
||||
) : null}
|
||||
<SidebarProvider className="min-h-0 flex-1 flex-col overflow-hidden">
|
||||
{!settingsOpen ? (
|
||||
<AppTopChrome
|
||||
canGoBack={canGoBack}
|
||||
canGoForward={canGoForward}
|
||||
hasCommunityRail={hasCommunityRail}
|
||||
onGoBack={goBack}
|
||||
onGoForward={goForward}
|
||||
/>
|
||||
) : null}
|
||||
{settingsOpen ? (
|
||||
<div className="flex min-h-0 flex-1 overflow-hidden">
|
||||
<React.Suspense fallback={null}>
|
||||
<LazySettingsScreen
|
||||
<AppProfilePanelProvider>
|
||||
{!settingsOpen ? (
|
||||
<AppTopChrome
|
||||
canGoBack={canGoBack}
|
||||
canGoForward={canGoForward}
|
||||
hasCommunityRail={hasCommunityRail}
|
||||
onGoBack={goBack}
|
||||
onGoForward={goForward}
|
||||
/>
|
||||
) : null}
|
||||
{settingsOpen ? (
|
||||
<div className="flex min-h-0 flex-1 overflow-hidden">
|
||||
<React.Suspense fallback={null}>
|
||||
<LazySettingsScreen
|
||||
currentPubkey={identityQuery.data?.pubkey}
|
||||
fallbackDisplayName={
|
||||
identityQuery.data?.displayName
|
||||
}
|
||||
isUpdatingDesktopNotifications={
|
||||
notificationSettings.isUpdatingDesktopEnabled
|
||||
}
|
||||
notificationErrorMessage={
|
||||
notificationSettings.errorMessage
|
||||
}
|
||||
notificationPermission={
|
||||
notificationSettings.permission
|
||||
}
|
||||
notificationSettings={
|
||||
notificationSettings.settings
|
||||
}
|
||||
onClose={handleCloseSettings}
|
||||
onSectionChange={handleSettingsSectionChange}
|
||||
onSetDesktopNotificationsEnabled={
|
||||
notificationSettings.setDesktopEnabled
|
||||
}
|
||||
onSetHomeBadgeEnabled={
|
||||
notificationSettings.setHomeBadgeEnabled
|
||||
}
|
||||
onSetSlotAlertsEnabled={
|
||||
notificationSettings.setSlotAlertsEnabled
|
||||
}
|
||||
onSetNotifyWhileViewing={
|
||||
notificationSettings.setNotifyWhileViewing
|
||||
}
|
||||
onSetAllSlotAlertsEnabled={
|
||||
notificationSettings.setAllSlotAlertsEnabled
|
||||
}
|
||||
onSetSoundForSlot={
|
||||
notificationSettings.setSoundForSlot
|
||||
}
|
||||
section={settingsSection}
|
||||
/>
|
||||
</React.Suspense>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex min-h-0 flex-1 overflow-hidden">
|
||||
<AppSidebar
|
||||
activeCommunity={communitiesHook.activeCommunity}
|
||||
channels={sidebarChannels}
|
||||
currentPubkey={identityQuery.data?.pubkey}
|
||||
errorMessage={channelsErrorMessage}
|
||||
fallbackDisplayName={
|
||||
identityQuery.data?.displayName
|
||||
}
|
||||
isUpdatingDesktopNotifications={
|
||||
notificationSettings.isUpdatingDesktopEnabled
|
||||
}
|
||||
notificationErrorMessage={
|
||||
notificationSettings.errorMessage
|
||||
}
|
||||
notificationPermission={
|
||||
notificationSettings.permission
|
||||
}
|
||||
notificationSettings={notificationSettings.settings}
|
||||
onClose={handleCloseSettings}
|
||||
onSectionChange={handleSettingsSectionChange}
|
||||
onSetDesktopNotificationsEnabled={
|
||||
notificationSettings.setDesktopEnabled
|
||||
}
|
||||
onSetHomeBadgeEnabled={
|
||||
notificationSettings.setHomeBadgeEnabled
|
||||
}
|
||||
onSetSlotAlertsEnabled={
|
||||
notificationSettings.setSlotAlertsEnabled
|
||||
}
|
||||
onSetNotifyWhileViewing={
|
||||
notificationSettings.setNotifyWhileViewing
|
||||
}
|
||||
onSetAllSlotAlertsEnabled={
|
||||
notificationSettings.setAllSlotAlertsEnabled
|
||||
}
|
||||
onSetSoundForSlot={
|
||||
notificationSettings.setSoundForSlot
|
||||
}
|
||||
section={settingsSection}
|
||||
/>
|
||||
</React.Suspense>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex min-h-0 flex-1 overflow-hidden">
|
||||
<AppSidebar
|
||||
activeCommunity={communitiesHook.activeCommunity}
|
||||
channels={sidebarChannels}
|
||||
currentPubkey={identityQuery.data?.pubkey}
|
||||
errorMessage={channelsErrorMessage}
|
||||
fallbackDisplayName={identityQuery.data?.displayName}
|
||||
homeBadgeCount={homeBadgeCount + dueReminderBadge}
|
||||
addCommunityPrefill={addCommunityDialog.prefill}
|
||||
isAddCommunityOpen={addCommunityDialog.open}
|
||||
relayConnectionCard={relayConnectionCard}
|
||||
isCreatingChannel={createChannelMutation.isPending}
|
||||
isCreatingForum={createForumMutation.isPending}
|
||||
isLoading={channelsQuery.isLoading}
|
||||
isCreateChannelOpen={isCreateChannelOpen}
|
||||
isPresencePending={presenceSession.isPending}
|
||||
onAddCommunity={(community) => {
|
||||
const id = communitiesHook.addCommunity({
|
||||
...community,
|
||||
pubkey:
|
||||
community.pubkey ?? identityQuery.data?.pubkey,
|
||||
});
|
||||
handleSwitchCommunity(id);
|
||||
}}
|
||||
onAddCommunityOpenChange={
|
||||
addCommunityDialog.onOpenChange
|
||||
}
|
||||
onNewMessage={handleOpenNewDm}
|
||||
onBackgroundClick={requestFocusedThreadClose}
|
||||
onCreateChannelOpenChange={setIsCreateChannelOpen}
|
||||
onOpenAddCommunity={addCommunityDialog.openDialog}
|
||||
onSendFeedback={() => setIsSendFeedbackOpen(true)}
|
||||
onUpdateCommunity={communitiesHook.updateCommunity}
|
||||
onRemoveCommunity={(id) =>
|
||||
void handleRemoveCommunity(id)
|
||||
}
|
||||
onSwitchCommunity={handleSwitchCommunity}
|
||||
onCreateAgent={() => requestOpenCreateAgent()}
|
||||
selfPresenceStatus={presenceSession.currentStatus}
|
||||
communities={communitiesHook.communities}
|
||||
onCreateChannel={handleCreateChannel}
|
||||
onCreateForum={handleCreateForum}
|
||||
onHideDm={handleHideDm}
|
||||
onMarkAllChannelsRead={markAllChannelsRead}
|
||||
onMarkChannelRead={markChannelRead}
|
||||
onMarkChannelUnread={markChannelUnread}
|
||||
onBrowseChannels={handleOpenBrowseChannels}
|
||||
onOpenDm={async ({ pubkeys }) => {
|
||||
const directMessage =
|
||||
await openDmMutation.mutateAsync({
|
||||
pubkeys,
|
||||
homeBadgeCount={homeBadgeCount + dueReminderBadge}
|
||||
addCommunityPrefill={addCommunityDialog.prefill}
|
||||
isAddCommunityOpen={addCommunityDialog.open}
|
||||
relayConnectionCard={relayConnectionCard}
|
||||
isCreatingChannel={createChannelMutation.isPending}
|
||||
isCreatingForum={createForumMutation.isPending}
|
||||
isLoading={channelsQuery.isLoading}
|
||||
isCreateChannelOpen={isCreateChannelOpen}
|
||||
isPresencePending={presenceSession.isPending}
|
||||
onAddCommunity={(community) => {
|
||||
const id = communitiesHook.addCommunity({
|
||||
...community,
|
||||
pubkey:
|
||||
community.pubkey ??
|
||||
identityQuery.data?.pubkey,
|
||||
});
|
||||
await goChannel(directMessage.id);
|
||||
}}
|
||||
onSelectAgents={() => void goAgents()}
|
||||
onSelectChannel={(channelId) =>
|
||||
void goChannel(channelId)
|
||||
}
|
||||
onOpenSearchResult={handleOpenSearchResult}
|
||||
searchChannels={channels}
|
||||
searchFocusRequest={searchFocusRequest}
|
||||
onSelectHome={() => void goHome()}
|
||||
onSelectProjects={() => void goProjects()}
|
||||
onSelectPulse={() => void goPulse()}
|
||||
onSelectSettings={handleOpenSettings}
|
||||
onSelectWorkflows={() => void goWorkflows()}
|
||||
onSetPresenceStatus={(status) =>
|
||||
presenceSession.setStatus(status)
|
||||
}
|
||||
onSetUserStatus={(text, emoji) =>
|
||||
setUserStatusMutation.mutate({ text, emoji })
|
||||
}
|
||||
onClearUserStatus={() =>
|
||||
setUserStatusMutation.mutate({
|
||||
text: "",
|
||||
emoji: "",
|
||||
})
|
||||
}
|
||||
profile={profileQuery.data}
|
||||
selfUserStatus={
|
||||
deferredPubkey
|
||||
? (selfStatusQuery.data?.[
|
||||
deferredPubkey.toLowerCase()
|
||||
] ?? undefined)
|
||||
: undefined
|
||||
}
|
||||
selectedChannelId={selectedChannelId}
|
||||
selectedView={selectedView}
|
||||
unreadChannelIds={unreadChannelIds}
|
||||
unreadChannelCounts={unreadChannelCounts}
|
||||
mutedChannelIds={mutedChannelIds}
|
||||
onMuteChannel={muteChannel}
|
||||
onUnmuteChannel={unmuteChannel}
|
||||
starredChannelIds={starredChannelIds}
|
||||
onStarChannel={starChannel}
|
||||
onUnstarChannel={unstarChannel}
|
||||
/>
|
||||
<MainInsetProvider mainInsetRef={mainInsetRef}>
|
||||
<SidebarInset
|
||||
ref={mainInsetRef}
|
||||
className="isolate min-h-0 min-w-0 overflow-hidden bg-sidebar"
|
||||
data-buzz-glass-inset
|
||||
data-buzz-shadow-viewport
|
||||
style={chromeCssVarDefaults as React.CSSProperties}
|
||||
>
|
||||
<BuzzTheme.ContentSurface>
|
||||
<Outlet />
|
||||
</BuzzTheme.ContentSurface>
|
||||
</SidebarInset>
|
||||
</MainInsetProvider>
|
||||
<RelayConnectionOverlay
|
||||
card={relayConnectionCard}
|
||||
errorMessage={channelsErrorMessage}
|
||||
hasCommunityRail={hasCommunityRail}
|
||||
isHuddleDrawerOpen={isHuddleDrawerOpen}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<RequestedAgentCreateDialogs />
|
||||
<AgentManagementDialogs />
|
||||
<AppShellOverlays
|
||||
activeChannel={managedChannel}
|
||||
browseDialogType={browseDialogType}
|
||||
channels={channels}
|
||||
currentPubkey={identityQuery.data?.pubkey}
|
||||
isChannelManagementOpen={isChannelManagementOpen}
|
||||
isCreatingBrowseChannel={
|
||||
createChannelMutation.isPending ||
|
||||
createForumMutation.isPending
|
||||
}
|
||||
onBrowseChannelJoin={handleBrowseChannelJoin}
|
||||
onBrowseChannelCreate={handleBrowseChannelCreate}
|
||||
onBrowseDialogOpenChange={handleBrowseDialogOpenChange}
|
||||
onChannelManagementOpenChange={(open) => {
|
||||
setIsChannelManagementOpen(open);
|
||||
if (!open) {
|
||||
setManagedChannelId(null);
|
||||
handleSwitchCommunity(id);
|
||||
}}
|
||||
onAddCommunityOpenChange={
|
||||
addCommunityDialog.onOpenChange
|
||||
}
|
||||
onNewMessage={goNewMessage}
|
||||
onBackgroundClick={requestFocusedThreadClose}
|
||||
onCreateChannelOpenChange={setIsCreateChannelOpen}
|
||||
onOpenAddCommunity={addCommunityDialog.openDialog}
|
||||
onSendFeedback={() => setIsSendFeedbackOpen(true)}
|
||||
onUpdateCommunity={communitiesHook.updateCommunity}
|
||||
onRemoveCommunity={(id) =>
|
||||
void handleRemoveCommunity(id)
|
||||
}
|
||||
onSwitchCommunity={handleSwitchCommunity}
|
||||
onCreateAgent={() => requestOpenCreateAgent()}
|
||||
selfPresenceStatus={presenceSession.currentStatus}
|
||||
communities={communitiesHook.communities}
|
||||
onCreateChannel={handleCreateChannel}
|
||||
onCreateForum={handleCreateForum}
|
||||
onHideDm={handleHideDm}
|
||||
onMarkAllChannelsRead={markAllChannelsRead}
|
||||
onMarkChannelRead={markChannelRead}
|
||||
onMarkChannelUnread={markChannelUnread}
|
||||
onBrowseChannels={handleOpenBrowseChannels}
|
||||
onOpenDm={async ({ pubkeys }) => {
|
||||
const directMessage =
|
||||
await openDmMutation.mutateAsync({
|
||||
pubkeys,
|
||||
});
|
||||
await goChannel(directMessage.id);
|
||||
}}
|
||||
onSelectAgents={() => void goAgents()}
|
||||
onSelectChannel={(channelId) =>
|
||||
void goChannel(channelId)
|
||||
}
|
||||
onOpenSearchResult={handleOpenSearchResult}
|
||||
searchChannels={channels}
|
||||
searchFocusRequest={searchFocusRequest}
|
||||
onSelectHome={() => void goHome()}
|
||||
onSelectProjects={() => void goProjects()}
|
||||
onSelectPulse={() => void goPulse()}
|
||||
onSelectSettings={handleOpenSettings}
|
||||
onSelectWorkflows={() => void goWorkflows()}
|
||||
onSetPresenceStatus={(status) =>
|
||||
presenceSession.setStatus(status)
|
||||
}
|
||||
onSetUserStatus={(text, emoji) =>
|
||||
setUserStatusMutation.mutate({ text, emoji })
|
||||
}
|
||||
onClearUserStatus={() =>
|
||||
setUserStatusMutation.mutate({
|
||||
text: "",
|
||||
emoji: "",
|
||||
})
|
||||
}
|
||||
profile={profileQuery.data}
|
||||
selfUserStatus={
|
||||
deferredPubkey
|
||||
? (selfStatusQuery.data?.[
|
||||
deferredPubkey.toLowerCase()
|
||||
] ?? undefined)
|
||||
: undefined
|
||||
}
|
||||
selectedChannelId={selectedChannelId}
|
||||
selectedView={selectedView}
|
||||
unreadChannelIds={unreadChannelIds}
|
||||
unreadChannelCounts={unreadChannelCounts}
|
||||
mutedChannelIds={mutedChannelIds}
|
||||
onMuteChannel={muteChannel}
|
||||
onUnmuteChannel={unmuteChannel}
|
||||
starredChannelIds={starredChannelIds}
|
||||
onStarChannel={starChannel}
|
||||
onUnstarChannel={unstarChannel}
|
||||
/>
|
||||
<MainInsetProvider mainInsetRef={mainInsetRef}>
|
||||
<SidebarInset
|
||||
ref={mainInsetRef}
|
||||
className="isolate min-h-0 min-w-0 overflow-hidden bg-sidebar"
|
||||
data-buzz-glass-inset
|
||||
data-buzz-shadow-viewport
|
||||
style={
|
||||
chromeCssVarDefaults as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<BuzzTheme.ContentSurface>
|
||||
<Outlet />
|
||||
</BuzzTheme.ContentSurface>
|
||||
</SidebarInset>
|
||||
</MainInsetProvider>
|
||||
<RelayConnectionOverlay
|
||||
card={relayConnectionCard}
|
||||
errorMessage={channelsErrorMessage}
|
||||
hasCommunityRail={hasCommunityRail}
|
||||
isHuddleDrawerOpen={isHuddleDrawerOpen}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<RequestedAgentCreateDialogs />
|
||||
<AgentManagementDialogs />
|
||||
<AppShellOverlays
|
||||
activeChannel={managedChannel}
|
||||
browseDialogType={browseDialogType}
|
||||
channels={channels}
|
||||
currentPubkey={identityQuery.data?.pubkey}
|
||||
isChannelManagementOpen={isChannelManagementOpen}
|
||||
isCreatingBrowseChannel={
|
||||
createChannelMutation.isPending ||
|
||||
createForumMutation.isPending
|
||||
}
|
||||
}}
|
||||
onDeleteActiveChannel={() => {
|
||||
setIsChannelManagementOpen(false);
|
||||
setManagedChannelId(null);
|
||||
void goHome({ replace: true });
|
||||
}}
|
||||
onSelectChannel={(channelId) => {
|
||||
void goChannel(channelId);
|
||||
}}
|
||||
/>
|
||||
<SendFeedbackController
|
||||
onOpenChange={setIsSendFeedbackOpen}
|
||||
open={isSendFeedbackOpen}
|
||||
/>
|
||||
onBrowseChannelJoin={handleBrowseChannelJoin}
|
||||
onBrowseChannelCreate={handleBrowseChannelCreate}
|
||||
onBrowseDialogOpenChange={handleBrowseDialogOpenChange}
|
||||
onChannelManagementOpenChange={(open) => {
|
||||
setIsChannelManagementOpen(open);
|
||||
if (!open) {
|
||||
setManagedChannelId(null);
|
||||
}
|
||||
}}
|
||||
onDeleteActiveChannel={() => {
|
||||
setIsChannelManagementOpen(false);
|
||||
setManagedChannelId(null);
|
||||
void goHome({ replace: true });
|
||||
}}
|
||||
onSelectChannel={(channelId) => {
|
||||
void goChannel(channelId);
|
||||
}}
|
||||
/>
|
||||
<SendFeedbackController
|
||||
onOpenChange={setIsSendFeedbackOpen}
|
||||
open={isSendFeedbackOpen}
|
||||
/>
|
||||
</AppProfilePanelProvider>
|
||||
</SidebarProvider>
|
||||
</div>
|
||||
|
||||
<div className="absolute inset-x-0 bottom-0 z-0 h-(--buzz-huddle-drawer-height)">
|
||||
<HuddleBar
|
||||
className="h-full"
|
||||
<AppHuddleBar
|
||||
onOpenThread={(channelId, messageId) => {
|
||||
void goChannel(channelId, {
|
||||
messageId,
|
||||
|
||||
@@ -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(
|
||||
@@ -303,6 +315,7 @@ export function useAppNavigation() {
|
||||
goProject,
|
||||
goProjects,
|
||||
goPulse,
|
||||
goProfile,
|
||||
goSettings,
|
||||
goWorkflow,
|
||||
goWorkflows,
|
||||
|
||||
@@ -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";
|
||||
@@ -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 &&
|
||||
@@ -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"
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
@@ -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">
|
||||
|
||||
@@ -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 = {
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
@@ -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");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user