Migrate frontend realtime messaging to SSE.
Switch service integrations and screen/store consumers from websocket events to SSE, and ignore generated dist-web artifacts. Made-with: Cursor
This commit is contained in:
@@ -21,7 +21,7 @@ import {
|
||||
Image,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import * as ImagePicker from 'expo-image-picker';
|
||||
import { colors, spacing, fontSizes, borderRadius, shadows } from '../../theme';
|
||||
@@ -31,6 +31,7 @@ import { ApiError } from '../../services/api';
|
||||
import { uploadService } from '../../services/uploadService';
|
||||
import VoteEditor from '../../components/business/VoteEditor';
|
||||
import { useResponsive, useResponsiveValue } from '../../hooks';
|
||||
import { RootStackParamList } from '../../navigation/types';
|
||||
|
||||
const MAX_TITLE_LENGTH = 100;
|
||||
const MAX_CONTENT_LENGTH = 2000;
|
||||
@@ -57,9 +58,9 @@ const EMOJIS = [
|
||||
'👍', '👎', '✊', '👊', '🤛', '🤜', '👏', '🙌',
|
||||
'👐', '🤲', '🤝', '🙏', '✍️', '💪', '🦾', '🦵',
|
||||
'❤️', '🧡', '💛', '💚', '💙', '💜', '🖤', '🤍',
|
||||
'🤎', '💔', '❤️\u200d🔥', '❤️\u200d🩹', '💕', '💞', '💓', '💗',
|
||||
'💖', '💘', '💝', '🎉', '🎊', '🎁', '🎈', '✨',
|
||||
'🔥', '💯', '💢', '💥', '💫', '💦', '💨', '🕳️',
|
||||
'🤎', '💔', '🩹', '💕', '💞', '💓', '💗', '💖',
|
||||
'💘', '💝', '🎉', '🎊', '🎁', '🎈', '✨', '🔥',
|
||||
'💯', '💢', '💥', '💫', '💦', '💨', '🕳️',
|
||||
];
|
||||
|
||||
// 动画值
|
||||
@@ -74,6 +75,9 @@ const getPublishErrorMessage = (error: unknown): string => {
|
||||
|
||||
export const CreatePostScreen: React.FC = () => {
|
||||
const navigation = useNavigation();
|
||||
const route = useRoute<RouteProp<RootStackParamList, 'CreatePost'>>();
|
||||
const isEditMode = route.params?.mode === 'edit' && !!route.params?.postId;
|
||||
const editPostID = route.params?.postId || '';
|
||||
|
||||
// 响应式布局
|
||||
const { isWideScreen, width } = useResponsive();
|
||||
@@ -86,6 +90,7 @@ export const CreatePostScreen: React.FC = () => {
|
||||
const [showTagInput, setShowTagInput] = useState(false);
|
||||
const [showEmojiPanel, setShowEmojiPanel] = useState(false);
|
||||
const [posting, setPosting] = useState(false);
|
||||
const [loadingPost, setLoadingPost] = useState(false);
|
||||
|
||||
// 投票相关状态
|
||||
const [isVotePost, setIsVotePost] = useState(false);
|
||||
@@ -122,6 +127,41 @@ export const CreatePostScreen: React.FC = () => {
|
||||
]).start();
|
||||
}, []);
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
title: isEditMode ? '编辑帖子' : '发布帖子',
|
||||
});
|
||||
}, [navigation, isEditMode]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isEditMode || !editPostID) {
|
||||
return;
|
||||
}
|
||||
|
||||
const loadPostForEdit = async () => {
|
||||
setLoadingPost(true);
|
||||
try {
|
||||
const existingPost = await postService.getPost(editPostID);
|
||||
if (!existingPost) {
|
||||
Alert.alert('提示', '帖子不存在或已被删除');
|
||||
navigation.goBack();
|
||||
return;
|
||||
}
|
||||
setTitle(existingPost.title || '');
|
||||
setContent(existingPost.content || '');
|
||||
setImages((existingPost.images || []).map((img) => ({ uri: img.url, uploading: false })));
|
||||
} catch (error) {
|
||||
console.error('加载待编辑帖子失败:', error);
|
||||
Alert.alert('错误', '加载帖子失败,请稍后重试');
|
||||
navigation.goBack();
|
||||
} finally {
|
||||
setLoadingPost(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadPostForEdit();
|
||||
}, [isEditMode, editPostID, navigation]);
|
||||
|
||||
// 选择图片
|
||||
const handlePickImage = async () => {
|
||||
const permissionResult = await ImagePicker.requestMediaLibraryPermissionsAsync();
|
||||
@@ -323,19 +363,37 @@ export const CreatePostScreen: React.FC = () => {
|
||||
});
|
||||
navigation.goBack();
|
||||
} else {
|
||||
// 创建普通帖子
|
||||
await postService.createPost({
|
||||
title: title.trim() || '无标题',
|
||||
content: content.trim(),
|
||||
images: imageUrls,
|
||||
});
|
||||
showPrompt({
|
||||
type: 'info',
|
||||
title: '审核中',
|
||||
message: '帖子已提交,内容审核中,稍后展示',
|
||||
duration: 2600,
|
||||
});
|
||||
navigation.goBack();
|
||||
if (isEditMode && editPostID) {
|
||||
const updated = await postService.updatePost(editPostID, {
|
||||
title: title.trim() || '无标题',
|
||||
content: content.trim(),
|
||||
images: imageUrls,
|
||||
});
|
||||
if (!updated) {
|
||||
throw new Error('更新帖子失败');
|
||||
}
|
||||
showPrompt({
|
||||
type: 'success',
|
||||
title: '修改成功',
|
||||
message: '帖子内容已更新',
|
||||
duration: 2200,
|
||||
});
|
||||
navigation.goBack();
|
||||
} else {
|
||||
// 创建普通帖子
|
||||
await postService.createPost({
|
||||
title: title.trim() || '无标题',
|
||||
content: content.trim(),
|
||||
images: imageUrls,
|
||||
});
|
||||
showPrompt({
|
||||
type: 'info',
|
||||
title: '审核中',
|
||||
message: '帖子已提交,内容审核中,稍后展示',
|
||||
duration: 2600,
|
||||
});
|
||||
navigation.goBack();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('发布帖子失败:', error);
|
||||
@@ -631,7 +689,7 @@ export const CreatePostScreen: React.FC = () => {
|
||||
<MaterialCommunityIcons name="loading" size={18} color={colors.primary.contrast} />
|
||||
) : (
|
||||
<Text variant="body" color={colors.primary.contrast} style={styles.postButtonText}>
|
||||
发布
|
||||
{isEditMode ? '保存' : '发布'}
|
||||
</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
@@ -677,10 +735,24 @@ export const CreatePostScreen: React.FC = () => {
|
||||
>
|
||||
{isWideScreen ? (
|
||||
<ResponsiveContainer maxWidth={800}>
|
||||
{renderMainContent()}
|
||||
{loadingPost ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<MaterialCommunityIcons name="loading" size={28} color={colors.primary.main} />
|
||||
<Text variant="body" color={colors.text.secondary} style={styles.loadingText}>加载帖子中...</Text>
|
||||
</View>
|
||||
) : (
|
||||
renderMainContent()
|
||||
)}
|
||||
</ResponsiveContainer>
|
||||
) : (
|
||||
renderMainContent()
|
||||
loadingPost ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<MaterialCommunityIcons name="loading" size={28} color={colors.primary.main} />
|
||||
<Text variant="body" color={colors.text.secondary} style={styles.loadingText}>加载帖子中...</Text>
|
||||
</View>
|
||||
) : (
|
||||
renderMainContent()
|
||||
)
|
||||
)}
|
||||
</KeyboardAvoidingView>
|
||||
</SafeAreaView>
|
||||
@@ -934,6 +1006,15 @@ const styles = StyleSheet.create({
|
||||
emojiText: {
|
||||
fontSize: 24,
|
||||
},
|
||||
loadingContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: spacing.md,
|
||||
},
|
||||
loadingText: {
|
||||
fontSize: fontSizes.md,
|
||||
},
|
||||
});
|
||||
|
||||
export default CreatePostScreen;
|
||||
|
||||
@@ -15,6 +15,8 @@ import {
|
||||
TouchableOpacity,
|
||||
NativeScrollEvent,
|
||||
NativeSyntheticEvent,
|
||||
Alert,
|
||||
Clipboard,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
@@ -357,8 +359,16 @@ export const HomeScreen: React.FC = () => {
|
||||
};
|
||||
|
||||
// 分享帖子
|
||||
const handleShare = (post: Post) => {
|
||||
void post;
|
||||
const handleShare = async (post: Post) => {
|
||||
if (!post?.id) return;
|
||||
try {
|
||||
await postService.sharePost(post.id);
|
||||
} catch (error) {
|
||||
console.error('上报分享次数失败:', error);
|
||||
}
|
||||
const postUrl = `https://browser.littlelan.cn/posts/${encodeURIComponent(post.id)}`;
|
||||
Clipboard.setString(postUrl);
|
||||
Alert.alert('已复制', '帖子链接已复制到剪贴板');
|
||||
};
|
||||
|
||||
// 删除帖子
|
||||
|
||||
@@ -20,14 +20,13 @@ import {
|
||||
Alert,
|
||||
ScrollView,
|
||||
Image,
|
||||
Clipboard,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import * as ImagePicker from 'expo-image-picker';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { zhCN } from 'date-fns/locale';
|
||||
import { colors, spacing, fontSizes, borderRadius } from '../../theme';
|
||||
import { Post, Comment, VoteResultDTO } from '../../types';
|
||||
import { useUserStore } from '../../stores';
|
||||
@@ -35,11 +34,11 @@ import { useCurrentUser } from '../../stores/authStore';
|
||||
import { postService, commentService, uploadService, authService, showPrompt, voteService } from '../../services';
|
||||
import { CommentItem, VoteCard } from '../../components/business';
|
||||
import { Avatar, Button, Loading, EmptyState, Text, ImageGallery, ImageGrid, ImageGridItem, AdaptiveLayout } from '../../components/common';
|
||||
import { HomeStackParamList } from '../../navigation/types';
|
||||
import { RootStackParamList } from '../../navigation/types';
|
||||
import { useResponsive, useResponsiveValue, useResponsiveSpacing } from '../../hooks/useResponsive';
|
||||
|
||||
type NavigationProp = NativeStackNavigationProp<HomeStackParamList, 'PostDetail'>;
|
||||
type PostDetailRouteProp = RouteProp<HomeStackParamList, 'PostDetail'>;
|
||||
type NavigationProp = NativeStackNavigationProp<RootStackParamList, 'PostDetail'>;
|
||||
type PostDetailRouteProp = RouteProp<RootStackParamList, 'PostDetail'>;
|
||||
|
||||
export const PostDetailScreen: React.FC = () => {
|
||||
const navigation = useNavigation<NavigationProp>();
|
||||
@@ -181,6 +180,13 @@ export const PostDetailScreen: React.FC = () => {
|
||||
loadPostDetail(true);
|
||||
}, [loadPostDetail]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = navigation.addListener('focus', () => {
|
||||
loadPostDetail(false);
|
||||
});
|
||||
return unsubscribe;
|
||||
}, [navigation, loadPostDetail]);
|
||||
|
||||
// 如果是从评论按钮跳转过来的,加载完成后滚动到评论区
|
||||
useEffect(() => {
|
||||
if (shouldScrollToComments && !loading && comments.length > 0) {
|
||||
@@ -262,16 +268,44 @@ export const PostDetailScreen: React.FC = () => {
|
||||
setRefreshing(false);
|
||||
}, [loadPostDetail]);
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (dateString: string): string => {
|
||||
try {
|
||||
return formatDistanceToNow(new Date(dateString), {
|
||||
addSuffix: true,
|
||||
locale: zhCN,
|
||||
});
|
||||
} catch {
|
||||
return '';
|
||||
const formatDateTime = (dateString?: string | null): string => {
|
||||
if (!dateString) return '';
|
||||
const date = new Date(dateString);
|
||||
if (Number.isNaN(date.getTime())) return '';
|
||||
const pad = (num: number) => String(num).padStart(2, '0');
|
||||
return `${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
||||
};
|
||||
|
||||
const formatRelativeTime = (dateString?: string | null): string => {
|
||||
if (!dateString) return '';
|
||||
const date = new Date(dateString);
|
||||
if (Number.isNaN(date.getTime())) return '';
|
||||
|
||||
const now = Date.now();
|
||||
const diffMs = now - date.getTime();
|
||||
if (diffMs < 0) return formatDateTime(dateString);
|
||||
|
||||
const minuteMs = 60 * 1000;
|
||||
const hourMs = 60 * minuteMs;
|
||||
const dayMs = 24 * hourMs;
|
||||
|
||||
if (diffMs < minuteMs) return '刚刚';
|
||||
if (diffMs < hourMs) return `${Math.floor(diffMs / minuteMs)}分钟前`;
|
||||
if (diffMs < dayMs) return `${Math.floor(diffMs / hourMs)}小时前`;
|
||||
if (diffMs < 2 * dayMs) {
|
||||
const pad = (num: number) => String(num).padStart(2, '0');
|
||||
return `昨天 ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
||||
}
|
||||
|
||||
return formatDateTime(dateString);
|
||||
};
|
||||
|
||||
const isPostEdited = (createdAt?: string, updatedAt?: string): boolean => {
|
||||
if (!createdAt || !updatedAt) return false;
|
||||
const created = new Date(createdAt).getTime();
|
||||
const updated = new Date(updatedAt).getTime();
|
||||
if (Number.isNaN(created) || Number.isNaN(updated)) return false;
|
||||
return updated-created > 1000;
|
||||
};
|
||||
|
||||
// 格式化数字
|
||||
@@ -350,9 +384,16 @@ export const PostDetailScreen: React.FC = () => {
|
||||
}, [post, favoritePost, unfavoritePost]);
|
||||
|
||||
// 分享帖子
|
||||
const handleShare = useCallback(() => {
|
||||
// TODO: 实现分享功能
|
||||
void post;
|
||||
const handleShare = useCallback(async () => {
|
||||
if (!post?.id) return;
|
||||
try {
|
||||
await postService.sharePost(post.id);
|
||||
} catch (error) {
|
||||
console.error('上报分享次数失败:', error);
|
||||
}
|
||||
const postUrl = `https://browser.littlelan.cn/posts/${encodeURIComponent(post.id)}`;
|
||||
Clipboard.setString(postUrl);
|
||||
Alert.alert('已复制', '帖子链接已复制到剪贴板');
|
||||
}, [post?.id]);
|
||||
|
||||
// 投票处理函数
|
||||
@@ -484,6 +525,14 @@ export const PostDetailScreen: React.FC = () => {
|
||||
);
|
||||
}, [post, isDeleting, currentUser?.id, navigation]);
|
||||
|
||||
const handleEditPost = useCallback(() => {
|
||||
if (!post) return;
|
||||
navigation.navigate('CreatePost', {
|
||||
mode: 'edit',
|
||||
postId: post.id,
|
||||
});
|
||||
}, [navigation, post]);
|
||||
|
||||
// 点击图片查看大图
|
||||
const handleImagePress = useCallback((images: ImageGridItem[], index: number) => {
|
||||
setAllImages(images);
|
||||
@@ -1012,33 +1061,59 @@ export const PostDetailScreen: React.FC = () => {
|
||||
|
||||
{/* 发帖时间和浏览量 - 放在图片下方 */}
|
||||
<View style={[styles.postMetaInfo, { marginTop: responsiveGap }]}>
|
||||
<Text variant="caption" color={colors.text.hint} style={styles.metaInfoText}>
|
||||
{formatTime(post.created_at)}
|
||||
</Text>
|
||||
{post.views_count !== undefined && post.views_count > 0 && (
|
||||
<>
|
||||
<Text style={styles.metaInfoDot}>·</Text>
|
||||
<Text variant="caption" color={colors.text.hint} style={styles.metaInfoText}>
|
||||
{formatNumber(post.views_count)} 浏览
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
{/* 删除按钮 - 只对帖子作者显示 */}
|
||||
<View style={styles.metaInfoMain}>
|
||||
<Text variant="caption" color={colors.text.hint} style={styles.metaInfoText}>
|
||||
发布 {formatRelativeTime(post.created_at)}
|
||||
</Text>
|
||||
{isPostEdited(post.created_at, post.updated_at) && (
|
||||
<>
|
||||
<Text style={styles.metaInfoDot}>·</Text>
|
||||
<Text variant="caption" color={colors.text.hint} style={styles.metaInfoText}>
|
||||
修改 {formatRelativeTime(post.updated_at)}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
{post.views_count !== undefined && post.views_count > 0 && (
|
||||
<>
|
||||
<Text style={styles.metaInfoDot}>·</Text>
|
||||
<Text variant="caption" color={colors.text.hint} style={styles.metaInfoText}>
|
||||
{formatNumber(post.views_count)} 浏览
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{currentUser?.id === post.author?.id && (
|
||||
<TouchableOpacity
|
||||
style={styles.deleteButtonInline}
|
||||
onPress={handleDeletePost}
|
||||
disabled={isDeleting}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
name={isDeleting ? 'loading' : 'delete-outline'}
|
||||
size={14}
|
||||
color={colors.text.hint}
|
||||
/>
|
||||
<Text variant="caption" color={colors.text.hint} style={styles.deleteButtonText}>
|
||||
删除
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<View style={styles.metaActions}>
|
||||
<TouchableOpacity
|
||||
style={styles.editButtonInline}
|
||||
onPress={handleEditPost}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
name="pencil-outline"
|
||||
size={14}
|
||||
color={colors.text.hint}
|
||||
/>
|
||||
<Text variant="caption" color={colors.text.hint} style={styles.editButtonText}>
|
||||
编辑
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
{/* 删除按钮 - 只对帖子作者显示 */}
|
||||
<TouchableOpacity
|
||||
style={styles.deleteButtonInline}
|
||||
onPress={handleDeletePost}
|
||||
disabled={isDeleting}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
name={isDeleting ? 'loading' : 'delete-outline'}
|
||||
size={14}
|
||||
color={colors.text.hint}
|
||||
/>
|
||||
<Text variant="caption" color={colors.text.hint} style={styles.deleteButtonText}>
|
||||
删除
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
@@ -1107,7 +1182,7 @@ export const PostDetailScreen: React.FC = () => {
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}, [post, postImages, currentUser?.id, isDeleting, handleLike, handleShare, handleFavorite, handleDeletePost, handleImagePress, voteResult, isVoteLoading, handleVote, handleUnvote, isDesktop, isTablet, isWideScreen, responsivePadding, responsiveGap]);
|
||||
}, [post, postImages, currentUser?.id, isDeleting, handleLike, handleShare, handleFavorite, handleDeletePost, handleEditPost, handleImagePress, voteResult, isVoteLoading, handleVote, handleUnvote, isDesktop, isTablet, isWideScreen, responsivePadding, responsiveGap]);
|
||||
|
||||
// 回复评论
|
||||
const [replyingTo, setReplyingTo] = useState<Comment | null>(null);
|
||||
@@ -1548,8 +1623,16 @@ const styles = StyleSheet.create({
|
||||
postMetaInfo: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
metaInfoMain: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
},
|
||||
metaInfoText: {
|
||||
fontSize: fontSizes.sm,
|
||||
color: colors.text.hint,
|
||||
@@ -1563,13 +1646,28 @@ const styles = StyleSheet.create({
|
||||
deleteButtonInline: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginLeft: 'auto',
|
||||
marginLeft: spacing.sm,
|
||||
padding: spacing.xs,
|
||||
},
|
||||
deleteButtonText: {
|
||||
marginLeft: 2,
|
||||
fontSize: fontSizes.sm,
|
||||
},
|
||||
metaActions: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginLeft: spacing.sm,
|
||||
flexShrink: 0,
|
||||
},
|
||||
editButtonInline: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: spacing.xs,
|
||||
},
|
||||
editButtonText: {
|
||||
marginLeft: 2,
|
||||
fontSize: fontSizes.sm,
|
||||
},
|
||||
imagesContainer: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
|
||||
@@ -117,8 +117,8 @@ const GroupInfoScreen: React.FC = () => {
|
||||
|
||||
// 并行加载群组信息和成员列表
|
||||
const [groupData, membersData] = await Promise.all([
|
||||
groupManager.getGroup(groupId),
|
||||
groupManager.getMembers(groupId, 1, 100),
|
||||
groupManager.getGroup(groupId, true),
|
||||
groupManager.getMembers(groupId, 1, 100, true),
|
||||
]);
|
||||
|
||||
setGroup(groupData);
|
||||
|
||||
@@ -92,11 +92,16 @@ const GroupMembersScreen: React.FC = () => {
|
||||
const isAdmin = currentMember?.role === 'admin' || isOwner;
|
||||
|
||||
// 加载成员列表
|
||||
const loadMembers = useCallback(async (pageNum: number = 1, refresh: boolean = false) => {
|
||||
const loadMembers = useCallback(
|
||||
async (
|
||||
pageNum: number = 1,
|
||||
refresh: boolean = false,
|
||||
forceRefresh: boolean = false
|
||||
) => {
|
||||
if (!hasMore && !refresh) return;
|
||||
|
||||
try {
|
||||
const response = await groupManager.getMembers(groupId, pageNum, 50);
|
||||
const response = await groupManager.getMembers(groupId, pageNum, 50, forceRefresh);
|
||||
|
||||
if (refresh) {
|
||||
setMembers(response.list);
|
||||
@@ -121,14 +126,14 @@ const GroupMembersScreen: React.FC = () => {
|
||||
|
||||
// 初始加载
|
||||
useEffect(() => {
|
||||
loadMembers(1, true);
|
||||
loadMembers(1, true, true);
|
||||
}, [groupId]);
|
||||
|
||||
// 下拉刷新
|
||||
const onRefresh = useCallback(() => {
|
||||
setRefreshing(true);
|
||||
setHasMore(true);
|
||||
loadMembers(1, true);
|
||||
loadMembers(1, true, true);
|
||||
}, [loadMembers]);
|
||||
|
||||
// 加载更多
|
||||
@@ -246,6 +251,8 @@ const GroupMembersScreen: React.FC = () => {
|
||||
}
|
||||
return m;
|
||||
}));
|
||||
// 强制刷新远端状态,避免命中旧缓存导致解禁后仍显示禁言
|
||||
await loadMembers(1, true, true);
|
||||
|
||||
setActionModalVisible(false);
|
||||
Alert.alert('成功', `已${actionText}`);
|
||||
|
||||
@@ -72,9 +72,10 @@ const truncateDisplayName = (name: string, maxLength: number = MAX_CONVERSATION_
|
||||
*/
|
||||
const AsyncMessagePreview: React.FC<{
|
||||
segments?: MessageSegment[];
|
||||
status?: string;
|
||||
isGroupChat?: boolean;
|
||||
senderName?: string;
|
||||
}> = ({ segments, isGroupChat, senderName }) => {
|
||||
}> = ({ segments, status, isGroupChat, senderName }) => {
|
||||
const [displayText, setDisplayText] = useState<string>('');
|
||||
const isMountedRef = useRef(true);
|
||||
|
||||
@@ -82,6 +83,13 @@ const AsyncMessagePreview: React.FC<{
|
||||
isMountedRef.current = true;
|
||||
|
||||
const loadPreview = async () => {
|
||||
if (status === 'recalled') {
|
||||
if (isMountedRef.current) {
|
||||
setDisplayText('消息已撤回');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const initialText = extractTextFromSegments(segments);
|
||||
if (isMountedRef.current) {
|
||||
setDisplayText(initialText);
|
||||
@@ -112,7 +120,7 @@ const AsyncMessagePreview: React.FC<{
|
||||
return () => {
|
||||
isMountedRef.current = false;
|
||||
};
|
||||
}, [segments]);
|
||||
}, [segments, status]);
|
||||
|
||||
if (!displayText) return null;
|
||||
|
||||
@@ -556,6 +564,7 @@ export const MessageListScreen: React.FC = () => {
|
||||
) : (
|
||||
<AsyncMessagePreview
|
||||
segments={item.last_message?.segments}
|
||||
status={item.last_message?.status}
|
||||
isGroupChat={isGroupChat}
|
||||
senderName={getSenderName()}
|
||||
/>
|
||||
|
||||
@@ -27,9 +27,6 @@ import { uploadService } from '../../../../services/uploadService';
|
||||
import { ApiError } from '../../../../services/api';
|
||||
// 【新架构】使用 MessageManager
|
||||
import { useChat, useGroupTyping, useGroupMuted, messageManager } from '../../../../stores';
|
||||
import {
|
||||
websocketService,
|
||||
} from '../../../../services/websocketService';
|
||||
import { groupService } from '../../../../services/groupService';
|
||||
import { userManager } from '../../../../stores/userManager';
|
||||
import { groupManager } from '../../../../stores/groupManager';
|
||||
@@ -728,14 +725,7 @@ export const useChatScreen = () => {
|
||||
const segments = buildTextSegments(trimmedText, replyingTo);
|
||||
|
||||
if (isGroupChat && routeGroupId) {
|
||||
// 群聊消息发送
|
||||
websocketService.sendGroupChatMessage({
|
||||
conversationId: conversationId,
|
||||
groupId: routeGroupId,
|
||||
segments: segments,
|
||||
mentionUsers: selectedMentions.length > 0 ? selectedMentions : undefined,
|
||||
mentionAll: mentionAll || undefined,
|
||||
});
|
||||
await messageService.sendMessageByAction('group', conversationId, segments);
|
||||
|
||||
setInputText('');
|
||||
setSelectedMentions([]);
|
||||
@@ -790,12 +780,7 @@ export const useChatScreen = () => {
|
||||
const segments = buildImageSegments(uploadResult.url, uploadResult.url, undefined, undefined, replyingTo);
|
||||
|
||||
if (isGroupChat && routeGroupId) {
|
||||
websocketService.sendGroupChatMessage({
|
||||
conversationId: conversationId,
|
||||
groupId: routeGroupId,
|
||||
mediaUrl: uploadResult.url,
|
||||
segments: segments,
|
||||
});
|
||||
await messageService.sendMessageByAction('group', conversationId, segments);
|
||||
setReplyingTo(null);
|
||||
} else {
|
||||
// 【新架构】私聊图片通过 MessageManager 发送
|
||||
@@ -922,12 +907,7 @@ export const useChatScreen = () => {
|
||||
const segments = buildImageSegments(stickerUrl, stickerUrl, undefined, undefined, replyingTo);
|
||||
|
||||
if (isGroupChat && routeGroupId) {
|
||||
websocketService.sendGroupChatMessage({
|
||||
conversationId: conversationId,
|
||||
groupId: routeGroupId,
|
||||
mediaUrl: stickerUrl,
|
||||
segments: segments,
|
||||
});
|
||||
await messageService.sendMessageByAction('group', conversationId, segments);
|
||||
setReplyingTo(null);
|
||||
} else {
|
||||
// 【新架构】私聊表情通过 MessageManager 发送
|
||||
@@ -983,7 +963,7 @@ export const useChatScreen = () => {
|
||||
const handleRecall = useCallback(async (messageId: string) => {
|
||||
try {
|
||||
if (isGroupChat && routeGroupId) {
|
||||
websocketService.sendGroupRecall(routeGroupId, conversationId!, messageId);
|
||||
await messageService.recallMessage(messageId);
|
||||
} else {
|
||||
await messageService.recallMessage(messageId);
|
||||
}
|
||||
|
||||
@@ -145,7 +145,7 @@ export const EmbeddedChat: React.FC<EmbeddedChatProps> = ({ conversation, onBack
|
||||
<Text style={styles.senderName}>{item.sender?.nickname || item.sender?.username}</Text>
|
||||
)}
|
||||
<Text style={[styles.messageText, isMe ? styles.messageTextMe : styles.messageTextOther]}>
|
||||
{extractTextFromSegments(item.segments)}
|
||||
{item.status === 'recalled' ? '消息已撤回' : extractTextFromSegments(item.segments)}
|
||||
</Text>
|
||||
</View>
|
||||
{isMe && (
|
||||
|
||||
Reference in New Issue
Block a user