From 405cd271dbf52b7ac3ad4f07deaa0b6ddd7feb65 Mon Sep 17 00:00:00 2001 From: lafay <2021211506@stu.hit.edu.cn> Date: Thu, 26 Mar 2026 01:25:42 +0800 Subject: [PATCH] feat(Comment): enhance like functionality for comments and replies - Updated CommentItem component to accept the comment object in the onLike callback, allowing for more detailed like handling. - Added a like button for replies, improving user interaction with nested comments. - Refactored handleLikeComment function in PostDetailScreen for optimized state management and error handling during like operations. - Enhanced SettingsScreen to provide debug information for theme preferences, improving user experience in theme selection. - Updated themeStore to dynamically manage system theme changes and improve overall theme handling. --- app.json | 1 + plugins/withMainActivityConfigChange.ts | 77 +++++++++++++++++++++++++ src/components/business/CommentItem.tsx | 23 +++++++- src/screens/home/PostDetailScreen.tsx | 67 +++++++++------------ src/screens/profile/SettingsScreen.tsx | 51 ++++++++++------ src/stores/themeStore.ts | 53 ++++++++++++++--- 6 files changed, 202 insertions(+), 70 deletions(-) create mode 100644 plugins/withMainActivityConfigChange.ts diff --git a/app.json b/app.json index 0037e51..a4a4974 100644 --- a/app.json +++ b/app.json @@ -59,6 +59,7 @@ "favicon": "./assets/favicon.png" }, "plugins": [ + "./plugins/withMainActivityConfigChange", [ "expo-router", { diff --git a/plugins/withMainActivityConfigChange.ts b/plugins/withMainActivityConfigChange.ts new file mode 100644 index 0000000..4a8f47e --- /dev/null +++ b/plugins/withMainActivityConfigChange.ts @@ -0,0 +1,77 @@ +import { + ConfigPlugin, + withMainActivity, + AndroidConfig, +} from '@expo/config-plugins'; + +/** + * 在 MainActivity 中添加 onConfigurationChanged 方法 + * 用于监听系统深色模式变化并通知 React Native + */ +const withMainActivityConfigChange: ConfigPlugin = (config) => { + return withMainActivity(config, async (config) => { + const { contents } = config.modResults; + + // 检查是否已经添加了 onConfigurationChanged + if (contents.includes('onConfigurationChanged')) { + return config; + } + + // 需要添加的 imports + const importsToAdd = `import android.content.Intent +import android.content.res.Configuration`; + + // 需要添加的方法 + const methodToAdd = ` + /** + * 监听系统配置变化(包括深色模式切换),并广播给 React Native + * 这对于 Appearance API 正确检测系统主题至关重要 + */ + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + val intent = Intent("onConfigurationChanged") + intent.putExtra("newConfig", newConfig) + sendBroadcast(intent) + } +`; + + let newContents = contents; + + // 添加 imports(在已有 imports 后面) + if (!newContents.includes('import android.content.Intent')) { + // 找到最后一个 import 语句 + const importRegex = /import [^\n]+\n/g; + const imports = newContents.match(importRegex); + if (imports && imports.length > 0) { + const lastImport = imports[imports.length - 1]; + const lastImportIndex = newContents.lastIndexOf(lastImport); + const insertPosition = lastImportIndex + lastImport.length; + newContents = + newContents.slice(0, insertPosition) + + importsToAdd + + '\n' + + newContents.slice(insertPosition); + } + } + + // 在类体的开头添加 onConfigurationChanged 方法 + // 找到类定义后的第一个方法或属性 + const classBodyRegex = /class MainActivity\s*:\s*ReactActivity\s*\(\)\s*\{([\s\S]*)/; + const match = newContents.match(classBodyRegex); + if (match) { + // 找到 onCreate 方法之前插入 + const onCreateIndex = newContents.indexOf('override fun onCreate'); + if (onCreateIndex !== -1) { + newContents = + newContents.slice(0, onCreateIndex) + + methodToAdd + + newContents.slice(onCreateIndex); + } + } + + config.modResults.contents = newContents; + return config; + }); +}; + +export default withMainActivityConfigChange; diff --git a/src/components/business/CommentItem.tsx b/src/components/business/CommentItem.tsx index 7e53802..305eabe 100644 --- a/src/components/business/CommentItem.tsx +++ b/src/components/business/CommentItem.tsx @@ -18,7 +18,7 @@ interface CommentItemProps { comment: Comment; onUserPress: () => void; onReply: () => void; - onLike: () => void; + onLike: (comment: Comment) => void; // 点赞回调,传入评论对象 floorNumber?: number; // 楼层号 isAuthor?: boolean; // 是否是楼主 replyToUser?: string; // 回复给哪位用户 @@ -504,6 +504,25 @@ const CommentItem: React.FC = ({ 回复 + {/* 点赞按钮 */} + onLike?.(reply)} + activeOpacity={0.7} + > + + + {reply.likes_count > 0 ? formatNumber(reply.likes_count) : '赞'} + + {/* 删除按钮 - 子评论作者可见 */} {isSubReplyAuthor && ( = ({ {/* 操作按钮 - 更紧凑 */} {/* 点赞 */} - + onLike(comment)}> { } }; - // 点赞评论(包括回复) + // 点赞评论(包括回复)- 优化版 const handleLikeComment = async (comment: Comment) => { - // 先保存旧状态用于回滚 - const oldIsLiked = comment.is_liked; - const oldLikesCount = comment.likes_count; + const { id, is_liked, likes_count } = comment; - // 乐观更新本地状态 - 辅助函数用于更新评论或回复 - const updateCommentLike = (c: Comment): Comment => { - // 如果是目标评论/回复 - if (c.id === comment.id) { - return { - ...c, - is_liked: !c.is_liked, - likes_count: c.is_liked ? c.likes_count - 1 : c.likes_count + 1 - }; + // 乐观更新:切换点赞状态 + const newIsLiked = !is_liked; + const newLikesCount = newIsLiked ? likes_count + 1 : Math.max(0, likes_count - 1); + + // 递归更新评论树中的点赞状态 + const updateLikeInTree = (c: Comment): Comment => { + if (c.id === id) { + return { ...c, is_liked: newIsLiked, likes_count: newLikesCount }; } - // 如果有回复,递归查找 - if (c.replies && c.replies.length > 0) { - return { - ...c, - replies: c.replies.map(r => updateCommentLike(r)) - }; + if (c.replies?.length) { + return { ...c, replies: c.replies.map(r => updateLikeInTree(r)) }; } return c; }; - // 更新本地状态 - setComments(prev => prev.map(c => updateCommentLike(c))); + setComments(prev => prev.map(c => updateLikeInTree(c))); try { - if (oldIsLiked) { - await commentService.unlikeComment(comment.id); - } else { - await commentService.likeComment(comment.id); + const success = newIsLiked + ? await commentService.likeComment(id) + : await commentService.unlikeComment(id); + + if (!success) { + throw new Error('点赞操作失败'); } } catch (error) { console.error('评论点赞操作失败:', error); - // 失败时回滚状态 - const rollbackCommentLike = (c: Comment): Comment => { - if (c.id === comment.id) { - return { - ...c, - is_liked: oldIsLiked, - likes_count: oldLikesCount - }; + // 回滚状态 + const rollbackLikeInTree = (c: Comment): Comment => { + if (c.id === id) { + return { ...c, is_liked, likes_count }; } - if (c.replies && c.replies.length > 0) { - return { - ...c, - replies: c.replies.map(r => rollbackCommentLike(r)) - }; + if (c.replies?.length) { + return { ...c, replies: c.replies.map(r => rollbackLikeInTree(r)) }; } return c; }; - setComments(prev => prev.map(c => rollbackCommentLike(c))); + setComments(prev => prev.map(c => rollbackLikeInTree(c))); } }; @@ -1373,7 +1360,7 @@ export const PostDetailScreen: React.FC = () => { return ( handleLikeComment(item)} + onLike={handleLikeComment} onReply={() => handleReply(item)} onUserPress={() => authorId && handleUserPress(authorId)} floorNumber={index + 1} diff --git a/src/screens/profile/SettingsScreen.tsx b/src/screens/profile/SettingsScreen.tsx index d8e3e92..0ba6413 100644 --- a/src/screens/profile/SettingsScreen.tsx +++ b/src/screens/profile/SettingsScreen.tsx @@ -23,6 +23,7 @@ import { borderRadius, useThemePreference, useSetThemePreference, + useThemeStore, type AppColors, } from '../../theme'; import { useAuthStore } from '../../stores'; @@ -204,29 +205,41 @@ export const SettingsScreen: React.FC = () => { const handleItemPress = (key: string) => { switch (key) { - case 'theme': - Alert.alert('主题模式', '选择应用界面明暗', [ - { text: '取消', style: 'cancel' }, - { - text: '跟随系统', - onPress: () => { - void setThemePreference('system'); + case 'theme': { + // 获取调试信息 + const { Appearance } = require('react-native'); + const systemScheme = Appearance?.getColorScheme?.() || 'undefined'; + const resolvedScheme = useThemeStore.getState().resolvedScheme; + const storedPref = useThemeStore.getState().preference; + const storedSystem = useThemeStore.getState().systemScheme; + + Alert.alert( + '主题模式', + `选择应用界面明暗\n\n[调试信息]\n系统主题: ${systemScheme}\n存储偏好: ${storedPref}\n存储系统: ${storedSystem}\n实际应用: ${resolvedScheme}`, + [ + { text: '取消', style: 'cancel' }, + { + text: '跟随系统', + onPress: () => { + void setThemePreference('system'); + }, }, - }, - { - text: '浅色', - onPress: () => { - void setThemePreference('light'); + { + text: '浅色', + onPress: () => { + void setThemePreference('light'); + }, }, - }, - { - text: '深色', - onPress: () => { - void setThemePreference('dark'); + { + text: '深色', + onPress: () => { + void setThemePreference('dark'); + }, }, - }, - ]); + ] + ); break; + } case 'edit_profile': router.push(hrefs.hrefProfileEdit()); break; diff --git a/src/stores/themeStore.ts b/src/stores/themeStore.ts index f0126f3..2064bae 100644 --- a/src/stores/themeStore.ts +++ b/src/stores/themeStore.ts @@ -1,7 +1,7 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; -import { useColorScheme } from 'react-native'; +import { Appearance, AppState, AppStateStatus } from 'react-native'; import { create } from 'zustand'; -import { useEffect } from 'react'; +import { useEffect, useRef } from 'react'; import { lightColors, darkColors, type AppColors } from '../theme/palettes'; import { buildPaperTheme } from '../theme/paperTheme'; import type { MD3Theme } from 'react-native-paper'; @@ -46,13 +46,19 @@ type ThemeState = { hydrate: () => Promise; }; -const initialSystem: ResolvedScheme = 'light'; +/** 获取系统主题 */ +function getSystemScheme(): ResolvedScheme { + const scheme = Appearance.getColorScheme(); + return scheme === 'dark' ? 'dark' : 'light'; +} + +const initialSystemScheme = getSystemScheme(); export const useThemeStore = create((set, get) => ({ preference: 'system', - systemScheme: initialSystem, + systemScheme: initialSystemScheme, hydrated: false, - ...buildState('system', initialSystem), + ...buildState('system', initialSystemScheme), setSystemScheme: (systemScheme) => { const { preference, systemScheme: cur } = get(); @@ -86,10 +92,11 @@ export const useThemeStore = create((set, get) => ({ } catch { /* ignore */ } - const { systemScheme } = get(); + const systemScheme = getSystemScheme(); set({ preference, hydrated: true, + systemScheme, ...buildState(preference, systemScheme), }); }, @@ -97,17 +104,42 @@ export const useThemeStore = create((set, get) => ({ /** 在根布局中同步系统配色并恢复持久化偏好 */ export function ThemeBootstrap() { - const system = useColorScheme(); const setSystemScheme = useThemeStore((s) => s.setSystemScheme); const hydrate = useThemeStore((s) => s.hydrate); + const appState = useRef(AppState.currentState); useEffect(() => { void hydrate(); }, [hydrate]); + // 监听系统主题变化 useEffect(() => { - setSystemScheme(system === 'dark' ? 'dark' : 'light'); - }, [system, setSystemScheme]); + const subscription = Appearance.addChangeListener(({ colorScheme }) => { + const newScheme = colorScheme === 'dark' ? 'dark' : 'light'; + setSystemScheme(newScheme); + }); + + return () => { + subscription.remove(); + }; + }, [setSystemScheme]); + + // 监听应用状态变化,从后台恢复时重新检测系统主题 + useEffect(() => { + const subscription = AppState.addEventListener('change', (nextAppState) => { + if ( + appState.current.match(/inactive|background/) && + nextAppState === 'active' + ) { + setSystemScheme(getSystemScheme()); + } + appState.current = nextAppState; + }); + + return () => { + subscription.remove(); + }; + }, [setSystemScheme]); return null; } @@ -131,3 +163,6 @@ export function useSetThemePreference() { export function usePaperThemeFromStore() { return useThemeStore((s) => s.paperTheme); } + +// 导出调试函数 +export { getSystemScheme }; \ No newline at end of file