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

@@ -228,7 +228,8 @@ export function useMessages(conversationId: string | null): UseMessagesReturn {
// ==================== useSendMessage - 发送消息 ====================
interface UseSendMessageReturn {
sendMessage: (segments: MessageSegment[], options?: { replyToId?: string }) => Promise<MessageResponse | null>;
sendMessage: (segments: MessageSegment[], options?: { replyToId?: string; detailType?: 'private' | 'group' }) => Promise<MessageResponse | null>;
retrySendMessage: (failedMessage: MessageResponse) => Promise<MessageResponse | null>;
isSending: boolean;
}
@@ -239,7 +240,7 @@ export function useSendMessage(conversationId: string | null): UseSendMessageRet
const [isSending, setIsSending] = useState(false);
const sendMessage = useCallback(
async (segments: MessageSegment[], options?: { replyToId?: string }): Promise<MessageResponse | null> => {
async (segments: MessageSegment[], options?: { replyToId?: string; detailType?: 'private' | 'group' }): Promise<MessageResponse | null> => {
if (!conversationId) return null;
setIsSending(true);
@@ -253,8 +254,24 @@ export function useSendMessage(conversationId: string | null): UseSendMessageRet
[conversationId]
);
const retrySendMessage = useCallback(
async (failedMessage: MessageResponse): Promise<MessageResponse | null> => {
if (!conversationId) return null;
setIsSending(true);
try {
const message = await messageManager.retrySendMessage(conversationId, failedMessage);
return message;
} finally {
setIsSending(false);
}
},
[conversationId]
);
return {
sendMessage,
retrySendMessage,
isSending,
};
}
@@ -537,7 +554,8 @@ interface UseChatReturn {
refreshMessages: () => Promise<void>;
// 发送消息
sendMessage: (segments: MessageSegment[], options?: { replyToId?: string }) => Promise<MessageResponse | null>;
sendMessage: (segments: MessageSegment[], options?: { replyToId?: string; detailType?: 'private' | 'group' }) => Promise<MessageResponse | null>;
retrySendMessage: (failedMessage: MessageResponse) => Promise<MessageResponse | null>;
isSending: boolean;
// 已读相关
@@ -549,7 +567,7 @@ interface UseChatReturn {
export function useChat(conversationId: string | null): UseChatReturn {
const { messages, isLoading: isLoadingMessages, hasMore: hasMoreMessages, loadMore, refresh } = useMessages(conversationId);
const { sendMessage, isSending } = useSendMessage(conversationId);
const { sendMessage, retrySendMessage, isSending } = useSendMessage(conversationId);
const { markAsRead } = useMarkAsRead(conversationId);
const { conversation } = useConversation(conversationId);
@@ -560,6 +578,7 @@ export function useChat(conversationId: string | null): UseChatReturn {
loadMoreMessages: loadMore,
refreshMessages: refresh,
sendMessage,
retrySendMessage,
isSending,
markAsRead,
conversation,