feat(Comment): enhance like functionality for comments and replies
Some checks failed
Frontend CI / build-and-push-web (push) Failing after 1m54s
Frontend CI / ota-android (push) Failing after 5m36s
Frontend CI / build-android-apk (push) Failing after 9m3s

- 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.
This commit is contained in:
lafay
2026-03-26 01:25:42 +08:00
parent 9529ea39c4
commit 405cd271db
6 changed files with 202 additions and 70 deletions

View File

@@ -59,6 +59,7 @@
"favicon": "./assets/favicon.png" "favicon": "./assets/favicon.png"
}, },
"plugins": [ "plugins": [
"./plugins/withMainActivityConfigChange",
[ [
"expo-router", "expo-router",
{ {

View File

@@ -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;

View File

@@ -18,7 +18,7 @@ interface CommentItemProps {
comment: Comment; comment: Comment;
onUserPress: () => void; onUserPress: () => void;
onReply: () => void; onReply: () => void;
onLike: () => void; onLike: (comment: Comment) => void; // 点赞回调,传入评论对象
floorNumber?: number; // 楼层号 floorNumber?: number; // 楼层号
isAuthor?: boolean; // 是否是楼主 isAuthor?: boolean; // 是否是楼主
replyToUser?: string; // 回复给哪位用户 replyToUser?: string; // 回复给哪位用户
@@ -504,6 +504,25 @@ const CommentItem: React.FC<CommentItemProps> = ({
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
{/* 点赞按钮 */}
<TouchableOpacity
style={styles.subActionButton}
onPress={() => onLike?.(reply)}
activeOpacity={0.7}
>
<MaterialCommunityIcons
name={reply.is_liked ? 'heart' : 'heart-outline'}
size={12}
color={reply.is_liked ? colors.error.main : colors.text.hint}
/>
<Text
variant="caption"
color={reply.is_liked ? colors.error.main : colors.text.hint}
style={styles.subActionText}
>
{reply.likes_count > 0 ? formatNumber(reply.likes_count) : '赞'}
</Text>
</TouchableOpacity>
{/* 删除按钮 - 子评论作者可见 */} {/* 删除按钮 - 子评论作者可见 */}
{isSubReplyAuthor && ( {isSubReplyAuthor && (
<TouchableOpacity <TouchableOpacity
@@ -592,7 +611,7 @@ const CommentItem: React.FC<CommentItemProps> = ({
{/* 操作按钮 - 更紧凑 */} {/* 操作按钮 - 更紧凑 */}
<View style={styles.actions}> <View style={styles.actions}>
{/* 点赞 */} {/* 点赞 */}
<TouchableOpacity style={styles.actionButton} onPress={onLike}> <TouchableOpacity style={styles.actionButton} onPress={() => onLike(comment)}>
<MaterialCommunityIcons <MaterialCommunityIcons
name={comment.is_liked ? 'heart' : 'heart-outline'} name={comment.is_liked ? 'heart' : 'heart-outline'}
size={14} size={14}

View File

@@ -895,61 +895,48 @@ export const PostDetailScreen: React.FC = () => {
} }
}; };
// 点赞评论(包括回复) // 点赞评论(包括回复)- 优化版
const handleLikeComment = async (comment: Comment) => { const handleLikeComment = async (comment: Comment) => {
// 先保存旧状态用于回滚 const { id, is_liked, likes_count } = comment;
const oldIsLiked = comment.is_liked;
const oldLikesCount = comment.likes_count;
// 乐观更新本地状态 - 辅助函数用于更新评论或回复 // 乐观更新:切换点赞状态
const updateCommentLike = (c: Comment): Comment => { const newIsLiked = !is_liked;
// 如果是目标评论/回复 const newLikesCount = newIsLiked ? likes_count + 1 : Math.max(0, likes_count - 1);
if (c.id === comment.id) {
return { // 递归更新评论树中的点赞状态
...c, const updateLikeInTree = (c: Comment): Comment => {
is_liked: !c.is_liked, if (c.id === id) {
likes_count: c.is_liked ? c.likes_count - 1 : c.likes_count + 1 return { ...c, is_liked: newIsLiked, likes_count: newLikesCount };
};
} }
// 如果有回复,递归查找 if (c.replies?.length) {
if (c.replies && c.replies.length > 0) { return { ...c, replies: c.replies.map(r => updateLikeInTree(r)) };
return {
...c,
replies: c.replies.map(r => updateCommentLike(r))
};
} }
return c; return c;
}; };
// 更新本地状态 setComments(prev => prev.map(c => updateLikeInTree(c)));
setComments(prev => prev.map(c => updateCommentLike(c)));
try { try {
if (oldIsLiked) { const success = newIsLiked
await commentService.unlikeComment(comment.id); ? await commentService.likeComment(id)
} else { : await commentService.unlikeComment(id);
await commentService.likeComment(comment.id);
if (!success) {
throw new Error('点赞操作失败');
} }
} catch (error) { } catch (error) {
console.error('评论点赞操作失败:', error); console.error('评论点赞操作失败:', error);
// 失败时回滚状态 // 回滚状态
const rollbackCommentLike = (c: Comment): Comment => { const rollbackLikeInTree = (c: Comment): Comment => {
if (c.id === comment.id) { if (c.id === id) {
return { return { ...c, is_liked, likes_count };
...c,
is_liked: oldIsLiked,
likes_count: oldLikesCount
};
} }
if (c.replies && c.replies.length > 0) { if (c.replies?.length) {
return { return { ...c, replies: c.replies.map(r => rollbackLikeInTree(r)) };
...c,
replies: c.replies.map(r => rollbackCommentLike(r))
};
} }
return c; 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 ( return (
<CommentItem <CommentItem
comment={item} comment={item}
onLike={() => handleLikeComment(item)} onLike={handleLikeComment}
onReply={() => handleReply(item)} onReply={() => handleReply(item)}
onUserPress={() => authorId && handleUserPress(authorId)} onUserPress={() => authorId && handleUserPress(authorId)}
floorNumber={index + 1} floorNumber={index + 1}

View File

@@ -23,6 +23,7 @@ import {
borderRadius, borderRadius,
useThemePreference, useThemePreference,
useSetThemePreference, useSetThemePreference,
useThemeStore,
type AppColors, type AppColors,
} from '../../theme'; } from '../../theme';
import { useAuthStore } from '../../stores'; import { useAuthStore } from '../../stores';
@@ -204,29 +205,41 @@ export const SettingsScreen: React.FC = () => {
const handleItemPress = (key: string) => { const handleItemPress = (key: string) => {
switch (key) { switch (key) {
case 'theme': case 'theme': {
Alert.alert('主题模式', '选择应用界面明暗', [ // 获取调试信息
{ text: '取消', style: 'cancel' }, const { Appearance } = require('react-native');
{ const systemScheme = Appearance?.getColorScheme?.() || 'undefined';
text: '跟随系统', const resolvedScheme = useThemeStore.getState().resolvedScheme;
onPress: () => { const storedPref = useThemeStore.getState().preference;
void setThemePreference('system'); 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: '浅色',
text: '浅色', onPress: () => {
onPress: () => { void setThemePreference('light');
void setThemePreference('light'); },
}, },
}, {
{ text: '深色',
text: '深色', onPress: () => {
onPress: () => { void setThemePreference('dark');
void setThemePreference('dark'); },
}, },
}, ]
]); );
break; break;
}
case 'edit_profile': case 'edit_profile':
router.push(hrefs.hrefProfileEdit()); router.push(hrefs.hrefProfileEdit());
break; break;

View File

@@ -1,7 +1,7 @@
import AsyncStorage from '@react-native-async-storage/async-storage'; 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 { create } from 'zustand';
import { useEffect } from 'react'; import { useEffect, useRef } from 'react';
import { lightColors, darkColors, type AppColors } from '../theme/palettes'; import { lightColors, darkColors, type AppColors } from '../theme/palettes';
import { buildPaperTheme } from '../theme/paperTheme'; import { buildPaperTheme } from '../theme/paperTheme';
import type { MD3Theme } from 'react-native-paper'; import type { MD3Theme } from 'react-native-paper';
@@ -46,13 +46,19 @@ type ThemeState = {
hydrate: () => Promise<void>; hydrate: () => Promise<void>;
}; };
const initialSystem: ResolvedScheme = 'light'; /** 获取系统主题 */
function getSystemScheme(): ResolvedScheme {
const scheme = Appearance.getColorScheme();
return scheme === 'dark' ? 'dark' : 'light';
}
const initialSystemScheme = getSystemScheme();
export const useThemeStore = create<ThemeState>((set, get) => ({ export const useThemeStore = create<ThemeState>((set, get) => ({
preference: 'system', preference: 'system',
systemScheme: initialSystem, systemScheme: initialSystemScheme,
hydrated: false, hydrated: false,
...buildState('system', initialSystem), ...buildState('system', initialSystemScheme),
setSystemScheme: (systemScheme) => { setSystemScheme: (systemScheme) => {
const { preference, systemScheme: cur } = get(); const { preference, systemScheme: cur } = get();
@@ -86,10 +92,11 @@ export const useThemeStore = create<ThemeState>((set, get) => ({
} catch { } catch {
/* ignore */ /* ignore */
} }
const { systemScheme } = get(); const systemScheme = getSystemScheme();
set({ set({
preference, preference,
hydrated: true, hydrated: true,
systemScheme,
...buildState(preference, systemScheme), ...buildState(preference, systemScheme),
}); });
}, },
@@ -97,17 +104,42 @@ export const useThemeStore = create<ThemeState>((set, get) => ({
/** 在根布局中同步系统配色并恢复持久化偏好 */ /** 在根布局中同步系统配色并恢复持久化偏好 */
export function ThemeBootstrap() { export function ThemeBootstrap() {
const system = useColorScheme();
const setSystemScheme = useThemeStore((s) => s.setSystemScheme); const setSystemScheme = useThemeStore((s) => s.setSystemScheme);
const hydrate = useThemeStore((s) => s.hydrate); const hydrate = useThemeStore((s) => s.hydrate);
const appState = useRef<AppStateStatus>(AppState.currentState);
useEffect(() => { useEffect(() => {
void hydrate(); void hydrate();
}, [hydrate]); }, [hydrate]);
// 监听系统主题变化
useEffect(() => { useEffect(() => {
setSystemScheme(system === 'dark' ? 'dark' : 'light'); const subscription = Appearance.addChangeListener(({ colorScheme }) => {
}, [system, setSystemScheme]); 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; return null;
} }
@@ -131,3 +163,6 @@ export function useSetThemePreference() {
export function usePaperThemeFromStore() { export function usePaperThemeFromStore() {
return useThemeStore((s) => s.paperTheme); return useThemeStore((s) => s.paperTheme);
} }
// 导出调试函数
export { getSystemScheme };