mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 14:05:18 +00:00
Snapshot delivery status in message rows so read receipts render immediately
The publish chain was healthy: the store mutates the shared BitchatMessage and republishes, the .statusChanged fan-out reaches both mirrored conversations, and PrivateInboxModel fires objectWillChange for the selected DM under either key. The break was at the row view: TextMessageView/MediaMessageView stored the reference-typed message and read deliveryStatus in body, so SwiftUI's structural diff compared the field by identity - same instance, mutated in place, row body skipped. The blue tick waited for an unrelated invalidation (proven empirically with a hosting-view probe). Rows now snapshot deliveryStatus as a value at init; every republish rebuilds row values with a fresh enum, the diff sees the change, and the row re-renders immediately. Also fixes in-place send-progress updates in media rows. Regression tests cover both mirrored selection keyings at the feature-model level and the snapshot mechanic itself. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,10 +12,23 @@ import BitFoundation
|
||||
struct TextMessageView: View {
|
||||
@Environment(\.colorScheme) private var colorScheme: ColorScheme
|
||||
@EnvironmentObject private var conversationUIModel: ConversationUIModel
|
||||
|
||||
|
||||
let message: BitchatMessage
|
||||
/// Value snapshot of the message's mutable delivery status, captured at
|
||||
/// construction. `BitchatMessage` is a reference type mutated in place by
|
||||
/// `ConversationStore`, and SwiftUI compares reference-typed view fields
|
||||
/// by identity — so a status-only change (e.g. delivered → read) on the
|
||||
/// SAME instance would otherwise compare "unchanged" and this row's body
|
||||
/// would be skipped even though the parent list re-rendered. Snapshotting
|
||||
/// the enum makes the change visible to SwiftUI's structural diff.
|
||||
private let deliveryStatus: DeliveryStatus?
|
||||
@State private var expandedMessageIDs: Set<String> = []
|
||||
|
||||
|
||||
init(message: BitchatMessage) {
|
||||
self.message = message
|
||||
self.deliveryStatus = message.deliveryStatus
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
// Precompute heavy token scans once per row
|
||||
@@ -31,7 +44,7 @@ struct TextMessageView: View {
|
||||
|
||||
// Delivery status indicator for private messages
|
||||
if message.isPrivate && conversationUIModel.isSentByCurrentUser(message),
|
||||
let status = message.deliveryStatus {
|
||||
let status = deliveryStatus {
|
||||
DeliveryStatusView(status: status)
|
||||
.padding(.leading, 4)
|
||||
}
|
||||
|
||||
@@ -13,11 +13,24 @@ struct MediaMessageView: View {
|
||||
@EnvironmentObject private var conversationUIModel: ConversationUIModel
|
||||
let message: BitchatMessage
|
||||
let media: BitchatMessage.Media
|
||||
/// Value snapshot of the message's mutable delivery status, captured at
|
||||
/// construction (see `TextMessageView.deliveryStatus`): `BitchatMessage`
|
||||
/// is a reference type mutated in place, and SwiftUI compares reference
|
||||
/// fields by identity, so without the snapshot a status-only change
|
||||
/// (send progress, delivered → read) would not re-render this row.
|
||||
private let deliveryStatus: DeliveryStatus?
|
||||
|
||||
@Binding var imagePreviewURL: URL?
|
||||
|
||||
init(message: BitchatMessage, media: BitchatMessage.Media, imagePreviewURL: Binding<URL?>) {
|
||||
self.message = message
|
||||
self.media = media
|
||||
self.deliveryStatus = message.deliveryStatus
|
||||
self._imagePreviewURL = imagePreviewURL
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
let state = mediaSendState(for: message)
|
||||
let state = mediaSendState(for: deliveryStatus)
|
||||
let isFromMe = conversationUIModel.isMediaMessageFromCurrentUser(message)
|
||||
let cancelAction: (() -> Void)? = state.canCancel ? { conversationUIModel.cancelMediaSend(messageID: message.id) } : nil
|
||||
|
||||
@@ -27,7 +40,7 @@ struct MediaMessageView: View {
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
if message.isPrivate && conversationUIModel.isSentByCurrentUser(message),
|
||||
let status = message.deliveryStatus {
|
||||
let status = deliveryStatus {
|
||||
DeliveryStatusView(status: status)
|
||||
.padding(.leading, 4)
|
||||
}
|
||||
@@ -63,10 +76,10 @@ struct MediaMessageView: View {
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
|
||||
private func mediaSendState(for message: BitchatMessage) -> (isSending: Bool, progress: Double?, canCancel: Bool) {
|
||||
private func mediaSendState(for deliveryStatus: DeliveryStatus?) -> (isSending: Bool, progress: Double?, canCancel: Bool) {
|
||||
var isSending = false
|
||||
var progress: Double?
|
||||
if let status = message.deliveryStatus {
|
||||
if let status = deliveryStatus {
|
||||
switch status {
|
||||
case .sending:
|
||||
isSending = true
|
||||
|
||||
Reference in New Issue
Block a user