[codex] Fix settings dialog open jank (#254)

This commit is contained in:
Wes
2026-04-06 19:43:29 -07:00
committed by GitHub
parent 41d56cfcbf
commit dde459dc5d
4 changed files with 53 additions and 81 deletions
+24 -31
View File
@@ -20,6 +20,7 @@ import { useHomeFeedNotifications } from "@/features/notifications/hooks";
import { usePresenceSession } from "@/features/presence/hooks";
import { useProfileQuery } from "@/features/profile/hooks";
import type { SettingsSection } from "@/features/settings/ui/SettingsPanels";
import { SettingsScreen } from "@/features/settings/ui/SettingsScreen";
import { AppSidebar } from "@/features/sidebar/ui/AppSidebar";
import { relayClient } from "@/shared/api/relayClient";
import { useIdentityQuery } from "@/shared/api/hooks";
@@ -44,10 +45,6 @@ const ChannelScreen = React.lazy(async () => {
const module = await import("@/features/channels/ui/ChannelScreen");
return { default: module.ChannelScreen };
});
const SettingsScreen = React.lazy(async () => {
const module = await import("@/features/settings/ui/SettingsScreen");
return { default: module.SettingsScreen };
});
const WorkflowsScreen = React.lazy(async () => {
const module = await import("@/features/workflows/ui/WorkflowsScreen");
return { default: module.WorkflowsScreen };
@@ -472,33 +469,29 @@ export function AppShell() {
/>
{settingsOpen ? (
<React.Suspense
fallback={<ViewLoadingFallback label="Loading settings..." />}
>
<SettingsScreen
currentPubkey={identityQuery.data?.pubkey}
fallbackDisplayName={identityQuery.data?.displayName}
isUpdatingDesktopNotifications={
notificationSettings.isUpdatingDesktopEnabled
}
notificationErrorMessage={notificationSettings.errorMessage}
notificationPermission={notificationSettings.permission}
notificationSettings={notificationSettings.settings}
onClose={handleCloseSettings}
onSectionChange={setSettingsSection}
onSetDesktopNotificationsEnabled={
notificationSettings.setDesktopEnabled
}
onSetHomeBadgeEnabled={notificationSettings.setHomeBadgeEnabled}
onSetMentionNotificationsEnabled={
notificationSettings.setMentionsEnabled
}
onSetNeedsActionNotificationsEnabled={
notificationSettings.setNeedsActionEnabled
}
section={settingsSection}
/>
</React.Suspense>
<SettingsScreen
currentPubkey={identityQuery.data?.pubkey}
fallbackDisplayName={identityQuery.data?.displayName}
isUpdatingDesktopNotifications={
notificationSettings.isUpdatingDesktopEnabled
}
notificationErrorMessage={notificationSettings.errorMessage}
notificationPermission={notificationSettings.permission}
notificationSettings={notificationSettings.settings}
onClose={handleCloseSettings}
onSectionChange={setSettingsSection}
onSetDesktopNotificationsEnabled={
notificationSettings.setDesktopEnabled
}
onSetHomeBadgeEnabled={notificationSettings.setHomeBadgeEnabled}
onSetMentionNotificationsEnabled={
notificationSettings.setMentionsEnabled
}
onSetNeedsActionNotificationsEnabled={
notificationSettings.setNeedsActionEnabled
}
section={settingsSection}
/>
) : null}
</SidebarProvider>
</ChannelNavigationProvider>
@@ -133,8 +133,10 @@ export function ProfilePopover({
className={MENU_ITEM_CLASS}
data-testid="profile-popover-settings"
onClick={() => {
onOpenSettings();
onOpenChange(false);
window.requestAnimationFrame(() => {
onOpenSettings();
});
}}
role="menuitem"
type="button"
@@ -1,14 +1,7 @@
import * as React from "react";
import type { DesktopNotificationPermissionState } from "@/features/notifications/hooks";
import type { NotificationSettings } from "@/features/notifications/hooks";
import type { SettingsSection } from "@/features/settings/ui/SettingsPanels";
import { ViewLoadingFallback } from "@/shared/ui/ViewLoadingFallback";
const SettingsView = React.lazy(async () => {
const module = await import("@/features/settings/ui/SettingsView");
return { default: module.SettingsView };
});
import { SettingsView } from "@/features/settings/ui/SettingsView";
type SettingsScreenProps = {
currentPubkey?: string;
@@ -42,26 +35,22 @@ export function SettingsScreen({
section,
}: SettingsScreenProps) {
return (
<React.Suspense
fallback={<ViewLoadingFallback label="Loading settings..." />}
>
<SettingsView
currentPubkey={currentPubkey}
fallbackDisplayName={fallbackDisplayName}
isUpdatingDesktopNotifications={isUpdatingDesktopNotifications}
notificationErrorMessage={notificationErrorMessage}
notificationPermission={notificationPermission}
notificationSettings={notificationSettings}
onClose={onClose}
onSectionChange={onSectionChange}
onSetDesktopNotificationsEnabled={onSetDesktopNotificationsEnabled}
onSetHomeBadgeEnabled={onSetHomeBadgeEnabled}
onSetMentionNotificationsEnabled={onSetMentionNotificationsEnabled}
onSetNeedsActionNotificationsEnabled={
onSetNeedsActionNotificationsEnabled
}
section={section}
/>
</React.Suspense>
<SettingsView
currentPubkey={currentPubkey}
fallbackDisplayName={fallbackDisplayName}
isUpdatingDesktopNotifications={isUpdatingDesktopNotifications}
notificationErrorMessage={notificationErrorMessage}
notificationPermission={notificationPermission}
notificationSettings={notificationSettings}
onClose={onClose}
onSectionChange={onSectionChange}
onSetDesktopNotificationsEnabled={onSetDesktopNotificationsEnabled}
onSetHomeBadgeEnabled={onSetHomeBadgeEnabled}
onSetMentionNotificationsEnabled={onSetMentionNotificationsEnabled}
onSetNeedsActionNotificationsEnabled={
onSetNeedsActionNotificationsEnabled
}
section={section}
/>
);
}
@@ -22,13 +22,11 @@ type SettingsViewProps = SettingsPanelProps & {
function SettingsSectionButton({
active,
index,
isLoaded,
onSelect,
section,
}: {
active: boolean;
index: number;
isLoaded: boolean;
onSelect: (section: SettingsSection) => void;
section: (typeof settingsSections)[number];
@@ -39,7 +37,7 @@ function SettingsSectionButton({
<button
aria-pressed={active}
className={cn(
"group inline-flex min-w-fit items-center gap-2 rounded-lg border px-3 py-2 text-sm font-medium whitespace-nowrap motion-safe:transition-all motion-safe:duration-300 motion-safe:ease-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
"group inline-flex min-w-fit items-center gap-2 rounded-lg border px-3 py-2 text-sm font-medium whitespace-nowrap motion-safe:transition-all motion-safe:duration-200 motion-safe:ease-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
active
? "border-border bg-background text-foreground shadow-sm"
: "border-transparent bg-transparent text-muted-foreground hover:bg-background/70 hover:text-foreground",
@@ -47,7 +45,6 @@ function SettingsSectionButton({
)}
data-testid={`settings-nav-${section.value}`}
onClick={() => onSelect(section.value)}
style={{ transitionDelay: isLoaded ? "0ms" : `${index * 40 + 300}ms` }}
type="button"
>
<Icon
@@ -78,14 +75,12 @@ export function SettingsView({
onSetNeedsActionNotificationsEnabled,
section,
}: SettingsViewProps) {
// Entrance animation state
const [isLoaded, setIsLoaded] = React.useState(false);
React.useEffect(() => {
const timer = setTimeout(() => setIsLoaded(true), 50);
return () => clearTimeout(timer);
const frameId = window.requestAnimationFrame(() => setIsLoaded(true));
return () => window.cancelAnimationFrame(frameId);
}, []);
// Escape key to close
React.useEffect(() => {
function handleKeyDown(event: KeyboardEvent) {
if (event.key === "Escape" && !event.defaultPrevented) {
@@ -101,32 +96,29 @@ export function SettingsView({
return (
<div
className={cn(
"fixed inset-0 z-50 flex items-center justify-center motion-safe:transition-opacity motion-safe:duration-300",
"fixed inset-0 z-50 flex items-center justify-center motion-safe:transition-opacity motion-safe:duration-200",
isLoaded ? "opacity-100" : "opacity-0",
)}
data-testid="settings-overlay"
>
{/* Backdrop — click to close */}
<div
aria-hidden="true"
className="absolute inset-0 bg-background/80 backdrop-blur-sm"
onClick={onClose}
/>
{/* Modal container */}
{/* biome-ignore lint/a11y/useKeyWithClickEvents: Click stops propagation to backdrop; keyboard dismiss handled by Escape key */}
{/* biome-ignore lint/a11y/useKeyWithClickEvents: Click stops propagation to backdrop; keyboard dismiss handled by Escape key. */}
<div
aria-labelledby="settings-title"
aria-modal="true"
className={cn(
"relative mx-auto flex h-[min(600px,calc(100vh-8rem))] w-[calc(100%-4rem)] max-w-3xl flex-col overflow-hidden rounded-xl border border-border bg-background shadow-lg motion-safe:transition-all motion-safe:duration-500 motion-safe:ease-out",
"relative mx-auto flex h-[min(600px,calc(100vh-8rem))] w-[calc(100%-4rem)] max-w-3xl flex-col overflow-hidden rounded-xl border border-border bg-background shadow-lg motion-safe:transition-all motion-safe:duration-200 motion-safe:ease-out",
isLoaded ? "opacity-100 scale-100" : "opacity-0 scale-95",
)}
data-testid="settings-view"
onClick={(e) => e.stopPropagation()}
role="dialog"
>
{/* Header with title and close button */}
<header
className="flex shrink-0 items-center justify-between border-b border-border px-4 py-3"
data-tauri-drag-region
@@ -149,12 +141,10 @@ export function SettingsView({
</button>
</header>
{/* Two-column layout */}
<div className="grid min-h-0 flex-1 grid-rows-[auto_minmax(0,1fr)] overflow-hidden lg:grid-cols-[220px_minmax(0,1fr)] lg:grid-rows-1">
{/* Sidebar nav */}
<aside
className={cn(
"border-b border-border/70 bg-muted/20 motion-safe:transition-all motion-safe:duration-700 motion-safe:ease-out lg:border-b-0 lg:border-r",
"border-b border-border/70 bg-muted/20 motion-safe:transition-all motion-safe:duration-200 motion-safe:ease-out lg:border-b-0 lg:border-r",
isLoaded
? "opacity-100 translate-x-0"
: "opacity-0 -translate-x-2",
@@ -164,10 +154,9 @@ export function SettingsView({
aria-label="Settings sections"
className="flex gap-1 overflow-x-auto px-3 py-3 lg:flex-col lg:overflow-y-auto lg:pt-1"
>
{settingsSections.map((entry, index) => (
{settingsSections.map((entry) => (
<SettingsSectionButton
active={entry.value === section}
index={index}
isLoaded={isLoaded}
key={entry.value}
onSelect={onSectionChange}
@@ -177,7 +166,6 @@ export function SettingsView({
</nav>
</aside>
{/* Content area */}
<section className="min-h-0 overflow-y-auto px-4 py-4 sm:px-6">
<div
className="mx-auto flex w-full max-w-4xl flex-col gap-4"