Skip to content

Commit b734448

Browse files
isaacclaude
andcommitted
feat(richtext): persist rich-text composer drafts across attachment-menu sessions
Store the rich-text editor's document/media/emoji as a Codable draft in the item cache keyed by peerId+threadId: load on attachment-menu open, save on syncContent, clear on send. Media/TelegramMediaFile (not Codable) round-trip as self-describing root objects. Adds Namespaces.CachedItemCollection.richTextComposerDrafts and reserves ApplicationSpecificItemCacheCollectionId.richTextArticleDrafts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent d57c452 commit b734448

3 files changed

Lines changed: 116 additions & 5 deletions

File tree

‎submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ public struct Namespaces {
155155
public static let cachedGiftUpgradesAttributes: Int8 = 52
156156
public static let cachedCloudAITextStyles: Int8 = 53
157157
public static let cachedCommunityPeerLinkRequests: Int8 = 54
158+
public static let richTextComposerDrafts: Int8 = 55
158159
}
159160

160161
public struct UnorderedItemList {

‎submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift‎

Lines changed: 113 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,110 @@ extension ChatControllerImpl {
5858
return
5959
}
6060

61-
var richTextDraft: (document: RichTextAttachmentScreen.Document?, media: [String: Media], emojiFiles: [Int64: TelegramMediaFile])?
61+
struct RichTextDraft: Codable {
62+
enum LoadError: Error {
63+
case generic
64+
}
65+
66+
private enum CodingKeys: String, CodingKey {
67+
case document
68+
case mediaKeys
69+
case mediaValues
70+
case emojiFilesKeys
71+
case emojiFilesValues
72+
}
73+
74+
let document: RichTextAttachmentScreen.Document
75+
let media: [String: Media]
76+
let emojiFiles: [Int64: TelegramMediaFile]
77+
78+
init(document: RichTextAttachmentScreen.Document, media: [String: Media], emojiFiles: [Int64: TelegramMediaFile]) {
79+
self.document = document
80+
self.media = media
81+
self.emojiFiles = emojiFiles
82+
}
83+
84+
init(from decoder: Decoder) throws {
85+
let container = try decoder.container(keyedBy: CodingKeys.self)
86+
87+
let documentData = try container.decode(Data.self, forKey: .document)
88+
self.document = try JSONDecoder().decode(RichTextAttachmentScreen.Document.self, from: documentData)
89+
90+
// `Media`/`TelegramMediaFile` are Postbox-coded (not `Codable`), so each value is stored as a
91+
// self-describing root object (its concrete type is recovered from the embedded type hash).
92+
let mediaKeys = try container.decode([String].self, forKey: .mediaKeys)
93+
let mediaValues = try container.decode([Data].self, forKey: .mediaValues)
94+
if mediaKeys.count != mediaValues.count {
95+
throw LoadError.generic
96+
}
97+
var media: [String: Media] = [:]
98+
for i in 0 ..< mediaKeys.count {
99+
guard let mediaValue = PostboxDecoder(buffer: MemoryBuffer(data: mediaValues[i])).decodeRootObject() as? Media else {
100+
throw LoadError.generic
101+
}
102+
media[mediaKeys[i]] = mediaValue
103+
}
104+
self.media = media
105+
106+
let emojiFilesKeys = try container.decode([Int64].self, forKey: .emojiFilesKeys)
107+
let emojiFilesValues = try container.decode([Data].self, forKey: .emojiFilesValues)
108+
if emojiFilesKeys.count != emojiFilesValues.count {
109+
throw LoadError.generic
110+
}
111+
var emojiFiles: [Int64: TelegramMediaFile] = [:]
112+
for i in 0 ..< emojiFilesKeys.count {
113+
guard let emojiFileValue = PostboxDecoder(buffer: MemoryBuffer(data: emojiFilesValues[i])).decodeRootObject() as? TelegramMediaFile else {
114+
throw LoadError.generic
115+
}
116+
emojiFiles[emojiFilesKeys[i]] = emojiFileValue
117+
}
118+
self.emojiFiles = emojiFiles
119+
}
120+
121+
func encode(to encoder: Encoder) throws {
122+
var container = encoder.container(keyedBy: CodingKeys.self)
123+
124+
let documentData = try JSONEncoder().encode(self.document)
125+
try container.encode(documentData, forKey: .document)
126+
127+
var mediaKeys: [String] = []
128+
var mediaValues: [Data] = []
129+
for (key, value) in self.media {
130+
let mediaEncoder = PostboxEncoder()
131+
mediaEncoder.encodeRootObject(value)
132+
mediaKeys.append(key)
133+
mediaValues.append(mediaEncoder.makeData())
134+
}
135+
try container.encode(mediaKeys, forKey: .mediaKeys)
136+
try container.encode(mediaValues, forKey: .mediaValues)
137+
138+
var emojiFilesKeys: [Int64] = []
139+
var emojiFilesValues: [Data] = []
140+
for (key, value) in self.emojiFiles {
141+
let fileEncoder = PostboxEncoder()
142+
fileEncoder.encodeRootObject(value)
143+
emojiFilesKeys.append(key)
144+
emojiFilesValues.append(fileEncoder.makeData())
145+
}
146+
try container.encode(emojiFilesKeys, forKey: .emojiFilesKeys)
147+
try container.encode(emojiFilesValues, forKey: .emojiFilesValues)
148+
}
149+
}
150+
151+
var richTextDraft: RichTextDraft?
152+
153+
var richTextDraftKey: EngineDataBuffer?
154+
if let peerId = self.chatLocation.peerId {
155+
let key = EngineDataBuffer(length: 8 + 8)
156+
key.setInt64(0, value: peerId.toInt64())
157+
key.setInt64(8, value: self.chatLocation.threadId ?? 0)
158+
richTextDraftKey = key
159+
if let entry = await self.context.engine.data.get(
160+
TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: Namespaces.CachedItemCollection.richTextComposerDrafts, id: key)
161+
).get() {
162+
richTextDraft = entry.get(RichTextDraft.self)
163+
}
164+
}
62165

63166
let context = self.context
64167
let inputIsActive = self.presentationInterfaceState.inputMode == .text
@@ -852,9 +955,11 @@ extension ChatControllerImpl {
852955
return
853956
}
854957

855-
//TODO:clear draft in db
856958
richTextDraft = nil
857-
959+
if let richTextDraftKey {
960+
let _ = strongSelf.context.engine.itemCache.remove(collectionId: Namespaces.CachedItemCollection.richTextComposerDrafts, id: richTextDraftKey).start()
961+
}
962+
858963
let text: String
859964
let attributes: [EngineMessage.Attribute]
860965
if sendWithoutFormatting {
@@ -918,8 +1023,11 @@ extension ChatControllerImpl {
9181023
guard let self else {
9191024
return
9201025
}
921-
let _ = self
922-
richTextDraft = (document, media, emojiFiles)
1026+
let draft = RichTextDraft(document: document, media: media, emojiFiles: emojiFiles)
1027+
richTextDraft = draft
1028+
if let richTextDraftKey {
1029+
let _ = self.context.engine.itemCache.put(collectionId: Namespaces.CachedItemCollection.richTextComposerDrafts, id: richTextDraftKey, item: draft).start()
1030+
}
9231031
},
9241032
presentAttachmentMenu: { [weak self] photoVideoOnly, completion in
9251033
guard let self else {

‎submodules/TelegramUIPreferences/Sources/PostboxKeys.swift‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ private enum ApplicationSpecificItemCacheCollectionIdValues: Int8 {
9393
case mediaEditorState = 12
9494
case shareWithPeersState = 13
9595
case webAppPermissionsState = 14
96+
case richTextArticleDrafts = 15
9697
}
9798

9899
public struct ApplicationSpecificItemCacheCollectionId {
@@ -108,6 +109,7 @@ public struct ApplicationSpecificItemCacheCollectionId {
108109
public static let mediaEditorState = applicationSpecificItemCacheCollectionId(ApplicationSpecificItemCacheCollectionIdValues.mediaEditorState.rawValue)
109110
public static let shareWithPeersState = applicationSpecificItemCacheCollectionId(ApplicationSpecificItemCacheCollectionIdValues.shareWithPeersState.rawValue)
110111
public static let webAppPermissionsState = applicationSpecificItemCacheCollectionId(ApplicationSpecificItemCacheCollectionIdValues.webAppPermissionsState.rawValue)
112+
public static let richTextArticleDrafts = applicationSpecificItemCacheCollectionId(ApplicationSpecificItemCacheCollectionIdValues.richTextArticleDrafts.rawValue)
111113
}
112114

113115
private enum ApplicationSpecificOrderedItemListCollectionIdValues: Int32 {

0 commit comments

Comments
 (0)