diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index f5fb94d..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -# 默认忽略的文件 -/shelf/ -/workspace.xml -# 基于编辑器的 HTTP 客户端请求 -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml -# Screenshots -screenshots/ diff --git a/.idea/frontend.iml b/.idea/frontend.iml deleted file mode 100644 index d6ebd48..0000000 --- a/.idea/frontend.iml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml deleted file mode 100644 index 6752ffb..0000000 --- a/.idea/gradle.xml +++ /dev/null @@ -1,448 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index ade1ecd..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index f3d93d7..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1dd..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/_layout.tsx b/app/_layout.tsx index caa3c29..b1d55b9 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -53,24 +53,33 @@ if (Platform.OS === 'web' && typeof document !== 'undefined') { } *:focus { outline: none !important; } *:focus-visible { outline: none !important; } - /* 修复移动端 FlatList/ScrollView 滑动问题 */ + /* 修复移动端滑动问题 - 仅对根容器限制 */ html, body { overscroll-behavior: none; - touch-action: pan-y; + touch-action: manipulation; + -webkit-overflow-scrolling: touch; } - /* React Native Web 生成的滚动容器 */ - [class*="css-"] { - touch-action: pan-y !important; - -webkit-overflow-scrolling: touch !important; + /* React Native Web 生成的滚动容器 - 允许双向滑动 */ + [class*="css-view"] { + touch-action: auto; + -webkit-overflow-scrolling: touch; } - /* 确保所有可滚动元素都可以垂直滑动 */ - div[style*="overflow"] { - touch-action: pan-y !important; - -webkit-overflow-scrolling: touch !important; + /* 水平滚动容器 */ + [data-horizontal-scroll="true"], + div[style*="overflow-x"], + div[style*="overflow: scroll"], + div[style*="overflow: auto"] { + touch-action: pan-x pan-y; + -webkit-overflow-scrolling: touch; } - /* 禁用水平滑动,只允许垂直滑动 */ - * { - touch-action: pan-y pinch-zoom; + /* 手势区域 - 允许滑动手势 */ + [data-gesture-area="true"] { + touch-action: pan-x pan-y; + } + /* 图片查看器手势区域 */ + .react-native-image-viewer, + [data-image-viewer="true"] { + touch-action: pinch-zoom pan-x pan-y; } `; document.head.appendChild(style); diff --git a/src/components/common/ImageGallery.tsx b/src/components/common/ImageGallery.tsx index 5bac549..18e06d8 100644 --- a/src/components/common/ImageGallery.tsx +++ b/src/components/common/ImageGallery.tsx @@ -361,7 +361,7 @@ export const ImageGallery: React.FC = ({ onRequestClose={requestClose} statusBarTranslucent > - + {/* 顶部控制栏 */} {showControls && ( diff --git a/src/screens/home/HomeScreen.tsx b/src/screens/home/HomeScreen.tsx index 4d2e7a7..8af9968 100644 --- a/src/screens/home/HomeScreen.tsx +++ b/src/screens/home/HomeScreen.tsx @@ -676,6 +676,7 @@ export const HomeScreen: React.FC = () => { ref={capsuleHScrollRef} horizontal showsHorizontalScrollIndicator={false} + nestedScrollEnabled contentContainerStyle={styles.capsuleList} onScroll={onCapsuleHorizontalScroll} scrollEventThrottle={16} diff --git a/src/screens/home/PostDetailScreen.tsx b/src/screens/home/PostDetailScreen.tsx index 6438ba1..ded82b6 100644 --- a/src/screens/home/PostDetailScreen.tsx +++ b/src/screens/home/PostDetailScreen.tsx @@ -1182,7 +1182,7 @@ export const PostDetailScreen: React.FC = () => { )} - {currentUser?.id === post.author?.id && ( + {!!currentUser && currentUser.id === post.author?.id && ( { )} - {/* 举报按钮 - 只对非帖子作者显示 */} - {currentUser?.id !== post.author?.id && ( + {/* 举报按钮 - 只对已登录且非帖子作者显示 */} + {!!currentUser && currentUser.id !== post.author?.id && ( { diff --git a/src/screens/message/ChatScreen.tsx b/src/screens/message/ChatScreen.tsx index ee7a732..cf7dfcb 100644 --- a/src/screens/message/ChatScreen.tsx +++ b/src/screens/message/ChatScreen.tsx @@ -415,7 +415,7 @@ export const ChatScreen: React.FC = (props) => { return ( diff --git a/src/screens/message/NotificationsScreen.tsx b/src/screens/message/NotificationsScreen.tsx index 7eb4944..d8c6175 100644 --- a/src/screens/message/NotificationsScreen.tsx +++ b/src/screens/message/NotificationsScreen.tsx @@ -23,6 +23,7 @@ import { BackHandler, Animated, StatusBar, + ScrollView, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useIsFocused } from '@react-navigation/native'; @@ -444,6 +445,11 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack {renderHeader()} {/* 分类筛选 */} + {MESSAGE_TYPES.map(type => { const count = type.key === 'all' ? displayMessages.length @@ -469,9 +475,7 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack activeOpacity={0.8} > {type.title} @@ -479,7 +483,7 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack {count} @@ -488,6 +492,7 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack ); })} + {/* 消息列表 */} @@ -526,6 +531,11 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack {renderHeader()} {/* 分类筛选 */} + {MESSAGE_TYPES.map(type => { const count = type.key === 'all' ? displayMessages.length @@ -550,9 +560,7 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack activeOpacity={0.8} > {type.title} @@ -560,7 +568,7 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack {count} @@ -569,6 +577,7 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack ); })} + {/* 消息列表 */} @@ -677,52 +686,51 @@ function createNotificationsStyles(colors: AppColors) { }, // 分类筛选 - 扁平化风格(分段式设计) filterContainer: { - flexDirection: 'row', - paddingHorizontal: 16, - paddingVertical: 12, backgroundColor: colors.background.paper, borderBottomWidth: 1, borderBottomColor: colors.divider || '#E5E5EA', }, filterContainerWideWeb: { - paddingHorizontal: 32, - paddingVertical: 16, - justifyContent: 'center', + alignItems: 'center', + }, + filterList: { + paddingVertical: spacing.md, + gap: spacing.xs, + paddingLeft: spacing.lg, + paddingRight: spacing.lg, }, filterTag: { flexDirection: 'row', alignItems: 'center', - paddingHorizontal: 14, - paddingVertical: 8, - marginRight: 8, - borderRadius: 14, - backgroundColor: colors.background.default, - borderWidth: 1, - borderColor: 'transparent', + paddingHorizontal: spacing.md, + paddingVertical: spacing.xs, + borderRadius: borderRadius.full, }, filterTagWide: { - paddingHorizontal: 18, - paddingVertical: 10, - marginRight: 10, + paddingHorizontal: spacing.lg, + paddingVertical: spacing.sm, }, filterTagActive: { - backgroundColor: colors.primary.main, - borderColor: colors.primary.main, + }, + filterTagText: { + fontSize: 13, + fontWeight: '600', + color: colors.text.secondary, }, filterTagTextActive: { - fontWeight: '600', + color: colors.primary.main, }, filterCount: { - marginLeft: 6, - backgroundColor: colors.primary.light + '40', - paddingHorizontal: 6, + marginLeft: spacing.xs, + backgroundColor: colors.background.default, + paddingHorizontal: spacing.xs, paddingVertical: 1, - borderRadius: 8, + borderRadius: borderRadius.full, minWidth: 18, alignItems: 'center', }, filterCountActive: { - backgroundColor: 'rgba(255, 255, 255, 0.25)', + backgroundColor: colors.primary.light + '30', }, listContent: { flexGrow: 1, diff --git a/src/screens/message/components/ChatScreen/SwipeableMessageBubble.tsx b/src/screens/message/components/ChatScreen/SwipeableMessageBubble.tsx index 6d47c97..137ead3 100644 --- a/src/screens/message/components/ChatScreen/SwipeableMessageBubble.tsx +++ b/src/screens/message/components/ChatScreen/SwipeableMessageBubble.tsx @@ -4,13 +4,12 @@ */ import React, { useRef, useCallback } from 'react'; -import { View, StyleSheet, Animated } from 'react-native'; +import { View, StyleSheet, Animated, Platform } from 'react-native'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import { PanGestureHandler, State } from 'react-native-gesture-handler'; import * as Haptics from 'expo-haptics'; import { useAppColors } from '../../../../theme'; -// 滑动阈值 const SWIPE_THRESHOLD = 30; const MAX_SWIPE_DISTANCE = 50; @@ -31,45 +30,39 @@ export const SwipeableMessageBubble: React.FC = ({ const translateX = useRef(new Animated.Value(0)).current; const hasTriggeredRef = useRef(false); - // 处理手势事件 - 使用原生驱动,不干预值 const onGestureEvent = Animated.event( [{ nativeEvent: { translationX: translateX } }], { useNativeDriver: true } ); - // 处理手势状态变化 const onHandlerStateChange = useCallback((event: any) => { const { nativeEvent } = event; if (nativeEvent.state === State.END) { const translationX = nativeEvent.translationX; - // 只允许特定方向的滑动 const shouldTrigger = isMe - ? translationX < -SWIPE_THRESHOLD // 自己消息只能向左滑 - : translationX > SWIPE_THRESHOLD; // 对方消息只能向右滑 + ? translationX < -SWIPE_THRESHOLD + : translationX > SWIPE_THRESHOLD; if (shouldTrigger && !hasTriggeredRef.current) { hasTriggeredRef.current = true; - // 触觉反馈 - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + if (Platform.OS !== 'web') { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + } - // 立即触发回复 onReply(); - // 重置标记 setTimeout(() => { hasTriggeredRef.current = false; }, 300); } - // 立即回到原位 translateX.setValue(0); } }, [isMe, onReply]); - // 计算回复图标的透明度 - 只在允许的方向显示 const iconOpacity = translateX.interpolate({ inputRange: isMe ? [-MAX_SWIPE_DISTANCE, -SWIPE_THRESHOLD, 0, 10] @@ -78,7 +71,6 @@ export const SwipeableMessageBubble: React.FC = ({ extrapolate: 'clamp', }); - // 计算消息的位移 - 限制在允许的方向 const clampedTranslateX = translateX.interpolate({ inputRange: isMe ? [-MAX_SWIPE_DISTANCE - 10, -MAX_SWIPE_DISTANCE, 0, 10] @@ -89,21 +81,117 @@ export const SwipeableMessageBubble: React.FC = ({ extrapolate: 'clamp', }); - // 如果禁用手势,直接返回子元素 + const webTouchStartRef = useRef<{ x: number; time: number } | null>(null); + const webTranslateXRef = useRef(new Animated.Value(0)).current; + const webIconOpacity = webTranslateXRef.interpolate({ + inputRange: isMe + ? [-MAX_SWIPE_DISTANCE, -SWIPE_THRESHOLD, 0, 10] + : [-10, 0, SWIPE_THRESHOLD, MAX_SWIPE_DISTANCE], + outputRange: isMe ? [1, 0.8, 0, 0] : [0, 0, 0.8, 1], + extrapolate: 'clamp', + }); + const webClampedTranslateX = webTranslateXRef.interpolate({ + inputRange: isMe + ? [-MAX_SWIPE_DISTANCE - 10, -MAX_SWIPE_DISTANCE, 0, 10] + : [-10, 0, MAX_SWIPE_DISTANCE, MAX_SWIPE_DISTANCE + 10], + outputRange: isMe + ? [-MAX_SWIPE_DISTANCE, -MAX_SWIPE_DISTANCE, 0, 0] + : [0, 0, MAX_SWIPE_DISTANCE, MAX_SWIPE_DISTANCE], + extrapolate: 'clamp', + }); + const webLastTouchRef = useRef<{ x: number } | null>(null); + + const handleWebTouchStart = useCallback((e: any) => { + const touch = e.nativeEvent?.touches?.[0] || e.nativeEvent; + if (touch && touch.pageX !== undefined) { + webTouchStartRef.current = { x: touch.pageX, time: Date.now() }; + webLastTouchRef.current = { x: touch.pageX }; + } + }, []); + + const handleWebTouchMove = useCallback((e: any) => { + const start = webTouchStartRef.current; + if (!start) return; + const touch = e.nativeEvent?.touches?.[0] || e.nativeEvent; + if (!touch || touch.pageX === undefined) return; + const dx = touch.pageX - start.x; + const allowed = isMe ? dx < 5 : dx > -5; + if (allowed) { + webTranslateXRef.setValue(dx); + webLastTouchRef.current = { x: touch.pageX }; + } + }, [isMe]); + + const handleWebTouchEnd = useCallback((e: any) => { + const start = webTouchStartRef.current; + if (!start) return; + webTouchStartRef.current = null; + const touch = e.nativeEvent?.changedTouches?.[0] || e.nativeEvent; + if (!touch || touch.pageX === undefined) { + Animated.spring(webTranslateXRef, { toValue: 0, useNativeDriver: true }).start(); + return; + } + const dx = touch.pageX - start.x; + const shouldTrigger = isMe + ? dx < -SWIPE_THRESHOLD + : dx > SWIPE_THRESHOLD; + + if (shouldTrigger && !hasTriggeredRef.current) { + hasTriggeredRef.current = true; + onReply(); + setTimeout(() => { + hasTriggeredRef.current = false; + }, 300); + } + + Animated.spring(webTranslateXRef, { toValue: 0, useNativeDriver: true }).start(); + }, [isMe, onReply]); + if (!enabled) { return <>{children}; } + if (Platform.OS === 'web') { + return ( + + + + + + + {children} + + + + ); + } + return ( - {/* 回复图标 - 在消息后面 */} = ({ color={colors.primary.main} /> - - {/* 可滑动的消息内容 */} { {/* 用户名和简介 */} - {nickname || currentUser?.nickname} + {nickname || currentUser?.nickname || ''} - @{currentUser?.username} + @{currentUser?.username || ''} {bio ? ( @@ -624,7 +624,7 @@ export const EditProfileScreen: React.FC = () => { - {website.replace(/^https?:\/\//, '')} + {typeof website === 'string' ? website.replace(/^https?:\/\//, '') : ''} ) : ( @@ -638,7 +638,7 @@ export const EditProfileScreen: React.FC = () => { - 加入于 {new Date(currentUser?.created_at || Date.now()).getFullYear()}年 + {`加入于 ${new Date(currentUser?.created_at || Date.now()).getFullYear()}年`} diff --git a/src/screens/profile/FollowListScreen.tsx b/src/screens/profile/FollowListScreen.tsx index 17c2ce2..72091ab 100644 --- a/src/screens/profile/FollowListScreen.tsx +++ b/src/screens/profile/FollowListScreen.tsx @@ -186,7 +186,7 @@ const FollowListScreen: React.FC = () => { ) : null} - {!isSelf && ( + {isCurrentUser && !isSelf && (