Files
ichmagmaus111andClaude Opus 5.5 0622057a4f Ghostgram: open-source release
Ghostgram for iOS, rebuilt from scratch on Telegram iOS 13.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 17:57:09 +01:00

113 lines
4.3 KiB
Swift

import Foundation
// Successive drafts must be distinct messages, not one message that changes: node-local
// state in the chat UI (the text-reveal cursor, the rich-data expand state) is keyed by
// MessageId, so a shared sentinel would let a replacement inherit its predecessor's state.
//
// Deterministic in randomId — every update of one draft must land on the same id, or each
// streaming chunk would mint a new item and destroy the reveal. Namespace 1 is the Local
// namespace, which the chat layer relies on to suppress the context menu for drafts. The
// band sits far above any id a pending outgoing local message will reach. A hash collision
// between two successive drafts is harmless: stableId still differs, so the list still
// builds a new node, and node-local state does not carry across nodes.
func typingDraftMessageId(peerId: PeerId, randomId: Int64) -> MessageId {
let folded = UInt64(bitPattern: randomId)
let hashed = folded ^ (folded >> 32)
let offset = Int32(truncatingIfNeeded: hashed % 65536)
return MessageId(peerId: peerId, namespace: 1, id: Int32.max - 50000 - offset)
}
final class MutableTypingDraftsView: MutablePostboxView {
fileprivate let peerAndThreadId: PeerAndThreadId
fileprivate var typingDraft: Message?
init(postbox: PostboxImpl, peerAndThreadId: PeerAndThreadId) {
self.peerAndThreadId = peerAndThreadId
self.reload(postbox: postbox)
}
private func reload(postbox: PostboxImpl) {
if let typingDraft = postbox.currentTypingDrafts[self.peerAndThreadId] {
self.typingDraft = self.renderTypingDraft(postbox: postbox, typingDraft: typingDraft)
} else {
self.typingDraft = nil
}
}
func replay(postbox: PostboxImpl, transaction: PostboxTransaction) -> Bool {
var updated = false
if let typingDraftUpdate = transaction.updatedTypingDrafts[self.peerAndThreadId] {
if let typingDraft = typingDraftUpdate.value {
self.typingDraft = self.renderTypingDraft(postbox: postbox, typingDraft: typingDraft)
} else {
self.typingDraft = nil
}
updated = true
}
return updated
}
private func renderTypingDraft(postbox: PostboxImpl, typingDraft: PostboxImpl.TypingDraft) -> Message? {
guard let peer = postbox.peerTable.get(self.peerAndThreadId.peerId), let author = postbox.peerTable.get(typingDraft.authorId) else {
return nil
}
var peers = SimpleDictionary<PeerId, Peer>()
peers[peer.id] = peer
peers[author.id] = author
var associatedThreadInfo: Message.AssociatedThreadInfo?
if let threadId = typingDraft.threadId, let data = postbox.messageHistoryThreadIndexTable.get(peerId: self.peerAndThreadId.peerId, threadId: threadId) {
associatedThreadInfo = postbox.seedConfiguration.decodeMessageThreadInfo(data.data)
}
return Message(
stableId: typingDraft.stableId,
stableVersion: typingDraft.stableVersion,
id: typingDraftMessageId(peerId: self.peerAndThreadId.peerId, randomId: typingDraft.id),
globallyUniqueId: nil,
groupingKey: nil,
groupInfo: nil,
threadId: typingDraft.threadId,
timestamp: typingDraft.timestamp,
flags: [.Incoming],
tags: [],
globalTags: [],
localTags: [],
customTags: [],
forwardInfo: nil,
author: author,
text: typingDraft.text,
attributes: typingDraft.attributes,
media: [],
peers: peers,
associatedMessages: SimpleDictionary(),
associatedMessageIds: [],
associatedMedia: [:],
associatedThreadInfo: associatedThreadInfo,
associatedStories: [:]
)
}
func refreshDueToExternalTransaction(postbox: PostboxImpl) -> Bool {
self.reload(postbox: postbox)
return true
}
func immutableView() -> PostboxView {
return TypingDraftsView(self)
}
}
public final class TypingDraftsView: PostboxView {
public let typingDraft: Message?
init(_ view: MutableTypingDraftsView) {
self.typingDraft = view.typingDraft
}
}