mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Merge main (post-#3259 squash) into TTS playback speed
Retack after #3259 landed on main as squashc104eecfb. The squash severed ancestry from this branch's previous merge parent (3259 head0ef85c21b), manufacturing phantom conflicts in exactly the two known 3259x3194 interaction files. Main's blobs for both files are byte-identical to 3259's head blobs, so the resolution re-applies the already-blessed union frome8b00d43aunchanged (verified by blob hash): - huddle/tts_settings.rs:099e6ad34(3259 pocket_voice_reference + 3194 playback_speed plumbing) - testing/e2eBridge.ts:a58b76092(union arms, 245 unique labels) Also brings in #3751 (avatars fix), previously verified zero overlap with this PR's surface. Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
This commit is contained in:
commit
9ff70fff53
@@ -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