@@ -596,15 +596,91 @@ private func forwardedMessageToBeReuploaded(transaction: Transaction, id: Messag
596596/// Build a standalone media reference for AyuForward / secret-chat reupload, remapping to local MediaIds
597597/// so PendingMessageManager uploads instead of calling forwardMessages.
598598private func ayuForwardMediaReference( account: Account , sourceMessage: Message ) -> AnyMediaReference ? {
599+ // Locally deleted: prefer durable Saved Attachments before remapping stale cloud media ids.
600+ if sourceMessage. isLocallyDeleted {
601+ if let path = sourceMessage. locallyDeletedMediaPath
602+ ?? MessageSavingAttachments . existingCopyPath ( message: sourceMessage, mediaBox: account. postbox. mediaBox) ,
603+ FileManager . default. fileExists ( atPath: path) ,
604+ let reference = ayuForwardMediaReferenceFromLocalPath ( path, account: account) {
605+ return reference
606+ }
607+ }
599608 for media in sourceMessage. media {
600609 if media is TelegramMediaImage || media is TelegramMediaFile {
601610 return . standalone( media: convertMediaForAyuForward ( media, mediaBox: account. postbox. mediaBox) )
602611 }
603612 }
604- // Deleted message whose cloud media is gone — fall back to Saved Attachments / mediaPath.
613+ // Cloud media gone / unsupported — fall back to Saved Attachments / mediaPath.
605614 let path = sourceMessage. locallyDeletedMediaPath
606615 ?? MessageSavingAttachments . existingCopyPath ( message: sourceMessage, mediaBox: account. postbox. mediaBox)
607- if let path, FileManager . default. fileExists ( atPath: path) , let data = try ? Data ( contentsOf: URL ( fileURLWithPath: path) ) {
616+ if let path, FileManager . default. fileExists ( atPath: path) , let reference = ayuForwardMediaReferenceFromLocalPath ( path, account: account) {
617+ return reference
618+ }
619+ return nil
620+ }
621+
622+ /// Load a local attachment for AyuForward. Prefer MediaBox path references over reading whole files into RAM
623+ /// when the file is already present as a MediaBox resource; otherwise store via mapped read for modest sizes.
624+ private func ayuForwardMediaReferenceFromLocalPath( _ path: String , account: Account ) -> AnyMediaReference ? {
625+ guard FileManager . default. fileExists ( atPath: path) else {
626+ return nil
627+ }
628+ // Bound sync read — huge videos should already be in mediaBox from save-media; skip if oversized.
629+ let attrs = try ? FileManager . default. attributesOfItem ( atPath: path)
630+ let fileSize = ( attrs ? [ . size] as? NSNumber ) ? . int64Value ?? 0
631+ if fileSize <= 0 || fileSize > 64 * 1024 * 1024 {
632+ // Still try MediaBox-friendly LocalFileReference when possible without loading bytes.
633+ if fileSize > 0 , fileSize <= 512 * 1024 * 1024 {
634+ let resource = LocalFileReferenceMediaResource ( localFilePath: path, randomId: Int64 . random ( in: Int64 . min ... Int64 . max) , size: fileSize)
635+ let ext = ( path as NSString ) . pathExtension. lowercased ( )
636+ if [ " jpg " , " jpeg " , " png " , " webp " , " heic " , " gif " ] . contains ( ext) {
637+ let representation = TelegramMediaImageRepresentation (
638+ dimensions: PixelDimensions ( width: 1280 , height: 1280 ) ,
639+ resource: resource,
640+ progressiveSizes: [ ] ,
641+ immediateThumbnailData: nil ,
642+ hasVideo: false ,
643+ isPersonal: false
644+ )
645+ let image = TelegramMediaImage (
646+ imageId: MediaId ( namespace: Namespaces . Media. LocalImage, id: Int64 . random ( in: Int64 . min ... Int64 . max) ) ,
647+ representations: [ representation] ,
648+ immediateThumbnailData: nil ,
649+ reference: nil ,
650+ partialReference: nil ,
651+ flags: [ ]
652+ )
653+ return . standalone( media: image)
654+ }
655+ let mime : String
656+ switch ext {
657+ case " mp4 " , " m4v " : mime = " video/mp4 "
658+ case " mov " : mime = " video/quicktime "
659+ case " mp3 " : mime = " audio/mpeg "
660+ case " ogg " , " opus " : mime = " audio/ogg "
661+ default : mime = " application/octet-stream "
662+ }
663+ var attributes : [ TelegramMediaFileAttribute ] = [ . FileName( fileName: ( path as NSString ) . lastPathComponent) ]
664+ if mime. hasPrefix ( " video/ " ) {
665+ attributes. append ( . Video( duration: 0.0 , size: PixelDimensions ( width: 1280 , height: 720 ) , flags: [ ] , preloadSize: nil , coverTime: nil , videoCodec: nil ) )
666+ }
667+ let file = TelegramMediaFile (
668+ fileId: MediaId ( namespace: Namespaces . Media. LocalFile, id: Int64 . random ( in: Int64 . min ... Int64 . max) ) ,
669+ partialReference: nil ,
670+ resource: resource,
671+ previewRepresentations: [ ] ,
672+ videoThumbnails: [ ] ,
673+ immediateThumbnailData: nil ,
674+ mimeType: mime,
675+ size: fileSize,
676+ attributes: attributes,
677+ alternativeRepresentations: [ ]
678+ )
679+ return . standalone( media: file)
680+ }
681+ return nil
682+ }
683+ if let data = try ? Data ( contentsOf: URL ( fileURLWithPath: path) , options: [ . mappedIfSafe] ) {
608684 let resource = LocalFileMediaResource ( fileId: Int64 . random ( in: Int64 . min ... Int64 . max) , size: Int64 ( data. count) )
609685 account. postbox. mediaBox. storeResourceData ( resource. id, data: data, synchronous: true )
610686 let ext = ( path as NSString ) . pathExtension. lowercased ( )
@@ -891,6 +967,8 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId,
891967 attributes. append ( attribute)
892968 }
893969 }
970+ // Do not carry source chat TTL into the re-uploaded copy (would expire Saved Messages).
971+ attributes. removeAll { $0 is AutoremoveTimeoutMessageAttribute || $0 is AutoclearTimeoutMessageAttribute }
894972
895973 // Saved Messages (Избранное): associate with the source peer topic. applyUpdateMessage
896974 // preserves local threadId; SourceReference mirrors the normal forward→Saved path.
0 commit comments