Merge pull request #1 from ichmagmaus111/ghostgram-update

Ghostgram update
This commit is contained in:
kuromiplayer
2026-03-04 23:53:08 +01:00
committed by GitHub
8 changed files with 89 additions and 28 deletions
@@ -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 (subscribefetch)
// 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)
@@ -152,6 +152,9 @@ func settingsItems(data: PeerInfoScreenData?, context: AccountContext, presentat
items[.myProfile]!.append(PeerInfoScreenDisclosureItem(id: 0, text: presentationData.strings.Settings_MyProfile, icon: PresentationResourcesSettings.myProfile, action: {
interaction.openSettings(.profile)
}))
items[.myProfile]!.append(PeerInfoScreenDisclosureItem(id: 1001, text: "Ghostgram Settings", icon: UIImage(bundleImageName: "Settings/Menu/GhostgramSettings"), action: {
interaction.openSettings(.ghostgram)
}))
if !settings.proxySettings.servers.isEmpty {
let proxyType: String
@@ -232,9 +235,6 @@ func settingsItems(data: PeerInfoScreenData?, context: AccountContext, presentat
items[.advanced]!.append(PeerInfoScreenDisclosureItem(id: 1, text: presentationData.strings.Settings_PrivacySettings, icon: PresentationResourcesSettings.security, action: {
interaction.openSettings(.privacyAndSecurity)
}))
items[.advanced]!.append(PeerInfoScreenDisclosureItem(id: 1001, text: "Ghostgram Settings", icon: UIImage(bundleImageName: "Settings/Menu/Appearance"), action: {
interaction.openSettings(.ghostgram)
}))
items[.advanced]!.append(PeerInfoScreenDisclosureItem(id: 2, text: presentationData.strings.Settings_ChatSettings, icon: PresentationResourcesSettings.dataAndStorage, action: {
interaction.openSettings(.dataAndStorage)
}))
@@ -0,0 +1,23 @@
{
"images": [
{
"filename": "ghost_settings.png",
"idiom": "universal",
"scale": "1x"
},
{
"filename": "ghost_settings@2x.png",
"idiom": "universal",
"scale": "2x"
},
{
"filename": "ghost_settings@3x.png",
"idiom": "universal",
"scale": "3x"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

@@ -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]