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>
98 lines
3.3 KiB
Swift
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
|
|
}
|
|
}
|