feat(message): add embedded mode to ChatScreen for desktop dual-column layout
All checks were successful
Frontend CI / build-and-push-web (push) Successful in 2m37s
Frontend CI / ota-android (push) Successful in 10m43s
Frontend CI / build-android-apk (push) Successful in 40m4s

Replace EmbeddedChat with unified ChatScreen supporting embedded mode through props. Pass conversation data, layout constraints, and navigation callbacks directly to ChatScreen instead of using separate embedded component. Consolidates dual-column layout logic into single component with conditional rendering based on isEmbedded prop.
This commit is contained in:
lafay
2026-04-13 01:52:43 +08:00
parent fe6a03da5d
commit 4f31926eb5
8 changed files with 131 additions and 1095 deletions

View File

@@ -49,9 +49,10 @@ import {
ChatInput,
GroupInfoPanel,
PANEL_HEIGHTS,
ChatScreenProps,
} from './components/ChatScreen';
export const ChatScreen: React.FC = () => {
export const ChatScreen: React.FC<ChatScreenProps> = (props) => {
const navigation = useNavigation();
const router = useRouter();
const insets = useSafeAreaInsets();
@@ -62,12 +63,12 @@ export const ChatScreen: React.FC = () => {
// 响应式布局
const isWideScreen = useBreakpointGTE('lg');
// 监听屏幕宽度变化当变为大屏幕时自动跳转到web端首页
// 监听屏幕宽度变化当变为大屏幕时自动跳转到web端首页(嵌入式模式下不跳转)
useEffect(() => {
if (isWideScreen) {
if (isWideScreen && !props.isEmbedded) {
router.replace(hrefs.hrefHome());
}
}, [isWideScreen, router]);
}, [isWideScreen, router, props.isEmbedded]);
// 输入框区域高度(用于定位浮动 mention 面板)
const [inputWrapperHeight, setInputWrapperHeight] = useState(60);
@@ -88,20 +89,20 @@ export const ChatScreen: React.FC = () => {
const containerStyle = useMemo(() => ([
styles.container,
isWideScreen ? { maxWidth: 1200, alignSelf: 'center' as const, width: '100%' as const } : null,
]), [isWideScreen, styles.container]);
isWideScreen && !props.isEmbedded ? { maxWidth: 1200, alignSelf: 'center' as const, width: '100%' as const } : null,
]), [isWideScreen, styles.container, props.isEmbedded]);
const listContentStyle = useMemo(() => ([
styles.listContent,
isWideScreen
isWideScreen && !props.isEmbedded
? { paddingHorizontal: 24, maxWidth: 900, alignSelf: 'center' as const }
: { paddingHorizontal: 16 },
]), [isWideScreen, styles.listContent]);
]), [isWideScreen, styles.listContent, props.isEmbedded]);
const inputWrapperStyle = useMemo(() => ([
styles.inputWrapper,
isWideScreen ? { maxWidth: 900, alignSelf: 'center' as const, width: '100%' as const } : null,
]), [isWideScreen, styles.inputWrapper]);
isWideScreen && !props.isEmbedded ? { maxWidth: 900, alignSelf: 'center' as const, width: '100%' as const } : null,
]), [isWideScreen, styles.inputWrapper, props.isEmbedded]);
// 图片查看器状态
const [showImageViewer, setShowImageViewer] = useState(false);
@@ -208,7 +209,7 @@ export const ChatScreen: React.FC = () => {
handleReachLatestEdge,
jumpToLatestMessages,
setBrowsingHistory,
} = useChatScreen();
} = useChatScreen(props);
const displayMessages = useMemo(() => [...messages].reverse(), [messages]);
const longPressMenuMemberMap = useMemo(() => {
@@ -412,22 +413,22 @@ export const ChatScreen: React.FC = () => {
}, [handleMessageListContentSizeChange]);
return (
<SafeAreaView style={containerStyle} edges={['top']}>
<SafeAreaView style={containerStyle} edges={props.isEmbedded ? [] : ['top']}>
<KeyboardAvoidingView
style={styles.container}
style={[styles.container, { overflow: 'hidden' }]}
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : 0}
>
<StatusBar style={resolved === 'dark' ? 'light' : 'dark'} backgroundColor={colors.background.paper} />
{!props.isEmbedded && <StatusBar style={resolved === 'dark' ? 'light' : 'dark'} backgroundColor={colors.background.paper} />}
{/* 顶部栏 */}
<ChatHeader
isGroupChat={isGroupChat}
groupInfo={groupInfo}
otherUser={otherUser}
routeGroupName={routeGroupName}
routeGroupName={routeGroupName ?? undefined}
typingHint={typingHint}
onBack={() => router.back()}
onBack={props.onEmbeddedBack ? props.onEmbeddedBack : () => router.back()}
onTitlePress={navigateToInfo}
onMorePress={navigateToChatSettings}
onGroupInfoPress={handleGroupInfoPress}

View File

@@ -54,8 +54,8 @@ import {
import { Avatar, Text, EmptyState, ResponsiveContainer, AppBackButton } from '../../components/common';
import { useResponsive, useBreakpointGTE } from '../../hooks/useResponsive';
import * as hrefs from '../../navigation/hrefs';
// 导入 EmbeddedChat 组件用于桌面端双栏布局
import { EmbeddedChat } from './components/EmbeddedChat';
// 导入 ChatScreen 组件用于桌面端双栏布局
import { ChatScreen } from './ChatScreen';
import { ConversationListRow } from './components/ConversationListRow';
// 导入 NotificationsScreen 用于在内部显示
import { NotificationsScreen } from './NotificationsScreen';
@@ -852,9 +852,13 @@ export const MessageListScreen: React.FC = () => {
<NotificationsScreen onBack={() => setShowNotifications(false)} />
) : selectedConversation ? (
// 显示选中会话的聊天内容
<EmbeddedChat
conversation={selectedConversation}
onBack={() => setSelectedConversation(null)}
<ChatScreen
isEmbedded
embeddedConversationId={String(selectedConversation.id)}
embeddedIsGroupChat={selectedConversation.type === 'group'}
embeddedGroupId={selectedConversation.group ? String(selectedConversation.group.id) : undefined}
embeddedGroupName={selectedConversation.group?.name}
onEmbeddedBack={() => setSelectedConversation(null)}
/>
) : (
// 默认占位符

View File

@@ -51,6 +51,7 @@ export const ChatHeader: React.FC<ChatHeaderProps> = ({
header: {
...baseStyles.header,
paddingHorizontal: isWideScreen ? spacing.xl : spacing.md,
height: isWideScreen ? 60 : 52,
},
headerName: {
...baseStyles.headerName,

View File

@@ -62,7 +62,18 @@ export interface MenuItem {
// ChatScreen Props
export interface ChatScreenProps {
// 路由参数通过 useRoute 获取
/** 嵌入式模式:外部传入的会话 ID优先于路由参数 */
embeddedConversationId?: string;
/** 嵌入式模式:是否为群聊 */
embeddedIsGroupChat?: boolean;
/** 嵌入式模式:群组 ID */
embeddedGroupId?: string;
/** 嵌入式模式:群组名称 */
embeddedGroupName?: string;
/** 嵌入式模式:返回回调 */
onEmbeddedBack?: () => void;
/** 嵌入式模式:是否为嵌入式布局 */
isEmbedded?: boolean;
}
// 消息气泡 Props

View File

@@ -40,11 +40,12 @@ import {
ChatRouteParams,
MenuPosition,
PendingChatAttachment,
ChatScreenProps,
} from './types';
import { TIME_SEPARATOR_INTERVAL, MEMBERS_PAGE_SIZE, MAX_PENDING_CHAT_IMAGES } from './constants';
import { messageRepository } from '@/database';
export const useChatScreen = () => {
export const useChatScreen = (props?: ChatScreenProps) => {
const getSendErrorMessage = useCallback((error: unknown, fallback: string) => {
if (error instanceof ApiError && error.message) {
return error.message;
@@ -60,7 +61,17 @@ export const useChatScreen = () => {
groupName?: string | string[];
}>();
// 路由参数(动态段 + query群 ID 勿用 Number(),避免超过 2^53-1 时精度丢失
// 嵌入式模式下优先使用 props否则从路由参数获取
const routeParams = useMemo(() => {
if (props?.embeddedConversationId) {
return {
conversationId: props.embeddedConversationId,
userId: null as string | null,
isGroupChat: props.embeddedIsGroupChat ?? false,
groupId: props.embeddedGroupId ?? null,
groupName: props.embeddedGroupName ?? null,
};
}
const isGroupFlag = firstRouteParam(rawParams.isGroupChat);
return {
conversationId: firstRouteParam(rawParams.conversationId) ?? null,
@@ -70,6 +81,10 @@ export const useChatScreen = () => {
groupName: firstRouteParam(rawParams.groupName),
};
}, [
props?.embeddedConversationId,
props?.embeddedIsGroupChat,
props?.embeddedGroupId,
props?.embeddedGroupName,
rawParams.conversationId,
rawParams.userId,
rawParams.isGroupChat,

File diff suppressed because it is too large Load Diff