mirror of
https://github.com/ichmagmaus111/ghostgram.git
synced 2026-04-23 16:16:08 +02:00
fix: three critical bugs in scheduled send and account switcher
Bug 1 — Video recorder freeze with SendDelayManager enabled
When SendDelayManager is active, video notes (кружки) and media are
enqueued into Namespaces.Message.ScheduledLocal, not the main chat
history. This broke setupSendActionOnViewUpdate which expects the message
to appear in the regular history before triggering its callback (dismiss
recorder, collapse input). The callback never fired → recorder overlay
stayed on screen → app froze.
Fix: in requestVideoRecorder's completion closure, detect when
SendDelayManager.shared.isEnabled, immediately dismiss the recorder and
clear the interface state, bypassing the broken animation path.
Bug 2 — Scheduled messages remain visible after being sent
When AntiDeleteManager is enabled, the .DeleteMessages case in
AccountStateManagementUtils uses to skip non-Cloud namespaces
(ScheduledCloud, ScheduledLocal). However after the loop no code
physically removed those skipped messages from Postbox — they stayed
in the scheduled list forever, appearing as 'planned' messages that
never disappeared.
Fix: collect non-Cloud IDs during the Anti-Delete loop and physically
delete them via _internal_deleteMessages after the loop.
Bug 3 — Account switcher avatar not loading
Race condition in avatar loading: resourceData was subscribed to first,
then fetchedMediaResource triggered the network fetch. The signal's
callback fired before data arrived, calling buildButton(nil)
which discarded the real avatar.
Fix: trigger fetchedMediaResource first, then subscribe to resourceData
with filter { $0.complete } |> take(1) so the signal stays alive until
the download completes.
This commit is contained in:
@@ -6695,20 +6695,10 @@ private final class ChatListLocationContext {
|
||||
let resource = representation.resource
|
||||
let account = nextAccount.account
|
||||
|
||||
// Try to read cached data first; if not ready, trigger a fetch then watch for completion
|
||||
self.accountSwitcherAvatarDisposable = (account.postbox.mediaBox
|
||||
.resourceData(resource)
|
||||
|> deliverOnMainQueue)
|
||||
.start(next: { data in
|
||||
if data.complete, let uiImage = UIImage(contentsOfFile: data.path) {
|
||||
buildButton(uiImage)
|
||||
}
|
||||
}, completed: {
|
||||
// If resource was never complete after signal ended, show placeholder
|
||||
buildButton(nil)
|
||||
})
|
||||
|
||||
// Trigger the actual network fetch so mediaBox populates the resource
|
||||
// GHOSTGRAM: Fetch first so the resource is populated by the time
|
||||
// resourceData emits a complete result. The old order (subscribe→fetch)
|
||||
// had a race where `completed` fired before data arrived, causing
|
||||
// buildButton(nil) to be called and the avatar to never show.
|
||||
if let peerReference = PeerReference(nextPeer) {
|
||||
let _ = fetchedMediaResource(
|
||||
mediaBox: account.postbox.mediaBox,
|
||||
@@ -6717,6 +6707,19 @@ private final class ChatListLocationContext {
|
||||
reference: .avatar(peer: peerReference, resource: resource)
|
||||
).start()
|
||||
}
|
||||
|
||||
self.accountSwitcherAvatarDisposable = (account.postbox.mediaBox
|
||||
.resourceData(resource)
|
||||
|> filter { $0.complete }
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue)
|
||||
.start(next: { data in
|
||||
if let uiImage = UIImage(contentsOfFile: data.path) {
|
||||
buildButton(uiImage)
|
||||
} else {
|
||||
buildButton(nil)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// No photo — show placeholder
|
||||
buildButton(nil)
|
||||
|
||||
@@ -4378,9 +4378,18 @@ func replayFinalState(
|
||||
|
||||
// ANTI-DELETE: Mark messages as deleted instead of removing them
|
||||
if AntiDeleteManager.shared.isEnabled {
|
||||
// GHOSTGRAM: Collect non-Cloud IDs (scheduled/local) that must be
|
||||
// physically removed even when AntiDelete is on. Without this, sent
|
||||
// scheduled messages stay stuck in the scheduled list forever because
|
||||
// the `continue` guard skips them but nothing else removes them.
|
||||
var nonCloudIdsToDelete: [MessageId] = []
|
||||
|
||||
for messageId in ids {
|
||||
// Skip scheduled/local/quick-reply messages — they get deleted when sent, not by the remote peer
|
||||
guard messageId.namespace == Namespaces.Message.Cloud else { continue }
|
||||
guard messageId.namespace == Namespaces.Message.Cloud else {
|
||||
nonCloudIdsToDelete.append(messageId)
|
||||
continue
|
||||
}
|
||||
|
||||
// Mark as deleted for icon display
|
||||
AntiDeleteManager.shared.markAsDeleted(peerId: messageId.peerId.toInt64(), messageId: messageId.id)
|
||||
@@ -4396,6 +4405,13 @@ func replayFinalState(
|
||||
return .update(StoreMessage(id: currentMessage.id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
|
||||
})
|
||||
}
|
||||
|
||||
// Physically remove scheduled/local messages that were skipped above
|
||||
if !nonCloudIdsToDelete.isEmpty {
|
||||
_internal_deleteMessages(transaction: transaction, mediaBox: mediaBox, ids: nonCloudIdsToDelete, manualAddMessageThreadStatsDifference: { id, add, remove in
|
||||
addMessageThreadStatsDifference(threadKey: id, remove: remove, addedMessagePeer: nil, addedMessageId: nil, isOutgoing: false)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
_internal_deleteMessages(transaction: transaction, mediaBox: mediaBox, ids: ids, manualAddMessageThreadStatsDifference: { id, add, remove in
|
||||
addMessageThreadStatsDifference(threadKey: id, remove: remove, addedMessagePeer: nil, addedMessageId: nil, isOutgoing: false)
|
||||
|
||||
@@ -208,7 +208,16 @@ extension ChatControllerImpl {
|
||||
}
|
||||
|
||||
var usedCorrelationId = false
|
||||
if scheduleTime == nil, shouldAnimateMessageTransition, let extractedView = videoController.extractVideoSnapshot() {
|
||||
|
||||
// GHOSTGRAM: When SendDelayManager is active the message lands in
|
||||
// ScheduledLocal namespace, NOT in the main history. This means
|
||||
// setupSendActionOnViewUpdate's callback would NEVER fire (it waits
|
||||
// for the message to appear in the normal chat view), causing the
|
||||
// video recorder overlay to stay on screen and the app to freeze.
|
||||
// Solution: dismiss the recorder immediately and skip the animation.
|
||||
let isSendDelayActive = SendDelayManager.shared.isEnabled
|
||||
|
||||
if !isSendDelayActive, scheduleTime == nil, shouldAnimateMessageTransition, let extractedView = videoController.extractVideoSnapshot() {
|
||||
usedCorrelationId = true
|
||||
self.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .videoMessage(ChatMessageTransitionNodeImpl.Source.VideoMessage(view: extractedView)), initiated: { [weak videoController, weak self] in
|
||||
videoController?.hideVideoSnapshot()
|
||||
@@ -221,15 +230,25 @@ extension ChatControllerImpl {
|
||||
self.videoRecorder.set(.single(nil))
|
||||
}
|
||||
|
||||
self.chatDisplayNode.setupSendActionOnViewUpdate({ [weak self] in
|
||||
if let self {
|
||||
self.chatDisplayNode.collapseInput()
|
||||
|
||||
self.updateChatPresentationInterfaceState(animated: true, interactive: false, {
|
||||
$0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil).withUpdatedMediaDraftState(nil).withUpdatedPostSuggestionState(nil) }
|
||||
})
|
||||
}
|
||||
}, usedCorrelationId ? correlationId : nil)
|
||||
if isSendDelayActive {
|
||||
// Dismiss recorder and clear state immediately without waiting
|
||||
// for the scheduled message to appear in history.
|
||||
self.chatDisplayNode.collapseInput()
|
||||
self.updateChatPresentationInterfaceState(animated: true, interactive: false, {
|
||||
$0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil).withUpdatedMediaDraftState(nil).withUpdatedPostSuggestionState(nil) }
|
||||
})
|
||||
} else {
|
||||
self.chatDisplayNode.setupSendActionOnViewUpdate({ [weak self] in
|
||||
if let self {
|
||||
self.chatDisplayNode.collapseInput()
|
||||
|
||||
self.updateChatPresentationInterfaceState(animated: true, interactive: false, {
|
||||
$0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil).withUpdatedMediaDraftState(nil).withUpdatedPostSuggestionState(nil) }
|
||||
})
|
||||
}
|
||||
}, usedCorrelationId ? correlationId : nil)
|
||||
}
|
||||
|
||||
|
||||
let messages = [message]
|
||||
let transformedMessages: [EnqueueMessage]
|
||||
|
||||
Reference in New Issue
Block a user