diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 8de604b6..6b6cf315 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -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) diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index d252bc6b..5215a042 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -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) diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerMediaRecording.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerMediaRecording.swift index 807b2f12..3142a054 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerMediaRecording.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerMediaRecording.swift @@ -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]