Skip to content

Commit c30d727

Browse files
authored
Refine app loading skeletons (#1001)
1 parent ee34ca8 commit c30d727

14 files changed

Lines changed: 1419 additions & 330 deletions

desktop/src/app/AppShell.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ export function AppShell() {
186186
React.useState(false);
187187
const [searchFocusRequest, setSearchFocusRequest] = React.useState(0);
188188
const [topbarSearchHidden, setTopbarSearchHidden] = React.useState(false);
189+
const [topbarSearchLoading, setTopbarSearchLoading] = React.useState(false);
189190
const [browseDialogType, setBrowseDialogType] =
190191
React.useState<BrowseDialogType>(null);
191192
const [isNewDmOpen, setIsNewDmOpen] = React.useState(false);
@@ -746,6 +747,7 @@ export function AppShell() {
746747
isFollowingThread,
747748
isNotifiedForThread,
748749
setTopbarSearchHidden,
750+
setTopbarSearchLoading,
749751
threadActivityItems,
750752
}}
751753
>
@@ -765,6 +767,7 @@ export function AppShell() {
765767
}}
766768
onOpenResult={handleOpenSearchResult}
767769
searchHidden={topbarSearchHidden}
770+
searchLoading={topbarSearchLoading}
768771
searchFocusRequest={searchFocusRequest}
769772
/>
770773
) : null}

desktop/src/app/AppShellContext.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ type AppShellContextValue = {
2222
isFollowingThread: (rootId: string) => boolean;
2323
isNotifiedForThread: (rootId: string) => boolean;
2424
setTopbarSearchHidden: (hidden: boolean) => void;
25+
setTopbarSearchLoading: (loading: boolean) => void;
2526
threadActivityItems: ThreadActivityItem[];
2627
};
2728

@@ -38,6 +39,7 @@ const AppShellContext = React.createContext<AppShellContextValue>({
3839
isFollowingThread: () => false,
3940
isNotifiedForThread: () => false,
4041
setTopbarSearchHidden: () => {},
42+
setTopbarSearchLoading: () => {},
4143
threadActivityItems: [],
4244
});
4345

desktop/src/features/agents/ui/UnifiedAgentsSection.tsx

Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import type {
1919
} from "@/shared/api/types";
2020
import { Badge } from "@/shared/ui/badge";
2121
import { Button } from "@/shared/ui/button";
22-
import { Card } from "@/shared/ui/card";
2322
import {
2423
DropdownMenu,
2524
DropdownMenuContent,
@@ -332,7 +331,7 @@ export function UnifiedAgentsSection(props: UnifiedAgentsSectionProps) {
332331
size="sm"
333332
variant="ghost"
334333
>
335-
<Trash2 className="mr-1.5 h-3.5 w-3.5" />
334+
<Trash2 className="mr-1.5 h-4 w-4" />
336335
Remove stopped
337336
</Button>
338337
</div>
@@ -474,18 +473,72 @@ function SectionHeader({
474473

475474
function LoadingSkeleton() {
476475
return (
477-
<Card className="overflow-hidden">
478-
{["a", "b", "c"].map((k) => (
476+
<div className="space-y-3">
477+
{["a", "b", "c"].map((k, index) => (
479478
<div
480-
className="flex items-center gap-4 border-b border-border/60 px-4 py-3 last:border-b-0"
479+
className="overflow-hidden rounded-xl border border-border/70 bg-card/40"
481480
key={k}
482481
>
483-
<Skeleton className="h-8 w-8 rounded-lg" />
484-
<Skeleton className="h-4 w-28" />
485-
<Skeleton className="h-5 w-16 rounded-full" />
482+
<div className="flex items-center gap-2 px-3 py-2">
483+
<div className="flex min-w-0 flex-1 items-center gap-2 py-1">
484+
<Skeleton className="h-4 w-4 shrink-0 rounded-sm" />
485+
<Skeleton className="h-8 w-8 shrink-0 rounded-lg" />
486+
<div className="flex min-w-0 items-center gap-2">
487+
<Skeleton className={index === 2 ? "h-4 w-36" : "h-4 w-32"} />
488+
<Skeleton className="h-5 w-14 rounded-full" />
489+
</div>
490+
<Skeleton className="ml-1 h-3 w-20 shrink-0" />
491+
</div>
492+
{index === 1 ? (
493+
<Skeleton className="h-5 w-16 rounded-full" />
494+
) : null}
495+
<Skeleton className="h-8 w-8 shrink-0 rounded-lg" />
496+
</div>
497+
<div className="divide-y divide-border/50 border-t border-border/50">
498+
<div className="flex items-start gap-3 px-4 py-3">
499+
<div className="min-w-0 flex-1">
500+
<div className="grid gap-3 lg:grid-cols-[minmax(0,1.8fr)_minmax(120px,0.8fr)_minmax(0,1.1fr)] lg:gap-4">
501+
<div className="min-w-0">
502+
<div className="flex items-start gap-3">
503+
<Skeleton className="mt-0.5 h-4 w-4 shrink-0 rounded-sm" />
504+
<Skeleton className="mt-1 h-2 w-2 shrink-0 rounded-full" />
505+
<div className="min-w-0 flex-1">
506+
<div className="flex flex-wrap items-center gap-2">
507+
<Skeleton className="h-4 w-36" />
508+
<Skeleton className="h-5 w-16 rounded-full" />
509+
</div>
510+
<div className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1">
511+
<Skeleton className="h-3 w-20" />
512+
<Skeleton className="h-3 w-24" />
513+
</div>
514+
{index === 0 ? (
515+
<div className="mt-1.5 flex flex-wrap items-center gap-1.5">
516+
<Skeleton className="h-5 w-20 rounded-full" />
517+
<Skeleton className="h-5 w-24 rounded-full" />
518+
</div>
519+
) : null}
520+
</div>
521+
</div>
522+
</div>
523+
<div className="space-y-1.5">
524+
<Skeleton className="h-5 w-20 rounded-full" />
525+
<Skeleton className="h-3 w-24" />
526+
</div>
527+
<div className="space-y-1.5">
528+
<Skeleton className="h-4 w-28" />
529+
<Skeleton className="h-3 w-20" />
530+
</div>
531+
</div>
532+
</div>
533+
<div className="flex shrink-0 items-start gap-2 lg:pt-0.5">
534+
<Skeleton className="h-7 w-24 rounded-md" />
535+
<Skeleton className="h-7 w-7 rounded-md" />
536+
</div>
537+
</div>
538+
</div>
486539
</div>
487540
))}
488-
</Card>
541+
</div>
489542
);
490543
}
491544

desktop/src/features/channels/ui/ChannelPane.tsx

Lines changed: 56 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import { Bot, Hash, LogIn, Plus, Sparkles, UserPlus } from "lucide-react";
44
import { useMediaUpload } from "@/features/messages/lib/useMediaUpload";
55
import { MessageComposer } from "@/features/messages/ui/MessageComposer";
66
import { DropZoneOverlay } from "@/features/messages/ui/ComposerAttachments";
7-
import { MessageThreadPanel } from "@/features/messages/ui/MessageThreadPanel";
7+
import {
8+
MessageThreadPanel,
9+
MessageThreadPanelSkeleton,
10+
} from "@/features/messages/ui/MessageThreadPanel";
811
import { MessageTimeline } from "@/features/messages/ui/MessageTimeline";
912
import type { ImetaMedia } from "@/features/messages/lib/imetaMediaMarkdown";
1013
import { useComposerHeightPadding } from "@/features/messages/ui/useComposerHeightPadding";
@@ -714,7 +717,7 @@ export const ChannelPane = React.memo(function ChannelPane({
714717
size="sm"
715718
variant="default"
716719
>
717-
<LogIn className="mr-1.5 h-3.5 w-3.5" />
720+
<LogIn className="mr-1.5 h-4 w-4" />
718721
{isJoining ? "Joining..." : "Join to participate"}
719722
</Button>
720723
</div>
@@ -854,27 +857,15 @@ export const ChannelPane = React.memo(function ChannelPane({
854857
panel
855858
);
856859
})()
857-
: activeChannel && selectedAgent
860+
: openThreadHeadId && activeChannel
858861
? (() => {
859862
const panel = (
860-
<AgentSessionThreadPanel
861-
agent={selectedAgent}
862-
canInterruptTurn={selectedAgent.canInterruptTurn}
863-
channel={activeChannel}
864-
isWorking={botTypingEntries.some(
865-
(entry) =>
866-
entry.pubkey.toLowerCase() ===
867-
selectedAgent.pubkey.toLowerCase(),
868-
)}
863+
<MessageThreadPanelSkeleton
869864
isSinglePanelView={
870865
useSplitAuxiliaryPane ? false : isSinglePanelView
871866
}
872867
layout={useSplitAuxiliaryPane ? "split" : "standalone"}
873-
profiles={profiles}
874-
onBackToProfile={() =>
875-
onOpenProfilePanel(selectedAgent.pubkey)
876-
}
877-
onClose={onCloseAgentSession}
868+
onClose={onCloseThread}
878869
widthPx={threadPanelWidthPx}
879870
/>
880871
);
@@ -883,7 +874,7 @@ export const ChannelPane = React.memo(function ChannelPane({
883874
canResetWidth={canResetThreadPanelWidth}
884875
onResetWidth={onResetThreadPanelWidth}
885876
onResizeStart={onThreadPanelResizeStart}
886-
testId="agent-session-thread-panel"
877+
testId="message-thread-panel"
887878
widthPx={threadPanelWidthPx}
888879
>
889880
{panel}
@@ -892,19 +883,27 @@ export const ChannelPane = React.memo(function ChannelPane({
892883
panel
893884
);
894885
})()
895-
: profilePanelPubkey
886+
: activeChannel && selectedAgent
896887
? (() => {
897888
const panel = (
898-
<UserProfilePanel
899-
currentPubkey={currentPubkey}
889+
<AgentSessionThreadPanel
890+
agent={selectedAgent}
891+
canInterruptTurn={selectedAgent.canInterruptTurn}
892+
channel={activeChannel}
893+
isWorking={botTypingEntries.some(
894+
(entry) =>
895+
entry.pubkey.toLowerCase() ===
896+
selectedAgent.pubkey.toLowerCase(),
897+
)}
900898
isSinglePanelView={
901899
useSplitAuxiliaryPane ? false : isSinglePanelView
902900
}
903901
layout={useSplitAuxiliaryPane ? "split" : "standalone"}
904-
onClose={onCloseProfilePanel}
905-
onOpenDm={onOpenDm}
906-
pubkey={profilePanelPubkey}
907-
splitPaneClamp
902+
profiles={profiles}
903+
onBackToProfile={() =>
904+
onOpenProfilePanel(selectedAgent.pubkey)
905+
}
906+
onClose={onCloseAgentSession}
908907
widthPx={threadPanelWidthPx}
909908
/>
910909
);
@@ -913,7 +912,7 @@ export const ChannelPane = React.memo(function ChannelPane({
913912
canResetWidth={canResetThreadPanelWidth}
914913
onResetWidth={onResetThreadPanelWidth}
915914
onResizeStart={onThreadPanelResizeStart}
916-
testId="user-profile-panel"
915+
testId="agent-session-thread-panel"
917916
widthPx={threadPanelWidthPx}
918917
>
919918
{panel}
@@ -922,7 +921,37 @@ export const ChannelPane = React.memo(function ChannelPane({
922921
panel
923922
);
924923
})()
925-
: null}
924+
: profilePanelPubkey
925+
? (() => {
926+
const panel = (
927+
<UserProfilePanel
928+
currentPubkey={currentPubkey}
929+
isSinglePanelView={
930+
useSplitAuxiliaryPane ? false : isSinglePanelView
931+
}
932+
layout={useSplitAuxiliaryPane ? "split" : "standalone"}
933+
onClose={onCloseProfilePanel}
934+
onOpenDm={onOpenDm}
935+
pubkey={profilePanelPubkey}
936+
splitPaneClamp
937+
widthPx={threadPanelWidthPx}
938+
/>
939+
);
940+
return useSplitAuxiliaryPane ? (
941+
<RightAuxiliaryPane
942+
canResetWidth={canResetThreadPanelWidth}
943+
onResetWidth={onResetThreadPanelWidth}
944+
onResizeStart={onThreadPanelResizeStart}
945+
testId="user-profile-panel"
946+
widthPx={threadPanelWidthPx}
947+
>
948+
{panel}
949+
</RightAuxiliaryPane>
950+
) : (
951+
panel
952+
);
953+
})()
954+
: null}
926955
</div>
927956
);
928957
});

desktop/src/features/channels/ui/ChannelScreen.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -429,11 +429,13 @@ export function ChannelScreen({
429429
setThreadReplyTargetId,
430430
setThreadScrollTargetId,
431431
});
432+
const hasTimelineData = messagesQuery.data !== undefined;
432433
const isTimelineLoading =
433434
activeChannel !== null &&
434435
activeChannel.channelType !== "forum" &&
435-
(messagesQuery.isPending ||
436-
(messagesQuery.isFetching && resolvedMessages.length === 0));
436+
!hasTimelineData &&
437+
messagesQuery.isPending;
438+
const shouldShowInitialChannelLoading = isTimelineLoading;
437439
const resetComposerTargets = React.useCallback(
438440
(_channelId: string | null) => {
439441
setOpenThreadHeadId(null);
@@ -549,7 +551,9 @@ export function ChannelScreen({
549551
ref={channelContentRef}
550552
>
551553
{activeChannel ? (
552-
activeChannel.channelType === "forum" ? (
554+
shouldShowInitialChannelLoading ? (
555+
<ViewLoadingFallback includeHeader kind="channel" />
556+
) : activeChannel.channelType === "forum" ? (
553557
<>
554558
{channelHeader}
555559
<React.Suspense fallback={<ViewLoadingFallback kind="forum" />}>
@@ -564,7 +568,9 @@ export function ChannelScreen({
564568
</React.Suspense>
565569
</>
566570
) : (
567-
<React.Suspense fallback={<ViewLoadingFallback kind="channel" />}>
571+
<React.Suspense
572+
fallback={<ViewLoadingFallback includeHeader kind="channel" />}
573+
>
568574
<ChannelPane
569575
activeChannel={activeChannel}
570576
agentPubkeys={agentPubkeys}

0 commit comments

Comments
 (0)