mirror of
https://github.com/ichmagmaus111/ghostgram.git
synced 2026-11-03 22:27:13 +01:00
Ghostgram for iOS, rebuilt from scratch on Telegram iOS 13.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
98 lines
5.1 KiB
Swift
98 lines
5.1 KiB
Swift
import Foundation
|
|
import Postbox
|
|
import TelegramCore
|
|
import RichTextEditorCore
|
|
|
|
/// Convenience converters for the chat composer ↔ expanded article-editor (`RichTextAttachmentScreen`)
|
|
/// handoff. They wrap the structure-preserving `Document ↔ ChatInputContent` bridge with the media-store
|
|
/// collection / lookup the article editor speaks (`(Document, [String: Media])`), so the handoff can flow
|
|
/// entirely through the chat input STATE (`ChatInputContent`) rather than poking the composer node directly.
|
|
|
|
/// Stable, opaque editor-side key for a `Media`, derived from its `MediaId` (`"namespace:id"`), or an
|
|
/// object-identity fallback for an id-less medium. Shared by the composer node's media store
|
|
/// (`RichTextEditorChatInputNode.registerMediaValue`), the `documentMediaAndEmoji(fromChatInputContent:)`
|
|
/// expand seed, and `RichTextAttachmentScreen.attachedMedia`, so a medium keyed one way resolves the other.
|
|
public func composerMediaKey(_ media: Media) -> String {
|
|
if let id = media.id {
|
|
return "\(id.namespace):\(id.id)"
|
|
} else {
|
|
// No stable media id (rare): fall back to an object-identity key so the same value still
|
|
// round-trips within a single set/get cycle.
|
|
return "anon:\(ObjectIdentifier(media as AnyObject).hashValue)"
|
|
}
|
|
}
|
|
|
|
/// Convert a chat-layer `ChatInputContent` to an editor `Document`, the media store it references (keyed by
|
|
/// `composerMediaKey`), and the custom-emoji file store (keyed by fileId) — the OUT (expand) half of the
|
|
/// composer ↔ article-editor handoff. The editor `Document` itself carries each custom emoji by fileId STRING
|
|
/// only (it has no `TelegramMediaFile` slot), so the files ride alongside in `emojiFiles`; the article editor
|
|
/// seeds its emoji store from that map so the emoji render and their files survive back out. The inverse is
|
|
/// `chatInputContent(fromDocument:media:emojiFiles:)`.
|
|
public func documentMediaAndEmoji(fromChatInputContent content: ChatInputContent) -> (document: Document, media: [String: Media], emojiFiles: [Int64: TelegramMediaFile]) {
|
|
var media: [String: Media] = [:]
|
|
var emojiFiles: [Int64: TelegramMediaFile] = [:]
|
|
let doc = document(
|
|
fromChatInputContent: content,
|
|
registerEmoji: { fileId, file in
|
|
if let file {
|
|
emojiFiles[fileId] = file
|
|
}
|
|
return EmojiRef(id: String(fileId), instanceID: BlockID.generate().rawValue, altText: nil)
|
|
},
|
|
registerMedia: { value in
|
|
let key = composerMediaKey(value)
|
|
media[key] = value
|
|
return key
|
|
}
|
|
)
|
|
return (document: doc, media: media, emojiFiles: emojiFiles)
|
|
}
|
|
|
|
public func documentMediaAndEmojiAsync(engine: TelegramEngine, fromChatInputContent content: ChatInputContent) async -> (document: Document, media: [String: Media], emojiFiles: [Int64: TelegramMediaFile]) {
|
|
var media: [String: Media] = [:]
|
|
var emojiFiles: [Int64: TelegramMediaFile] = [:]
|
|
var emojiFileIds = Set<Int64>()
|
|
let doc = document(
|
|
fromChatInputContent: content,
|
|
registerEmoji: { fileId, file in
|
|
emojiFileIds.insert(fileId)
|
|
if let file {
|
|
emojiFiles[fileId] = file
|
|
}
|
|
return EmojiRef(id: String(fileId), instanceID: BlockID.generate().rawValue, altText: nil)
|
|
},
|
|
registerMedia: { value in
|
|
let key = composerMediaKey(value)
|
|
media[key] = value
|
|
return key
|
|
}
|
|
)
|
|
let missingFileIds = emojiFileIds.subtracting(Set(emojiFiles.keys))
|
|
if !missingFileIds.isEmpty {
|
|
let addedFiles = await engine.stickers.resolveInlineStickersLocal(fileIds: Array(missingFileIds)).get()
|
|
emojiFiles.merge(addedFiles, uniquingKeysWith: { lhs, _ in lhs })
|
|
}
|
|
return (document: doc, media: media, emojiFiles: emojiFiles)
|
|
}
|
|
|
|
/// Convert an expanded article editor's `Document` + its media store + its custom-emoji file store back to the
|
|
/// chat-layer `ChatInputContent` — the IN (collapse) half of the handoff (`RichTextAttachmentScreen.sendMessage`).
|
|
/// The inverse of `documentMediaAndEmoji(fromChatInputContent:)`. A medium is resolved from `media` (an
|
|
/// unresolved key drops the block); a custom emoji's `TelegramMediaFile` is resolved from `emojiFiles` (the
|
|
/// editor `Document` carries only the fileId) — re-attaching it is REQUIRED for the chat composer to render the
|
|
/// emoji, since its native node caches/renders by the file carried on the run. A non-numeric emoji id (should
|
|
/// not occur from this path) degrades the run to plain text; a missing file falls back to `nil` (the run still
|
|
/// round-trips structurally, but won't render until decoration re-derives the file).
|
|
public func chatInputContent(fromDocument document: Document, media: [String: Media], emojiFiles: [Int64: TelegramMediaFile]) -> ChatInputContent {
|
|
return chatInputContent(
|
|
fromDocument: document,
|
|
resolveEmoji: { emojiRef in
|
|
guard let fileId = Int64(emojiRef.id) else {
|
|
return nil
|
|
}
|
|
return (fileId: fileId, file: emojiFiles[fileId])
|
|
},
|
|
resolveMedia: { mediaID in media[mediaID] }
|
|
)
|
|
}
|