WinterGram/submodules/Postbox/Sources/TypingDraftsView.swift
isaac e7662a3de5 Add typing-draft send delay
Delay outgoing messages while the peer in the same (peerId, threadId)
chat is live-typing an incoming message, gated via a new per-account
Postbox `.allTypingDrafts` view feeding a synchronous Set membership
check inside PendingMessageManager.

Postbox:
- Add `AllTypingDraftsView` tracking `Set<PeerAndThreadId>` of active
  typing drafts; expose via `PostboxViewKey.allTypingDrafts`.
- Disambiguate `PostboxViewKey` hash arms for `.typingDrafts` (constant
  prefix 23) and `.contacts` (16 → 24) to avoid runtime collisions
  with peerId-only arms and `.combinedReadState`. Hash values are
  runtime-only, so this is safe.

PendingMessageManager:
- Add `.waitingForSendGate(groupId:content:)` parking state for single
  messages and albums whose upload finished but are gated on a
  typing-draft-clearing event; update `groupId` accessor and
  `dataForPendingMessageGroup` so partially-parked albums drain on
  gate open.
- Add `.waitingForForwardSendGate` parking state for forwards (no
  associated values) so drain section (1) routes single non-grouped
  messages via `commitSendingSingleMessage` and section (3) routes
  forwards via `sendGroupMessagesContent`, avoiding the double-dispatch
  that occurred when forwards reused `.waitingForSendGate`.
- Subscribe to `.allTypingDrafts`; wire the gate at single-message,
  album, and forward send paths and at drain.
- Cleanup parked forwards on pending-message removal; snapshot
  Dictionary keys before iterating to avoid mutation during iteration.

Specs and plan documents are included under `docs/superpowers/`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 12:15:55 +04:00

98 lines
3.3 KiB
Swift

import Foundation
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: MessageId(
peerId: self.peerAndThreadId.peerId,
namespace: 1,
id: Int32.max - 50000),
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
}
}