feat(Theme): enhance theming and UI consistency across components
- Updated app.json to set userInterfaceStyle to automatic for improved theme adaptability. - Refactored layout components to utilize useAppColors for dynamic theming, ensuring consistent color usage. - Introduced SystemChrome component to manage system UI background color based on theme. - Enhanced TabsLayout, ProfileStackLayout, and other components to leverage new theming structure. - Improved QRCodeScanner, SearchBar, and CommentItem styles to align with the updated theme system. - Consolidated styles in SystemMessageItem and TabBar for better maintainability and visual coherence.
This commit is contained in:
@@ -26,18 +26,20 @@ import {
|
||||
ActivityIndicator,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
TouchableOpacity,
|
||||
} from 'react-native';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import { useNavigation, useRouter } from 'expo-router';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { Text, ImageGallery, ImageGridItem } from '../../components/common';
|
||||
import { colors } from '../../theme';
|
||||
import { useAppColors } from '../../theme';
|
||||
import * as hrefs from '../../navigation/hrefs';
|
||||
import { messageManager } from '../../stores';
|
||||
import { useBreakpointGTE } from '../../hooks/useResponsive';
|
||||
import {
|
||||
useChatScreen,
|
||||
chatScreenStyles as baseStyles,
|
||||
useChatScreenStyles,
|
||||
EmojiPanel,
|
||||
MorePanel,
|
||||
MentionPanel,
|
||||
@@ -53,10 +55,11 @@ export const ChatScreen: React.FC = () => {
|
||||
const navigation = useNavigation();
|
||||
const router = useRouter();
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
const colors = useAppColors();
|
||||
const styles = useChatScreenStyles();
|
||||
|
||||
// 响应式布局
|
||||
const isWideScreen = useBreakpointGTE('lg');
|
||||
const styles = baseStyles;
|
||||
|
||||
// 监听屏幕宽度变化,当变为大屏幕时自动跳转到web端首页
|
||||
useEffect(() => {
|
||||
@@ -79,6 +82,8 @@ export const ChatScreen: React.FC = () => {
|
||||
beforeHeight: 0,
|
||||
});
|
||||
const preloadCooldownTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const showJumpToLatestRef = useRef(false);
|
||||
const [showJumpToLatest, setShowJumpToLatest] = useState(false);
|
||||
|
||||
const containerStyle = useMemo(() => ([
|
||||
styles.container,
|
||||
@@ -200,6 +205,7 @@ export const ChatScreen: React.FC = () => {
|
||||
loadMoreHistory,
|
||||
handleMessageListContentSizeChange,
|
||||
handleReachLatestEdge,
|
||||
jumpToLatestMessages,
|
||||
setBrowsingHistory,
|
||||
} = useChatScreen();
|
||||
const displayMessages = useMemo(() => [...messages].reverse(), [messages]);
|
||||
@@ -210,6 +216,18 @@ export const ChatScreen: React.FC = () => {
|
||||
}
|
||||
}, [loadingMore, showEdgeLoadingIndicator]);
|
||||
|
||||
useEffect(() => {
|
||||
if (loading) {
|
||||
showJumpToLatestRef.current = false;
|
||||
setShowJumpToLatest(false);
|
||||
}
|
||||
}, [loading]);
|
||||
|
||||
useEffect(() => {
|
||||
showJumpToLatestRef.current = false;
|
||||
setShowJumpToLatest(false);
|
||||
}, [conversationId]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (preloadCooldownTimerRef.current) {
|
||||
@@ -314,6 +332,14 @@ export const ChatScreen: React.FC = () => {
|
||||
viewportHeight: layoutMeasurement.height,
|
||||
};
|
||||
|
||||
// inverted:offset 越大离最新消息端越远,与 setBrowsingHistory(120) 阈值一致
|
||||
const shouldShowJump =
|
||||
contentOffset.y > 120 && !loading && messages.length > 0;
|
||||
if (showJumpToLatestRef.current !== shouldShowJump) {
|
||||
showJumpToLatestRef.current = shouldShowJump;
|
||||
setShowJumpToLatest(shouldShowJump);
|
||||
}
|
||||
|
||||
// 离开最新消息端后进入“浏览历史”模式,屏蔽自动跟随到底
|
||||
if (contentOffset.y > 120) {
|
||||
setBrowsingHistory(true);
|
||||
@@ -360,7 +386,17 @@ export const ChatScreen: React.FC = () => {
|
||||
}, 350);
|
||||
});
|
||||
}
|
||||
}, [scrollPositionRef, hasMoreHistory, loadingMore, loading, loadMoreHistory, handleReachLatestEdge, showEdgeLoadingIndicator, setBrowsingHistory]);
|
||||
}, [
|
||||
scrollPositionRef,
|
||||
hasMoreHistory,
|
||||
loadingMore,
|
||||
loading,
|
||||
messages.length,
|
||||
loadMoreHistory,
|
||||
handleReachLatestEdge,
|
||||
showEdgeLoadingIndicator,
|
||||
setBrowsingHistory,
|
||||
]);
|
||||
|
||||
const handleContentSizeChange = useCallback((contentWidth: number, contentHeight: number) => {
|
||||
handleMessageListContentSizeChange(contentWidth, contentHeight);
|
||||
@@ -474,6 +510,18 @@ export const ChatScreen: React.FC = () => {
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
{showJumpToLatest && (
|
||||
<TouchableOpacity
|
||||
style={styles.jumpToLatestFab}
|
||||
onPress={jumpToLatestMessages}
|
||||
activeOpacity={0.85}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="回到最新消息"
|
||||
>
|
||||
<MaterialCommunityIcons name="chevron-down" size={20} color={colors.primary.main} />
|
||||
<Text style={styles.jumpToLatestFabText}>最新消息</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 底部区域:输入框 + 面板 */}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* 支持响应式布局
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import {
|
||||
View,
|
||||
StyleSheet,
|
||||
@@ -19,7 +19,7 @@ import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import * as ImagePicker from 'expo-image-picker';
|
||||
import { colors, spacing, fontSizes, borderRadius, shadows } from '../../theme';
|
||||
import { spacing, fontSizes, borderRadius, shadows, useAppColors, type AppColors } from '../../theme';
|
||||
import { groupService } from '../../services/groupService';
|
||||
import { uploadService } from '../../services/uploadService';
|
||||
import { Avatar, Text, Button, Loading } from '../../components/common';
|
||||
@@ -27,6 +27,8 @@ import { User } from '../../types';
|
||||
import MutualFollowSelectorModal from './components/MutualFollowSelectorModal';
|
||||
|
||||
const CreateGroupScreen: React.FC = () => {
|
||||
const colors = useAppColors();
|
||||
const styles = useMemo(() => createCreateGroupStyles(colors), [colors]);
|
||||
const router = useRouter();
|
||||
|
||||
// 表单状态
|
||||
@@ -310,171 +312,173 @@ const CreateGroupScreen: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.background.default,
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContent: {
|
||||
padding: spacing.lg,
|
||||
paddingBottom: spacing.xl,
|
||||
},
|
||||
// 头部区域样式
|
||||
headerSection: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
marginBottom: spacing.xl,
|
||||
},
|
||||
avatarContainer: {
|
||||
marginRight: spacing.lg,
|
||||
},
|
||||
avatarWrapper: {
|
||||
position: 'relative',
|
||||
},
|
||||
avatarImage: {
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: 40,
|
||||
},
|
||||
avatarPlaceholder: {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.background.default,
|
||||
borderRadius: 40,
|
||||
},
|
||||
avatarBadge: {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
backgroundColor: colors.primary.main,
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 14,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderWidth: 2,
|
||||
borderColor: colors.background.paper,
|
||||
},
|
||||
nameInputContainer: {
|
||||
flex: 1,
|
||||
paddingTop: spacing.sm,
|
||||
},
|
||||
inputLabel: {
|
||||
marginBottom: spacing.sm,
|
||||
fontWeight: '600',
|
||||
},
|
||||
nameInput: {
|
||||
backgroundColor: colors.background.paper,
|
||||
borderRadius: borderRadius.lg,
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.md,
|
||||
fontSize: fontSizes.lg,
|
||||
color: colors.text.primary,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.divider,
|
||||
},
|
||||
charCount: {
|
||||
textAlign: 'right',
|
||||
marginTop: spacing.xs,
|
||||
},
|
||||
// 区域样式
|
||||
section: {
|
||||
marginBottom: spacing.xl,
|
||||
},
|
||||
sectionHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontWeight: '600',
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
// 文本域样式
|
||||
textAreaContainer: {
|
||||
backgroundColor: colors.background.paper,
|
||||
borderRadius: borderRadius.lg,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.divider,
|
||||
padding: spacing.md,
|
||||
},
|
||||
textArea: {
|
||||
minHeight: 100,
|
||||
fontSize: fontSizes.md,
|
||||
color: colors.text.primary,
|
||||
lineHeight: 22,
|
||||
},
|
||||
textAreaCharCount: {
|
||||
textAlign: 'right',
|
||||
marginTop: spacing.sm,
|
||||
},
|
||||
// 已选成员样式
|
||||
selectedMembersList: {
|
||||
paddingVertical: spacing.sm,
|
||||
},
|
||||
selectedMemberItem: {
|
||||
alignItems: 'center',
|
||||
marginRight: spacing.lg,
|
||||
width: 64,
|
||||
},
|
||||
removeMemberButton: {
|
||||
position: 'absolute',
|
||||
top: -4,
|
||||
right: 4,
|
||||
},
|
||||
removeIconContainer: {
|
||||
backgroundColor: colors.text.secondary,
|
||||
borderRadius: 10,
|
||||
width: 20,
|
||||
height: 20,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderWidth: 2,
|
||||
borderColor: colors.background.paper,
|
||||
},
|
||||
selectedMemberName: {
|
||||
marginTop: spacing.xs,
|
||||
textAlign: 'center',
|
||||
width: '100%',
|
||||
},
|
||||
// 邀请按钮样式
|
||||
inviteButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.background.paper,
|
||||
borderRadius: borderRadius.lg,
|
||||
padding: spacing.md,
|
||||
marginBottom: spacing.xl,
|
||||
...shadows.sm,
|
||||
},
|
||||
inviteIconContainer: {
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: borderRadius.lg,
|
||||
backgroundColor: colors.primary.light + '20',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginRight: spacing.md,
|
||||
},
|
||||
inviteTextContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
inviteTitle: {
|
||||
fontWeight: '600',
|
||||
marginBottom: 2,
|
||||
},
|
||||
// 底部按钮样式
|
||||
footer: {
|
||||
padding: spacing.lg,
|
||||
paddingBottom: spacing.xl,
|
||||
backgroundColor: colors.background.paper,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.divider,
|
||||
},
|
||||
});
|
||||
function createCreateGroupStyles(colors: AppColors) {
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.background.default,
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContent: {
|
||||
padding: spacing.lg,
|
||||
paddingBottom: spacing.xl,
|
||||
},
|
||||
// 头部区域样式
|
||||
headerSection: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
marginBottom: spacing.xl,
|
||||
},
|
||||
avatarContainer: {
|
||||
marginRight: spacing.lg,
|
||||
},
|
||||
avatarWrapper: {
|
||||
position: 'relative',
|
||||
},
|
||||
avatarImage: {
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: 40,
|
||||
},
|
||||
avatarPlaceholder: {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.background.default,
|
||||
borderRadius: 40,
|
||||
},
|
||||
avatarBadge: {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
backgroundColor: colors.primary.main,
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 14,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderWidth: 2,
|
||||
borderColor: colors.background.paper,
|
||||
},
|
||||
nameInputContainer: {
|
||||
flex: 1,
|
||||
paddingTop: spacing.sm,
|
||||
},
|
||||
inputLabel: {
|
||||
marginBottom: spacing.sm,
|
||||
fontWeight: '600',
|
||||
},
|
||||
nameInput: {
|
||||
backgroundColor: colors.background.paper,
|
||||
borderRadius: borderRadius.lg,
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.md,
|
||||
fontSize: fontSizes.lg,
|
||||
color: colors.text.primary,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.divider,
|
||||
},
|
||||
charCount: {
|
||||
textAlign: 'right',
|
||||
marginTop: spacing.xs,
|
||||
},
|
||||
// 区域样式
|
||||
section: {
|
||||
marginBottom: spacing.xl,
|
||||
},
|
||||
sectionHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontWeight: '600',
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
// 文本域样式
|
||||
textAreaContainer: {
|
||||
backgroundColor: colors.background.paper,
|
||||
borderRadius: borderRadius.lg,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.divider,
|
||||
padding: spacing.md,
|
||||
},
|
||||
textArea: {
|
||||
minHeight: 100,
|
||||
fontSize: fontSizes.md,
|
||||
color: colors.text.primary,
|
||||
lineHeight: 22,
|
||||
},
|
||||
textAreaCharCount: {
|
||||
textAlign: 'right',
|
||||
marginTop: spacing.sm,
|
||||
},
|
||||
// 已选成员样式
|
||||
selectedMembersList: {
|
||||
paddingVertical: spacing.sm,
|
||||
},
|
||||
selectedMemberItem: {
|
||||
alignItems: 'center',
|
||||
marginRight: spacing.lg,
|
||||
width: 64,
|
||||
},
|
||||
removeMemberButton: {
|
||||
position: 'absolute',
|
||||
top: -4,
|
||||
right: 4,
|
||||
},
|
||||
removeIconContainer: {
|
||||
backgroundColor: colors.text.secondary,
|
||||
borderRadius: 10,
|
||||
width: 20,
|
||||
height: 20,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderWidth: 2,
|
||||
borderColor: colors.background.paper,
|
||||
},
|
||||
selectedMemberName: {
|
||||
marginTop: spacing.xs,
|
||||
textAlign: 'center',
|
||||
width: '100%',
|
||||
},
|
||||
// 邀请按钮样式
|
||||
inviteButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.background.paper,
|
||||
borderRadius: borderRadius.lg,
|
||||
padding: spacing.md,
|
||||
marginBottom: spacing.xl,
|
||||
...shadows.sm,
|
||||
},
|
||||
inviteIconContainer: {
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: borderRadius.lg,
|
||||
backgroundColor: colors.primary.light + '20',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginRight: spacing.md,
|
||||
},
|
||||
inviteTextContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
inviteTitle: {
|
||||
fontWeight: '600',
|
||||
marginBottom: 2,
|
||||
},
|
||||
// 底部按钮样式
|
||||
footer: {
|
||||
padding: spacing.lg,
|
||||
paddingBottom: spacing.xl,
|
||||
backgroundColor: colors.background.paper,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.divider,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export default CreateGroupScreen;
|
||||
|
||||
@@ -25,7 +25,14 @@ import { useFocusEffect } from '@react-navigation/native';
|
||||
import { useLocalSearchParams, useRouter } from 'expo-router';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import * as ImagePicker from 'expo-image-picker';
|
||||
import { colors, spacing, fontSizes, borderRadius, shadows } from '../../theme';
|
||||
import {
|
||||
spacing,
|
||||
fontSizes,
|
||||
borderRadius,
|
||||
shadows,
|
||||
useAppColors,
|
||||
type AppColors,
|
||||
} from '../../theme';
|
||||
import { useAuthStore } from '../../stores';
|
||||
import { groupService } from '../../services/groupService';
|
||||
import { uploadService } from '../../services/uploadService';
|
||||
@@ -56,6 +63,8 @@ const JOIN_TYPE_OPTIONS: { value: JoinType; label: string; icon: string; desc: s
|
||||
];
|
||||
|
||||
const GroupInfoScreen: React.FC = () => {
|
||||
const colors = useAppColors();
|
||||
const styles = useMemo(() => createGroupInfoStyles(colors), [colors]);
|
||||
const router = useRouter();
|
||||
const { groupId: groupIdParam, conversationId: conversationIdParam } = useLocalSearchParams<{
|
||||
groupId?: string | string[];
|
||||
@@ -1071,7 +1080,8 @@ const GroupInfoScreen: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
function createGroupInfoStyles(colors: AppColors) {
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.background.default,
|
||||
@@ -1594,6 +1604,7 @@ const styles = StyleSheet.create({
|
||||
backgroundColor: colors.primary.main,
|
||||
borderColor: colors.primary.main,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export default GroupInfoScreen;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { View, StyleSheet, ActivityIndicator, Alert, ScrollView, TouchableOpacit
|
||||
import { useRouter, useLocalSearchParams } from 'expo-router';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import { colors, spacing, borderRadius, shadows } from '../../theme';
|
||||
import { spacing, borderRadius, shadows, useAppColors, type AppColors } from '../../theme';
|
||||
import { Avatar, Text } from '../../components/common';
|
||||
import { routePayloadCache } from '../../stores/routePayloadCache';
|
||||
import { groupService } from '../../services/groupService';
|
||||
@@ -12,6 +12,8 @@ import { GroupMemberResponse } from '../../types/dto';
|
||||
import { GroupInfoSummaryCard, DecisionFooter } from './components/GroupRequestShared';
|
||||
|
||||
const GroupInviteDetailScreen: React.FC = () => {
|
||||
const colors = useAppColors();
|
||||
const styles = useMemo(() => createGroupInviteDetailStyles(colors), [colors]);
|
||||
const router = useRouter();
|
||||
const { messageId } = useLocalSearchParams<{ messageId?: string }>();
|
||||
const message = messageId ? routePayloadCache.getSystemMessage(messageId) : undefined;
|
||||
@@ -174,84 +176,86 @@ const GroupInviteDetailScreen: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.background.default,
|
||||
},
|
||||
emptyWrap: {
|
||||
flex: 1,
|
||||
padding: spacing.lg,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: spacing.md,
|
||||
},
|
||||
backBtn: {
|
||||
padding: spacing.sm,
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContent: {
|
||||
padding: spacing.lg,
|
||||
paddingBottom: spacing.xl,
|
||||
},
|
||||
card: {
|
||||
backgroundColor: colors.background.paper,
|
||||
borderRadius: borderRadius.lg,
|
||||
padding: spacing.lg,
|
||||
...shadows.sm,
|
||||
},
|
||||
cardHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
cardIconContainer: {
|
||||
width: 30,
|
||||
height: 30,
|
||||
borderRadius: 15,
|
||||
backgroundColor: colors.info.light + '30',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginRight: spacing.sm,
|
||||
},
|
||||
cardTitle: {
|
||||
fontWeight: '600',
|
||||
flex: 1,
|
||||
},
|
||||
memberCount: {
|
||||
marginRight: spacing.xs,
|
||||
},
|
||||
loadingWrap: {
|
||||
paddingVertical: spacing.md,
|
||||
alignItems: 'center',
|
||||
},
|
||||
memberPreview: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
},
|
||||
memberAvatar: {
|
||||
marginLeft: -8,
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
memberAvatarFirst: {
|
||||
marginLeft: 0,
|
||||
},
|
||||
ownerBadge: {
|
||||
position: 'absolute',
|
||||
bottom: -2,
|
||||
right: -2,
|
||||
backgroundColor: colors.warning.main,
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 4,
|
||||
paddingVertical: 1,
|
||||
},
|
||||
ownerBadgeText: {
|
||||
fontSize: 10,
|
||||
fontWeight: '700',
|
||||
},
|
||||
});
|
||||
function createGroupInviteDetailStyles(colors: AppColors) {
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.background.default,
|
||||
},
|
||||
emptyWrap: {
|
||||
flex: 1,
|
||||
padding: spacing.lg,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: spacing.md,
|
||||
},
|
||||
backBtn: {
|
||||
padding: spacing.sm,
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContent: {
|
||||
padding: spacing.lg,
|
||||
paddingBottom: spacing.xl,
|
||||
},
|
||||
card: {
|
||||
backgroundColor: colors.background.paper,
|
||||
borderRadius: borderRadius.lg,
|
||||
padding: spacing.lg,
|
||||
...shadows.sm,
|
||||
},
|
||||
cardHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
cardIconContainer: {
|
||||
width: 30,
|
||||
height: 30,
|
||||
borderRadius: 15,
|
||||
backgroundColor: colors.info.light + '30',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginRight: spacing.sm,
|
||||
},
|
||||
cardTitle: {
|
||||
fontWeight: '600',
|
||||
flex: 1,
|
||||
},
|
||||
memberCount: {
|
||||
marginRight: spacing.xs,
|
||||
},
|
||||
loadingWrap: {
|
||||
paddingVertical: spacing.md,
|
||||
alignItems: 'center',
|
||||
},
|
||||
memberPreview: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
},
|
||||
memberAvatar: {
|
||||
marginLeft: -8,
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
memberAvatarFirst: {
|
||||
marginLeft: 0,
|
||||
},
|
||||
ownerBadge: {
|
||||
position: 'absolute',
|
||||
bottom: -2,
|
||||
right: -2,
|
||||
backgroundColor: colors.warning.main,
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 4,
|
||||
paddingVertical: 1,
|
||||
},
|
||||
ownerBadgeText: {
|
||||
fontSize: 10,
|
||||
fontWeight: '700',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export default GroupInviteDetailScreen;
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { useLocalSearchParams } from 'expo-router';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import { colors, spacing, fontSizes, borderRadius } from '../../theme';
|
||||
import { spacing, fontSizes, borderRadius, useAppColors, type AppColors } from '../../theme';
|
||||
import { useAuthStore } from '../../stores';
|
||||
import { groupService } from '../../services/groupService';
|
||||
import {
|
||||
@@ -55,6 +55,8 @@ interface MemberGroup {
|
||||
}
|
||||
|
||||
const GroupMembersScreen: React.FC = () => {
|
||||
const colors = useAppColors();
|
||||
const styles = useMemo(() => createGroupMembersStyles(colors), [colors]);
|
||||
const { groupId: groupIdParam } = useLocalSearchParams<{ groupId?: string | string[] }>();
|
||||
const groupId = firstRouteParam(groupIdParam) ?? '';
|
||||
const { currentUser } = useAuthStore();
|
||||
@@ -646,122 +648,124 @@ const GroupMembersScreen: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.background.default,
|
||||
},
|
||||
section: {
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
sectionHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.sm,
|
||||
backgroundColor: colors.background.default,
|
||||
},
|
||||
memberItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.md,
|
||||
backgroundColor: colors.background.paper,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.divider,
|
||||
},
|
||||
memberInfo: {
|
||||
flex: 1,
|
||||
marginLeft: spacing.md,
|
||||
},
|
||||
memberNameRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: 2,
|
||||
},
|
||||
memberName: {
|
||||
fontWeight: '500',
|
||||
},
|
||||
roleBadge: {
|
||||
paddingHorizontal: spacing.xs,
|
||||
paddingVertical: 2,
|
||||
borderRadius: borderRadius.sm,
|
||||
marginLeft: spacing.sm,
|
||||
},
|
||||
mutedBadge: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginTop: 2,
|
||||
},
|
||||
// 模态框样式
|
||||
modalOverlay: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
modalContent: {
|
||||
backgroundColor: colors.background.paper,
|
||||
borderTopLeftRadius: borderRadius.lg,
|
||||
borderTopRightRadius: borderRadius.lg,
|
||||
padding: spacing.lg,
|
||||
maxHeight: '80%',
|
||||
},
|
||||
modalHeader: {
|
||||
alignItems: 'center',
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
modalTitle: {
|
||||
fontWeight: '700',
|
||||
marginTop: spacing.sm,
|
||||
marginBottom: spacing.xs,
|
||||
},
|
||||
actionItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingVertical: spacing.md,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.divider,
|
||||
},
|
||||
actionText: {
|
||||
marginLeft: spacing.md,
|
||||
},
|
||||
inputLabel: {
|
||||
marginBottom: spacing.xs,
|
||||
},
|
||||
input: {
|
||||
backgroundColor: colors.background.default,
|
||||
borderRadius: borderRadius.md,
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.md,
|
||||
fontSize: fontSizes.md,
|
||||
color: colors.text.primary,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.divider,
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
modalButtons: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
marginTop: spacing.md,
|
||||
},
|
||||
modalButton: {
|
||||
flex: 1,
|
||||
marginHorizontal: spacing.xs,
|
||||
},
|
||||
// 分页加载样式
|
||||
loadingFooter: {
|
||||
paddingVertical: spacing.md,
|
||||
alignItems: 'center',
|
||||
},
|
||||
loadMoreBtn: {
|
||||
paddingVertical: spacing.md,
|
||||
alignItems: 'center',
|
||||
},
|
||||
noMoreText: {
|
||||
textAlign: 'center',
|
||||
paddingVertical: spacing.md,
|
||||
},
|
||||
});
|
||||
function createGroupMembersStyles(colors: AppColors) {
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.background.default,
|
||||
},
|
||||
section: {
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
sectionHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.sm,
|
||||
backgroundColor: colors.background.default,
|
||||
},
|
||||
memberItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.md,
|
||||
backgroundColor: colors.background.paper,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.divider,
|
||||
},
|
||||
memberInfo: {
|
||||
flex: 1,
|
||||
marginLeft: spacing.md,
|
||||
},
|
||||
memberNameRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: 2,
|
||||
},
|
||||
memberName: {
|
||||
fontWeight: '500',
|
||||
},
|
||||
roleBadge: {
|
||||
paddingHorizontal: spacing.xs,
|
||||
paddingVertical: 2,
|
||||
borderRadius: borderRadius.sm,
|
||||
marginLeft: spacing.sm,
|
||||
},
|
||||
mutedBadge: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginTop: 2,
|
||||
},
|
||||
// 模态框样式
|
||||
modalOverlay: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
modalContent: {
|
||||
backgroundColor: colors.background.paper,
|
||||
borderTopLeftRadius: borderRadius.lg,
|
||||
borderTopRightRadius: borderRadius.lg,
|
||||
padding: spacing.lg,
|
||||
maxHeight: '80%',
|
||||
},
|
||||
modalHeader: {
|
||||
alignItems: 'center',
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
modalTitle: {
|
||||
fontWeight: '700',
|
||||
marginTop: spacing.sm,
|
||||
marginBottom: spacing.xs,
|
||||
},
|
||||
actionItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingVertical: spacing.md,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.divider,
|
||||
},
|
||||
actionText: {
|
||||
marginLeft: spacing.md,
|
||||
},
|
||||
inputLabel: {
|
||||
marginBottom: spacing.xs,
|
||||
},
|
||||
input: {
|
||||
backgroundColor: colors.background.default,
|
||||
borderRadius: borderRadius.md,
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.md,
|
||||
fontSize: fontSizes.md,
|
||||
color: colors.text.primary,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.divider,
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
modalButtons: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
marginTop: spacing.md,
|
||||
},
|
||||
modalButton: {
|
||||
flex: 1,
|
||||
marginHorizontal: spacing.xs,
|
||||
},
|
||||
// 分页加载样式
|
||||
loadingFooter: {
|
||||
paddingVertical: spacing.md,
|
||||
alignItems: 'center',
|
||||
},
|
||||
loadMoreBtn: {
|
||||
paddingVertical: spacing.md,
|
||||
alignItems: 'center',
|
||||
},
|
||||
noMoreText: {
|
||||
textAlign: 'center',
|
||||
paddingVertical: spacing.md,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export default GroupMembersScreen;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useRouter, useLocalSearchParams } from 'expo-router';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
|
||||
import { colors, spacing, borderRadius, shadows } from '../../theme';
|
||||
import { spacing, borderRadius, shadows, useAppColors, type AppColors } from '../../theme';
|
||||
import { Avatar, Text } from '../../components/common';
|
||||
import * as hrefs from '../../navigation/hrefs';
|
||||
import { routePayloadCache } from '../../stores/routePayloadCache';
|
||||
@@ -14,6 +14,8 @@ import { userManager } from '../../stores/userManager';
|
||||
import { GroupInfoSummaryCard, DecisionFooter } from './components/GroupRequestShared';
|
||||
|
||||
const GroupRequestDetailScreen: React.FC = () => {
|
||||
const colors = useAppColors();
|
||||
const styles = useMemo(() => createGroupRequestDetailStyles(colors), [colors]);
|
||||
const router = useRouter();
|
||||
const { messageId } = useLocalSearchParams<{ messageId?: string }>();
|
||||
const message = messageId ? routePayloadCache.getSystemMessage(messageId) : undefined;
|
||||
@@ -174,52 +176,54 @@ const GroupRequestDetailScreen: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.background.default,
|
||||
},
|
||||
emptyWrap: {
|
||||
flex: 1,
|
||||
padding: spacing.lg,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: spacing.md,
|
||||
},
|
||||
backBtn: {
|
||||
padding: spacing.sm,
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContent: {
|
||||
padding: spacing.lg,
|
||||
paddingBottom: spacing.xl,
|
||||
},
|
||||
card: {
|
||||
backgroundColor: colors.background.paper,
|
||||
borderRadius: borderRadius.lg,
|
||||
padding: spacing.lg,
|
||||
marginBottom: spacing.md,
|
||||
...shadows.sm,
|
||||
},
|
||||
sectionTitle: {
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
meta: {
|
||||
marginLeft: spacing.md,
|
||||
flex: 1,
|
||||
},
|
||||
name: {
|
||||
marginBottom: 2,
|
||||
},
|
||||
subDesc: {
|
||||
marginTop: spacing.sm,
|
||||
},
|
||||
});
|
||||
function createGroupRequestDetailStyles(colors: AppColors) {
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.background.default,
|
||||
},
|
||||
emptyWrap: {
|
||||
flex: 1,
|
||||
padding: spacing.lg,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: spacing.md,
|
||||
},
|
||||
backBtn: {
|
||||
padding: spacing.sm,
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContent: {
|
||||
padding: spacing.lg,
|
||||
paddingBottom: spacing.xl,
|
||||
},
|
||||
card: {
|
||||
backgroundColor: colors.background.paper,
|
||||
borderRadius: borderRadius.lg,
|
||||
padding: spacing.lg,
|
||||
marginBottom: spacing.md,
|
||||
...shadows.sm,
|
||||
},
|
||||
sectionTitle: {
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
meta: {
|
||||
marginLeft: spacing.md,
|
||||
flex: 1,
|
||||
},
|
||||
name: {
|
||||
marginBottom: 2,
|
||||
},
|
||||
subDesc: {
|
||||
marginTop: spacing.sm,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export default GroupRequestDetailScreen;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import {
|
||||
View,
|
||||
StyleSheet,
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
import { useRouter } from 'expo-router';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
|
||||
import { colors, spacing, borderRadius } from '../../theme';
|
||||
import { spacing, borderRadius, useAppColors, type AppColors } from '../../theme';
|
||||
import Avatar from '../../components/common/Avatar';
|
||||
import Text from '../../components/common/Text';
|
||||
import { groupService } from '../../services/groupService';
|
||||
@@ -22,7 +22,160 @@ import { GroupResponse, JoinType } from '../../types/dto';
|
||||
import { useCursorPagination } from '../../hooks/useCursorPagination';
|
||||
import { EmptyState } from '../../components/common';
|
||||
|
||||
function createJoinGroupStyles(colors: AppColors) {
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.background.default,
|
||||
padding: spacing.lg,
|
||||
},
|
||||
heroCard: {
|
||||
backgroundColor: colors.background.paper,
|
||||
borderRadius: borderRadius.lg,
|
||||
padding: spacing.lg,
|
||||
marginBottom: spacing.lg,
|
||||
},
|
||||
heroIconWrap: {
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 24,
|
||||
backgroundColor: colors.primary.light + '26',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
heroTitle: {
|
||||
marginBottom: spacing.xs,
|
||||
},
|
||||
tip: {
|
||||
lineHeight: 20,
|
||||
},
|
||||
formCard: {
|
||||
backgroundColor: colors.background.paper,
|
||||
borderRadius: borderRadius.lg,
|
||||
padding: spacing.lg,
|
||||
flex: 1,
|
||||
},
|
||||
label: {
|
||||
marginBottom: spacing.xs,
|
||||
},
|
||||
input: {
|
||||
flex: 1,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.divider,
|
||||
borderRadius: borderRadius.md,
|
||||
backgroundColor: colors.background.paper,
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.md,
|
||||
color: colors.text.primary,
|
||||
},
|
||||
searchRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
searchBtn: {
|
||||
width: 46,
|
||||
height: 46,
|
||||
borderRadius: borderRadius.md,
|
||||
backgroundColor: colors.primary.main,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginLeft: spacing.sm,
|
||||
},
|
||||
searchResultSection: {
|
||||
marginBottom: spacing.lg,
|
||||
},
|
||||
sectionTitle: {
|
||||
marginBottom: spacing.sm,
|
||||
fontWeight: '600',
|
||||
},
|
||||
listSection: {
|
||||
flex: 1,
|
||||
},
|
||||
groupCard: {
|
||||
borderWidth: 1,
|
||||
borderColor: colors.divider,
|
||||
borderRadius: borderRadius.md,
|
||||
padding: spacing.md,
|
||||
backgroundColor: colors.background.default,
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
groupHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
groupMeta: {
|
||||
marginLeft: spacing.md,
|
||||
flex: 1,
|
||||
},
|
||||
groupName: {
|
||||
marginBottom: spacing.xs,
|
||||
fontWeight: '600',
|
||||
},
|
||||
groupDesc: {
|
||||
marginTop: spacing.sm,
|
||||
lineHeight: 18,
|
||||
},
|
||||
groupInfoRow: {
|
||||
marginTop: spacing.sm,
|
||||
marginBottom: spacing.xs,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
groupNoRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
copyBtn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: spacing.sm,
|
||||
paddingVertical: 4,
|
||||
borderRadius: borderRadius.sm,
|
||||
backgroundColor: colors.primary.light + '22',
|
||||
},
|
||||
copyBtnText: {
|
||||
marginLeft: 4,
|
||||
},
|
||||
submitBtn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: borderRadius.md,
|
||||
backgroundColor: colors.primary.main,
|
||||
minHeight: 46,
|
||||
},
|
||||
submitBtnDisabled: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
submitText: {
|
||||
marginLeft: spacing.xs,
|
||||
},
|
||||
emptyText: {
|
||||
marginTop: spacing.sm,
|
||||
textAlign: 'center',
|
||||
},
|
||||
loadingFooter: {
|
||||
paddingVertical: spacing.md,
|
||||
alignItems: 'center',
|
||||
},
|
||||
loadMoreBtn: {
|
||||
paddingVertical: spacing.md,
|
||||
alignItems: 'center',
|
||||
},
|
||||
noMoreText: {
|
||||
textAlign: 'center',
|
||||
paddingVertical: spacing.md,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const JoinGroupScreen: React.FC = () => {
|
||||
const colors = useAppColors();
|
||||
const styles = useMemo(() => createJoinGroupStyles(colors), [colors]);
|
||||
const router = useRouter();
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const [searching, setSearching] = useState(false);
|
||||
@@ -295,153 +448,4 @@ const JoinGroupScreen: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.background.default,
|
||||
padding: spacing.lg,
|
||||
},
|
||||
heroCard: {
|
||||
backgroundColor: colors.background.paper,
|
||||
borderRadius: borderRadius.lg,
|
||||
padding: spacing.lg,
|
||||
marginBottom: spacing.lg,
|
||||
},
|
||||
heroIconWrap: {
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 24,
|
||||
backgroundColor: colors.primary.light + '26',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
heroTitle: {
|
||||
marginBottom: spacing.xs,
|
||||
},
|
||||
tip: {
|
||||
lineHeight: 20,
|
||||
},
|
||||
formCard: {
|
||||
backgroundColor: colors.background.paper,
|
||||
borderRadius: borderRadius.lg,
|
||||
padding: spacing.lg,
|
||||
flex: 1,
|
||||
},
|
||||
label: {
|
||||
marginBottom: spacing.xs,
|
||||
},
|
||||
input: {
|
||||
flex: 1,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.divider,
|
||||
borderRadius: borderRadius.md,
|
||||
backgroundColor: colors.background.paper,
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.md,
|
||||
color: colors.text.primary,
|
||||
},
|
||||
searchRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
searchBtn: {
|
||||
width: 46,
|
||||
height: 46,
|
||||
borderRadius: borderRadius.md,
|
||||
backgroundColor: colors.primary.main,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginLeft: spacing.sm,
|
||||
},
|
||||
searchResultSection: {
|
||||
marginBottom: spacing.lg,
|
||||
},
|
||||
sectionTitle: {
|
||||
marginBottom: spacing.sm,
|
||||
fontWeight: '600',
|
||||
},
|
||||
listSection: {
|
||||
flex: 1,
|
||||
},
|
||||
groupCard: {
|
||||
borderWidth: 1,
|
||||
borderColor: colors.divider,
|
||||
borderRadius: borderRadius.md,
|
||||
padding: spacing.md,
|
||||
backgroundColor: colors.background.default,
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
groupHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
groupMeta: {
|
||||
marginLeft: spacing.md,
|
||||
flex: 1,
|
||||
},
|
||||
groupName: {
|
||||
marginBottom: spacing.xs,
|
||||
fontWeight: '600',
|
||||
},
|
||||
groupDesc: {
|
||||
marginTop: spacing.sm,
|
||||
lineHeight: 18,
|
||||
},
|
||||
groupInfoRow: {
|
||||
marginTop: spacing.sm,
|
||||
marginBottom: spacing.xs,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
groupNoRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
copyBtn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: spacing.sm,
|
||||
paddingVertical: 4,
|
||||
borderRadius: borderRadius.sm,
|
||||
backgroundColor: colors.primary.light + '22',
|
||||
},
|
||||
copyBtnText: {
|
||||
marginLeft: 4,
|
||||
},
|
||||
submitBtn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: borderRadius.md,
|
||||
backgroundColor: colors.primary.main,
|
||||
minHeight: 46,
|
||||
},
|
||||
submitBtnDisabled: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
submitText: {
|
||||
marginLeft: spacing.xs,
|
||||
},
|
||||
emptyText: {
|
||||
marginTop: spacing.sm,
|
||||
textAlign: 'center',
|
||||
},
|
||||
loadingFooter: {
|
||||
paddingVertical: spacing.md,
|
||||
alignItems: 'center',
|
||||
},
|
||||
loadMoreBtn: {
|
||||
paddingVertical: spacing.md,
|
||||
alignItems: 'center',
|
||||
},
|
||||
noMoreText: {
|
||||
textAlign: 'center',
|
||||
paddingVertical: spacing.md,
|
||||
},
|
||||
});
|
||||
|
||||
export default JoinGroupScreen;
|
||||
|
||||
@@ -30,7 +30,14 @@ import { useRouter } from 'expo-router';
|
||||
import { useIsFocused } from '@react-navigation/native';
|
||||
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import { colors, spacing, fontSizes, shadows, borderRadius } from '../../theme';
|
||||
import {
|
||||
spacing,
|
||||
fontSizes,
|
||||
shadows,
|
||||
borderRadius,
|
||||
useAppColors,
|
||||
type AppColors,
|
||||
} from '../../theme';
|
||||
import { ConversationResponse, UserDTO, MessageResponse, extractTextFromSegments } from '../../types/dto';
|
||||
import { authService } from '../../services';
|
||||
import { useUserStore, useAuthStore } from '../../stores';
|
||||
@@ -73,6 +80,8 @@ interface SearchResultItem {
|
||||
* 支持响应式双栏布局
|
||||
*/
|
||||
export const MessageListScreen: React.FC = () => {
|
||||
const colors = useAppColors();
|
||||
const styles = useMemo(() => createMessageListStyles(colors), [colors]);
|
||||
const router = useRouter();
|
||||
const isFocused = useIsFocused();
|
||||
const insets = useSafeAreaInsets();
|
||||
@@ -565,7 +574,7 @@ export const MessageListScreen: React.FC = () => {
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
<MaterialCommunityIcons name="chevron-right" size={20} color="#CCC" />
|
||||
<MaterialCommunityIcons name="chevron-right" size={20} color={colors.chat.iconSoft} />
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
@@ -614,7 +623,7 @@ export const MessageListScreen: React.FC = () => {
|
||||
</View>
|
||||
)}
|
||||
{!user.is_following && (
|
||||
<MaterialCommunityIcons name="chevron-right" size={20} color="#CCC" />
|
||||
<MaterialCommunityIcons name="chevron-right" size={20} color={colors.chat.iconSoft} />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
@@ -661,11 +670,11 @@ export const MessageListScreen: React.FC = () => {
|
||||
const renderSearchMode = () => (
|
||||
<View style={styles.searchModeContainer}>
|
||||
<View style={styles.searchInputContainer}>
|
||||
<AppBackButton onPress={handleCloseSearch} iconColor="#666" />
|
||||
<AppBackButton onPress={handleCloseSearch} iconColor={colors.text.secondary} />
|
||||
<TextInput
|
||||
style={styles.searchInput}
|
||||
placeholder={activeSearchTab === 'chat' ? '搜索聊天记录' : '搜索用户'}
|
||||
placeholderTextColor="#999"
|
||||
placeholderTextColor={colors.text.hint}
|
||||
value={searchText}
|
||||
onChangeText={setSearchText}
|
||||
autoFocus
|
||||
@@ -676,7 +685,7 @@ export const MessageListScreen: React.FC = () => {
|
||||
/>
|
||||
{searchText.length > 0 && (
|
||||
<TouchableOpacity onPress={() => setSearchText('')}>
|
||||
<MaterialCommunityIcons name="close-circle" size={18} color="#999" />
|
||||
<MaterialCommunityIcons name="close-circle" size={18} color={colors.text.hint} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
@@ -733,7 +742,7 @@ export const MessageListScreen: React.FC = () => {
|
||||
|
||||
<View style={styles.headerRightContainer}>
|
||||
<TouchableOpacity style={styles.headerRightBtn} onPress={handleOpenActionMenu}>
|
||||
<MaterialCommunityIcons name="plus" size={24} color="#666" />
|
||||
<MaterialCommunityIcons name="plus" size={24} color={colors.text.secondary} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
@@ -744,7 +753,7 @@ export const MessageListScreen: React.FC = () => {
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={styles.searchBox}>
|
||||
<MaterialCommunityIcons name="magnify" size={18} color="#999" />
|
||||
<MaterialCommunityIcons name="magnify" size={18} color={colors.text.hint} />
|
||||
<Text style={styles.searchPlaceholder}>搜索</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
@@ -793,19 +802,19 @@ export const MessageListScreen: React.FC = () => {
|
||||
<Pressable style={styles.actionMenuBackdrop} onPress={() => setActionMenuVisible(false)}>
|
||||
<View style={styles.actionMenu}>
|
||||
<TouchableOpacity style={styles.actionMenuItem} onPress={() => runMenuAction('scanQRCode')}>
|
||||
<MaterialCommunityIcons name="qrcode-scan" size={21} color="#444" />
|
||||
<MaterialCommunityIcons name="qrcode-scan" size={21} color={colors.text.primary} />
|
||||
<Text style={styles.actionMenuText}>扫码登录</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.actionMenuItem} onPress={() => runMenuAction('join')}>
|
||||
<MaterialCommunityIcons name="account-plus-outline" size={21} color="#444" />
|
||||
<MaterialCommunityIcons name="account-plus-outline" size={21} color={colors.text.primary} />
|
||||
<Text style={styles.actionMenuText}>加入群聊</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.actionMenuItem} onPress={() => runMenuAction('create')}>
|
||||
<MaterialCommunityIcons name="account-multiple-plus" size={21} color="#444" />
|
||||
<MaterialCommunityIcons name="account-multiple-plus" size={21} color={colors.text.primary} />
|
||||
<Text style={styles.actionMenuText}>创建群聊</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.actionMenuItem} onPress={() => runMenuAction('readAll')} disabled={isMarking}>
|
||||
<MaterialCommunityIcons name="message-check-outline" size={21} color="#444" />
|
||||
<MaterialCommunityIcons name="message-check-outline" size={21} color={colors.text.primary} />
|
||||
<Text style={styles.actionMenuText}>{isMarking ? '处理中...' : '全部已读'}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
@@ -838,7 +847,7 @@ export const MessageListScreen: React.FC = () => {
|
||||
// 默认占位符
|
||||
<View style={styles.chatPlaceholder}>
|
||||
<View style={styles.chatPlaceholderContent}>
|
||||
<MaterialCommunityIcons name="message-text-outline" size={64} color="#DDD" />
|
||||
<MaterialCommunityIcons name="message-text-outline" size={64} color={colors.chat.iconMuted} />
|
||||
<Text style={styles.chatPlaceholderText}>选择一个会话开始聊天</Text>
|
||||
</View>
|
||||
</View>
|
||||
@@ -870,288 +879,290 @@ export const MessageListScreen: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#FAFAFA',
|
||||
},
|
||||
// 桌面端双栏布局
|
||||
desktopContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
},
|
||||
sidebar: {
|
||||
backgroundColor: '#FAFAFA',
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#E8E8E8',
|
||||
},
|
||||
chatArea: {
|
||||
flex: 1,
|
||||
backgroundColor: '#F5F7FA',
|
||||
},
|
||||
chatPlaceholder: {
|
||||
flex: 1,
|
||||
backgroundColor: '#F5F7FA',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
chatPlaceholderContent: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
chatPlaceholderText: {
|
||||
marginTop: spacing.md,
|
||||
fontSize: 16,
|
||||
color: '#999',
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.md,
|
||||
backgroundColor: '#FAFAFA',
|
||||
...shadows.sm,
|
||||
},
|
||||
headerWide: {
|
||||
paddingHorizontal: spacing.lg,
|
||||
paddingVertical: spacing.lg,
|
||||
},
|
||||
headerTitleWide: {
|
||||
fontSize: 22,
|
||||
},
|
||||
searchContainerWide: {
|
||||
paddingHorizontal: spacing.lg,
|
||||
},
|
||||
listContentWide: {
|
||||
paddingHorizontal: spacing.lg,
|
||||
},
|
||||
headerLeft: {
|
||||
width: 44,
|
||||
},
|
||||
headerCenter: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: spacing.xs,
|
||||
},
|
||||
headerTitle: {
|
||||
fontSize: 19,
|
||||
fontWeight: '700',
|
||||
color: '#333',
|
||||
},
|
||||
totalBadge: {
|
||||
minWidth: 18,
|
||||
height: 18,
|
||||
borderRadius: 9,
|
||||
backgroundColor: '#FF4757',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 5,
|
||||
},
|
||||
totalBadgeText: {
|
||||
color: '#FFF',
|
||||
fontSize: 11,
|
||||
fontWeight: '600',
|
||||
},
|
||||
headerRight: {
|
||||
width: 44,
|
||||
height: 44,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
headerRightContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
headerRightBtn: {
|
||||
width: 36,
|
||||
height: 44,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
actionMenuBackdrop: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(0,0,0,0.08)',
|
||||
},
|
||||
actionMenu: {
|
||||
position: 'absolute',
|
||||
top: 88,
|
||||
right: spacing.md,
|
||||
width: 188,
|
||||
backgroundColor: '#FFF',
|
||||
borderRadius: 12,
|
||||
paddingVertical: spacing.sm,
|
||||
...shadows.md,
|
||||
},
|
||||
actionMenuItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.md,
|
||||
},
|
||||
actionMenuText: {
|
||||
marginLeft: spacing.sm,
|
||||
fontSize: 16,
|
||||
color: '#333',
|
||||
},
|
||||
searchContainer: {
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingBottom: spacing.sm,
|
||||
backgroundColor: '#FAFAFA',
|
||||
},
|
||||
searchBox: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#F0F0F0',
|
||||
borderRadius: 10,
|
||||
paddingHorizontal: spacing.sm,
|
||||
paddingVertical: 10,
|
||||
},
|
||||
searchPlaceholder: {
|
||||
fontSize: 14,
|
||||
color: '#999',
|
||||
marginLeft: spacing.xs,
|
||||
},
|
||||
listContent: {
|
||||
flexGrow: 1,
|
||||
backgroundColor: '#FAFAFA',
|
||||
},
|
||||
loadingContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingVertical: spacing.xl * 2,
|
||||
},
|
||||
loadingMoreContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: spacing.md,
|
||||
},
|
||||
loadingMoreText: {
|
||||
marginLeft: spacing.sm,
|
||||
fontSize: 14,
|
||||
color: '#999',
|
||||
},
|
||||
searchModeContainer: {
|
||||
flex: 1,
|
||||
backgroundColor: '#FAFAFA',
|
||||
},
|
||||
searchInputContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#F0F0F0',
|
||||
borderRadius: 10,
|
||||
paddingHorizontal: spacing.md,
|
||||
marginHorizontal: spacing.md,
|
||||
marginTop: spacing.md,
|
||||
height: 40,
|
||||
},
|
||||
searchInput: {
|
||||
flex: 1,
|
||||
fontSize: 15,
|
||||
color: '#333',
|
||||
marginLeft: spacing.md,
|
||||
},
|
||||
searchTabs: {
|
||||
flexDirection: 'row',
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.sm,
|
||||
gap: spacing.sm,
|
||||
},
|
||||
searchTab: {
|
||||
flex: 1,
|
||||
paddingVertical: spacing.sm,
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#F0F0F0',
|
||||
borderRadius: 8,
|
||||
},
|
||||
searchTabActive: {
|
||||
backgroundColor: colors.primary.main,
|
||||
},
|
||||
searchTabText: {
|
||||
fontSize: 14,
|
||||
color: '#666',
|
||||
fontWeight: '500',
|
||||
},
|
||||
searchTabTextActive: {
|
||||
color: '#FFF',
|
||||
},
|
||||
searchResultsContent: {
|
||||
flexGrow: 1,
|
||||
paddingHorizontal: spacing.md,
|
||||
},
|
||||
searchResultItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingVertical: spacing.md,
|
||||
backgroundColor: '#FFF',
|
||||
borderRadius: 10,
|
||||
marginBottom: spacing.sm,
|
||||
paddingHorizontal: spacing.md,
|
||||
},
|
||||
searchResultContent: {
|
||||
flex: 1,
|
||||
marginLeft: spacing.md,
|
||||
},
|
||||
searchResultHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 2,
|
||||
},
|
||||
searchResultName: {
|
||||
fontSize: 15,
|
||||
fontWeight: '600',
|
||||
color: '#333',
|
||||
},
|
||||
searchResultTime: {
|
||||
fontSize: 12,
|
||||
color: '#999',
|
||||
},
|
||||
searchResultMessage: {
|
||||
fontSize: 13,
|
||||
color: '#888',
|
||||
},
|
||||
searchingText: {
|
||||
marginTop: spacing.sm,
|
||||
fontSize: 14,
|
||||
color: '#999',
|
||||
},
|
||||
followingBadge: {
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: 12,
|
||||
backgroundColor: colors.primary.light + '30',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
highlightText: {
|
||||
color: colors.primary.main,
|
||||
fontWeight: '700',
|
||||
backgroundColor: colors.primary.light + '30',
|
||||
},
|
||||
matchedMessagesContainer: {
|
||||
marginTop: spacing.sm,
|
||||
},
|
||||
matchedMessageItem: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
paddingVertical: spacing.xs,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomColor: '#F0F0F0',
|
||||
},
|
||||
matchedMessageText: {
|
||||
flex: 1,
|
||||
fontSize: 13,
|
||||
color: '#555',
|
||||
lineHeight: 18,
|
||||
},
|
||||
matchedMessageTime: {
|
||||
fontSize: 11,
|
||||
color: '#AAA',
|
||||
marginLeft: spacing.sm,
|
||||
minWidth: 45,
|
||||
},
|
||||
});
|
||||
function createMessageListStyles(colors: AppColors) {
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.background.default,
|
||||
},
|
||||
// 桌面端双栏布局
|
||||
desktopContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
},
|
||||
sidebar: {
|
||||
backgroundColor: colors.background.default,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: colors.chat.border,
|
||||
},
|
||||
chatArea: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.chat.surfaceMuted,
|
||||
},
|
||||
chatPlaceholder: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.chat.surfaceMuted,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
chatPlaceholderContent: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
chatPlaceholderText: {
|
||||
marginTop: spacing.md,
|
||||
fontSize: 16,
|
||||
color: colors.text.hint,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.md,
|
||||
backgroundColor: colors.background.default,
|
||||
...shadows.sm,
|
||||
},
|
||||
headerWide: {
|
||||
paddingHorizontal: spacing.lg,
|
||||
paddingVertical: spacing.lg,
|
||||
},
|
||||
headerTitleWide: {
|
||||
fontSize: 22,
|
||||
},
|
||||
searchContainerWide: {
|
||||
paddingHorizontal: spacing.lg,
|
||||
},
|
||||
listContentWide: {
|
||||
paddingHorizontal: spacing.lg,
|
||||
},
|
||||
headerLeft: {
|
||||
width: 44,
|
||||
},
|
||||
headerCenter: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: spacing.xs,
|
||||
},
|
||||
headerTitle: {
|
||||
fontSize: 19,
|
||||
fontWeight: '700',
|
||||
color: colors.text.primary,
|
||||
},
|
||||
totalBadge: {
|
||||
minWidth: 18,
|
||||
height: 18,
|
||||
borderRadius: 9,
|
||||
backgroundColor: colors.error.main,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 5,
|
||||
},
|
||||
totalBadgeText: {
|
||||
color: colors.primary.contrast,
|
||||
fontSize: 11,
|
||||
fontWeight: '600',
|
||||
},
|
||||
headerRight: {
|
||||
width: 44,
|
||||
height: 44,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
headerRightContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
headerRightBtn: {
|
||||
width: 36,
|
||||
height: 44,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
actionMenuBackdrop: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(0,0,0,0.08)',
|
||||
},
|
||||
actionMenu: {
|
||||
position: 'absolute',
|
||||
top: 88,
|
||||
right: spacing.md,
|
||||
width: 188,
|
||||
backgroundColor: colors.background.paper,
|
||||
borderRadius: 12,
|
||||
paddingVertical: spacing.sm,
|
||||
...shadows.md,
|
||||
},
|
||||
actionMenuItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.md,
|
||||
},
|
||||
actionMenuText: {
|
||||
marginLeft: spacing.sm,
|
||||
fontSize: 16,
|
||||
color: colors.text.primary,
|
||||
},
|
||||
searchContainer: {
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingBottom: spacing.sm,
|
||||
backgroundColor: colors.background.default,
|
||||
},
|
||||
searchBox: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.chat.surfaceInput,
|
||||
borderRadius: 10,
|
||||
paddingHorizontal: spacing.sm,
|
||||
paddingVertical: 10,
|
||||
},
|
||||
searchPlaceholder: {
|
||||
fontSize: 14,
|
||||
color: colors.text.hint,
|
||||
marginLeft: spacing.xs,
|
||||
},
|
||||
listContent: {
|
||||
flexGrow: 1,
|
||||
backgroundColor: colors.background.default,
|
||||
},
|
||||
loadingContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingVertical: spacing.xl * 2,
|
||||
},
|
||||
loadingMoreContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: spacing.md,
|
||||
},
|
||||
loadingMoreText: {
|
||||
marginLeft: spacing.sm,
|
||||
fontSize: 14,
|
||||
color: colors.text.hint,
|
||||
},
|
||||
searchModeContainer: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.background.default,
|
||||
},
|
||||
searchInputContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.chat.surfaceInput,
|
||||
borderRadius: 10,
|
||||
paddingHorizontal: spacing.md,
|
||||
marginHorizontal: spacing.md,
|
||||
marginTop: spacing.md,
|
||||
height: 40,
|
||||
},
|
||||
searchInput: {
|
||||
flex: 1,
|
||||
fontSize: 15,
|
||||
color: colors.text.primary,
|
||||
marginLeft: spacing.md,
|
||||
},
|
||||
searchTabs: {
|
||||
flexDirection: 'row',
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.sm,
|
||||
gap: spacing.sm,
|
||||
},
|
||||
searchTab: {
|
||||
flex: 1,
|
||||
paddingVertical: spacing.sm,
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.chat.surfaceInput,
|
||||
borderRadius: 8,
|
||||
},
|
||||
searchTabActive: {
|
||||
backgroundColor: colors.primary.main,
|
||||
},
|
||||
searchTabText: {
|
||||
fontSize: 14,
|
||||
color: colors.text.secondary,
|
||||
fontWeight: '500',
|
||||
},
|
||||
searchTabTextActive: {
|
||||
color: colors.primary.contrast,
|
||||
},
|
||||
searchResultsContent: {
|
||||
flexGrow: 1,
|
||||
paddingHorizontal: spacing.md,
|
||||
},
|
||||
searchResultItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingVertical: spacing.md,
|
||||
backgroundColor: colors.background.paper,
|
||||
borderRadius: 10,
|
||||
marginBottom: spacing.sm,
|
||||
paddingHorizontal: spacing.md,
|
||||
},
|
||||
searchResultContent: {
|
||||
flex: 1,
|
||||
marginLeft: spacing.md,
|
||||
},
|
||||
searchResultHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 2,
|
||||
},
|
||||
searchResultName: {
|
||||
fontSize: 15,
|
||||
fontWeight: '600',
|
||||
color: colors.text.primary,
|
||||
},
|
||||
searchResultTime: {
|
||||
fontSize: 12,
|
||||
color: colors.text.hint,
|
||||
},
|
||||
searchResultMessage: {
|
||||
fontSize: 13,
|
||||
color: colors.text.secondary,
|
||||
},
|
||||
searchingText: {
|
||||
marginTop: spacing.sm,
|
||||
fontSize: 14,
|
||||
color: colors.text.hint,
|
||||
},
|
||||
followingBadge: {
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: 12,
|
||||
backgroundColor: colors.primary.light + '30',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
highlightText: {
|
||||
color: colors.primary.main,
|
||||
fontWeight: '700',
|
||||
backgroundColor: colors.primary.light + '30',
|
||||
},
|
||||
matchedMessagesContainer: {
|
||||
marginTop: spacing.sm,
|
||||
},
|
||||
matchedMessageItem: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
paddingVertical: spacing.xs,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomColor: colors.divider,
|
||||
},
|
||||
matchedMessageText: {
|
||||
flex: 1,
|
||||
fontSize: 13,
|
||||
color: colors.text.secondary,
|
||||
lineHeight: 18,
|
||||
},
|
||||
matchedMessageTime: {
|
||||
fontSize: 11,
|
||||
color: colors.text.hint,
|
||||
marginLeft: spacing.sm,
|
||||
minWidth: 45,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -16,11 +16,11 @@ import {
|
||||
Dimensions,
|
||||
BackHandler,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { useIsFocused } from '@react-navigation/native';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import { colors, spacing, borderRadius, shadows } from '../../theme';
|
||||
import { spacing, borderRadius, shadows, useAppColors, type AppColors } from '../../theme';
|
||||
import { SystemMessageResponse } from '../../types/dto';
|
||||
import { messageService } from '../../services/messageService';
|
||||
import { commentService } from '../../services/commentService';
|
||||
@@ -45,6 +45,8 @@ const LIKE_SYSTEM_TYPES = new Set(['like_post', 'like_comment', 'like_reply', 'f
|
||||
const GROUP_SYSTEM_TYPES = new Set(['group_invite', 'group_join_apply', 'group_join_approved', 'group_join_rejected']);
|
||||
|
||||
export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack }) => {
|
||||
const colors = useAppColors();
|
||||
const styles = useMemo(() => createNotificationsStyles(colors), [colors]);
|
||||
const isFocused = useIsFocused();
|
||||
const { setSystemUnreadCount, decrementSystemUnreadCount } = useMessageManagerSystemUnreadCount();
|
||||
const router = useRouter();
|
||||
@@ -554,143 +556,145 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.background.default,
|
||||
},
|
||||
// Header 样式 - 美化版
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: spacing.lg,
|
||||
paddingVertical: spacing.md,
|
||||
backgroundColor: colors.background.paper,
|
||||
...shadows.sm,
|
||||
},
|
||||
headerWide: {
|
||||
paddingHorizontal: spacing.xl,
|
||||
paddingVertical: spacing.lg,
|
||||
},
|
||||
headerTitleContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
headerTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
color: colors.text.primary,
|
||||
},
|
||||
headerTitleWide: {
|
||||
fontSize: 20,
|
||||
},
|
||||
unreadBadge: {
|
||||
backgroundColor: colors.primary.main,
|
||||
borderRadius: 10,
|
||||
paddingHorizontal: 6,
|
||||
paddingVertical: 2,
|
||||
marginLeft: spacing.sm,
|
||||
minWidth: 20,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
unreadBadgeText: {
|
||||
color: colors.text.inverse,
|
||||
fontSize: 11,
|
||||
fontWeight: '600',
|
||||
},
|
||||
backButton: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
backButtonPlaceholder: {
|
||||
width: 40,
|
||||
},
|
||||
// 分类筛选 - 美化版(使用分段式设计)
|
||||
filterContainer: {
|
||||
flexDirection: 'row',
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.sm,
|
||||
backgroundColor: colors.background.paper,
|
||||
},
|
||||
filterContainerWideWeb: {
|
||||
paddingHorizontal: spacing.xl,
|
||||
paddingVertical: spacing.md,
|
||||
justifyContent: 'center',
|
||||
backgroundColor: colors.background.paper,
|
||||
},
|
||||
filterTag: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.sm,
|
||||
marginRight: spacing.xs,
|
||||
borderRadius: borderRadius.lg,
|
||||
backgroundColor: colors.background.default,
|
||||
borderWidth: 1,
|
||||
borderColor: 'transparent',
|
||||
},
|
||||
filterTagWide: {
|
||||
paddingHorizontal: spacing.lg,
|
||||
paddingVertical: spacing.md,
|
||||
marginRight: spacing.sm,
|
||||
},
|
||||
filterTagActive: {
|
||||
backgroundColor: colors.primary.main,
|
||||
borderColor: colors.primary.main,
|
||||
},
|
||||
filterTagTextActive: {
|
||||
fontWeight: '600',
|
||||
},
|
||||
filterCount: {
|
||||
marginLeft: spacing.xs,
|
||||
backgroundColor: colors.primary.light + '40',
|
||||
paddingHorizontal: 6,
|
||||
paddingVertical: 1,
|
||||
borderRadius: 8,
|
||||
minWidth: 18,
|
||||
alignItems: 'center',
|
||||
},
|
||||
filterCountActive: {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.25)',
|
||||
},
|
||||
listContent: {
|
||||
flexGrow: 1,
|
||||
paddingTop: spacing.md,
|
||||
paddingBottom: spacing.lg,
|
||||
},
|
||||
listContentWide: {
|
||||
paddingHorizontal: spacing.xl,
|
||||
},
|
||||
loadingContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingVertical: spacing.xl * 2,
|
||||
},
|
||||
loadingMore: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingVertical: spacing.lg,
|
||||
},
|
||||
loadingMoreText: {
|
||||
marginLeft: spacing.sm,
|
||||
},
|
||||
emptyContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingTop: spacing['3xl'],
|
||||
},
|
||||
emptyContainerWeb: {
|
||||
minHeight: 500,
|
||||
justifyContent: 'center',
|
||||
paddingTop: 100,
|
||||
},
|
||||
});
|
||||
function createNotificationsStyles(colors: AppColors) {
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.background.default,
|
||||
},
|
||||
// Header 样式 - 美化版
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: spacing.lg,
|
||||
paddingVertical: spacing.md,
|
||||
backgroundColor: colors.background.paper,
|
||||
...shadows.sm,
|
||||
},
|
||||
headerWide: {
|
||||
paddingHorizontal: spacing.xl,
|
||||
paddingVertical: spacing.lg,
|
||||
},
|
||||
headerTitleContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
headerTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
color: colors.text.primary,
|
||||
},
|
||||
headerTitleWide: {
|
||||
fontSize: 20,
|
||||
},
|
||||
unreadBadge: {
|
||||
backgroundColor: colors.primary.main,
|
||||
borderRadius: 10,
|
||||
paddingHorizontal: 6,
|
||||
paddingVertical: 2,
|
||||
marginLeft: spacing.sm,
|
||||
minWidth: 20,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
unreadBadgeText: {
|
||||
color: colors.text.inverse,
|
||||
fontSize: 11,
|
||||
fontWeight: '600',
|
||||
},
|
||||
backButton: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
backButtonPlaceholder: {
|
||||
width: 40,
|
||||
},
|
||||
// 分类筛选 - 美化版(使用分段式设计)
|
||||
filterContainer: {
|
||||
flexDirection: 'row',
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.sm,
|
||||
backgroundColor: colors.background.paper,
|
||||
},
|
||||
filterContainerWideWeb: {
|
||||
paddingHorizontal: spacing.xl,
|
||||
paddingVertical: spacing.md,
|
||||
justifyContent: 'center',
|
||||
backgroundColor: colors.background.paper,
|
||||
},
|
||||
filterTag: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.sm,
|
||||
marginRight: spacing.xs,
|
||||
borderRadius: borderRadius.lg,
|
||||
backgroundColor: colors.background.default,
|
||||
borderWidth: 1,
|
||||
borderColor: 'transparent',
|
||||
},
|
||||
filterTagWide: {
|
||||
paddingHorizontal: spacing.lg,
|
||||
paddingVertical: spacing.md,
|
||||
marginRight: spacing.sm,
|
||||
},
|
||||
filterTagActive: {
|
||||
backgroundColor: colors.primary.main,
|
||||
borderColor: colors.primary.main,
|
||||
},
|
||||
filterTagTextActive: {
|
||||
fontWeight: '600',
|
||||
},
|
||||
filterCount: {
|
||||
marginLeft: spacing.xs,
|
||||
backgroundColor: colors.primary.light + '40',
|
||||
paddingHorizontal: 6,
|
||||
paddingVertical: 1,
|
||||
borderRadius: 8,
|
||||
minWidth: 18,
|
||||
alignItems: 'center',
|
||||
},
|
||||
filterCountActive: {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.25)',
|
||||
},
|
||||
listContent: {
|
||||
flexGrow: 1,
|
||||
paddingTop: spacing.md,
|
||||
paddingBottom: spacing.lg,
|
||||
},
|
||||
listContentWide: {
|
||||
paddingHorizontal: spacing.xl,
|
||||
},
|
||||
loadingContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingVertical: spacing.xl * 2,
|
||||
},
|
||||
loadingMore: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingVertical: spacing.lg,
|
||||
},
|
||||
loadingMoreText: {
|
||||
marginLeft: spacing.sm,
|
||||
},
|
||||
emptyContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingTop: spacing['3xl'],
|
||||
},
|
||||
emptyContainerWeb: {
|
||||
minHeight: 500,
|
||||
justifyContent: 'center',
|
||||
paddingTop: 100,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* 参考QQ和微信的实现,提供聊天管理功能
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import {
|
||||
View,
|
||||
StyleSheet,
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { useRouter, useLocalSearchParams } from 'expo-router';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import { colors, spacing, fontSizes, borderRadius, shadows } from '../../theme';
|
||||
import { spacing, borderRadius, shadows, useAppColors, type AppColors } from '../../theme';
|
||||
import { useAuthStore } from '../../stores';
|
||||
import { authService } from '../../services/authService';
|
||||
import { messageService } from '../../services/messageService';
|
||||
@@ -33,6 +33,8 @@ import * as hrefs from '../../navigation/hrefs';
|
||||
import { AppBackButton } from '../../components/common';
|
||||
|
||||
const PrivateChatInfoScreen: React.FC = () => {
|
||||
const colors = useAppColors();
|
||||
const styles = useMemo(() => createPrivateChatInfoStyles(colors), [colors]);
|
||||
const router = useRouter();
|
||||
const raw = useLocalSearchParams<{
|
||||
conversationId?: string;
|
||||
@@ -277,7 +279,7 @@ const PrivateChatInfoScreen: React.FC = () => {
|
||||
value={value}
|
||||
onValueChange={onValueChange}
|
||||
trackColor={{ false: '#E0E0E0', true: colors.primary.light }}
|
||||
thumbColor={value ? colors.primary.main : '#F5F5F5'}
|
||||
thumbColor={value ? colors.primary.main : colors.background.default}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
@@ -420,101 +422,103 @@ const PrivateChatInfoScreen: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.background.default,
|
||||
},
|
||||
pageHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.sm,
|
||||
backgroundColor: colors.background.paper,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.divider,
|
||||
},
|
||||
pageTitle: {
|
||||
fontWeight: '600',
|
||||
},
|
||||
pageHeaderPlaceholder: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContent: {
|
||||
paddingBottom: spacing.xl,
|
||||
},
|
||||
headerCard: {
|
||||
backgroundColor: colors.background.paper,
|
||||
marginHorizontal: spacing.md,
|
||||
marginTop: spacing.md,
|
||||
borderRadius: borderRadius.lg,
|
||||
padding: spacing.lg,
|
||||
...shadows.sm,
|
||||
},
|
||||
userHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
userInfo: {
|
||||
flex: 1,
|
||||
marginLeft: spacing.md,
|
||||
},
|
||||
userName: {
|
||||
fontWeight: '600',
|
||||
marginBottom: spacing.xs,
|
||||
},
|
||||
section: {
|
||||
marginTop: spacing.lg,
|
||||
paddingHorizontal: spacing.md,
|
||||
},
|
||||
sectionTitle: {
|
||||
marginBottom: spacing.sm,
|
||||
marginLeft: spacing.sm,
|
||||
fontWeight: '500',
|
||||
},
|
||||
card: {
|
||||
backgroundColor: colors.background.paper,
|
||||
borderRadius: borderRadius.lg,
|
||||
...shadows.sm,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
settingItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingVertical: spacing.sm,
|
||||
paddingHorizontal: spacing.md,
|
||||
},
|
||||
settingIconContainer: {
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: borderRadius.md,
|
||||
backgroundColor: colors.primary.light + '20',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginRight: spacing.md,
|
||||
},
|
||||
settingIconDanger: {
|
||||
backgroundColor: colors.error.light + '20',
|
||||
},
|
||||
settingTitle: {
|
||||
flex: 1,
|
||||
},
|
||||
dangerText: {
|
||||
color: colors.error.main,
|
||||
},
|
||||
divider: {
|
||||
marginLeft: spacing.xl + 36,
|
||||
width: 'auto',
|
||||
marginVertical: spacing.xs,
|
||||
},
|
||||
deleteButton: {
|
||||
marginTop: spacing.sm,
|
||||
},
|
||||
});
|
||||
function createPrivateChatInfoStyles(colors: AppColors) {
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.background.default,
|
||||
},
|
||||
pageHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.sm,
|
||||
backgroundColor: colors.background.paper,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.divider,
|
||||
},
|
||||
pageTitle: {
|
||||
fontWeight: '600',
|
||||
},
|
||||
pageHeaderPlaceholder: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContent: {
|
||||
paddingBottom: spacing.xl,
|
||||
},
|
||||
headerCard: {
|
||||
backgroundColor: colors.background.paper,
|
||||
marginHorizontal: spacing.md,
|
||||
marginTop: spacing.md,
|
||||
borderRadius: borderRadius.lg,
|
||||
padding: spacing.lg,
|
||||
...shadows.sm,
|
||||
},
|
||||
userHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
userInfo: {
|
||||
flex: 1,
|
||||
marginLeft: spacing.md,
|
||||
},
|
||||
userName: {
|
||||
fontWeight: '600',
|
||||
marginBottom: spacing.xs,
|
||||
},
|
||||
section: {
|
||||
marginTop: spacing.lg,
|
||||
paddingHorizontal: spacing.md,
|
||||
},
|
||||
sectionTitle: {
|
||||
marginBottom: spacing.sm,
|
||||
marginLeft: spacing.sm,
|
||||
fontWeight: '500',
|
||||
},
|
||||
card: {
|
||||
backgroundColor: colors.background.paper,
|
||||
borderRadius: borderRadius.lg,
|
||||
...shadows.sm,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
settingItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingVertical: spacing.sm,
|
||||
paddingHorizontal: spacing.md,
|
||||
},
|
||||
settingIconContainer: {
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: borderRadius.md,
|
||||
backgroundColor: colors.primary.light + '20',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginRight: spacing.md,
|
||||
},
|
||||
settingIconDanger: {
|
||||
backgroundColor: colors.error.light + '20',
|
||||
},
|
||||
settingTitle: {
|
||||
flex: 1,
|
||||
},
|
||||
dangerText: {
|
||||
color: colors.error.main,
|
||||
},
|
||||
divider: {
|
||||
marginLeft: spacing.xl + 36,
|
||||
width: 'auto',
|
||||
marginVertical: spacing.xs,
|
||||
},
|
||||
deleteButton: {
|
||||
marginTop: spacing.sm,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export default PrivateChatInfoScreen;
|
||||
|
||||
@@ -7,8 +7,8 @@ import React, { useMemo } from 'react';
|
||||
import { View, TouchableOpacity, StyleSheet } from 'react-native';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import { Avatar, Text, AppBackButton } from '../../../../components/common';
|
||||
import { colors, spacing } from '../../../../theme';
|
||||
import { chatScreenStyles as baseStyles } from './styles';
|
||||
import { useAppColors, spacing } from '../../../../theme';
|
||||
import { useChatScreenStyles } from './styles';
|
||||
import { useBreakpointGTE } from '../../../../hooks/useResponsive';
|
||||
import { ChatHeaderProps } from './types';
|
||||
|
||||
@@ -24,6 +24,9 @@ export const ChatHeader: React.FC<ChatHeaderProps> = ({
|
||||
onGroupInfoPress,
|
||||
isWideScreen: propIsWideScreen,
|
||||
}) => {
|
||||
const colors = useAppColors();
|
||||
const baseStyles = useChatScreenStyles();
|
||||
|
||||
// 响应式布局
|
||||
// 使用 768px 作为大屏幕和小屏幕的分界线
|
||||
const isWideScreenHook = useBreakpointGTE('lg');
|
||||
@@ -57,7 +60,7 @@ export const ChatHeader: React.FC<ChatHeaderProps> = ({
|
||||
fontSize: isWideScreen ? 13 : 12,
|
||||
},
|
||||
});
|
||||
}, [isWideScreen]);
|
||||
}, [isWideScreen, baseStyles]);
|
||||
|
||||
// 获取显示标题
|
||||
const getDisplayTitle = () => {
|
||||
|
||||
@@ -7,8 +7,8 @@ import React, { useMemo } from 'react';
|
||||
import { View, TouchableOpacity, TextInput, ActivityIndicator, ScrollView, StyleSheet, Image as RNImage } from 'react-native';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import { Text } from '../../../../components/common';
|
||||
import { colors } from '../../../../theme';
|
||||
import { chatScreenStyles as baseStyles } from './styles';
|
||||
import { useAppColors } from '../../../../theme';
|
||||
import { useChatScreenStyles } from './styles';
|
||||
import { useBreakpointGTE } from '../../../../hooks/useResponsive';
|
||||
import { ChatInputProps, GroupMessage, SenderInfo } from './types';
|
||||
import { extractTextFromSegments } from '../../../../types/dto';
|
||||
@@ -39,14 +39,15 @@ export const ChatInput: React.FC<ChatInputProps & {
|
||||
otherUser,
|
||||
getSenderInfo,
|
||||
}) => {
|
||||
const colors = useAppColors();
|
||||
const styles = useChatScreenStyles();
|
||||
const isDisabled = isGroupChat && isMuted;
|
||||
|
||||
|
||||
// 获取当前用户ID
|
||||
const currentUserId = currentUser?.id || '';
|
||||
|
||||
|
||||
// 响应式布局
|
||||
const isWideScreen = useBreakpointGTE('lg');
|
||||
const styles = baseStyles;
|
||||
|
||||
const inputContainerStyle = useMemo(() => ([
|
||||
styles.inputContainer,
|
||||
|
||||
@@ -10,11 +10,11 @@ import { Image as ExpoImage } from 'expo-image';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import * as ImagePicker from 'expo-image-picker';
|
||||
import { Text } from '../../../../components/common';
|
||||
import { chatScreenStyles as baseStyles } from './styles';
|
||||
import { useChatScreenStyles } from './styles';
|
||||
import { useResponsive, useBreakpointGTE } from '../../../../hooks/useResponsive';
|
||||
import { EMOJIS } from './constants';
|
||||
import { CustomSticker, getCustomStickers, batchDeleteStickers, addStickerFromUrl } from '../../../../services/stickerService';
|
||||
import { colors, spacing } from '../../../../theme';
|
||||
import { useAppColors, spacing } from '../../../../theme';
|
||||
|
||||
const { height: SCREEN_HEIGHT } = Dimensions.get('window');
|
||||
|
||||
@@ -41,6 +41,8 @@ export const EmojiPanel: React.FC<EmojiPanelProps> = ({
|
||||
onInsertSticker,
|
||||
onClose,
|
||||
}) => {
|
||||
const colors = useAppColors();
|
||||
const baseStyles = useChatScreenStyles();
|
||||
const [activeTab, setActiveTab] = useState<TabType>('emoji');
|
||||
const [stickers, setStickers] = useState<CustomSticker[]>([]);
|
||||
const [loadingStickers, setLoadingStickers] = useState(false);
|
||||
@@ -82,7 +84,7 @@ export const EmojiPanel: React.FC<EmojiPanelProps> = ({
|
||||
lineHeight: emojiSize.size + 6,
|
||||
},
|
||||
});
|
||||
}, [emojiSize, emojiColumns]);
|
||||
}, [emojiSize, emojiColumns, baseStyles]);
|
||||
|
||||
// 加载自定义表情
|
||||
const loadStickers = useCallback(async () => {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* 实现手机端群信息界面的核心功能
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import React, { useEffect, useState, useCallback, useMemo } from 'react';
|
||||
import {
|
||||
View,
|
||||
StyleSheet,
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
} from 'react-native';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import { Avatar, Text } from '../../../../components/common';
|
||||
import { colors, spacing, fontSizes, shadows } from '../../../../theme';
|
||||
import { useAppColors, spacing, fontSizes, shadows, type AppColors } from '../../../../theme';
|
||||
import { GroupMemberResponse, GroupResponse, GroupAnnouncementResponse } from '../../../../types/dto';
|
||||
import { useBreakpointGTE } from '../../../../hooks/useResponsive';
|
||||
import { groupManager } from '../../../../stores/groupManager';
|
||||
@@ -53,6 +53,8 @@ export const GroupInfoPanel: React.FC<GroupInfoPanelProps> = ({
|
||||
onInviteMembers,
|
||||
onViewAllMembers,
|
||||
}) => {
|
||||
const colors = useAppColors();
|
||||
const styles = useMemo(() => createGroupInfoPanelStyles(colors), [colors]);
|
||||
const isWideScreen = useBreakpointGTE('lg');
|
||||
const slideAnim = React.useRef(new Animated.Value(PANEL_WIDTH)).current;
|
||||
const fadeAnim = React.useRef(new Animated.Value(0)).current;
|
||||
@@ -363,7 +365,8 @@ export const GroupInfoPanel: React.FC<GroupInfoPanelProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
function createGroupInfoPanelStyles(colors: AppColors) {
|
||||
return StyleSheet.create({
|
||||
overlay: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.3)',
|
||||
@@ -378,7 +381,7 @@ const styles = StyleSheet.create({
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: PANEL_WIDTH,
|
||||
backgroundColor: '#FFF',
|
||||
backgroundColor: colors.background.paper,
|
||||
zIndex: 101,
|
||||
...shadows.lg,
|
||||
},
|
||||
@@ -389,12 +392,12 @@ const styles = StyleSheet.create({
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.md,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#E8E8E8',
|
||||
borderBottomColor: colors.divider,
|
||||
},
|
||||
headerTitle: {
|
||||
fontSize: fontSizes.xl,
|
||||
fontWeight: '600',
|
||||
color: '#333',
|
||||
color: colors.text.primary,
|
||||
},
|
||||
closeButton: {
|
||||
padding: spacing.xs,
|
||||
@@ -409,12 +412,12 @@ const styles = StyleSheet.create({
|
||||
groupName: {
|
||||
fontSize: fontSizes.xl,
|
||||
fontWeight: '600',
|
||||
color: '#333',
|
||||
color: colors.text.primary,
|
||||
marginTop: spacing.md,
|
||||
},
|
||||
memberCount: {
|
||||
fontSize: fontSizes.md,
|
||||
color: '#999',
|
||||
color: colors.text.secondary,
|
||||
marginTop: spacing.xs,
|
||||
},
|
||||
joinType: {
|
||||
@@ -442,7 +445,7 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
divider: {
|
||||
height: 1,
|
||||
backgroundColor: '#E8E8E8',
|
||||
backgroundColor: colors.divider,
|
||||
marginHorizontal: spacing.md,
|
||||
},
|
||||
section: {
|
||||
@@ -456,11 +459,11 @@ const styles = StyleSheet.create({
|
||||
sectionTitle: {
|
||||
fontSize: fontSizes.md,
|
||||
fontWeight: '600',
|
||||
color: '#333',
|
||||
color: colors.text.primary,
|
||||
marginLeft: spacing.xs,
|
||||
},
|
||||
noticeBox: {
|
||||
backgroundColor: '#FFF9E6',
|
||||
backgroundColor: colors.chat.tipBg,
|
||||
padding: spacing.md,
|
||||
borderRadius: 8,
|
||||
borderLeftWidth: 3,
|
||||
@@ -468,23 +471,23 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
noticeText: {
|
||||
fontSize: fontSizes.sm,
|
||||
color: '#666',
|
||||
color: colors.text.secondary,
|
||||
lineHeight: 20,
|
||||
},
|
||||
noticeTime: {
|
||||
fontSize: fontSizes.xs,
|
||||
color: '#999',
|
||||
color: colors.text.hint,
|
||||
marginTop: spacing.xs,
|
||||
textAlign: 'right',
|
||||
},
|
||||
descriptionBox: {
|
||||
backgroundColor: '#F5F7FA',
|
||||
backgroundColor: colors.chat.surfaceMuted,
|
||||
padding: spacing.md,
|
||||
borderRadius: 8,
|
||||
},
|
||||
description: {
|
||||
fontSize: fontSizes.sm,
|
||||
color: '#666',
|
||||
color: colors.text.secondary,
|
||||
lineHeight: 20,
|
||||
},
|
||||
memberList: {
|
||||
@@ -518,7 +521,7 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
memberName: {
|
||||
fontSize: fontSizes.md,
|
||||
color: '#333',
|
||||
color: colors.text.primary,
|
||||
flex: 1,
|
||||
},
|
||||
ownerBadge: {
|
||||
@@ -541,7 +544,7 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
moreMembers: {
|
||||
fontSize: fontSizes.sm,
|
||||
color: '#999',
|
||||
color: colors.text.secondary,
|
||||
textAlign: 'center',
|
||||
paddingVertical: spacing.sm,
|
||||
},
|
||||
@@ -550,15 +553,15 @@ const styles = StyleSheet.create({
|
||||
justifyContent: 'space-between',
|
||||
paddingVertical: spacing.sm,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#F0F0F0',
|
||||
borderBottomColor: colors.chat.borderHairline,
|
||||
},
|
||||
infoLabel: {
|
||||
fontSize: fontSizes.sm,
|
||||
color: '#999',
|
||||
color: colors.text.secondary,
|
||||
},
|
||||
infoValue: {
|
||||
fontSize: fontSizes.sm,
|
||||
color: '#333',
|
||||
color: colors.text.primary,
|
||||
},
|
||||
actionSection: {
|
||||
padding: spacing.md,
|
||||
@@ -588,6 +591,7 @@ const styles = StyleSheet.create({
|
||||
leaveButtonText: {
|
||||
color: colors.error.main,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export default GroupInfoPanel;
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import { chatScreenStyles as styles } from './styles';
|
||||
import { useChatScreenStyles } from './styles';
|
||||
import { LongPressMenuProps } from './types';
|
||||
import { RECALL_TIME_LIMIT } from './constants';
|
||||
import { extractTextFromSegments, ImageSegmentData } from '../../../../types/dto';
|
||||
@@ -34,6 +34,7 @@ export const LongPressMenu: React.FC<LongPressMenuProps> = ({
|
||||
onDelete,
|
||||
onAddSticker,
|
||||
}) => {
|
||||
const styles = useChatScreenStyles();
|
||||
const scaleAnimation = useRef(new Animated.Value(0)).current;
|
||||
const opacityAnimation = useRef(new Animated.Value(0)).current;
|
||||
const blurActiveElementOnWeb = () => {
|
||||
|
||||
@@ -6,7 +6,7 @@ import React from 'react';
|
||||
import { View, ScrollView, TouchableOpacity } from 'react-native';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import { Avatar, Text } from '../../../../components/common';
|
||||
import { chatScreenStyles as styles } from './styles';
|
||||
import { useChatScreenStyles } from './styles';
|
||||
import { MentionPanelProps } from './types';
|
||||
|
||||
export const MentionPanel: React.FC<MentionPanelProps> = ({
|
||||
@@ -18,6 +18,7 @@ export const MentionPanel: React.FC<MentionPanelProps> = ({
|
||||
onMentionAll,
|
||||
onClose,
|
||||
}) => {
|
||||
const styles = useChatScreenStyles();
|
||||
// 过滤成员列表
|
||||
const filteredMembers = members.filter(member => {
|
||||
if (member.user_id === currentUserId) return false; // 排除自己
|
||||
|
||||
@@ -15,20 +15,12 @@ import {
|
||||
} from 'react-native';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import { Avatar, Text, ImageGridItem } from '../../../../components/common';
|
||||
import { chatScreenStyles as baseStyles } from './styles';
|
||||
import { useChatScreenStyles } from './styles';
|
||||
import { useResponsive, useBreakpointGTE } from '../../../../hooks/useResponsive';
|
||||
import { MessageBubbleProps, SenderInfo, MenuPosition, GroupMessage } from './types';
|
||||
import { MessageSegmentsRenderer } from './SegmentRenderer';
|
||||
import { MessageSegment, ImageSegmentData, extractTextFromSegments } from '../../../../types/dto';
|
||||
import { SwipeableMessageBubble } from './SwipeableMessageBubble';
|
||||
import {
|
||||
getBubbleBorderRadius,
|
||||
getMessageGroupPosition,
|
||||
shouldShowSenderInfo,
|
||||
bubbleColors,
|
||||
bubbleStyles,
|
||||
} from './bubbleStyles';
|
||||
|
||||
// 获取屏幕宽度
|
||||
const { width: SCREEN_WIDTH } = Dimensions.get('window');
|
||||
|
||||
@@ -61,7 +53,8 @@ export const MessageBubble: React.FC<MessageBubbleProps> = ({
|
||||
}) => {
|
||||
const bubbleRef = useRef<View>(null);
|
||||
const pressPositionRef = useRef({ x: 0, y: 0 });
|
||||
|
||||
const baseStyles = useChatScreenStyles();
|
||||
|
||||
// 响应式布局
|
||||
const { width } = useResponsive();
|
||||
const isWideScreen = useBreakpointGTE('lg');
|
||||
@@ -89,7 +82,7 @@ export const MessageBubble: React.FC<MessageBubbleProps> = ({
|
||||
height: isWideScreen ? 280 : 220,
|
||||
},
|
||||
});
|
||||
}, [maxContentWidth, isWideScreen]);
|
||||
}, [maxContentWidth, isWideScreen, baseStyles]);
|
||||
|
||||
// 系统通知消息特殊处理
|
||||
// 支持两种判断方式:
|
||||
@@ -236,26 +229,46 @@ export const MessageBubble: React.FC<MessageBubbleProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const renderBubbleShell = useCallback(
|
||||
(
|
||||
child: React.ReactNode,
|
||||
innerExtra: (object | undefined | null | false)[] = []
|
||||
) => (
|
||||
<View
|
||||
style={[
|
||||
styles.messageBubbleOuter,
|
||||
isMe ? styles.myBubbleOuter : styles.theirBubbleOuter,
|
||||
]}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
styles.messageBubbleInner,
|
||||
isMe ? styles.myBubbleInner : styles.theirBubbleInner,
|
||||
...innerExtra.filter(Boolean),
|
||||
]}
|
||||
>
|
||||
{child}
|
||||
</View>
|
||||
</View>
|
||||
),
|
||||
[isMe, styles]
|
||||
);
|
||||
|
||||
// 渲染消息内容
|
||||
const renderMessageContent = () => {
|
||||
// 撤回消息
|
||||
if (isRecalled) {
|
||||
return (
|
||||
<View style={[
|
||||
styles.messageBubble,
|
||||
isMe ? styles.myBubble : styles.theirBubble,
|
||||
styles.recalledBubble,
|
||||
]}>
|
||||
<Text
|
||||
style={[
|
||||
isMe ? styles.myBubbleText : styles.theirBubbleText,
|
||||
styles.recalledText,
|
||||
]}
|
||||
selectable={false}
|
||||
>
|
||||
消息已撤回
|
||||
</Text>
|
||||
</View>
|
||||
return renderBubbleShell(
|
||||
<Text
|
||||
style={[
|
||||
isMe ? styles.myBubbleText : styles.theirBubbleText,
|
||||
styles.recalledText,
|
||||
]}
|
||||
selectable={false}
|
||||
>
|
||||
消息已撤回
|
||||
</Text>,
|
||||
[styles.recalledBubble]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -282,71 +295,60 @@ export const MessageBubble: React.FC<MessageBubbleProps> = ({
|
||||
|
||||
// 如果没有 segments,显示空内容或错误提示
|
||||
if (!hasSegments) {
|
||||
return (
|
||||
<View style={[
|
||||
styles.messageBubble,
|
||||
isMe ? styles.myBubble : styles.theirBubble,
|
||||
]}>
|
||||
<Text
|
||||
style={[
|
||||
isMe ? styles.myBubbleText : styles.theirBubbleText,
|
||||
{ opacity: 0.5 },
|
||||
]}
|
||||
selectable={false}
|
||||
>
|
||||
[消息格式错误]
|
||||
</Text>
|
||||
</View>
|
||||
return renderBubbleShell(
|
||||
<Text
|
||||
style={[
|
||||
isMe ? styles.myBubbleText : styles.theirBubbleText,
|
||||
{ opacity: 0.5 },
|
||||
]}
|
||||
selectable={false}
|
||||
>
|
||||
[消息格式错误]
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
// 检查是否有回复,用于调整气泡样式
|
||||
const hasReply = segments.some(s => s.type === 'reply');
|
||||
|
||||
return (
|
||||
<View style={[
|
||||
styles.messageBubble,
|
||||
isMe ? styles.myBubble : styles.theirBubble,
|
||||
hasReply && segmentStyles.replyBubble,
|
||||
isPureImageMessage && segmentStyles.pureImageBubble,
|
||||
]}>
|
||||
<MessageSegmentsRenderer
|
||||
segments={segments}
|
||||
isMe={isMe}
|
||||
currentUserId={currentUserId}
|
||||
memberMap={memberMap}
|
||||
replyMessage={getReplyMessage()}
|
||||
getSenderInfo={getSenderInfo}
|
||||
onAtPress={() => undefined}
|
||||
onReplyPress={onReplyPress}
|
||||
onImagePress={(url) => {
|
||||
// 查找点击的图片索引
|
||||
const clickIndex = imageSegments.findIndex(
|
||||
img => img.url === url || img.thumbnail_url === url
|
||||
);
|
||||
if (clickIndex !== -1 && onImagePress) {
|
||||
onImagePress(imageSegments, clickIndex);
|
||||
}
|
||||
}}
|
||||
onImageLongPress={(position?: { x: number; y: number }) => {
|
||||
// 图片长按触发和消息气泡一样的菜单
|
||||
// 如果有位置信息,直接使用;否则使用气泡位置
|
||||
if (position) {
|
||||
onLongPress(message, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
pressX: position.x,
|
||||
pressY: position.y,
|
||||
});
|
||||
} else {
|
||||
handleLongPress();
|
||||
}
|
||||
}}
|
||||
onLinkPress={() => undefined}
|
||||
/>
|
||||
</View>
|
||||
return renderBubbleShell(
|
||||
<MessageSegmentsRenderer
|
||||
segments={segments}
|
||||
isMe={isMe}
|
||||
currentUserId={currentUserId}
|
||||
memberMap={memberMap}
|
||||
replyMessage={getReplyMessage()}
|
||||
getSenderInfo={getSenderInfo}
|
||||
onAtPress={() => undefined}
|
||||
onReplyPress={onReplyPress}
|
||||
onImagePress={(url) => {
|
||||
// 查找点击的图片索引
|
||||
const clickIndex = imageSegments.findIndex(
|
||||
img => img.url === url || img.thumbnail_url === url
|
||||
);
|
||||
if (clickIndex !== -1 && onImagePress) {
|
||||
onImagePress(imageSegments, clickIndex);
|
||||
}
|
||||
}}
|
||||
onImageLongPress={(position?: { x: number; y: number }) => {
|
||||
// 图片长按触发和消息气泡一样的菜单
|
||||
// 如果有位置信息,直接使用;否则使用气泡位置
|
||||
if (position) {
|
||||
onLongPress(message, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
pressX: position.x,
|
||||
pressY: position.y,
|
||||
});
|
||||
} else {
|
||||
handleLongPress();
|
||||
}
|
||||
}}
|
||||
onLinkPress={() => undefined}
|
||||
/>,
|
||||
[hasReply && segmentStyles.replyBubble, isPureImageMessage && segmentStyles.pureImageBubble]
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import React from 'react';
|
||||
import { View, TouchableOpacity } from 'react-native';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import { Text } from '../../../../components/common';
|
||||
import { chatScreenStyles as styles } from './styles';
|
||||
import { useChatScreenStyles } from './styles';
|
||||
import { MORE_ACTIONS } from './constants';
|
||||
import { MorePanelProps } from './types';
|
||||
|
||||
@@ -14,6 +14,7 @@ export const MorePanel: React.FC<MorePanelProps> = ({
|
||||
onAction,
|
||||
disabledActionIds = [],
|
||||
}) => {
|
||||
const styles = useChatScreenStyles();
|
||||
const disabledSet = new Set(disabledActionIds);
|
||||
return (
|
||||
<View style={styles.panelContainer}>
|
||||
|
||||
@@ -31,7 +31,7 @@ import {
|
||||
UserDTO,
|
||||
extractTextFromSegments,
|
||||
} from '../../../../types/dto';
|
||||
import { colors, spacing } from '../../../../theme';
|
||||
import { spacing, useAppColors, type AppColors } from '../../../../theme';
|
||||
import { SenderInfo } from './types';
|
||||
|
||||
const IMAGE_MAX_WIDTH = 200;
|
||||
@@ -142,12 +142,11 @@ export const renderSegment = (
|
||||
/**
|
||||
* 渲染文本 Segment
|
||||
*/
|
||||
const renderTextSegment = (data: TextSegmentData, isMe: boolean): React.ReactNode => {
|
||||
// 兼容 content 和 text 两种字段
|
||||
const TextSegmentBody: React.FC<{ data: TextSegmentData; isMe: boolean }> = ({ data, isMe }) => {
|
||||
const styles = useSegmentStyles();
|
||||
const text = data.content ?? data.text ?? '';
|
||||
return (
|
||||
<Text
|
||||
key={`text-${text.substring(0, 10)}`}
|
||||
style={[styles.textContent, isMe ? styles.textMe : styles.textOther]}
|
||||
selectable={false}
|
||||
>
|
||||
@@ -156,6 +155,11 @@ const renderTextSegment = (data: TextSegmentData, isMe: boolean): React.ReactNod
|
||||
);
|
||||
};
|
||||
|
||||
const renderTextSegment = (data: TextSegmentData, isMe: boolean): React.ReactNode => {
|
||||
const key = `text-${(data.content ?? data.text ?? '').substring(0, 10)}`;
|
||||
return <TextSegmentBody key={key} data={data} isMe={isMe} />;
|
||||
};
|
||||
|
||||
/**
|
||||
* 渲染图片 Segment
|
||||
*/
|
||||
@@ -166,6 +170,7 @@ const ImageSegment: React.FC<{
|
||||
onImagePress?: (url: string) => void;
|
||||
onImageLongPress?: (position?: { x: number; y: number }) => void;
|
||||
}> = ({ data, isMe, onImagePress, onImageLongPress }) => {
|
||||
const styles = useSegmentStyles();
|
||||
const pressPositionRef = useRef({ x: 0, y: 0 });
|
||||
const [dimensions, setDimensions] = useState<{ width: number; height: number } | null>(null);
|
||||
const [loadError, setLoadError] = useState(false);
|
||||
@@ -362,16 +367,13 @@ const renderImageSegment = (
|
||||
/**
|
||||
* 渲染 @ Segment
|
||||
*/
|
||||
const renderAtSegment = (
|
||||
data: AtSegmentData,
|
||||
props: Omit<SegmentRendererProps, 'segment'>
|
||||
): React.ReactNode => {
|
||||
const AtSegmentBody: React.FC<{
|
||||
data: AtSegmentData;
|
||||
props: Omit<SegmentRendererProps, 'segment'>;
|
||||
}> = ({ data, props }) => {
|
||||
const styles = useSegmentStyles();
|
||||
const { currentUserId, memberMap, onAtPress, isMe } = props;
|
||||
|
||||
// 判断是否@当前用户
|
||||
const isAtMe = data.user_id === currentUserId || data.user_id === 'all';
|
||||
|
||||
// 优先从 memberMap 中查找昵称,兜底使用 data.nickname(兼容旧消息),再兜底显示"用户"
|
||||
let displayName: string;
|
||||
if (data.user_id === 'all') {
|
||||
displayName = '所有人';
|
||||
@@ -379,10 +381,8 @@ const renderAtSegment = (
|
||||
const member = memberMap?.get(data.user_id);
|
||||
displayName = member?.nickname || data.nickname || '用户';
|
||||
}
|
||||
|
||||
return (
|
||||
<Text
|
||||
key={`at-${data.user_id}`}
|
||||
style={[
|
||||
styles.atText,
|
||||
isMe ? styles.atTextMe : styles.atTextOther,
|
||||
@@ -395,15 +395,19 @@ const renderAtSegment = (
|
||||
);
|
||||
};
|
||||
|
||||
const renderAtSegment = (
|
||||
data: AtSegmentData,
|
||||
props: Omit<SegmentRendererProps, 'segment'>
|
||||
): React.ReactNode => <AtSegmentBody key={`at-${data.user_id}`} data={data} props={props} />;
|
||||
|
||||
/**
|
||||
* 渲染表情 Segment
|
||||
*/
|
||||
const renderFaceSegment = (data: FaceSegmentData, isMe: boolean): React.ReactNode => {
|
||||
// 如果有自定义表情URL,显示图片
|
||||
const FaceSegmentBody: React.FC<{ data: FaceSegmentData }> = ({ data }) => {
|
||||
const styles = useSegmentStyles();
|
||||
if (data.url) {
|
||||
return (
|
||||
<ExpoImage
|
||||
key={`face-${data.id}`}
|
||||
source={{ uri: data.url }}
|
||||
style={styles.faceImage}
|
||||
contentFit="cover"
|
||||
@@ -411,29 +415,32 @@ const renderFaceSegment = (data: FaceSegmentData, isMe: boolean): React.ReactNod
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 否则显示表情名称(后续可以映射到本地表情)
|
||||
return (
|
||||
<Text key={`face-${data.id}`} style={styles.faceText}>
|
||||
<Text style={styles.faceText}>
|
||||
[{data.name || `表情${data.id}`}]
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
const renderFaceSegment = (data: FaceSegmentData, _isMe: boolean): React.ReactNode => (
|
||||
<FaceSegmentBody key={`face-${data.id}`} data={data} />
|
||||
);
|
||||
|
||||
/**
|
||||
* 渲染语音 Segment
|
||||
*/
|
||||
const renderVoiceSegment = (data: VoiceSegmentData, isMe: boolean): React.ReactNode => {
|
||||
const VoiceSegmentBody: React.FC<{ data: VoiceSegmentData; isMe: boolean }> = ({ data, isMe }) => {
|
||||
const styles = useSegmentStyles();
|
||||
const themeColors = useAppColors();
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={`voice-${data.url}`}
|
||||
style={[styles.voiceContainer, isMe ? styles.voiceMe : styles.voiceOther]}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
name="microphone"
|
||||
size={20}
|
||||
color={isMe ? '#FFFFFF' : '#666666'}
|
||||
color={isMe ? themeColors.primary.contrast : themeColors.chat.textTertiary}
|
||||
/>
|
||||
<Text style={[styles.voiceDuration, isMe ? styles.textMe : styles.textOther]}>
|
||||
{data.duration ? `${data.duration}"` : '语音'}
|
||||
@@ -442,10 +449,16 @@ const renderVoiceSegment = (data: VoiceSegmentData, isMe: boolean): React.ReactN
|
||||
);
|
||||
};
|
||||
|
||||
const renderVoiceSegment = (data: VoiceSegmentData, isMe: boolean): React.ReactNode => (
|
||||
<VoiceSegmentBody key={`voice-${data.url}`} data={data} isMe={isMe} />
|
||||
);
|
||||
|
||||
/**
|
||||
* 视频 Segment 组件 - 支持点击播放
|
||||
*/
|
||||
const VideoSegment: React.FC<{ data: VideoSegmentData; isMe: boolean }> = ({ data, isMe }) => {
|
||||
const styles = useSegmentStyles();
|
||||
const themeColors = useAppColors();
|
||||
const [playerVisible, setPlayerVisible] = useState(false);
|
||||
|
||||
const handlePress = useCallback(() => {
|
||||
@@ -469,11 +482,11 @@ const VideoSegment: React.FC<{ data: VideoSegmentData; isMe: boolean }> = ({ dat
|
||||
/>
|
||||
) : (
|
||||
<View style={styles.videoPlaceholder}>
|
||||
<MaterialCommunityIcons name="video" size={32} color="#FFFFFF" />
|
||||
<MaterialCommunityIcons name="video" size={32} color={themeColors.primary.contrast} />
|
||||
</View>
|
||||
)}
|
||||
<View style={styles.videoPlayIcon}>
|
||||
<MaterialCommunityIcons name="play-circle" size={40} color="#FFFFFF" />
|
||||
<MaterialCommunityIcons name="play-circle" size={40} color={themeColors.primary.contrast} />
|
||||
</View>
|
||||
{data.duration && (
|
||||
<Text style={styles.videoDuration}>{formatDuration(data.duration)}</Text>
|
||||
@@ -496,12 +509,12 @@ const renderVideoSegment = (data: VideoSegmentData, isMe: boolean): React.ReactN
|
||||
/**
|
||||
* 渲染文件 Segment
|
||||
*/
|
||||
const renderFileSegment = (data: FileSegmentData, isMe: boolean): React.ReactNode => {
|
||||
const FileSegmentBody: React.FC<{ data: FileSegmentData; isMe: boolean }> = ({ data, isMe }) => {
|
||||
const styles = useSegmentStyles();
|
||||
const themeColors = useAppColors();
|
||||
const fileSize = data.size ? formatFileSize(data.size) : '';
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={`file-${data.url}`}
|
||||
style={[styles.fileContainer, isMe ? styles.fileMe : styles.fileOther]}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
@@ -509,33 +522,36 @@ const renderFileSegment = (data: FileSegmentData, isMe: boolean): React.ReactNod
|
||||
<MaterialCommunityIcons
|
||||
name="file-document"
|
||||
size={28}
|
||||
color={isMe ? '#FFFFFF' : colors.primary.main}
|
||||
color={isMe ? themeColors.primary.contrast : themeColors.primary.main}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.fileInfo}>
|
||||
<Text
|
||||
<Text
|
||||
style={[styles.fileName, isMe ? styles.textMe : styles.textOther]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{data.name}
|
||||
</Text>
|
||||
{fileSize && (
|
||||
<Text style={styles.fileSize}>{fileSize}</Text>
|
||||
)}
|
||||
{fileSize ? <Text style={styles.fileSize}>{fileSize}</Text> : null}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
const renderFileSegment = (data: FileSegmentData, isMe: boolean): React.ReactNode => (
|
||||
<FileSegmentBody key={`file-${data.url}`} data={data} isMe={isMe} />
|
||||
);
|
||||
|
||||
/**
|
||||
* 渲染链接 Segment
|
||||
*/
|
||||
const renderLinkSegment = (
|
||||
data: LinkSegmentData,
|
||||
props: Omit<SegmentRendererProps, 'segment'>
|
||||
): React.ReactNode => {
|
||||
const LinkSegmentBody: React.FC<{
|
||||
data: LinkSegmentData;
|
||||
props: Omit<SegmentRendererProps, 'segment'>;
|
||||
}> = ({ data, props }) => {
|
||||
const styles = useSegmentStyles();
|
||||
const { onLinkPress, isMe } = props;
|
||||
|
||||
|
||||
const handlePress = async () => {
|
||||
if (onLinkPress) {
|
||||
onLinkPress(data.url);
|
||||
@@ -552,26 +568,25 @@ const renderLinkSegment = (
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={`link-${data.url}`}
|
||||
style={[styles.linkContainer, isMe ? styles.linkMe : styles.linkOther]}
|
||||
onPress={handlePress}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
{data.image && (
|
||||
{data.image ? (
|
||||
<ExpoImage source={{ uri: data.image }} style={styles.linkImage} contentFit="cover" cachePolicy="memory" />
|
||||
)}
|
||||
) : null}
|
||||
<View style={styles.linkContent}>
|
||||
<Text style={styles.linkTitle} numberOfLines={2}>
|
||||
{data.title || data.url}
|
||||
</Text>
|
||||
{data.description && (
|
||||
{data.description ? (
|
||||
<Text style={styles.linkDescription} numberOfLines={2}>
|
||||
{data.description}
|
||||
</Text>
|
||||
)}
|
||||
) : null}
|
||||
<Text style={styles.linkUrl} numberOfLines={1}>
|
||||
{(() => {
|
||||
try {
|
||||
@@ -586,6 +601,11 @@ const renderLinkSegment = (
|
||||
);
|
||||
};
|
||||
|
||||
const renderLinkSegment = (
|
||||
data: LinkSegmentData,
|
||||
props: Omit<SegmentRendererProps, 'segment'>
|
||||
): React.ReactNode => <LinkSegmentBody key={`link-${data.url}`} data={data} props={props} />;
|
||||
|
||||
/**
|
||||
* 回复预览组件(用于消息气泡内显示回复引用)
|
||||
*/
|
||||
@@ -596,6 +616,9 @@ export const ReplyPreviewSegment: React.FC<ReplyPreviewSegmentProps> = ({
|
||||
onPress,
|
||||
getSenderInfo,
|
||||
}) => {
|
||||
const themeColors = useAppColors();
|
||||
const styles = useMemo(() => createSegmentStyles(themeColors), [themeColors]);
|
||||
|
||||
if (!replyMessage) {
|
||||
// 如果没有引用消息详情,只显示引用ID
|
||||
return (
|
||||
@@ -605,9 +628,11 @@ export const ReplyPreviewSegment: React.FC<ReplyPreviewSegmentProps> = ({
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={styles.replyLine} />
|
||||
<Text style={styles.replyText} numberOfLines={2}>
|
||||
引用消息
|
||||
</Text>
|
||||
<View style={styles.replyContent}>
|
||||
<Text style={styles.replyText} numberOfLines={2}>
|
||||
引用消息
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
@@ -658,6 +683,7 @@ export const ReplyPreviewSegment: React.FC<ReplyPreviewSegmentProps> = ({
|
||||
onPress={onPress}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={styles.replyLine} />
|
||||
<View style={styles.replyContent}>
|
||||
<Text style={styles.replySender} numberOfLines={1}>
|
||||
{senderName}:
|
||||
@@ -702,7 +728,9 @@ export const MessageSegmentsRenderer: React.FC<{
|
||||
onLinkPress,
|
||||
getSenderInfo,
|
||||
}) => {
|
||||
// 找出 reply segment(如果有)
|
||||
const themeColors = useAppColors();
|
||||
const segStyles = useMemo(() => createSegmentStyles(themeColors), [themeColors]);
|
||||
|
||||
const replySegment = segments.find(s => s.type === 'reply');
|
||||
const otherSegments = segments.filter(s => s.type !== 'reply');
|
||||
const chunks = partitionMessageSegments(otherSegments);
|
||||
@@ -717,58 +745,58 @@ export const MessageSegmentsRenderer: React.FC<{
|
||||
onImageLongPress,
|
||||
onLinkPress,
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<View style={styles.segmentsContainer}>
|
||||
{/* 回复预览 */}
|
||||
{replySegment && (
|
||||
<ReplyPreviewSegment
|
||||
replyData={(replySegment.data as ReplySegmentData)}
|
||||
replyMessage={replyMessage}
|
||||
isMe={isMe}
|
||||
onPress={() => onReplyPress?.((replySegment.data as ReplySegmentData).id)}
|
||||
getSenderInfo={getSenderInfo}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 其他 Segment:行内文案与块级图片/媒体分行展示,多图纵向排列 */}
|
||||
<View style={styles.segmentsColumn}>
|
||||
{chunks.map((chunk, i) => {
|
||||
if (chunk.kind === 'inline') {
|
||||
<SegmentStylesContext.Provider value={segStyles}>
|
||||
<View style={segStyles.segmentsContainer}>
|
||||
{replySegment ? (
|
||||
<ReplyPreviewSegment
|
||||
replyData={replySegment.data as ReplySegmentData}
|
||||
replyMessage={replyMessage}
|
||||
isMe={isMe}
|
||||
onPress={() => onReplyPress?.((replySegment.data as ReplySegmentData).id)}
|
||||
getSenderInfo={getSenderInfo}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<View style={segStyles.segmentsColumn}>
|
||||
{chunks.map((chunk, i) => {
|
||||
if (chunk.kind === 'inline') {
|
||||
return (
|
||||
<View key={`inl-${i}`} style={segStyles.inlineChunk}>
|
||||
{chunk.parts.map((segment, j) => (
|
||||
<React.Fragment
|
||||
key={`segment-${segment.type}-${(segment as any)?.data?.url || (segment as any)?.data?.id || (segment as any)?.data?.user_id || j}`}
|
||||
>
|
||||
{renderSegment(segment, renderProps)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
if (chunk.kind === 'images') {
|
||||
return (
|
||||
<View key={`imgs-${i}`} style={segStyles.imagesChunk}>
|
||||
{chunk.parts.map((segment, j) => (
|
||||
<View
|
||||
key={`img-${(segment.data as ImageSegmentData)?.url || j}`}
|
||||
style={j < chunk.parts.length - 1 ? segStyles.imageStackItem : segStyles.imageStackItemLast}
|
||||
>
|
||||
{renderSegment(segment, renderProps)}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<View key={`inl-${i}`} style={styles.inlineChunk}>
|
||||
{chunk.parts.map((segment, j) => (
|
||||
<React.Fragment
|
||||
key={`segment-${segment.type}-${(segment as any)?.data?.url || (segment as any)?.data?.id || (segment as any)?.data?.user_id || j}`}
|
||||
>
|
||||
{renderSegment(segment, renderProps)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
<View key={`blk-${i}`} style={segStyles.blockChunk}>
|
||||
{renderSegment(chunk.segment, renderProps)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
if (chunk.kind === 'images') {
|
||||
return (
|
||||
<View key={`imgs-${i}`} style={styles.imagesChunk}>
|
||||
{chunk.parts.map((segment, j) => (
|
||||
<View
|
||||
key={`img-${(segment.data as ImageSegmentData)?.url || j}`}
|
||||
style={j < chunk.parts.length - 1 ? styles.imageStackItem : styles.imageStackItemLast}
|
||||
>
|
||||
{renderSegment(segment, renderProps)}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<View key={`blk-${i}`} style={styles.blockChunk}>
|
||||
{renderSegment(chunk.segment, renderProps)}
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
})}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</SegmentStylesContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -787,7 +815,8 @@ const formatDuration = (seconds: number): string => {
|
||||
return mins > 0 ? `${mins}:${secs.toString().padStart(2, '0')}` : `0:${secs.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
function createSegmentStyles(colors: AppColors) {
|
||||
return StyleSheet.create({
|
||||
// 容器
|
||||
segmentsContainer: {
|
||||
flexDirection: 'column',
|
||||
@@ -826,10 +855,10 @@ const styles = StyleSheet.create({
|
||||
fontWeight: '400',
|
||||
},
|
||||
textMe: {
|
||||
color: '#1A1A1A', // 统一使用深色字体
|
||||
color: colors.chat.textPrimary,
|
||||
},
|
||||
textOther: {
|
||||
color: '#1A1A1A', // 统一使用深色字体
|
||||
color: colors.chat.textPrimary,
|
||||
},
|
||||
|
||||
// @提及 - Telegram风格:蓝色高亮
|
||||
@@ -845,7 +874,7 @@ const styles = StyleSheet.create({
|
||||
borderRadius: 4,
|
||||
},
|
||||
atTextOther: {
|
||||
color: '#4A88C7',
|
||||
color: colors.chat.link,
|
||||
backgroundColor: 'rgba(74, 136, 199, 0.12)',
|
||||
borderRadius: 4,
|
||||
},
|
||||
@@ -860,7 +889,7 @@ const styles = StyleSheet.create({
|
||||
borderRadius: 12,
|
||||
overflow: 'hidden',
|
||||
marginTop: 4,
|
||||
shadowColor: '#000',
|
||||
shadowColor: colors.chat.shadow,
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.15,
|
||||
shadowRadius: 4,
|
||||
@@ -892,7 +921,7 @@ const styles = StyleSheet.create({
|
||||
paddingVertical: spacing.sm + 2,
|
||||
borderRadius: 20,
|
||||
minWidth: 100,
|
||||
shadowColor: '#000',
|
||||
shadowColor: colors.chat.shadow,
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 2,
|
||||
@@ -916,7 +945,7 @@ const styles = StyleSheet.create({
|
||||
overflow: 'hidden',
|
||||
width: 220,
|
||||
height: 165,
|
||||
shadowColor: '#000',
|
||||
shadowColor: colors.chat.shadow,
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.15,
|
||||
shadowRadius: 4,
|
||||
@@ -953,7 +982,7 @@ const styles = StyleSheet.create({
|
||||
position: 'absolute',
|
||||
bottom: 10,
|
||||
right: 10,
|
||||
color: '#FFFFFF',
|
||||
color: colors.primary.contrast,
|
||||
fontSize: 13,
|
||||
fontWeight: '600',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.6)',
|
||||
@@ -971,7 +1000,7 @@ const styles = StyleSheet.create({
|
||||
borderRadius: 16,
|
||||
minWidth: 220,
|
||||
maxWidth: 300,
|
||||
shadowColor: '#000',
|
||||
shadowColor: colors.chat.shadow,
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.08,
|
||||
shadowRadius: 2,
|
||||
@@ -981,7 +1010,7 @@ const styles = StyleSheet.create({
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.25)',
|
||||
},
|
||||
fileOther: {
|
||||
backgroundColor: '#F8F9FA',
|
||||
backgroundColor: colors.chat.surfaceRaised,
|
||||
},
|
||||
fileIcon: {
|
||||
marginRight: spacing.md,
|
||||
@@ -998,11 +1027,11 @@ const styles = StyleSheet.create({
|
||||
fileName: {
|
||||
fontSize: 15,
|
||||
fontWeight: '600',
|
||||
color: '#1A1A1A',
|
||||
color: colors.chat.textPrimary,
|
||||
},
|
||||
fileSize: {
|
||||
fontSize: 13,
|
||||
color: '#8E8E93',
|
||||
color: colors.chat.textSecondary,
|
||||
marginTop: 4,
|
||||
fontWeight: '500',
|
||||
},
|
||||
@@ -1012,8 +1041,8 @@ const styles = StyleSheet.create({
|
||||
borderRadius: 16,
|
||||
overflow: 'hidden',
|
||||
maxWidth: 280,
|
||||
backgroundColor: '#FFFFFF',
|
||||
shadowColor: '#000',
|
||||
backgroundColor: colors.chat.card,
|
||||
shadowColor: colors.chat.shadow,
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
@@ -1023,7 +1052,7 @@ const styles = StyleSheet.create({
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
||||
},
|
||||
linkOther: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
backgroundColor: colors.chat.card,
|
||||
},
|
||||
linkImage: {
|
||||
width: '100%',
|
||||
@@ -1035,12 +1064,12 @@ const styles = StyleSheet.create({
|
||||
linkTitle: {
|
||||
fontSize: 15,
|
||||
fontWeight: '600',
|
||||
color: '#1A1A1A',
|
||||
color: colors.chat.textPrimary,
|
||||
lineHeight: 20,
|
||||
},
|
||||
linkDescription: {
|
||||
fontSize: 13,
|
||||
color: '#666666',
|
||||
color: colors.chat.textTertiary,
|
||||
marginTop: 6,
|
||||
lineHeight: 18,
|
||||
},
|
||||
@@ -1051,16 +1080,17 @@ const styles = StyleSheet.create({
|
||||
fontWeight: '500',
|
||||
},
|
||||
|
||||
// 回复预览 - Telegram风格:简洁设计
|
||||
// 回复预览 - Telegram风格:左侧色条 + 简洁背景
|
||||
replyPreview: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: 0,
|
||||
paddingVertical: spacing.sm,
|
||||
paddingHorizontal: spacing.sm + 2,
|
||||
borderRadius: 6,
|
||||
width: '100%',
|
||||
minWidth: 200,
|
||||
// 移除阴影
|
||||
gap: 8,
|
||||
shadowColor: 'transparent',
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowOpacity: 0,
|
||||
@@ -1074,28 +1104,45 @@ const styles = StyleSheet.create({
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.03)',
|
||||
},
|
||||
replyLine: {
|
||||
width: 0,
|
||||
marginRight: 0,
|
||||
width: 3,
|
||||
alignSelf: 'stretch',
|
||||
minHeight: 28,
|
||||
borderRadius: 2,
|
||||
backgroundColor: colors.chat.link,
|
||||
},
|
||||
replyContent: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'nowrap',
|
||||
minWidth: 0,
|
||||
},
|
||||
replySender: {
|
||||
fontSize: 13,
|
||||
fontWeight: '600',
|
||||
color: '#4A88C7',
|
||||
color: colors.chat.link,
|
||||
marginRight: 6,
|
||||
flexShrink: 0,
|
||||
},
|
||||
replyText: {
|
||||
fontSize: 13,
|
||||
color: '#666666',
|
||||
color: colors.chat.textTertiary,
|
||||
flex: 1,
|
||||
flexShrink: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
type SegmentStyles = ReturnType<typeof createSegmentStyles>;
|
||||
|
||||
const SegmentStylesContext = React.createContext<SegmentStyles | null>(null);
|
||||
|
||||
function useSegmentStyles(): SegmentStyles {
|
||||
const ctx = React.useContext(SegmentStylesContext);
|
||||
if (!ctx) {
|
||||
throw new Error('useSegmentStyles 必须在 MessageSegmentsRenderer 内使用');
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
|
||||
export default MessageSegmentsRenderer;
|
||||
@@ -8,7 +8,7 @@ import { View, StyleSheet, Animated } from 'react-native';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import { PanGestureHandler, State } from 'react-native-gesture-handler';
|
||||
import * as Haptics from 'expo-haptics';
|
||||
import { colors } from '../../../../theme';
|
||||
import { useAppColors } from '../../../../theme';
|
||||
|
||||
// 滑动阈值
|
||||
const SWIPE_THRESHOLD = 30;
|
||||
@@ -27,6 +27,7 @@ export const SwipeableMessageBubble: React.FC<SwipeableMessageBubbleProps> = ({
|
||||
onReply,
|
||||
enabled = true,
|
||||
}) => {
|
||||
const colors = useAppColors();
|
||||
const translateX = useRef(new Animated.Value(0)).current;
|
||||
const hasTriggeredRef = useRef(false);
|
||||
|
||||
|
||||
@@ -4,52 +4,40 @@
|
||||
*/
|
||||
|
||||
import { StyleSheet, ViewStyle, TextStyle } from 'react-native';
|
||||
import { colors, spacing } from '../../../../theme';
|
||||
import { spacing, type AppColors } from '../../../../theme';
|
||||
|
||||
// 气泡圆角大小(参考 Element X: 12pt)
|
||||
export const BUBBLE_RADIUS = 16;
|
||||
|
||||
// 气泡颜色方案(参考 Element X 的语义化颜色)
|
||||
export const bubbleColors = {
|
||||
// 自己发送的消息
|
||||
outgoing: {
|
||||
background: '#E8E8E8', // Element X 风格:灰色系
|
||||
text: '#1A1A1A',
|
||||
},
|
||||
// 收到的消息
|
||||
incoming: {
|
||||
background: '#FFFFFF',
|
||||
text: '#1A1A1A',
|
||||
},
|
||||
// 被回复的消息高亮
|
||||
replyHighlight: {
|
||||
background: 'rgba(74, 136, 199, 0.1)',
|
||||
borderLeft: '#4A88C7',
|
||||
},
|
||||
};
|
||||
export function getBubbleColors(colors: AppColors) {
|
||||
return {
|
||||
outgoing: {
|
||||
background: colors.chat.bubbleOutgoing,
|
||||
text: colors.chat.textPrimary,
|
||||
},
|
||||
incoming: {
|
||||
background: colors.chat.bubbleIncoming,
|
||||
text: colors.chat.textPrimary,
|
||||
},
|
||||
replyHighlight: {
|
||||
background: colors.chat.replyTint,
|
||||
borderLeft: colors.chat.replyBorder,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// 消息在组中的位置类型
|
||||
export type MessageGroupPosition = 'single' | 'first' | 'middle' | 'last';
|
||||
|
||||
/**
|
||||
* 根据消息在组中的位置获取圆角样式
|
||||
* 参考 Element X 的动态圆角逻辑
|
||||
*/
|
||||
export const getBubbleBorderRadius = (
|
||||
isMe: boolean,
|
||||
position: MessageGroupPosition
|
||||
): ViewStyle => {
|
||||
export const getBubbleBorderRadius = (isMe: boolean, position: MessageGroupPosition): ViewStyle => {
|
||||
const radius = BUBBLE_RADIUS;
|
||||
|
||||
|
||||
if (isMe) {
|
||||
// 自己发送的消息
|
||||
switch (position) {
|
||||
case 'single':
|
||||
return {
|
||||
borderTopLeftRadius: radius,
|
||||
borderTopRightRadius: radius,
|
||||
borderBottomLeftRadius: radius,
|
||||
borderBottomRightRadius: 4, // 尖角在右下
|
||||
borderBottomRightRadius: 4,
|
||||
};
|
||||
case 'first':
|
||||
return {
|
||||
@@ -74,11 +62,10 @@ export const getBubbleBorderRadius = (
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// 收到的消息
|
||||
switch (position) {
|
||||
case 'single':
|
||||
return {
|
||||
borderTopLeftRadius: 4, // 尖角在左上
|
||||
borderTopLeftRadius: 4,
|
||||
borderTopRightRadius: radius,
|
||||
borderBottomLeftRadius: radius,
|
||||
borderBottomRightRadius: radius,
|
||||
@@ -108,9 +95,6 @@ export const getBubbleBorderRadius = (
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 判断消息在组中的位置
|
||||
*/
|
||||
export const getMessageGroupPosition = (
|
||||
index: number,
|
||||
messages: Array<{ sender_id: string }>,
|
||||
@@ -118,13 +102,13 @@ export const getMessageGroupPosition = (
|
||||
): MessageGroupPosition => {
|
||||
const currentMessage = messages[index];
|
||||
if (!currentMessage) return 'single';
|
||||
|
||||
|
||||
const prevMessage = index > 0 ? messages[index - 1] : null;
|
||||
const nextMessage = index < messages.length - 1 ? messages[index + 1] : null;
|
||||
|
||||
|
||||
const isSameSenderAsPrev = prevMessage && prevMessage.sender_id === currentMessage.sender_id;
|
||||
const isSameSenderAsNext = nextMessage && nextMessage.sender_id === currentMessage.sender_id;
|
||||
|
||||
|
||||
if (!isSameSenderAsPrev && !isSameSenderAsNext) {
|
||||
return 'single';
|
||||
} else if (!isSameSenderAsPrev && isSameSenderAsNext) {
|
||||
@@ -136,135 +120,114 @@ export const getMessageGroupPosition = (
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 判断是否显示发送者信息(只在组的第一条消息显示)
|
||||
*/
|
||||
export const shouldShowSenderInfo = (
|
||||
index: number,
|
||||
messages: Array<{ sender_id: string }>,
|
||||
messages: Array<{ sender_id: string }>
|
||||
): boolean => {
|
||||
if (index === 0) return true;
|
||||
|
||||
|
||||
const currentMessage = messages[index];
|
||||
const prevMessage = messages[index - 1];
|
||||
|
||||
|
||||
return prevMessage.sender_id !== currentMessage.sender_id;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取气泡基础样式
|
||||
*/
|
||||
export const getBubbleBaseStyle = (isMe: boolean): ViewStyle => ({
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.sm + 4,
|
||||
minWidth: 60,
|
||||
maxWidth: '75%',
|
||||
backgroundColor: isMe ? bubbleColors.outgoing.background : bubbleColors.incoming.background,
|
||||
// Element X 风格的微妙阴影
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.06,
|
||||
shadowRadius: 2,
|
||||
elevation: 2,
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取文本样式
|
||||
*/
|
||||
export const getBubbleTextStyle = (isMe: boolean): TextStyle => ({
|
||||
color: isMe ? bubbleColors.outgoing.text : bubbleColors.incoming.text,
|
||||
fontSize: 16,
|
||||
lineHeight: 23,
|
||||
letterSpacing: 0.2,
|
||||
fontWeight: '400',
|
||||
});
|
||||
|
||||
/**
|
||||
* 消息气泡样式集合
|
||||
*/
|
||||
export const bubbleStyles = StyleSheet.create({
|
||||
// 基础气泡
|
||||
bubble: {
|
||||
export const getBubbleBaseStyle = (isMe: boolean, colors: AppColors): ViewStyle => {
|
||||
const bc = getBubbleColors(colors);
|
||||
return {
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.sm + 4,
|
||||
minWidth: 60,
|
||||
},
|
||||
|
||||
// 自己发送的消息
|
||||
outgoing: {
|
||||
backgroundColor: bubbleColors.outgoing.background,
|
||||
},
|
||||
|
||||
// 收到的消息
|
||||
incoming: {
|
||||
backgroundColor: bubbleColors.incoming.background,
|
||||
},
|
||||
|
||||
// 文本样式
|
||||
text: {
|
||||
fontSize: 16,
|
||||
lineHeight: 23,
|
||||
letterSpacing: 0.2,
|
||||
fontWeight: '400',
|
||||
},
|
||||
|
||||
// 阴影样式(Element X 风格)
|
||||
shadow: {
|
||||
shadowColor: '#000',
|
||||
maxWidth: '75%',
|
||||
backgroundColor: isMe ? bc.outgoing.background : bc.incoming.background,
|
||||
shadowColor: colors.chat.shadow,
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.06,
|
||||
shadowRadius: 2,
|
||||
elevation: 2,
|
||||
},
|
||||
|
||||
// 长按反馈阴影
|
||||
longPressShadow: {
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 3 },
|
||||
shadowOpacity: 0.12,
|
||||
shadowRadius: 8,
|
||||
elevation: 6,
|
||||
},
|
||||
|
||||
// 被回复消息的高亮边框
|
||||
replyHighlight: {
|
||||
borderLeftWidth: 3,
|
||||
borderLeftColor: colors.primary.main,
|
||||
backgroundColor: bubbleColors.replyHighlight.background,
|
||||
},
|
||||
|
||||
// 已撤回消息
|
||||
recalled: {
|
||||
backgroundColor: '#F5F7FA',
|
||||
borderWidth: 1,
|
||||
borderColor: '#E8E8E8',
|
||||
borderStyle: 'dashed',
|
||||
borderRadius: 12,
|
||||
},
|
||||
|
||||
// 系统通知
|
||||
systemNotice: {
|
||||
alignItems: 'center',
|
||||
marginVertical: spacing.sm,
|
||||
},
|
||||
systemNoticeText: {
|
||||
fontSize: 12,
|
||||
color: '#8E8E93',
|
||||
backgroundColor: 'rgba(142, 142, 147, 0.12)',
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.xs,
|
||||
borderRadius: 12,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export const getBubbleTextStyle = (isMe: boolean, colors: AppColors): TextStyle => {
|
||||
const bc = getBubbleColors(colors);
|
||||
return {
|
||||
color: isMe ? bc.outgoing.text : bc.incoming.text,
|
||||
fontSize: 16,
|
||||
lineHeight: 23,
|
||||
letterSpacing: 0.2,
|
||||
fontWeight: '400',
|
||||
};
|
||||
};
|
||||
|
||||
export function createBubbleStyles(colors: AppColors) {
|
||||
const bc = getBubbleColors(colors);
|
||||
return StyleSheet.create({
|
||||
bubble: {
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.sm + 4,
|
||||
minWidth: 60,
|
||||
},
|
||||
outgoing: {
|
||||
backgroundColor: bc.outgoing.background,
|
||||
},
|
||||
incoming: {
|
||||
backgroundColor: bc.incoming.background,
|
||||
},
|
||||
text: {
|
||||
fontSize: 16,
|
||||
lineHeight: 23,
|
||||
letterSpacing: 0.2,
|
||||
fontWeight: '400',
|
||||
},
|
||||
shadow: {
|
||||
shadowColor: colors.chat.shadow,
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.06,
|
||||
shadowRadius: 2,
|
||||
elevation: 2,
|
||||
},
|
||||
longPressShadow: {
|
||||
shadowColor: colors.chat.shadow,
|
||||
shadowOffset: { width: 0, height: 3 },
|
||||
shadowOpacity: 0.12,
|
||||
shadowRadius: 8,
|
||||
elevation: 6,
|
||||
},
|
||||
replyHighlight: {
|
||||
borderLeftWidth: 3,
|
||||
borderLeftColor: colors.primary.main,
|
||||
backgroundColor: bc.replyHighlight.background,
|
||||
},
|
||||
recalled: {
|
||||
backgroundColor: colors.chat.surfaceMuted,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.chat.border,
|
||||
borderStyle: 'dashed',
|
||||
borderRadius: 12,
|
||||
},
|
||||
systemNotice: {
|
||||
alignItems: 'center',
|
||||
marginVertical: spacing.sm,
|
||||
},
|
||||
systemNoticeText: {
|
||||
fontSize: 12,
|
||||
color: colors.chat.textSecondary,
|
||||
backgroundColor: colors.chat.surfaceMuted,
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.xs,
|
||||
borderRadius: 12,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
BUBBLE_RADIUS,
|
||||
bubbleColors,
|
||||
getBubbleColors,
|
||||
getBubbleBorderRadius,
|
||||
getMessageGroupPosition,
|
||||
shouldShowSenderInfo,
|
||||
getBubbleBaseStyle,
|
||||
getBubbleTextStyle,
|
||||
bubbleStyles,
|
||||
};
|
||||
createBubbleStyles,
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@ export * from './types';
|
||||
export * from './constants';
|
||||
|
||||
// 样式
|
||||
export { chatScreenStyles } from './styles';
|
||||
export { createChatScreenStyles, useChatScreenStyles } from './styles';
|
||||
|
||||
// 组件
|
||||
export { EmojiPanel } from './EmojiPanel';
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
* 支持响应式布局
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { StyleSheet, Dimensions } from 'react-native';
|
||||
import { colors, spacing, shadows } from '../../../../theme';
|
||||
import { spacing, shadows, useAppColors, type AppColors } from '../../../../theme';
|
||||
|
||||
const { width: SCREEN_WIDTH } = Dimensions.get('window');
|
||||
|
||||
@@ -18,19 +19,20 @@ const BREAKPOINTS = {
|
||||
const isWideScreen = SCREEN_WIDTH >= BREAKPOINTS.desktop;
|
||||
const isTablet = SCREEN_WIDTH >= BREAKPOINTS.tablet;
|
||||
|
||||
export const chatScreenStyles = StyleSheet.create({
|
||||
export function createChatScreenStyles(colors: AppColors) {
|
||||
return StyleSheet.create({
|
||||
// 容器
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#F0F2F5',
|
||||
backgroundColor: colors.chat.screen,
|
||||
},
|
||||
|
||||
// 头部
|
||||
headerContainer: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
backgroundColor: colors.chat.card,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: 'rgba(255, 107, 53, 0.12)',
|
||||
shadowColor: '#000',
|
||||
shadowColor: colors.chat.shadow,
|
||||
shadowOffset: { width: 0, height: 3 },
|
||||
shadowOpacity: 0.06,
|
||||
shadowRadius: 10,
|
||||
@@ -42,7 +44,7 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.sm + 2,
|
||||
backgroundColor: '#FFFFFF',
|
||||
backgroundColor: colors.chat.card,
|
||||
},
|
||||
backButton: {
|
||||
width: 38,
|
||||
@@ -112,36 +114,61 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 19,
|
||||
backgroundColor: '#F7F8FA',
|
||||
backgroundColor: colors.chat.surfaceRaised,
|
||||
borderWidth: 1,
|
||||
borderColor: '#ECEFF3',
|
||||
borderColor: colors.chat.borderLight,
|
||||
},
|
||||
|
||||
// 消息列表
|
||||
messageListContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
// 回到底部(Telegram 式浮动条)
|
||||
jumpToLatestFab: {
|
||||
position: 'absolute',
|
||||
right: 14,
|
||||
bottom: 10,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 20,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.96)',
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderColor: 'rgba(0, 0, 0, 0.08)',
|
||||
shadowColor: colors.chat.shadow,
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.12,
|
||||
shadowRadius: 6,
|
||||
elevation: 4,
|
||||
},
|
||||
jumpToLatestFabText: {
|
||||
fontSize: 13,
|
||||
fontWeight: '600',
|
||||
color: colors.primary.main,
|
||||
},
|
||||
listContent: {
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingTop: spacing.md,
|
||||
paddingBottom: spacing.xl,
|
||||
},
|
||||
|
||||
// 时间分隔 - QQ风格:更明显的时间标签
|
||||
// 时间分隔 - 轻量胶囊(弱化字重,接近 Telegram 日期条)
|
||||
timeContainer: {
|
||||
alignItems: 'center',
|
||||
marginVertical: spacing.lg,
|
||||
},
|
||||
timeText: {
|
||||
color: '#8E8E93',
|
||||
fontSize: 12,
|
||||
backgroundColor: 'rgba(142, 142, 147, 0.12)',
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 14,
|
||||
fontWeight: '600',
|
||||
color: 'rgba(142, 142, 147, 0.95)',
|
||||
fontSize: 11,
|
||||
backgroundColor: 'rgba(142, 142, 147, 0.1)',
|
||||
paddingHorizontal: spacing.sm + 4,
|
||||
paddingVertical: 5,
|
||||
borderRadius: 12,
|
||||
fontWeight: '500',
|
||||
overflow: 'hidden',
|
||||
letterSpacing: 0.3,
|
||||
letterSpacing: 0.2,
|
||||
},
|
||||
|
||||
// 系统通知
|
||||
@@ -152,7 +179,7 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
},
|
||||
systemNoticeText: {
|
||||
fontSize: 12,
|
||||
color: '#8E8E93',
|
||||
color: colors.chat.textSecondary,
|
||||
backgroundColor: 'rgba(142, 142, 147, 0.12)',
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.xs,
|
||||
@@ -177,7 +204,7 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
// 头像
|
||||
avatarWrapper: {
|
||||
marginHorizontal: 2,
|
||||
shadowColor: '#000',
|
||||
shadowColor: colors.chat.shadow,
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.08,
|
||||
shadowRadius: 2,
|
||||
@@ -185,7 +212,7 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
},
|
||||
groupAvatarWrapper: {
|
||||
marginHorizontal: 2,
|
||||
shadowColor: '#000',
|
||||
shadowColor: colors.chat.shadow,
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.08,
|
||||
shadowRadius: 2,
|
||||
@@ -200,53 +227,59 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
},
|
||||
senderName: {
|
||||
fontSize: 13,
|
||||
color: '#4A88C7',
|
||||
color: colors.chat.link,
|
||||
marginBottom: 4,
|
||||
marginLeft: 4,
|
||||
fontWeight: '600',
|
||||
},
|
||||
|
||||
// 消息气泡 - Telegram风格:浅色背景,统一字体颜色
|
||||
messageBubble: {
|
||||
// 消息气泡 - 外层极轻阴影(纯文字气泡更接近扁平 IM)
|
||||
messageBubbleOuter: {
|
||||
borderRadius: 16,
|
||||
minWidth: 60,
|
||||
shadowColor: colors.chat.shadow,
|
||||
shadowOffset: { width: 0, height: 0.5 },
|
||||
shadowOpacity: 0.04,
|
||||
shadowRadius: 1,
|
||||
elevation: 1,
|
||||
},
|
||||
myBubbleOuter: {
|
||||
borderBottomRightRadius: 4,
|
||||
},
|
||||
theirBubbleOuter: {
|
||||
borderTopLeftRadius: 4,
|
||||
},
|
||||
// 内层:背景 + 裁剪,防止回复条/@ 高亮在圆角外露出颜色
|
||||
messageBubbleInner: {
|
||||
borderRadius: 16,
|
||||
overflow: 'hidden',
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.sm + 4,
|
||||
borderRadius: 16,
|
||||
// 自适应内容宽度
|
||||
minWidth: 60,
|
||||
},
|
||||
myBubble: {
|
||||
backgroundColor: '#DFF2FF', // Telegram风格的浅蓝色
|
||||
borderBottomRightRadius: 4, // 右下角尖,箭头在右下
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.08,
|
||||
shadowRadius: 2,
|
||||
elevation: 2,
|
||||
myBubbleInner: {
|
||||
backgroundColor: colors.chat.replyTint,
|
||||
borderBottomRightRadius: 4,
|
||||
},
|
||||
theirBubble: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderTopLeftRadius: 4, // 左上角尖,箭头在左上
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.08,
|
||||
shadowRadius: 2,
|
||||
elevation: 2,
|
||||
theirBubbleInner: {
|
||||
backgroundColor: colors.chat.card,
|
||||
borderTopLeftRadius: 4,
|
||||
},
|
||||
recalledBubble: {
|
||||
backgroundColor: '#F5F7FA',
|
||||
backgroundColor: colors.chat.surfaceMuted,
|
||||
borderWidth: 1,
|
||||
borderColor: '#E8E8E8',
|
||||
borderColor: colors.chat.border,
|
||||
borderStyle: 'dashed',
|
||||
},
|
||||
myBubbleText: {
|
||||
color: '#1A1A1A', // 统一使用深色字体
|
||||
color: colors.chat.textPrimary, // 主文案
|
||||
fontSize: 16,
|
||||
lineHeight: 23,
|
||||
letterSpacing: 0.2,
|
||||
fontWeight: '400',
|
||||
},
|
||||
theirBubbleText: {
|
||||
color: '#1A1A1A', // 统一使用深色字体
|
||||
color: colors.chat.textPrimary, // 主文案
|
||||
fontSize: 16,
|
||||
lineHeight: 23,
|
||||
letterSpacing: 0.2,
|
||||
@@ -255,29 +288,27 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
// 选中状态
|
||||
selectedBubble: {
|
||||
borderWidth: 2,
|
||||
borderColor: '#7FB6E6',
|
||||
borderColor: colors.chat.replyBorder,
|
||||
},
|
||||
// 仅对齐右侧,不再套一层浅色底,避免与气泡叠色产生「边缘多出一块蓝」
|
||||
myMessageContentPanel: {
|
||||
backgroundColor: '#DFF2FF',
|
||||
borderRadius: 14,
|
||||
paddingHorizontal: 4,
|
||||
paddingBottom: 4,
|
||||
alignItems: 'flex-end',
|
||||
},
|
||||
mySelectedBubble: {
|
||||
backgroundColor: '#CFEAFF',
|
||||
backgroundColor: colors.chat.replyTintActive,
|
||||
borderWidth: 2,
|
||||
borderColor: '#7FB6E6',
|
||||
borderColor: colors.chat.replyBorder,
|
||||
},
|
||||
theirSelectedBubble: {
|
||||
backgroundColor: '#EEF5FC',
|
||||
backgroundColor: colors.chat.menuHighlight,
|
||||
borderWidth: 2,
|
||||
borderColor: '#7FB6E6',
|
||||
borderColor: colors.chat.replyBorder,
|
||||
},
|
||||
selectedText: {
|
||||
// 文本选中时的样式
|
||||
},
|
||||
recalledText: {
|
||||
color: '#8E8E93',
|
||||
color: colors.chat.textSecondary,
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
|
||||
@@ -285,7 +316,7 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
imageBubble: {
|
||||
borderRadius: 18,
|
||||
overflow: 'hidden',
|
||||
shadowColor: '#000',
|
||||
shadowColor: colors.chat.shadow,
|
||||
shadowOffset: { width: 0, height: 3 },
|
||||
shadowOpacity: 0.15,
|
||||
shadowRadius: 6,
|
||||
@@ -314,27 +345,28 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
},
|
||||
readStatusText: {
|
||||
fontSize: 10,
|
||||
color: '#8E8E93',
|
||||
color: colors.chat.textSecondary,
|
||||
fontWeight: '500',
|
||||
},
|
||||
readStatusTextRead: {
|
||||
color: '#34C759',
|
||||
color: colors.chat.success,
|
||||
},
|
||||
|
||||
// 输入框区域
|
||||
// 输入框区域 - 与列表背景同色底栏,顶部细分隔
|
||||
inputWrapper: {
|
||||
backgroundColor: 'transparent',
|
||||
backgroundColor: colors.chat.screen,
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
borderTopColor: 'rgba(0, 0, 0, 0.06)',
|
||||
},
|
||||
inputContainer: {
|
||||
backgroundColor: colors.background.paper,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.divider,
|
||||
borderRadius: 24,
|
||||
marginHorizontal: 14,
|
||||
marginBottom: 12,
|
||||
backgroundColor: 'transparent',
|
||||
borderWidth: 0,
|
||||
borderRadius: 0,
|
||||
marginHorizontal: 10,
|
||||
marginBottom: 10,
|
||||
marginTop: 2,
|
||||
paddingHorizontal: spacing.sm,
|
||||
paddingVertical: spacing.sm,
|
||||
// 移除明显的阴影效果,改用更微妙的边框
|
||||
shadowColor: 'transparent',
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowOpacity: 0,
|
||||
@@ -344,7 +376,7 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
inputInner: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#F7F8FA',
|
||||
backgroundColor: colors.chat.surfaceRaised,
|
||||
borderRadius: 20,
|
||||
paddingHorizontal: spacing.xs,
|
||||
paddingVertical: 4,
|
||||
@@ -357,7 +389,7 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
elevation: 0,
|
||||
},
|
||||
inputInnerMuted: {
|
||||
backgroundColor: '#F0F0F0',
|
||||
backgroundColor: colors.chat.borderHairline,
|
||||
opacity: 0.7,
|
||||
},
|
||||
|
||||
@@ -366,7 +398,7 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: '#FFF5F5',
|
||||
backgroundColor: colors.chat.warningBg,
|
||||
paddingVertical: spacing.xs,
|
||||
paddingHorizontal: spacing.md,
|
||||
marginBottom: spacing.xs,
|
||||
@@ -375,7 +407,7 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
},
|
||||
mutedBannerText: {
|
||||
fontSize: 13,
|
||||
color: '#FF3B30',
|
||||
color: colors.chat.danger,
|
||||
fontWeight: '500',
|
||||
},
|
||||
|
||||
@@ -421,14 +453,14 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
},
|
||||
input: {
|
||||
fontSize: 16,
|
||||
color: '#1A1A1A',
|
||||
color: colors.chat.textPrimary,
|
||||
paddingTop: 8,
|
||||
paddingBottom: 8,
|
||||
maxHeight: 100,
|
||||
lineHeight: 20,
|
||||
},
|
||||
inputMuted: {
|
||||
color: '#CCC',
|
||||
color: colors.chat.iconMuted,
|
||||
},
|
||||
inputTransparent: {
|
||||
color: 'transparent',
|
||||
@@ -444,7 +476,7 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
},
|
||||
inputHighlightText: {
|
||||
fontSize: 16,
|
||||
color: '#1A1A1A',
|
||||
color: colors.chat.textPrimary,
|
||||
lineHeight: 20,
|
||||
},
|
||||
inputHighlightMention: {
|
||||
@@ -462,10 +494,10 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
|
||||
// 面板
|
||||
panelWrapper: {
|
||||
backgroundColor: '#F5F7FA',
|
||||
backgroundColor: colors.chat.surfaceMuted,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#E8E8E8',
|
||||
shadowColor: '#000',
|
||||
borderTopColor: colors.chat.border,
|
||||
shadowColor: colors.chat.shadow,
|
||||
shadowOffset: { width: 0, height: -2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 8,
|
||||
@@ -473,11 +505,11 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
},
|
||||
// 表情面板包装器 - 底部 tab 栏是白色,安全区域也用白色填充
|
||||
emojiPanelWrapper: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
backgroundColor: colors.chat.card,
|
||||
},
|
||||
panelContainer: {
|
||||
flex: 1,
|
||||
backgroundColor: '#F5F7FA',
|
||||
backgroundColor: colors.chat.surfaceMuted,
|
||||
},
|
||||
|
||||
// 表情面板
|
||||
@@ -505,16 +537,16 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.sm,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#E8E8E8',
|
||||
borderTopColor: colors.chat.border,
|
||||
},
|
||||
panelCloseButton: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: '#FFFFFF',
|
||||
backgroundColor: colors.chat.card,
|
||||
borderRadius: 8,
|
||||
shadowColor: '#000',
|
||||
shadowColor: colors.chat.shadow,
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 2,
|
||||
@@ -533,8 +565,8 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
paddingHorizontal: spacing.sm,
|
||||
paddingVertical: spacing.xs,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#E8E8E8',
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderTopColor: colors.chat.border,
|
||||
backgroundColor: colors.chat.card,
|
||||
},
|
||||
panelTab: {
|
||||
width: 44,
|
||||
@@ -545,7 +577,7 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
marginRight: spacing.xs,
|
||||
},
|
||||
panelTabActive: {
|
||||
backgroundColor: '#DFF2FF',
|
||||
backgroundColor: colors.chat.replyTint,
|
||||
},
|
||||
panelTabEmoji: {
|
||||
fontSize: 24,
|
||||
@@ -553,7 +585,7 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
panelTabDivider: {
|
||||
width: 1,
|
||||
height: 24,
|
||||
backgroundColor: '#E8E8E8',
|
||||
backgroundColor: colors.chat.bubbleOutgoing,
|
||||
marginHorizontal: spacing.sm,
|
||||
},
|
||||
|
||||
@@ -571,13 +603,13 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
},
|
||||
stickerEmptyText: {
|
||||
fontSize: 16,
|
||||
color: '#666',
|
||||
color: colors.chat.textTertiary,
|
||||
marginTop: spacing.md,
|
||||
fontWeight: '500',
|
||||
},
|
||||
stickerEmptySubText: {
|
||||
fontSize: 13,
|
||||
color: '#999',
|
||||
color: colors.chat.textMuted,
|
||||
marginTop: spacing.xs,
|
||||
},
|
||||
stickerGrid: {
|
||||
@@ -593,7 +625,7 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
margin: 4,
|
||||
borderRadius: 8,
|
||||
overflow: 'hidden',
|
||||
backgroundColor: '#F5F5F5',
|
||||
backgroundColor: colors.chat.surfaceInput,
|
||||
},
|
||||
stickerImage: {
|
||||
width: '100%',
|
||||
@@ -618,8 +650,8 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: spacing.sm,
|
||||
backgroundColor: '#FFFFFF',
|
||||
shadowColor: '#000',
|
||||
backgroundColor: colors.chat.card,
|
||||
shadowColor: colors.chat.shadow,
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.08,
|
||||
shadowRadius: 4,
|
||||
@@ -627,7 +659,7 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
},
|
||||
moreItemText: {
|
||||
fontSize: 13,
|
||||
color: '#666',
|
||||
color: colors.chat.textTertiary,
|
||||
fontWeight: '500',
|
||||
},
|
||||
|
||||
@@ -636,7 +668,7 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
position: 'absolute',
|
||||
left: 12,
|
||||
right: 12,
|
||||
backgroundColor: '#FFFFFF',
|
||||
backgroundColor: colors.chat.card,
|
||||
borderRadius: 18,
|
||||
shadowColor: colors.primary.main,
|
||||
shadowOffset: { width: 0, height: -3 },
|
||||
@@ -651,7 +683,7 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
// @成员选择面板
|
||||
mentionPanelContainer: {
|
||||
flex: 1,
|
||||
backgroundColor: '#FFFFFF',
|
||||
backgroundColor: colors.chat.card,
|
||||
borderRadius: 18,
|
||||
},
|
||||
mentionPanelDragHandle: {
|
||||
@@ -663,7 +695,7 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
width: 32,
|
||||
height: 3,
|
||||
borderRadius: 2,
|
||||
backgroundColor: '#E0E0E0',
|
||||
backgroundColor: colors.chat.sheetGrip,
|
||||
},
|
||||
mentionPanelHeader: {
|
||||
flexDirection: 'row',
|
||||
@@ -673,12 +705,12 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
paddingTop: 4,
|
||||
paddingBottom: 8,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomColor: '#F0F0F0',
|
||||
borderBottomColor: colors.chat.borderHairline,
|
||||
},
|
||||
mentionPanelTitle: {
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
color: '#AAAAAA',
|
||||
color: colors.chat.textPlaceholder,
|
||||
letterSpacing: 0.5,
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
@@ -686,7 +718,7 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
width: 26,
|
||||
height: 26,
|
||||
borderRadius: 13,
|
||||
backgroundColor: '#F5F5F5',
|
||||
backgroundColor: colors.chat.surfaceInput,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
@@ -728,7 +760,7 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
mentionItemName: {
|
||||
fontSize: 15,
|
||||
fontWeight: '500',
|
||||
color: '#1A1A1A',
|
||||
color: colors.chat.textPrimary,
|
||||
},
|
||||
mentionItemNameAll: {
|
||||
fontSize: 15,
|
||||
@@ -737,7 +769,7 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
},
|
||||
mentionItemSub: {
|
||||
fontSize: 12,
|
||||
color: '#BBBBBB',
|
||||
color: colors.chat.iconMuted,
|
||||
marginTop: 1,
|
||||
},
|
||||
mentionItemRole: {
|
||||
@@ -752,7 +784,7 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
},
|
||||
mentionEmptyText: {
|
||||
fontSize: 14,
|
||||
color: '#C8C8C8',
|
||||
color: colors.chat.iconSoft,
|
||||
},
|
||||
|
||||
// 长按菜单 - 底部横条形式(类似QQ)
|
||||
@@ -763,10 +795,10 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
},
|
||||
// 旧版菜单样式(保留兼容)
|
||||
menuContainer: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
backgroundColor: colors.chat.card,
|
||||
borderRadius: 12,
|
||||
minWidth: 160,
|
||||
shadowColor: '#000',
|
||||
shadowColor: colors.chat.shadow,
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.15,
|
||||
shadowRadius: 12,
|
||||
@@ -781,21 +813,21 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
},
|
||||
menuItemBorder: {
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#F0F0F0',
|
||||
borderBottomColor: colors.chat.borderHairline,
|
||||
},
|
||||
menuItemText: {
|
||||
fontSize: 15,
|
||||
color: '#333',
|
||||
color: colors.text.primary,
|
||||
fontWeight: '500',
|
||||
},
|
||||
// 底部菜单容器
|
||||
bottomMenuContainer: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
backgroundColor: colors.chat.card,
|
||||
borderTopLeftRadius: 20,
|
||||
borderTopRightRadius: 20,
|
||||
paddingTop: spacing.md,
|
||||
paddingBottom: spacing.xl,
|
||||
shadowColor: '#000',
|
||||
shadowColor: colors.chat.shadow,
|
||||
shadowOffset: { width: 0, height: -4 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 8,
|
||||
@@ -803,7 +835,7 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
},
|
||||
// 消息预览区域
|
||||
messagePreviewContainer: {
|
||||
backgroundColor: '#F5F7FA',
|
||||
backgroundColor: colors.chat.surfaceMuted,
|
||||
marginHorizontal: spacing.md,
|
||||
marginBottom: spacing.md,
|
||||
paddingHorizontal: spacing.md,
|
||||
@@ -813,7 +845,7 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
},
|
||||
messagePreviewText: {
|
||||
fontSize: 14,
|
||||
color: '#666',
|
||||
color: colors.chat.textTertiary,
|
||||
lineHeight: 20,
|
||||
},
|
||||
// 操作按钮横条
|
||||
@@ -824,7 +856,7 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
paddingHorizontal: spacing.sm,
|
||||
paddingVertical: spacing.md,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#F0F0F0',
|
||||
borderBottomColor: colors.chat.borderHairline,
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
menuActionItem: {
|
||||
@@ -839,19 +871,19 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: 16,
|
||||
backgroundColor: '#F0F7FF',
|
||||
backgroundColor: colors.chat.overlayQuote,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: 8,
|
||||
},
|
||||
menuActionLabel: {
|
||||
fontSize: 13,
|
||||
color: '#333',
|
||||
color: colors.text.primary,
|
||||
fontWeight: '500',
|
||||
},
|
||||
// 取消按钮
|
||||
menuCancelButton: {
|
||||
backgroundColor: '#F5F7FA',
|
||||
backgroundColor: colors.chat.surfaceMuted,
|
||||
marginHorizontal: spacing.md,
|
||||
paddingVertical: spacing.md,
|
||||
borderRadius: 12,
|
||||
@@ -859,7 +891,7 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
},
|
||||
menuCancelText: {
|
||||
fontSize: 16,
|
||||
color: '#333',
|
||||
color: colors.text.primary,
|
||||
fontWeight: '600',
|
||||
},
|
||||
|
||||
@@ -874,7 +906,7 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
borderRadius: 6,
|
||||
paddingVertical: 6,
|
||||
paddingHorizontal: 2,
|
||||
shadowColor: '#000',
|
||||
shadowColor: colors.chat.shadow,
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 4,
|
||||
@@ -901,20 +933,21 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
},
|
||||
qqMenuItemLabel: {
|
||||
fontSize: 11,
|
||||
color: '#FFFFFF',
|
||||
color: colors.primary.contrast,
|
||||
fontWeight: '500',
|
||||
},
|
||||
|
||||
// 回复预览
|
||||
// 回复预览(输入区)- 与气泡内引用同色条
|
||||
replyPreviewContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#F5F7FA',
|
||||
backgroundColor: 'rgba(74, 136, 199, 0.08)',
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.sm,
|
||||
borderLeftWidth: 3,
|
||||
borderLeftColor: colors.primary.main,
|
||||
borderLeftColor: colors.chat.link,
|
||||
marginBottom: spacing.xs,
|
||||
borderRadius: 8,
|
||||
},
|
||||
replyPreviewContent: {
|
||||
flex: 1,
|
||||
@@ -932,7 +965,7 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
},
|
||||
replyPreviewMessage: {
|
||||
fontSize: 12,
|
||||
color: '#8E8E93',
|
||||
color: colors.chat.textSecondary,
|
||||
marginTop: 2,
|
||||
},
|
||||
replyPreviewClose: {
|
||||
@@ -948,11 +981,11 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
zIndex: 1000,
|
||||
},
|
||||
overlayContent: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
backgroundColor: colors.chat.card,
|
||||
padding: spacing.xl,
|
||||
borderRadius: 16,
|
||||
alignItems: 'center',
|
||||
shadowColor: '#000',
|
||||
shadowColor: colors.chat.shadow,
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.15,
|
||||
shadowRadius: 12,
|
||||
@@ -961,13 +994,13 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
overlayText: {
|
||||
marginTop: spacing.md,
|
||||
fontSize: 15,
|
||||
color: '#666',
|
||||
color: colors.chat.textTertiary,
|
||||
fontWeight: '500',
|
||||
},
|
||||
|
||||
// 表情包添加按钮(管理界面第一位)
|
||||
stickerAddButton: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
backgroundColor: colors.chat.card,
|
||||
borderWidth: 1.5,
|
||||
borderColor: colors.primary.main,
|
||||
borderStyle: 'dashed',
|
||||
@@ -986,9 +1019,9 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
|
||||
// 表情包管理按钮(表情面板第一位)
|
||||
stickerManageButton: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
backgroundColor: colors.chat.card,
|
||||
borderWidth: 1,
|
||||
borderColor: '#E8E8E8',
|
||||
borderColor: colors.chat.border,
|
||||
borderStyle: 'dashed',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
@@ -1000,7 +1033,7 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
},
|
||||
stickerManageText: {
|
||||
fontSize: 12,
|
||||
color: '#666',
|
||||
color: colors.chat.textTertiary,
|
||||
marginTop: 4,
|
||||
fontWeight: '500',
|
||||
},
|
||||
@@ -1018,20 +1051,20 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
width: 36,
|
||||
height: 5,
|
||||
borderRadius: 3,
|
||||
backgroundColor: '#E0E0E0',
|
||||
backgroundColor: colors.chat.sheetGrip,
|
||||
alignSelf: 'center',
|
||||
marginTop: 8,
|
||||
marginBottom: 4,
|
||||
},
|
||||
manageHeaderDivider: {
|
||||
height: StyleSheet.hairlineWidth,
|
||||
backgroundColor: '#F0F0F0',
|
||||
backgroundColor: colors.chat.borderHairline,
|
||||
},
|
||||
|
||||
// 表情包选择状态
|
||||
stickerItemSelected: {
|
||||
borderWidth: 2,
|
||||
borderColor: '#7FB6E6',
|
||||
borderColor: colors.chat.replyBorder,
|
||||
},
|
||||
stickerCheckOverlay: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
@@ -1045,19 +1078,19 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
height: 22,
|
||||
borderRadius: 11,
|
||||
borderWidth: 2,
|
||||
borderColor: '#FFF',
|
||||
borderColor: colors.chat.card,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.8)',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
stickerCheckBoxSelected: {
|
||||
backgroundColor: '#7FB6E6',
|
||||
borderColor: '#7FB6E6',
|
||||
backgroundColor: colors.chat.replyBorder,
|
||||
borderColor: colors.chat.replyBorder,
|
||||
},
|
||||
|
||||
// 表情管理模态框
|
||||
manageModalContainer: {
|
||||
backgroundColor: '#F5F7FA',
|
||||
backgroundColor: colors.chat.surfaceMuted,
|
||||
borderTopLeftRadius: 20,
|
||||
borderTopRightRadius: 20,
|
||||
overflow: 'hidden',
|
||||
@@ -1068,9 +1101,9 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.md,
|
||||
backgroundColor: '#FFFFFF',
|
||||
backgroundColor: colors.chat.card,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#E8E8E8',
|
||||
borderBottomColor: colors.chat.border,
|
||||
},
|
||||
manageHeaderButton: {
|
||||
minWidth: 60,
|
||||
@@ -1079,28 +1112,28 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
manageModalTitle: {
|
||||
fontSize: 17,
|
||||
fontWeight: '600',
|
||||
color: '#1A1A1A',
|
||||
color: colors.chat.textPrimary,
|
||||
},
|
||||
manageDoneText: {
|
||||
fontSize: 16,
|
||||
color: '#4A88C7',
|
||||
color: colors.chat.link,
|
||||
fontWeight: '500',
|
||||
},
|
||||
manageSelectText: {
|
||||
fontSize: 16,
|
||||
color: '#4A88C7',
|
||||
color: colors.chat.link,
|
||||
fontWeight: '500',
|
||||
},
|
||||
manageInfoBar: {
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.sm,
|
||||
backgroundColor: '#FFFFFF',
|
||||
backgroundColor: colors.chat.card,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#E8E8E8',
|
||||
borderBottomColor: colors.chat.border,
|
||||
},
|
||||
manageInfoText: {
|
||||
fontSize: 13,
|
||||
color: '#8E8E93',
|
||||
color: colors.chat.textSecondary,
|
||||
},
|
||||
manageStickerGrid: {
|
||||
flexDirection: 'row',
|
||||
@@ -1115,9 +1148,9 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.md,
|
||||
backgroundColor: '#FFFFFF',
|
||||
backgroundColor: colors.chat.card,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#E8E8E8',
|
||||
borderTopColor: colors.chat.border,
|
||||
paddingBottom: 34, // 安全区域
|
||||
},
|
||||
manageSelectAllButton: {
|
||||
@@ -1126,24 +1159,24 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
},
|
||||
manageSelectAllText: {
|
||||
fontSize: 15,
|
||||
color: '#4A88C7',
|
||||
color: colors.chat.link,
|
||||
fontWeight: '500',
|
||||
},
|
||||
manageDeleteButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#FF3B30',
|
||||
backgroundColor: colors.chat.danger,
|
||||
paddingHorizontal: spacing.lg,
|
||||
paddingVertical: spacing.sm + 2,
|
||||
borderRadius: 20,
|
||||
gap: 6,
|
||||
},
|
||||
manageDeleteButtonDisabled: {
|
||||
backgroundColor: '#CCC',
|
||||
backgroundColor: colors.chat.iconMuted,
|
||||
},
|
||||
manageDeleteText: {
|
||||
fontSize: 15,
|
||||
color: '#FFFFFF',
|
||||
color: colors.primary.contrast,
|
||||
fontWeight: '600',
|
||||
},
|
||||
manageTipBar: {
|
||||
@@ -1152,12 +1185,12 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.sm,
|
||||
backgroundColor: '#FFF9E6',
|
||||
backgroundColor: colors.chat.tipBg,
|
||||
gap: 6,
|
||||
},
|
||||
manageTipText: {
|
||||
fontSize: 12,
|
||||
color: '#999',
|
||||
color: colors.chat.textMuted,
|
||||
},
|
||||
|
||||
// 管理界面空状态
|
||||
@@ -1186,5 +1219,9 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
textAlign: 'center',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export default chatScreenStyles;
|
||||
export function useChatScreenStyles() {
|
||||
const colors = useAppColors();
|
||||
return useMemo(() => createChatScreenStyles(colors), [colors]);
|
||||
}
|
||||
|
||||
@@ -546,6 +546,13 @@ export const useChatScreen = () => {
|
||||
isBrowsingHistoryRef.current = false;
|
||||
}, [isHistoryLoadingLocked]);
|
||||
|
||||
/** 用户点击「回到底部」:解除浏览历史锁并滚到最新消息端(inverted 下 offset=0) */
|
||||
const jumpToLatestMessages = useCallback(() => {
|
||||
suppressAutoFollowRef.current = false;
|
||||
isBrowsingHistoryRef.current = false;
|
||||
scrollToLatest(true, true, 'jump-latest-button');
|
||||
}, [scrollToLatest]);
|
||||
|
||||
const setBrowsingHistory = useCallback((browsing: boolean) => {
|
||||
isBrowsingHistoryRef.current = browsing;
|
||||
}, []);
|
||||
@@ -1396,6 +1403,7 @@ export const useChatScreen = () => {
|
||||
handleClearConversation,
|
||||
handleMessageListContentSizeChange,
|
||||
handleReachLatestEdge,
|
||||
jumpToLatestMessages,
|
||||
setBrowsingHistory,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
* onPress 引用不参与比较,请配合父组件 ref + 按 id 查最新会话使用。
|
||||
*/
|
||||
|
||||
import React, { memo, useEffect, useRef, useState } from 'react';
|
||||
import React, { memo, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Animated, StyleSheet, TouchableOpacity, View } from 'react-native';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import { colors, spacing, shadows } from '../../../theme';
|
||||
import { spacing, shadows, useAppColors, type AppColors } from '../../../theme';
|
||||
import {
|
||||
ConversationResponse,
|
||||
extractTextFromSegments,
|
||||
@@ -150,6 +150,124 @@ function areConversationRowEqual(
|
||||
return true;
|
||||
}
|
||||
|
||||
function createConversationRowStyles(colors: AppColors) {
|
||||
return StyleSheet.create({
|
||||
conversationItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: 14,
|
||||
backgroundColor: colors.background.paper,
|
||||
marginHorizontal: spacing.md,
|
||||
marginTop: spacing.sm,
|
||||
borderRadius: 12,
|
||||
...shadows.sm,
|
||||
},
|
||||
conversationItemSelected: {
|
||||
backgroundColor: colors.primary.light + '20',
|
||||
borderColor: colors.primary.main,
|
||||
borderWidth: 1,
|
||||
},
|
||||
avatarContainer: {
|
||||
position: 'relative',
|
||||
},
|
||||
systemAvatar: {
|
||||
width: 50,
|
||||
height: 50,
|
||||
borderRadius: 12,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: colors.primary.main,
|
||||
},
|
||||
conversationContent: {
|
||||
flex: 1,
|
||||
marginLeft: spacing.md,
|
||||
},
|
||||
conversationHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 4,
|
||||
},
|
||||
nameRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
officialBadge: {
|
||||
backgroundColor: colors.primary.main,
|
||||
borderRadius: 4,
|
||||
paddingHorizontal: 6,
|
||||
paddingVertical: 2,
|
||||
marginRight: spacing.xs,
|
||||
},
|
||||
officialBadgeText: {
|
||||
color: colors.primary.contrast,
|
||||
fontSize: 10,
|
||||
fontWeight: '600',
|
||||
},
|
||||
userName: {
|
||||
fontWeight: '600',
|
||||
color: colors.text.primary,
|
||||
fontSize: 16,
|
||||
},
|
||||
groupIcon: {
|
||||
marginRight: 4,
|
||||
},
|
||||
memberCount: {
|
||||
fontSize: 12,
|
||||
color: colors.text.secondary,
|
||||
marginLeft: 2,
|
||||
},
|
||||
pinnedIcon: {
|
||||
marginLeft: 6,
|
||||
},
|
||||
groupAvatar: {
|
||||
width: 50,
|
||||
height: 50,
|
||||
},
|
||||
groupAvatarPlaceholder: {
|
||||
width: 50,
|
||||
height: 50,
|
||||
borderRadius: 12,
|
||||
backgroundColor: colors.primary.main,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
timeText: {
|
||||
color: colors.text.secondary,
|
||||
fontSize: 12,
|
||||
},
|
||||
messageRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
messageText: {
|
||||
flex: 1,
|
||||
color: colors.text.secondary,
|
||||
fontSize: 14,
|
||||
},
|
||||
unreadMessageText: {
|
||||
color: colors.text.primary,
|
||||
fontWeight: '500',
|
||||
},
|
||||
unreadBadge: {
|
||||
minWidth: 20,
|
||||
height: 20,
|
||||
borderRadius: 10,
|
||||
backgroundColor: colors.primary.main,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginLeft: spacing.sm,
|
||||
paddingHorizontal: 6,
|
||||
},
|
||||
unreadBadgeText: {
|
||||
color: colors.primary.contrast,
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const ConversationListRowInner: React.FC<ConversationListRowProps> = ({
|
||||
item,
|
||||
scale,
|
||||
@@ -158,6 +276,8 @@ const ConversationListRowInner: React.FC<ConversationListRowProps> = ({
|
||||
formatTime,
|
||||
systemChannelId,
|
||||
}) => {
|
||||
const colors = useAppColors();
|
||||
const styles = useMemo(() => createConversationRowStyles(colors), [colors]);
|
||||
const isSystemChannel = item.id === systemChannelId;
|
||||
const isGroupChat = !!(item.type === 'group' && item.group);
|
||||
|
||||
@@ -196,7 +316,7 @@ const ConversationListRowInner: React.FC<ConversationListRowProps> = ({
|
||||
<View style={styles.avatarContainer}>
|
||||
{isSystemChannel ? (
|
||||
<View style={styles.systemAvatar}>
|
||||
<MaterialCommunityIcons name="bell-ring" size={24} color="#FFF" />
|
||||
<MaterialCommunityIcons name="bell-ring" size={24} color={colors.primary.contrast} />
|
||||
</View>
|
||||
) : isGroupChat ? (
|
||||
<View style={styles.groupAvatar}>
|
||||
@@ -204,7 +324,7 @@ const ConversationListRowInner: React.FC<ConversationListRowProps> = ({
|
||||
<Avatar source={displayAvatar} size={50} name={displayName} />
|
||||
) : (
|
||||
<View style={styles.groupAvatarPlaceholder}>
|
||||
<MaterialCommunityIcons name="account-group" size={28} color="#FFF" />
|
||||
<MaterialCommunityIcons name="account-group" size={28} color={colors.primary.contrast} />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
@@ -271,119 +391,3 @@ const ConversationListRowInner: React.FC<ConversationListRowProps> = ({
|
||||
ConversationListRowInner.displayName = 'ConversationListRow';
|
||||
|
||||
export const ConversationListRow = memo(ConversationListRowInner, areConversationRowEqual);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
conversationItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: 14,
|
||||
backgroundColor: '#FFF',
|
||||
marginHorizontal: spacing.md,
|
||||
marginTop: spacing.sm,
|
||||
borderRadius: 12,
|
||||
...shadows.sm,
|
||||
},
|
||||
conversationItemSelected: {
|
||||
backgroundColor: colors.primary.light + '20',
|
||||
borderColor: colors.primary.main,
|
||||
borderWidth: 1,
|
||||
},
|
||||
avatarContainer: {
|
||||
position: 'relative',
|
||||
},
|
||||
systemAvatar: {
|
||||
width: 50,
|
||||
height: 50,
|
||||
borderRadius: 12,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: colors.primary.main,
|
||||
},
|
||||
conversationContent: {
|
||||
flex: 1,
|
||||
marginLeft: spacing.md,
|
||||
},
|
||||
conversationHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 4,
|
||||
},
|
||||
nameRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
officialBadge: {
|
||||
backgroundColor: colors.primary.main,
|
||||
borderRadius: 4,
|
||||
paddingHorizontal: 6,
|
||||
paddingVertical: 2,
|
||||
marginRight: spacing.xs,
|
||||
},
|
||||
officialBadgeText: {
|
||||
color: '#FFF',
|
||||
fontSize: 10,
|
||||
fontWeight: '600',
|
||||
},
|
||||
userName: {
|
||||
fontWeight: '600',
|
||||
color: '#333',
|
||||
fontSize: 16,
|
||||
},
|
||||
groupIcon: {
|
||||
marginRight: 4,
|
||||
},
|
||||
memberCount: {
|
||||
fontSize: 12,
|
||||
color: '#999',
|
||||
marginLeft: 2,
|
||||
},
|
||||
pinnedIcon: {
|
||||
marginLeft: 6,
|
||||
},
|
||||
groupAvatar: {
|
||||
width: 50,
|
||||
height: 50,
|
||||
},
|
||||
groupAvatarPlaceholder: {
|
||||
width: 50,
|
||||
height: 50,
|
||||
borderRadius: 12,
|
||||
backgroundColor: colors.primary.main,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
timeText: {
|
||||
color: '#999',
|
||||
fontSize: 12,
|
||||
},
|
||||
messageRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
messageText: {
|
||||
flex: 1,
|
||||
color: '#888',
|
||||
fontSize: 14,
|
||||
},
|
||||
unreadMessageText: {
|
||||
color: '#333',
|
||||
fontWeight: '500',
|
||||
},
|
||||
unreadBadge: {
|
||||
minWidth: 20,
|
||||
height: 20,
|
||||
borderRadius: 10,
|
||||
backgroundColor: colors.primary.main,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginLeft: spacing.sm,
|
||||
paddingHorizontal: 6,
|
||||
},
|
||||
unreadBadgeText: {
|
||||
color: '#FFF',
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* 嵌入式聊天组件 - 用于桌面端双栏布局右侧
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
||||
import {
|
||||
View,
|
||||
FlatList,
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
import { useRouter } from 'expo-router';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import { Image as ExpoImage } from 'expo-image';
|
||||
import { colors, spacing, fontSizes, shadows } from '../../../theme';
|
||||
import { spacing, fontSizes, shadows, useAppColors, type AppColors } from '../../../theme';
|
||||
import { ConversationResponse, MessageResponse, MessageSegment, GroupMemberResponse, ImageSegmentData } from '../../../types/dto';
|
||||
import { Avatar, Text, ImageGallery, AppBackButton } from '../../../components/common';
|
||||
import { useAuthStore, messageManager, MessageEvent, MessageSubscriber } from '../../../stores';
|
||||
@@ -34,12 +34,210 @@ import { LongPressMenu, MenuPosition, GroupMessage } from './ChatScreen';
|
||||
const { width: screenWidth } = Dimensions.get('window');
|
||||
const PANEL_WIDTH = 360;
|
||||
|
||||
function createEmbeddedChatStyles(colors: AppColors) {
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.chat.screen,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.sm,
|
||||
backgroundColor: colors.background.paper,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomColor: colors.divider,
|
||||
shadowColor: 'transparent',
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowOpacity: 0,
|
||||
shadowRadius: 0,
|
||||
elevation: 0,
|
||||
height: 56,
|
||||
},
|
||||
headerButton: {
|
||||
padding: spacing.xs,
|
||||
width: 40,
|
||||
height: 40,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
headerCenter: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
headerTitle: {
|
||||
fontSize: fontSizes.lg,
|
||||
fontWeight: '600',
|
||||
color: colors.text.primary,
|
||||
marginLeft: spacing.sm,
|
||||
maxWidth: 200,
|
||||
},
|
||||
messageList: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.chat.screen,
|
||||
},
|
||||
centerContainer: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: spacing.xl,
|
||||
},
|
||||
emptyText: {
|
||||
marginTop: spacing.md,
|
||||
fontSize: 16,
|
||||
color: colors.text.secondary,
|
||||
},
|
||||
emptySubtext: {
|
||||
marginTop: spacing.xs,
|
||||
fontSize: 14,
|
||||
color: colors.text.disabled,
|
||||
},
|
||||
listContent: {
|
||||
padding: spacing.md,
|
||||
paddingBottom: spacing.xl,
|
||||
},
|
||||
messageRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-end',
|
||||
marginBottom: spacing.md,
|
||||
maxWidth: screenWidth * 0.7,
|
||||
},
|
||||
messageRowLeft: {
|
||||
alignSelf: 'flex-start',
|
||||
},
|
||||
messageRowRight: {
|
||||
alignSelf: 'flex-end',
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
messageBubble: {
|
||||
maxWidth: screenWidth * 0.5,
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.sm,
|
||||
borderRadius: 18,
|
||||
marginHorizontal: spacing.sm,
|
||||
},
|
||||
messageBubbleMe: {
|
||||
backgroundColor: colors.chat.replyTint,
|
||||
borderBottomRightRadius: 4,
|
||||
},
|
||||
messageBubbleOther: {
|
||||
backgroundColor: colors.chat.bubbleIncoming,
|
||||
borderBottomLeftRadius: 4,
|
||||
shadowColor: colors.chat.shadow,
|
||||
shadowOffset: { width: 0, height: 0.5 },
|
||||
shadowOpacity: 0.04,
|
||||
shadowRadius: 1,
|
||||
elevation: 1,
|
||||
},
|
||||
senderName: {
|
||||
fontSize: 12,
|
||||
color: colors.text.secondary,
|
||||
marginBottom: 2,
|
||||
},
|
||||
messageText: {
|
||||
fontSize: 15,
|
||||
lineHeight: 20,
|
||||
},
|
||||
messageTextMe: {
|
||||
color: colors.chat.textPrimary,
|
||||
},
|
||||
messageTextOther: {
|
||||
color: colors.chat.textPrimary,
|
||||
},
|
||||
inputArea: {
|
||||
backgroundColor: colors.chat.screen,
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
borderTopColor: colors.divider,
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.sm,
|
||||
paddingBottom: Platform.OS === 'ios' ? spacing.md : spacing.sm,
|
||||
},
|
||||
inputRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
iconButton: {
|
||||
padding: spacing.xs,
|
||||
width: 44,
|
||||
height: 44,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
inputContainer: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.chat.surfaceInput,
|
||||
borderRadius: 20,
|
||||
paddingHorizontal: spacing.md,
|
||||
minHeight: 40,
|
||||
justifyContent: 'center',
|
||||
shadowColor: 'transparent',
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowOpacity: 0,
|
||||
shadowRadius: 0,
|
||||
elevation: 0,
|
||||
},
|
||||
textInput: {
|
||||
fontSize: 15,
|
||||
color: colors.text.primary,
|
||||
padding: 0,
|
||||
maxHeight: 100,
|
||||
},
|
||||
sendButton: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
backgroundColor: colors.primary.main,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginLeft: spacing.xs,
|
||||
},
|
||||
sendButtonDisabled: {
|
||||
backgroundColor: colors.background.disabled,
|
||||
},
|
||||
messageTextRecalled: {
|
||||
fontStyle: 'italic',
|
||||
color: colors.text.secondary,
|
||||
},
|
||||
imageGrid: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
marginTop: 4,
|
||||
},
|
||||
messageImage: {
|
||||
borderRadius: 8,
|
||||
marginRight: 4,
|
||||
marginBottom: 4,
|
||||
},
|
||||
imagePlaceholder: {
|
||||
width: 120,
|
||||
height: 120,
|
||||
borderRadius: 8,
|
||||
backgroundColor: colors.background.disabled,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginRight: 4,
|
||||
marginBottom: 4,
|
||||
},
|
||||
imagePlaceholderText: {
|
||||
color: colors.text.secondary,
|
||||
fontSize: 12,
|
||||
marginTop: 4,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
interface EmbeddedChatProps {
|
||||
conversation: ConversationResponse;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
export const EmbeddedChat: React.FC<EmbeddedChatProps> = ({ conversation, onBack }) => {
|
||||
const colors = useAppColors();
|
||||
const styles = useMemo(() => createEmbeddedChatStyles(colors), [colors]);
|
||||
const router = useRouter();
|
||||
const currentUser = useAuthStore(state => state.currentUser);
|
||||
|
||||
@@ -411,7 +609,7 @@ export const EmbeddedChat: React.FC<EmbeddedChatProps> = ({ conversation, onBack
|
||||
<View style={styles.header}>
|
||||
{/* 大屏幕(>= 768px)时隐藏返回按钮 */}
|
||||
{!isWideScreen ? (
|
||||
<AppBackButton onPress={onBack} style={styles.headerButton} iconColor="#666" />
|
||||
<AppBackButton onPress={onBack} style={styles.headerButton} iconColor={colors.text.secondary} />
|
||||
) : (
|
||||
<View style={styles.headerButton} />
|
||||
)}
|
||||
@@ -426,11 +624,11 @@ export const EmbeddedChat: React.FC<EmbeddedChatProps> = ({ conversation, onBack
|
||||
{/* 大屏幕 + 群聊时显示群信息按钮,否则显示放大按钮 */}
|
||||
{isWideScreen && isGroupChat ? (
|
||||
<TouchableOpacity onPress={toggleGroupInfoPanel} style={styles.headerButton}>
|
||||
<MaterialCommunityIcons name="dots-horizontal" size={22} color="#666" />
|
||||
<MaterialCommunityIcons name="dots-horizontal" size={22} color={colors.text.secondary} />
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
<TouchableOpacity onPress={handleNavigateToFullChat} style={styles.headerButton}>
|
||||
<MaterialCommunityIcons name="arrow-expand" size={22} color="#666" />
|
||||
<MaterialCommunityIcons name="arrow-expand" size={22} color={colors.text.secondary} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
@@ -443,7 +641,7 @@ export const EmbeddedChat: React.FC<EmbeddedChatProps> = ({ conversation, onBack
|
||||
</View>
|
||||
) : messages.length === 0 ? (
|
||||
<View style={styles.centerContainer}>
|
||||
<MaterialCommunityIcons name="message-text-outline" size={48} color="#DDD" />
|
||||
<MaterialCommunityIcons name="message-text-outline" size={48} color={colors.text.disabled} />
|
||||
<Text style={styles.emptyText}>暂无消息</Text>
|
||||
<Text style={styles.emptySubtext}>发送第一条消息开始聊天</Text>
|
||||
</View>
|
||||
@@ -472,7 +670,7 @@ export const EmbeddedChat: React.FC<EmbeddedChatProps> = ({ conversation, onBack
|
||||
<View style={styles.inputArea}>
|
||||
<View style={styles.inputRow}>
|
||||
<TouchableOpacity style={styles.iconButton}>
|
||||
<MaterialCommunityIcons name="plus-circle-outline" size={28} color="#666" />
|
||||
<MaterialCommunityIcons name="plus-circle-outline" size={28} color={colors.text.secondary} />
|
||||
</TouchableOpacity>
|
||||
|
||||
<View style={styles.inputContainer}>
|
||||
@@ -480,7 +678,7 @@ export const EmbeddedChat: React.FC<EmbeddedChatProps> = ({ conversation, onBack
|
||||
ref={inputRef}
|
||||
style={styles.textInput}
|
||||
placeholder="发送消息..."
|
||||
placeholderTextColor="#999"
|
||||
placeholderTextColor={colors.text.hint}
|
||||
value={inputText}
|
||||
onChangeText={setInputText}
|
||||
multiline={false}
|
||||
@@ -494,7 +692,7 @@ export const EmbeddedChat: React.FC<EmbeddedChatProps> = ({ conversation, onBack
|
||||
</View>
|
||||
|
||||
<TouchableOpacity style={styles.iconButton}>
|
||||
<MaterialCommunityIcons name="emoticon-outline" size={28} color="#666" />
|
||||
<MaterialCommunityIcons name="emoticon-outline" size={28} color={colors.text.secondary} />
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
@@ -503,9 +701,9 @@ export const EmbeddedChat: React.FC<EmbeddedChatProps> = ({ conversation, onBack
|
||||
disabled={!inputText.trim() || sending}
|
||||
>
|
||||
{sending ? (
|
||||
<ActivityIndicator size="small" color="#FFF" />
|
||||
<ActivityIndicator size="small" color={colors.text.inverse} />
|
||||
) : (
|
||||
<MaterialCommunityIcons name="send" size={20} color="#FFF" />
|
||||
<MaterialCommunityIcons name="send" size={20} color={colors.text.inverse} />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
@@ -604,190 +802,3 @@ export const EmbeddedChat: React.FC<EmbeddedChatProps> = ({ conversation, onBack
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#F5F7FA',
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.sm,
|
||||
backgroundColor: '#FFF',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#E8E8E8',
|
||||
...shadows.sm,
|
||||
height: 56,
|
||||
},
|
||||
headerButton: {
|
||||
padding: spacing.xs,
|
||||
width: 40,
|
||||
height: 40,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
headerCenter: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
headerTitle: {
|
||||
fontSize: fontSizes.lg,
|
||||
fontWeight: '600',
|
||||
color: '#333',
|
||||
marginLeft: spacing.sm,
|
||||
maxWidth: 200,
|
||||
},
|
||||
messageList: {
|
||||
flex: 1,
|
||||
backgroundColor: '#F5F7FA',
|
||||
},
|
||||
centerContainer: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: spacing.xl,
|
||||
},
|
||||
emptyText: {
|
||||
marginTop: spacing.md,
|
||||
fontSize: 16,
|
||||
color: '#999',
|
||||
},
|
||||
emptySubtext: {
|
||||
marginTop: spacing.xs,
|
||||
fontSize: 14,
|
||||
color: '#BBB',
|
||||
},
|
||||
listContent: {
|
||||
padding: spacing.md,
|
||||
paddingBottom: spacing.xl,
|
||||
},
|
||||
messageRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-end',
|
||||
marginBottom: spacing.md,
|
||||
maxWidth: screenWidth * 0.7,
|
||||
},
|
||||
messageRowLeft: {
|
||||
alignSelf: 'flex-start',
|
||||
},
|
||||
messageRowRight: {
|
||||
alignSelf: 'flex-end',
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
messageBubble: {
|
||||
maxWidth: screenWidth * 0.5,
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.sm,
|
||||
borderRadius: 18,
|
||||
marginHorizontal: spacing.sm,
|
||||
},
|
||||
messageBubbleMe: {
|
||||
backgroundColor: colors.primary.main,
|
||||
borderBottomRightRadius: 4,
|
||||
},
|
||||
messageBubbleOther: {
|
||||
backgroundColor: '#FFF',
|
||||
borderBottomLeftRadius: 4,
|
||||
...shadows.sm,
|
||||
},
|
||||
senderName: {
|
||||
fontSize: 12,
|
||||
color: '#999',
|
||||
marginBottom: 2,
|
||||
},
|
||||
messageText: {
|
||||
fontSize: 15,
|
||||
lineHeight: 20,
|
||||
},
|
||||
messageTextMe: {
|
||||
color: '#FFF',
|
||||
},
|
||||
messageTextOther: {
|
||||
color: '#333',
|
||||
},
|
||||
inputArea: {
|
||||
backgroundColor: '#FFF',
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#E8E8E8',
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.sm,
|
||||
paddingBottom: Platform.OS === 'ios' ? spacing.md : spacing.sm,
|
||||
},
|
||||
inputRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
iconButton: {
|
||||
padding: spacing.xs,
|
||||
width: 44,
|
||||
height: 44,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
inputContainer: {
|
||||
flex: 1,
|
||||
backgroundColor: '#F5F5F5',
|
||||
borderRadius: 20,
|
||||
paddingHorizontal: spacing.md,
|
||||
minHeight: 40,
|
||||
justifyContent: 'center',
|
||||
// 移除阴影
|
||||
shadowColor: 'transparent',
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowOpacity: 0,
|
||||
shadowRadius: 0,
|
||||
elevation: 0,
|
||||
},
|
||||
textInput: {
|
||||
fontSize: 15,
|
||||
color: '#333',
|
||||
padding: 0,
|
||||
maxHeight: 100,
|
||||
},
|
||||
sendButton: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
backgroundColor: colors.primary.main,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginLeft: spacing.xs,
|
||||
},
|
||||
sendButtonDisabled: {
|
||||
backgroundColor: '#E0E0E0',
|
||||
},
|
||||
messageTextRecalled: {
|
||||
fontStyle: 'italic',
|
||||
color: '#999',
|
||||
},
|
||||
imageGrid: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
marginTop: 4,
|
||||
},
|
||||
messageImage: {
|
||||
borderRadius: 8,
|
||||
marginRight: 4,
|
||||
marginBottom: 4,
|
||||
},
|
||||
imagePlaceholder: {
|
||||
width: 120,
|
||||
height: 120,
|
||||
borderRadius: 8,
|
||||
backgroundColor: 'rgba(0,0,0,0.1)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginRight: 4,
|
||||
marginBottom: 4,
|
||||
},
|
||||
imagePlaceholderText: {
|
||||
color: '#999',
|
||||
fontSize: 12,
|
||||
marginTop: 4,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { View, StyleSheet, TouchableOpacity, ActivityIndicator } from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
|
||||
import { Avatar, Text } from '../../../components/common';
|
||||
import { colors, spacing, borderRadius, shadows } from '../../../theme';
|
||||
import { spacing, borderRadius, shadows, useAppColors, type AppColors } from '../../../theme';
|
||||
|
||||
interface GroupInfoSummaryCardProps {
|
||||
groupName?: string;
|
||||
@@ -14,6 +14,79 @@ interface GroupInfoSummaryCardProps {
|
||||
memberCountText?: string;
|
||||
}
|
||||
|
||||
function createGroupRequestSharedStyles(colors: AppColors) {
|
||||
return StyleSheet.create({
|
||||
headerCard: {
|
||||
backgroundColor: colors.background.paper,
|
||||
borderRadius: borderRadius.lg,
|
||||
padding: spacing.lg,
|
||||
marginBottom: spacing.md,
|
||||
...shadows.sm,
|
||||
},
|
||||
groupHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
groupInfo: {
|
||||
marginLeft: spacing.lg,
|
||||
flex: 1,
|
||||
},
|
||||
groupName: {
|
||||
marginBottom: spacing.xs,
|
||||
fontWeight: '700',
|
||||
},
|
||||
groupMeta: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: spacing.xs,
|
||||
},
|
||||
groupNoText: {
|
||||
marginTop: spacing.xs,
|
||||
},
|
||||
descriptionContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
backgroundColor: colors.background.default,
|
||||
borderRadius: borderRadius.lg,
|
||||
padding: spacing.md,
|
||||
marginTop: spacing.md,
|
||||
},
|
||||
groupDesc: {
|
||||
marginLeft: spacing.sm,
|
||||
flex: 1,
|
||||
lineHeight: 20,
|
||||
},
|
||||
footer: {
|
||||
paddingHorizontal: spacing.lg,
|
||||
paddingTop: spacing.sm,
|
||||
flexDirection: 'row',
|
||||
backgroundColor: colors.background.default,
|
||||
},
|
||||
btn: {
|
||||
flex: 1,
|
||||
height: 42,
|
||||
borderRadius: borderRadius.md,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
reject: {
|
||||
marginRight: spacing.sm,
|
||||
backgroundColor: colors.error.light + '25',
|
||||
borderWidth: 1,
|
||||
borderColor: colors.error.light,
|
||||
},
|
||||
approve: {
|
||||
marginLeft: spacing.sm,
|
||||
backgroundColor: colors.primary.main,
|
||||
},
|
||||
statusBox: {
|
||||
alignItems: 'center',
|
||||
paddingTop: spacing.sm,
|
||||
backgroundColor: colors.background.default,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export const GroupInfoSummaryCard: React.FC<GroupInfoSummaryCardProps> = ({
|
||||
groupName,
|
||||
groupAvatar,
|
||||
@@ -21,12 +94,16 @@ export const GroupInfoSummaryCard: React.FC<GroupInfoSummaryCardProps> = ({
|
||||
groupDescription,
|
||||
memberCountText,
|
||||
}) => {
|
||||
const colors = useAppColors();
|
||||
const styles = useMemo(() => createGroupRequestSharedStyles(colors), [colors]);
|
||||
return (
|
||||
<View style={styles.headerCard}>
|
||||
<View style={styles.groupHeader}>
|
||||
<Avatar source={groupAvatar || ''} size={88} name={groupName} />
|
||||
<View style={styles.groupInfo}>
|
||||
<Text variant="h3" style={styles.groupName} numberOfLines={1}>{groupName || '群聊'}</Text>
|
||||
<Text variant="h3" style={styles.groupName} numberOfLines={1}>
|
||||
{groupName || '群聊'}
|
||||
</Text>
|
||||
<View style={styles.groupMeta}>
|
||||
<MaterialCommunityIcons name="account-group" size={16} color={colors.text.secondary} />
|
||||
<Text variant="caption" color={colors.text.secondary}>
|
||||
@@ -63,93 +140,34 @@ export const DecisionFooter: React.FC<DecisionFooterProps> = ({
|
||||
onApprove,
|
||||
processedText = '该请求已处理',
|
||||
}) => {
|
||||
const colors = useAppColors();
|
||||
const styles = useMemo(() => createGroupRequestSharedStyles(colors), [colors]);
|
||||
const insets = useSafeAreaInsets();
|
||||
if (canAction) {
|
||||
return (
|
||||
<View style={[styles.footer, { paddingBottom: Math.max(insets.bottom, spacing.sm) }]}>
|
||||
<TouchableOpacity style={[styles.btn, styles.reject]} onPress={onReject} disabled={submitting}>
|
||||
<Text variant="body" color={colors.error.main}>拒绝</Text>
|
||||
<Text variant="body" color={colors.error.main}>
|
||||
拒绝
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={[styles.btn, styles.approve]} onPress={onApprove} disabled={submitting}>
|
||||
{submitting ? <ActivityIndicator color="#fff" /> : <Text variant="body" color="#fff">同意</Text>}
|
||||
{submitting ? (
|
||||
<ActivityIndicator color={colors.text.inverse} />
|
||||
) : (
|
||||
<Text variant="body" color={colors.text.inverse}>
|
||||
同意
|
||||
</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<View style={[styles.statusBox, { paddingBottom: Math.max(insets.bottom, spacing.sm) }]}>
|
||||
<Text variant="caption" color={colors.text.secondary}>{processedText}</Text>
|
||||
<Text variant="caption" color={colors.text.secondary}>
|
||||
{processedText}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
headerCard: {
|
||||
backgroundColor: colors.background.paper,
|
||||
borderRadius: borderRadius.lg,
|
||||
padding: spacing.lg,
|
||||
marginBottom: spacing.md,
|
||||
...shadows.sm,
|
||||
},
|
||||
groupHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
groupInfo: {
|
||||
marginLeft: spacing.lg,
|
||||
flex: 1,
|
||||
},
|
||||
groupName: {
|
||||
marginBottom: spacing.xs,
|
||||
fontWeight: '700',
|
||||
},
|
||||
groupMeta: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: spacing.xs,
|
||||
},
|
||||
groupNoText: {
|
||||
marginTop: spacing.xs,
|
||||
},
|
||||
descriptionContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
backgroundColor: colors.background.default,
|
||||
borderRadius: borderRadius.lg,
|
||||
padding: spacing.md,
|
||||
marginTop: spacing.md,
|
||||
},
|
||||
groupDesc: {
|
||||
marginLeft: spacing.sm,
|
||||
flex: 1,
|
||||
lineHeight: 20,
|
||||
},
|
||||
footer: {
|
||||
paddingHorizontal: spacing.lg,
|
||||
paddingTop: spacing.sm,
|
||||
flexDirection: 'row',
|
||||
backgroundColor: colors.background.default,
|
||||
},
|
||||
btn: {
|
||||
flex: 1,
|
||||
height: 42,
|
||||
borderRadius: borderRadius.md,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
reject: {
|
||||
marginRight: spacing.sm,
|
||||
backgroundColor: colors.error.light + '25',
|
||||
borderWidth: 1,
|
||||
borderColor: colors.error.light,
|
||||
},
|
||||
approve: {
|
||||
marginLeft: spacing.sm,
|
||||
backgroundColor: colors.primary.main,
|
||||
},
|
||||
statusBox: {
|
||||
alignItems: 'center',
|
||||
paddingTop: spacing.sm,
|
||||
backgroundColor: colors.background.default,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
} from 'react-native';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
|
||||
import { colors, spacing, fontSizes, borderRadius } from '../../../theme';
|
||||
import { spacing, fontSizes, borderRadius, useAppColors, type AppColors } from '../../../theme';
|
||||
import { authService } from '../../../services/authService';
|
||||
import { useAuthStore } from '../../../stores';
|
||||
import { Avatar, EmptyState, Loading, Text } from '../../../components/common';
|
||||
@@ -41,6 +41,8 @@ const MutualFollowSelectorModal: React.FC<MutualFollowSelectorModalProps> = ({
|
||||
onClose,
|
||||
onConfirm,
|
||||
}) => {
|
||||
const colors = useAppColors();
|
||||
const styles = useMemo(() => createMutualFollowSelectorStyles(colors), [colors]);
|
||||
const { currentUser } = useAuthStore();
|
||||
|
||||
const [friendList, setFriendList] = useState<User[]>([]);
|
||||
@@ -209,93 +211,96 @@ const MutualFollowSelectorModal: React.FC<MutualFollowSelectorModalProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
modalOverlay: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
modalContent: {
|
||||
backgroundColor: colors.background.paper,
|
||||
borderTopLeftRadius: borderRadius['2xl'],
|
||||
borderTopRightRadius: borderRadius['2xl'],
|
||||
height: '80%',
|
||||
paddingHorizontal: spacing.lg,
|
||||
paddingTop: spacing.lg,
|
||||
paddingBottom: spacing.xl,
|
||||
},
|
||||
modalHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: spacing.lg,
|
||||
},
|
||||
modalHeaderButton: {
|
||||
paddingVertical: spacing.sm,
|
||||
minWidth: 60,
|
||||
},
|
||||
modalTitle: {
|
||||
fontWeight: '700',
|
||||
fontSize: fontSizes.xl,
|
||||
},
|
||||
confirmButton: {
|
||||
fontWeight: '600',
|
||||
textAlign: 'right',
|
||||
},
|
||||
tipContainer: {
|
||||
flexDirection: 'row',
|
||||
backgroundColor: colors.background.default,
|
||||
borderRadius: borderRadius.lg,
|
||||
padding: spacing.xs,
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
tipText: {
|
||||
fontWeight: '600',
|
||||
},
|
||||
selectedBadge: {
|
||||
backgroundColor: colors.primary.light + '20',
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.xs,
|
||||
borderRadius: borderRadius.sm,
|
||||
alignSelf: 'flex-start',
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
friendListContent: {
|
||||
paddingBottom: spacing.xl,
|
||||
},
|
||||
friendItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingVertical: spacing.md,
|
||||
paddingHorizontal: spacing.sm,
|
||||
borderRadius: borderRadius.lg,
|
||||
marginBottom: spacing.xs,
|
||||
},
|
||||
friendItemSelected: {
|
||||
backgroundColor: colors.primary.light + '15',
|
||||
},
|
||||
friendInfo: {
|
||||
flex: 1,
|
||||
marginLeft: spacing.md,
|
||||
marginRight: spacing.sm,
|
||||
},
|
||||
nickname: {
|
||||
fontWeight: '500',
|
||||
marginBottom: 2,
|
||||
},
|
||||
checkbox: {
|
||||
width: 26,
|
||||
height: 26,
|
||||
borderRadius: 13,
|
||||
borderWidth: 2,
|
||||
borderColor: colors.divider,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
checkboxSelected: {
|
||||
backgroundColor: colors.primary.main,
|
||||
borderColor: colors.primary.main,
|
||||
},
|
||||
});
|
||||
function createMutualFollowSelectorStyles(colors: AppColors) {
|
||||
return StyleSheet.create({
|
||||
modalOverlay: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
modalContent: {
|
||||
backgroundColor: colors.background.paper,
|
||||
borderTopLeftRadius: borderRadius['2xl'],
|
||||
borderTopRightRadius: borderRadius['2xl'],
|
||||
height: '80%',
|
||||
paddingHorizontal: spacing.lg,
|
||||
paddingTop: spacing.lg,
|
||||
paddingBottom: spacing.xl,
|
||||
},
|
||||
modalHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: spacing.lg,
|
||||
},
|
||||
modalHeaderButton: {
|
||||
paddingVertical: spacing.sm,
|
||||
minWidth: 60,
|
||||
},
|
||||
modalTitle: {
|
||||
fontWeight: '700',
|
||||
fontSize: fontSizes.xl,
|
||||
color: colors.text.primary,
|
||||
},
|
||||
confirmButton: {
|
||||
fontWeight: '600',
|
||||
textAlign: 'right',
|
||||
},
|
||||
tipContainer: {
|
||||
flexDirection: 'row',
|
||||
backgroundColor: colors.background.default,
|
||||
borderRadius: borderRadius.lg,
|
||||
padding: spacing.xs,
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
tipText: {
|
||||
fontWeight: '600',
|
||||
},
|
||||
selectedBadge: {
|
||||
backgroundColor: colors.primary.light + '20',
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.xs,
|
||||
borderRadius: borderRadius.sm,
|
||||
alignSelf: 'flex-start',
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
friendListContent: {
|
||||
paddingBottom: spacing.xl,
|
||||
},
|
||||
friendItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingVertical: spacing.md,
|
||||
paddingHorizontal: spacing.sm,
|
||||
borderRadius: borderRadius.lg,
|
||||
marginBottom: spacing.xs,
|
||||
},
|
||||
friendItemSelected: {
|
||||
backgroundColor: colors.primary.light + '15',
|
||||
},
|
||||
friendInfo: {
|
||||
flex: 1,
|
||||
marginLeft: spacing.md,
|
||||
marginRight: spacing.sm,
|
||||
},
|
||||
nickname: {
|
||||
fontWeight: '500',
|
||||
marginBottom: 2,
|
||||
},
|
||||
checkbox: {
|
||||
width: 26,
|
||||
height: 26,
|
||||
borderRadius: 13,
|
||||
borderWidth: 2,
|
||||
borderColor: colors.divider,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
checkboxSelected: {
|
||||
backgroundColor: colors.primary.main,
|
||||
borderColor: colors.primary.main,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export default MutualFollowSelectorModal;
|
||||
|
||||
Reference in New Issue
Block a user