feat(messaging): implement optimistic updates with retry for failed messages
All checks were successful
Frontend CI / ota (android) (push) Successful in 3m24s
Frontend CI / ota (ios) (push) Successful in 3m22s
Frontend CI / build-and-push-web (push) Successful in 21m12s
Frontend CI / build-android-apk (push) Successful in 41m40s

- Add pending/failed message status to track send state
- Implement optimistic UI: show message immediately, update on server response
- Add retry functionality for failed messages via tap-to-retry
- Change send button from icon to text "发送" for clarity
- Add MessageSendService with temp ID generation to prevent collisions

feat(notification): add JPush notification deduplication and clear on tap

- Deduplicate notificationArrived events from dual JPush/vendor channels
- Clear all notifications on tap (QQ-style behavior)

feat(post): add client-side idempotency key for post creation

- Generate client_request_id per publish intent to prevent duplicates on retry
- Pass idempotency key through to postService and voteService
This commit is contained in:
lafay
2026-06-26 17:01:09 +08:00
parent 2bad59afbb
commit be77a9d04c
20 changed files with 451 additions and 134 deletions

View File

@@ -795,7 +795,7 @@ export const HomeScreen: React.FC = () => {
const authorId = item.author?.id || '';
const isPostAuthor = currentUser?.id === authorId;
return (
<View style={{ marginBottom: 2, paddingHorizontal: 1 }}>
<View style={{ marginBottom: 2, paddingHorizontal: responsiveGap / 2 }}>
<PostCard
post={item}
variant="grid"
@@ -804,7 +804,7 @@ export const HomeScreen: React.FC = () => {
/>
</View>
);
}, [currentUser?.id, stableOnPostAction]);
}, [currentUser?.id, stableOnPostAction, responsiveGap]);
// 渲染响应式网格布局(所有端统一使用 FlashList masonry 模式)
const renderResponsiveGrid = () => {
@@ -820,7 +820,7 @@ export const HomeScreen: React.FC = () => {
masonry
optimizeItemArrangement
contentContainerStyle={{
paddingHorizontal: responsivePadding,
paddingHorizontal: responsiveGap / 2,
paddingBottom: 80 + responsivePadding,
}}
showsVerticalScrollIndicator={false}

View File

@@ -1358,21 +1358,15 @@ export const PostDetailScreen: React.FC = () => {
);
}, [currentUser?.id, handleDeleteComment, handleImagePress, handleLikeComment, handleLoadMoreReplies, post?.user_id, handleReportComment]);
// 渲染空评论 - 类似图片中的样式
// 渲染空评论 - 使用 Forum 图标
const renderEmptyComments = useCallback(() => (
<View style={styles.emptyCommentsContainer}>
{/* 四个方块图标 */}
<View style={styles.emptyCommentsGrid}>
<View style={styles.emptyCommentsGridItem} />
<View style={styles.emptyCommentsGridItem} />
<View style={styles.emptyCommentsGridItem} />
<View style={[styles.emptyCommentsGridItem, styles.emptyCommentsGridItemActive]} />
</View>
<MaterialCommunityIcons name="forum-outline" size={48} color={colors.text.disabled} />
<Text variant="caption" color={colors.text.hint} style={styles.emptyCommentsSubtitle}>
~
</Text>
</View>
), []);
), [colors]);
const openComposer = () => {
setIsComposerVisible(true);
@@ -1382,6 +1376,7 @@ export const PostDetailScreen: React.FC = () => {
const closeComposer = () => {
setIsComposerVisible(false);
setShowEmojiPanel(false);
setReplyingTo(null);
Keyboard.dismiss();
};
@@ -1522,6 +1517,16 @@ export const PostDetailScreen: React.FC = () => {
{/* Toolbar */}
<View style={styles.expandedToolbar}>
<View style={styles.expandedToolbarLeft}>
<TouchableOpacity
style={styles.expandedToolbarItem}
onPress={closeComposer}
>
<MaterialCommunityIcons
name="chevron-down"
size={22}
color={colors.text.secondary}
/>
</TouchableOpacity>
<TouchableOpacity
style={styles.expandedToolbarItem}
onPress={handlePickCommentImage}
@@ -2294,30 +2299,14 @@ function createPostDetailStyles(colors: AppColors) {
paddingVertical: spacing.xs,
borderRadius: borderRadius.md,
},
// 空评论状态样式(类似图片中的四个方块样式)
// 空评论状态样式
emptyCommentsContainer: {
alignItems: 'center',
justifyContent: 'center',
paddingVertical: spacing.xl * 2.5,
},
emptyCommentsGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
width: 48,
height: 48,
marginBottom: spacing.md,
},
emptyCommentsGridItem: {
width: 20,
height: 20,
backgroundColor: colors.background.disabled,
margin: 2,
borderRadius: borderRadius.sm,
},
emptyCommentsGridItemActive: {
backgroundColor: colors.text.hint,
},
emptyCommentsSubtitle: {
marginTop: spacing.md,
fontSize: fontSizes.sm,
textAlign: 'center',
color: colors.text.hint,