- DetailColumn: remark-edit draft and the history-search modal's open
state weren't reset on contact switch (no key={topicId}, same long-
lived instance) -- could silently save a typed remark against the
WRONG contact. History-search state lifted to MessagesLayout so
ConversationView's header search icon (previously wired to nothing)
can open the same modal
- ConversationView: loadOlder's in-flight "restore scroll position"
callback and its loading flag weren't guarded against a conversation
switch happening mid-request, and the scroll-to-bottom effect keyed
only on messages.length so two conversations that happened to load
the same page size never got scrolled down on open
- ChatListColumn: pinned 系统消息/文件助手 rows used solid-color-square
avatars like regular contacts; the actual design-code source
(ChatList.dc.html) and reference screenshots use a light tinted
background with a colored icon instead
- MessageBubble: own ("我") messages hash-colored their avatar like any
contact instead of the design's fixed accent blue
- Every send/recall/favorite/forward action swallowed its own failure
silently (empty .catch or, for forward, no catch at all) -- moved
Toast from features/auth into app/ (shared) and wired real error
feedback through it
- MessageContextMenu: added the recall countdown label, Escape-to-close,
and viewport-edge clamping (a right-click near the window edge could
render partially off-screen)
- EmojiPanel now defaults to 最近 per the reference (was defaulting to
表情)
- HistorySearchModal: pagination (searchTopicMessages/getTopicMedia both
supported cursor/next_cursor already, nothing used it -- silently
truncated at one page), Escape-to-close, and a brief "没有找到" flash
on the first keystroke before the debounce caught up
- A handful of design-token corrections (--shadow-btn, NavRail's border/
spacing, LoginPage's logo radius/brand-panel padding, a placeholder
icon color) verified against screens/*.dc.html's exact pixel values
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
42 lines
1.7 KiB
TypeScript
42 lines
1.7 KiB
TypeScript
import { Outlet } from 'react-router-dom';
|
|
|
|
import { useBootstrap } from '@/lib/api/hooks';
|
|
import { useChatList } from '@/lib/tinode/hooks';
|
|
|
|
import { BottomTabBar } from './BottomTabBar';
|
|
import { NavRail } from './NavRail';
|
|
|
|
/**
|
|
* Top-level chrome: persistent icon rail (>=640px) or bottom tab bar (<640px), wrapping whichever
|
|
* feature section is active. Each feature section owns its own list/main/detail responsive split
|
|
* internally (see e.g. features/messages) -- this shell only decides "rail vs tabs", nothing about
|
|
* columns within a section.
|
|
*/
|
|
export function AppShell() {
|
|
const contacts = useChatList();
|
|
const bootstrap = useBootstrap();
|
|
// "sys"/"slf" have their own authoritative unread source in entry_badges (bootstrap.ts) -- excluded
|
|
// here in case they ever also show up as regular Tinode subscriptions in `contacts`, which would
|
|
// otherwise double-count the same unread messages into this badge.
|
|
const unreadMessages =
|
|
contacts.reduce((sum, c) => (c.topic === 'sys' || c.topic === 'slf' ? sum : sum + (c.unread || 0)), 0) +
|
|
(bootstrap.data?.entry_badges?.system_unread_count || 0) +
|
|
(bootstrap.data?.entry_badges?.file_assistant_unread_count || 0);
|
|
|
|
return (
|
|
<div className="flex h-dvh w-full overflow-hidden bg-surface-page">
|
|
<div className="hidden sm:flex sm:h-full">
|
|
<NavRail unreadMessages={unreadMessages} />
|
|
</div>
|
|
<div className="flex min-w-0 flex-1 flex-col overflow-hidden">
|
|
<div className="min-h-0 flex-1 overflow-hidden">
|
|
<Outlet />
|
|
</div>
|
|
<div className="sm:hidden">
|
|
<BottomTabBar unreadMessages={unreadMessages} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|