Skip to content

Commit 363b5d0

Browse files
committed
feat(chat): add message retry option for failed deliveries
1 parent 83a0c04 commit 363b5d0

43 files changed

Lines changed: 459 additions & 40 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2828
- Font size setting in appearance preferences
2929
- PEP-based conversation list synchronisation (ConversationSync module)
3030
- XEP-0202: Entity Time — display contact local time in chat header and contact popover
31+
- Display message delivery errors, and offer the options to retry sending the mesage
3132

3233
### Changed
3334

RELEASE_NOTES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
- Font size setting in appearance preferences
2222
- PEP-based conversation list synchronisation (ConversationSync module)
2323
- XEP-0202: Entity Time — display contact local time in chat header and contact popover
24+
- Display message delivery errors, and offer the options to retry sending the mesage
2425

2526
### Changed
2627

apps/fluux/src-tauri/tauri.conf.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
],
6363
"macOS": {
6464
"minimumSystemVersion": "10.13",
65-
"bundleVersion": "b476c49",
65+
"bundleVersion": "83a0c04",
6666
"entitlements": "Entitlements.plist",
6767
"signingIdentity": null
6868
},

apps/fluux/src/components/ChatView.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export function ChatView({ onBack, onSwitchToMessages, mainContentRef, composerR
2626
const { t } = useTranslation()
2727
// Use useChatActive instead of useChat to avoid subscribing to the conversation list.
2828
// This prevents re-renders during background MAM sync of other conversations.
29-
const { activeConversation, activeMessages, activeTypingUsers, sendReaction, sendCorrection, retractMessage, activeAnimation, sendEasterEgg, clearAnimation, clearFirstNewMessageId, updateLastSeenMessageId, activeMAMState, fetchOlderHistory } = useChatActive()
29+
const { activeConversation, activeMessages, activeTypingUsers, sendReaction, sendCorrection, retractMessage, retryMessage, activeAnimation, sendEasterEgg, clearAnimation, clearFirstNewMessageId, updateLastSeenMessageId, activeMAMState, fetchOlderHistory } = useChatActive()
3030
// Use useContactIdentities instead of useRoster() to avoid re-renders on
3131
// presence changes. ChatView only needs contact names and avatars for display.
3232
const contactsByJid = useContactIdentities()
@@ -275,6 +275,7 @@ export function ChatView({ onBack, onSwitchToMessages, mainContentRef, composerR
275275
activeReactionPickerMessageId={activeReactionPickerMessageId}
276276
onReactionPickerChange={handleReactionPickerChange}
277277
retractMessage={retractMessage}
278+
retryMessage={retryMessage}
278279
selectedMessageId={selectedMessageId}
279280
hasKeyboardSelection={hasKeyboardSelection}
280281
showToolbarForSelection={showToolbarForSelection}
@@ -349,6 +350,7 @@ const ChatMessageList = memo(function ChatMessageList({
349350
activeReactionPickerMessageId,
350351
onReactionPickerChange,
351352
retractMessage,
353+
retryMessage,
352354
selectedMessageId,
353355
hasKeyboardSelection,
354356
showToolbarForSelection,
@@ -381,6 +383,7 @@ const ChatMessageList = memo(function ChatMessageList({
381383
activeReactionPickerMessageId: string | null
382384
onReactionPickerChange: (messageId: string, isOpen: boolean) => void
383385
retractMessage: (conversationId: string, messageId: string) => Promise<void>
386+
retryMessage: (conversationId: string, messageId: string) => Promise<void>
384387
selectedMessageId: string | null
385388
hasKeyboardSelection: boolean
386389
showToolbarForSelection: boolean
@@ -467,6 +470,7 @@ const ChatMessageList = memo(function ChatMessageList({
467470
hideToolbar={isComposing || (activeReactionPickerMessageId !== null && activeReactionPickerMessageId !== msg.id)}
468471
onReactionPickerChange={(isOpen) => onReactionPickerChange(msg.id, isOpen)}
469472
retractMessage={retractMessage}
473+
retryMessage={retryMessage}
470474
isSelected={msg.id === selectedMessageId}
471475
hasKeyboardSelection={hasKeyboardSelection}
472476
showToolbarForSelection={showToolbarForSelection}
@@ -481,7 +485,7 @@ const ChatMessageList = memo(function ChatMessageList({
481485
), [
482486
ownAvatar, contactsByJid, ownNickname, conversationId, conversationType,
483487
sendReaction, myBareJid, messagesById, onReply, onEdit, lastOutgoingMessageId, lastMessageId,
484-
isComposing, activeReactionPickerMessageId, onReactionPickerChange, retractMessage,
488+
isComposing, activeReactionPickerMessageId, onReactionPickerChange, retractMessage, retryMessage,
485489
selectedMessageId, hasKeyboardSelection, showToolbarForSelection, isDarkMode,
486490
hoveredMessageId, handleMessageHover, handleMessageLeave, formatTime, effectiveTimeFormat
487491
])
@@ -533,6 +537,7 @@ interface ChatMessageBubbleProps {
533537
hideToolbar?: boolean
534538
onReactionPickerChange?: (isOpen: boolean) => void
535539
retractMessage: (conversationId: string, messageId: string) => Promise<void>
540+
retryMessage: (conversationId: string, messageId: string) => Promise<void>
536541
isSelected?: boolean
537542
hasKeyboardSelection?: boolean
538543
showToolbarForSelection?: boolean
@@ -567,6 +572,7 @@ const ChatMessageBubble = memo(function ChatMessageBubble({
567572
hideToolbar,
568573
onReactionPickerChange,
569574
retractMessage,
575+
retryMessage,
570576
isSelected,
571577
hasKeyboardSelection,
572578
showToolbarForSelection,
@@ -690,6 +696,7 @@ const ChatMessageBubble = memo(function ChatMessageBubble({
690696
onReply={() => onReply(message)}
691697
onEdit={() => onEdit(message)}
692698
onDelete={async () => retractMessage(conversationId, message.id)}
699+
onRetry={message.deliveryError ? () => { void retryMessage(conversationId, message.id) } : undefined}
693700
onMediaLoad={onMediaLoad}
694701
replyContext={replyContext}
695702
onReactionPickerChange={onReactionPickerChange}

apps/fluux/src/components/conversation/MessageBubble.tsx

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
* the common bubble structure.
66
*/
77
import { useState, memo, type ReactNode } from 'react'
8-
import { CornerUpRight } from 'lucide-react'
9-
import { formatMessagePreview, type BaseMessage, type MentionReference, type Contact, type ContactIdentity, type RoomRole, type RoomAffiliation } from '@fluux/sdk'
8+
import { useTranslation } from 'react-i18next'
9+
import { CornerUpRight, AlertCircle, RefreshCw } from 'lucide-react'
10+
import { formatMessagePreview, formatXMPPError, type BaseMessage, type MentionReference, type Contact, type ContactIdentity, type RoomRole, type RoomAffiliation } from '@fluux/sdk'
1011
import { Avatar } from '../Avatar'
1112
import { AvatarLightbox } from '../AvatarLightbox'
1213
import { MessageToolbar } from './MessageToolbar'
@@ -68,6 +69,7 @@ export interface MessageBubbleProps {
6869
onReply: () => void
6970
onEdit: () => void
7071
onDelete: () => Promise<void>
72+
onRetry?: () => void
7173
onMediaLoad?: () => void
7274

7375
// Reply context (view-specific rendering)
@@ -112,6 +114,7 @@ function arePropsEqual(prev: MessageBubbleProps, next: MessageBubbleProps): bool
112114
if (prev.message.isEdited !== next.message.isEdited) return false
113115
if (prev.message.isRetracted !== next.message.isRetracted) return false
114116
if (prev.message.isOutgoing !== next.message.isOutgoing) return false
117+
if (prev.message.deliveryError !== next.message.deliveryError) return false
115118

116119
// Reactions - compare stringified since object reference will differ
117120
const prevReactions = JSON.stringify(prev.message.reactions ?? {})
@@ -207,6 +210,7 @@ export const MessageBubble = memo(function MessageBubble({
207210
onReply,
208211
onEdit,
209212
onDelete,
213+
onRetry,
210214
onMediaLoad,
211215
replyContext,
212216
mentions,
@@ -218,9 +222,11 @@ export const MessageBubble = memo(function MessageBubble({
218222
formatTime,
219223
timeFormat,
220224
}: MessageBubbleProps) {
225+
const { t } = useTranslation()
221226
const [showReactionPicker, setShowReactionPickerState] = useState(false)
222227
const [showMoreMenu, setShowMoreMenu] = useState(false)
223228
const [showAvatarLightbox, setShowAvatarLightbox] = useState(false)
229+
const [showErrorDetails, setShowErrorDetails] = useState(false)
224230

225231
// Whether reactions are enabled for this message (room has stable occupant identity)
226232
const reactionsEnabled = onReaction !== undefined
@@ -390,6 +396,40 @@ export const MessageBubble = memo(function MessageBubble({
390396
getReactorName={getReactorName}
391397
isRetracted={message.isRetracted}
392398
/>
399+
400+
{/* Delivery error indicator */}
401+
{message.deliveryError && (
402+
<div className="flex flex-col gap-1 pt-1">
403+
<div className="flex items-center gap-1.5 text-red-500">
404+
<AlertCircle className="w-3.5 h-3.5 flex-shrink-0" />
405+
<span className="text-xs font-medium">{t('chat.deliveryFailed')}</span>
406+
<span className="text-xs text-fluux-muted"></span>
407+
<button
408+
onClick={() => setShowErrorDetails(!showErrorDetails)}
409+
className="text-xs text-fluux-muted hover:text-fluux-text cursor-pointer underline"
410+
>
411+
{t('chat.viewError')}
412+
</button>
413+
{onRetry && (
414+
<>
415+
<span className="text-xs text-fluux-muted">·</span>
416+
<button
417+
onClick={onRetry}
418+
className="text-xs text-fluux-link hover:text-fluux-link-hover cursor-pointer underline flex items-center gap-1"
419+
>
420+
<RefreshCw className="w-3 h-3" />
421+
{t('chat.retry')}
422+
</button>
423+
</>
424+
)}
425+
</div>
426+
{showErrorDetails && (
427+
<div className="text-xs text-fluux-muted pl-5 py-1 bg-red-500/5 rounded">
428+
{t('chat.errorDetails', { error: formatXMPPError(message.deliveryError) })}
429+
</div>
430+
)}
431+
</div>
432+
)}
393433
</div>
394434

395435
{/* Avatar lightbox overlay */}

apps/fluux/src/data/changelog.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export const changelog: ChangelogEntry[] = [
3939
'Font size setting in appearance preferences',
4040
'PEP-based conversation list synchronisation (ConversationSync module)',
4141
'XEP-0202: Entity Time — display contact local time in chat header and contact popover',
42+
'Display message delivery errors, and offer the options to retry sending the mesage',
4243
],
4344
},
4445
{

apps/fluux/src/i18n/locales/be.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,11 @@
404404
"many": "{{name1}}, {{name2}} і яшчэ {{count}} друкуюць..."
405405
},
406406
"showMore": "Паказаць больш",
407-
"showLess": "Паказаць менш"
407+
"showLess": "Паказаць менш",
408+
"deliveryFailed": "Message not delivered",
409+
"viewError": "View error",
410+
"retry": "Retry",
411+
"errorDetails": "Error: {{error}}"
408412
},
409413
"dates": {
410414
"today": "Сёння",

apps/fluux/src/i18n/locales/bg.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,11 @@
404404
"many": "{{name1}}, {{name2}} и още {{count}} пишат..."
405405
},
406406
"showMore": "Покажи повече",
407-
"showLess": "Покажи по-малко"
407+
"showLess": "Покажи по-малко",
408+
"deliveryFailed": "Message not delivered",
409+
"viewError": "View error",
410+
"retry": "Retry",
411+
"errorDetails": "Error: {{error}}"
408412
},
409413
"dates": {
410414
"today": "Днес",

apps/fluux/src/i18n/locales/ca.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,11 @@
404404
"many": "{{name1}}, {{name2}} i {{count}} més estan escrivint..."
405405
},
406406
"showMore": "Mostra més",
407-
"showLess": "Mostra menys"
407+
"showLess": "Mostra menys",
408+
"deliveryFailed": "Message not delivered",
409+
"viewError": "View error",
410+
"retry": "Retry",
411+
"errorDetails": "Error: {{error}}"
408412
},
409413
"dates": {
410414
"today": "Avui",

apps/fluux/src/i18n/locales/cs.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,11 @@
404404
"many": "{{name1}}, {{name2}} a {{count}} dalších píše..."
405405
},
406406
"showMore": "Zobrazit více",
407-
"showLess": "Zobrazit méně"
407+
"showLess": "Zobrazit méně",
408+
"deliveryFailed": "Message not delivered",
409+
"viewError": "View error",
410+
"retry": "Retry",
411+
"errorDetails": "Error: {{error}}"
408412
},
409413
"dates": {
410414
"today": "Dnes",

0 commit comments

Comments
 (0)