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.
This commit is contained in:
1
app.json
1
app.json
@@ -59,6 +59,7 @@
|
|||||||
"favicon": "./assets/favicon.png"
|
"favicon": "./assets/favicon.png"
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
|
"./plugins/withMainActivityConfigChange",
|
||||||
[
|
[
|
||||||
"expo-router",
|
"expo-router",
|
||||||
{
|
{
|
||||||
|
|||||||
77
plugins/withMainActivityConfigChange.ts
Normal file
77
plugins/withMainActivityConfigChange.ts
Normal 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;
|
||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 };
|
||||||
Reference in New Issue
Block a user