AttachmentTransport: per-attachment транспортный сервер и тег, backward-compat Codable, download parity
This commit is contained in:
@@ -86,6 +86,39 @@ struct MessageAttachment: Codable, Equatable {
|
|||||||
var preview: String = ""
|
var preview: String = ""
|
||||||
var blob: String = ""
|
var blob: String = ""
|
||||||
var type: AttachmentType = .image
|
var type: AttachmentType = .image
|
||||||
|
var transportTag: String = ""
|
||||||
|
var transportServer: String = ""
|
||||||
|
|
||||||
|
/// Effective download tag: prefers per-attachment `transportTag`, falls back to preview-embedded tag.
|
||||||
|
/// Backward compat: old messages (before multi-transport) have empty `transportTag` but tag in preview.
|
||||||
|
var effectiveDownloadTag: String {
|
||||||
|
if !transportTag.isEmpty { return transportTag }
|
||||||
|
return AttachmentPreviewCodec.downloadTag(from: preview)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Explicit memberwise init (preserves all existing call sites).
|
||||||
|
init(id: String = "", preview: String = "", blob: String = "",
|
||||||
|
type: AttachmentType = .image,
|
||||||
|
transportTag: String = "", transportServer: String = "") {
|
||||||
|
self.id = id
|
||||||
|
self.preview = preview
|
||||||
|
self.blob = blob
|
||||||
|
self.type = type
|
||||||
|
self.transportTag = transportTag
|
||||||
|
self.transportServer = transportServer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom Decodable — `decodeIfPresent` for backward compat with old DB JSON
|
||||||
|
// that doesn't have transportTag/transportServer fields.
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
id = try container.decodeIfPresent(String.self, forKey: .id) ?? ""
|
||||||
|
preview = try container.decodeIfPresent(String.self, forKey: .preview) ?? ""
|
||||||
|
blob = try container.decodeIfPresent(String.self, forKey: .blob) ?? ""
|
||||||
|
type = try container.decodeIfPresent(AttachmentType.self, forKey: .type) ?? .image
|
||||||
|
transportTag = try container.decodeIfPresent(String.self, forKey: .transportTag) ?? ""
|
||||||
|
transportServer = try container.decodeIfPresent(String.self, forKey: .transportServer) ?? ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Reply Message Data (desktop parity)
|
// MARK: - Reply Message Data (desktop parity)
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ struct PacketMessage: Packet {
|
|||||||
stream.writeString(attachment.preview)
|
stream.writeString(attachment.preview)
|
||||||
stream.writeString(attachment.blob)
|
stream.writeString(attachment.blob)
|
||||||
stream.writeInt8(attachment.type.rawValue)
|
stream.writeInt8(attachment.type.rawValue)
|
||||||
|
stream.writeString(attachment.transportTag)
|
||||||
|
stream.writeString(attachment.transportServer)
|
||||||
}
|
}
|
||||||
stream.writeString(aesChachaKey)
|
stream.writeString(aesChachaKey)
|
||||||
}
|
}
|
||||||
@@ -43,14 +45,22 @@ struct PacketMessage: Packet {
|
|||||||
privateKey = stream.readString()
|
privateKey = stream.readString()
|
||||||
messageId = stream.readString()
|
messageId = stream.readString()
|
||||||
|
|
||||||
let attachmentCount = stream.readInt8()
|
let attachmentCount = max(stream.readInt8(), 0)
|
||||||
var list: [MessageAttachment] = []
|
var list: [MessageAttachment] = []
|
||||||
for _ in 0..<attachmentCount {
|
for _ in 0..<attachmentCount {
|
||||||
|
let attId = stream.readString()
|
||||||
|
let attPreview = stream.readString()
|
||||||
|
let attBlob = stream.readString()
|
||||||
|
let attType = AttachmentType(rawValue: stream.readInt8()) ?? .image
|
||||||
|
let attTransportTag = stream.readString()
|
||||||
|
let attTransportServer = stream.readString()
|
||||||
list.append(MessageAttachment(
|
list.append(MessageAttachment(
|
||||||
id: stream.readString(),
|
id: attId,
|
||||||
preview: stream.readString(),
|
preview: attPreview,
|
||||||
blob: stream.readString(),
|
blob: attBlob,
|
||||||
type: AttachmentType(rawValue: stream.readInt8()) ?? .image
|
type: attType,
|
||||||
|
transportTag: attTransportTag,
|
||||||
|
transportServer: attTransportServer
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
attachments = list
|
attachments = list
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ struct PacketOnlineState: Packet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mutating func read(from stream: Stream) {
|
mutating func read(from stream: Stream) {
|
||||||
let count = stream.readInt8()
|
let count = max(stream.readInt8(), 0)
|
||||||
var list: [OnlineStateEntry] = []
|
var list: [OnlineStateEntry] = []
|
||||||
for _ in 0..<count {
|
for _ in 0..<count {
|
||||||
let publicKey = stream.readString()
|
let publicKey = stream.readString()
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ struct PacketSearch: Packet {
|
|||||||
mutating func read(from stream: Stream) {
|
mutating func read(from stream: Stream) {
|
||||||
privateKey = stream.readString()
|
privateKey = stream.readString()
|
||||||
search = stream.readString()
|
search = stream.readString()
|
||||||
let userCount = stream.readInt16()
|
let userCount = max(stream.readInt16(), 0)
|
||||||
var list: [SearchUser] = []
|
var list: [SearchUser] = []
|
||||||
for _ in 0..<userCount {
|
for _ in 0..<userCount {
|
||||||
list.append(SearchUser(
|
list.append(SearchUser(
|
||||||
|
|||||||
@@ -3,12 +3,18 @@ import Foundation
|
|||||||
typealias Stream = PacketBitStream
|
typealias Stream = PacketBitStream
|
||||||
|
|
||||||
/// Bit-aligned binary stream for protocol packets.
|
/// Bit-aligned binary stream for protocol packets.
|
||||||
/// Matches the React Native / Android implementation exactly.
|
/// Matches the server (Java) implementation exactly.
|
||||||
|
///
|
||||||
|
/// Supports:
|
||||||
|
/// - signed: Int8/16/32/64 (two's complement)
|
||||||
|
/// - unsigned: UInt8/16/32/64
|
||||||
|
/// - String: length(UInt32) + chars(UInt16)
|
||||||
|
/// - Bytes: length(UInt32) + raw bytes
|
||||||
final class PacketBitStream: NSObject {
|
final class PacketBitStream: NSObject {
|
||||||
|
|
||||||
private var bytes: [UInt8]
|
private var bytes: [UInt8]
|
||||||
private var readPointer: Int = 0
|
private var readPointer: Int = 0 // bits
|
||||||
private var writePointer: Int = 0
|
private var writePointer: Int = 0 // bits
|
||||||
|
|
||||||
// MARK: - Init
|
// MARK: - Init
|
||||||
|
|
||||||
@@ -20,39 +26,30 @@ final class PacketBitStream: NSObject {
|
|||||||
init(data: Data) {
|
init(data: Data) {
|
||||||
bytes = Array(data)
|
bytes = Array(data)
|
||||||
super.init()
|
super.init()
|
||||||
|
writePointer = bytes.count << 3
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Output
|
// MARK: - Output
|
||||||
|
|
||||||
func toData() -> Data {
|
func toData() -> Data {
|
||||||
guard bytes.isEmpty == false else {
|
let byteCount = (writePointer + 7) >> 3
|
||||||
return Data()
|
guard byteCount > 0 else { return Data() }
|
||||||
}
|
return Data(bytes[0..<byteCount])
|
||||||
var data = Data(count: bytes.count)
|
|
||||||
data.replaceSubrange(0..<bytes.count, with: bytes)
|
|
||||||
return data
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Bit-Level I/O
|
// MARK: - Bit-Level I/O
|
||||||
|
|
||||||
func writeBit(_ value: Int) {
|
func writeBit(_ value: Int) {
|
||||||
let bit = UInt8(value & 1)
|
writeBits(value & 1, count: 1)
|
||||||
ensureCapacityForUpcomingBits(1)
|
|
||||||
let byteIndex = writePointer >> 3
|
|
||||||
let shift = 7 - (writePointer & 7)
|
|
||||||
bytes[byteIndex] = bytes[byteIndex] | (bit << shift)
|
|
||||||
writePointer += 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func readBit() -> Int {
|
func readBit() -> Int {
|
||||||
guard readPointer < bytes.count * 8 else {
|
guard remainingBits() >= 1 else { return 0 }
|
||||||
return 0
|
|
||||||
}
|
|
||||||
let byteIndex = readPointer >> 3
|
let byteIndex = readPointer >> 3
|
||||||
let shift = 7 - (readPointer & 7)
|
let shift = 7 - (readPointer & 7)
|
||||||
let bit = (bytes[byteIndex] >> shift) & 1
|
let bit = Int((bytes[byteIndex] >> shift) & 1)
|
||||||
readPointer += 1
|
readPointer += 1
|
||||||
return Int(bit)
|
return bit
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Bool
|
// MARK: - Bool
|
||||||
@@ -65,156 +62,266 @@ final class PacketBitStream: NSObject {
|
|||||||
readBit() == 1
|
readBit() == 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Int8 (9 bits: 1 sign + 8 data)
|
// MARK: - Byte
|
||||||
|
|
||||||
|
func writeByte(_ value: UInt8) {
|
||||||
|
writeUInt8(Int(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
func readByte() -> UInt8 {
|
||||||
|
UInt8(truncatingIfNeeded: readUInt8())
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UInt8 / Int8 (8 bits)
|
||||||
|
|
||||||
|
func writeUInt8(_ value: Int) {
|
||||||
|
let v = UInt8(truncatingIfNeeded: value)
|
||||||
|
|
||||||
|
// Fast path: byte-aligned
|
||||||
|
if (writePointer & 7) == 0 {
|
||||||
|
ensureCapacityForUpcomingBits(8)
|
||||||
|
bytes[writePointer >> 3] = v
|
||||||
|
writePointer += 8
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeBits(Int(v), count: 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
func readUInt8() -> Int {
|
||||||
|
guard remainingBits() >= 8 else { return 0 }
|
||||||
|
|
||||||
|
// Fast path: byte-aligned
|
||||||
|
if (readPointer & 7) == 0 {
|
||||||
|
let value = Int(bytes[readPointer >> 3])
|
||||||
|
readPointer += 8
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
return readBits(8)
|
||||||
|
}
|
||||||
|
|
||||||
func writeInt8(_ value: Int) {
|
func writeInt8(_ value: Int) {
|
||||||
let negationBit: UInt8 = value < 0 ? 1 : 0
|
writeUInt8(value)
|
||||||
let int8Value = UInt8(abs(value) & 0xFF)
|
|
||||||
ensureCapacityForUpcomingBits(9)
|
|
||||||
|
|
||||||
let byteIndex = writePointer >> 3
|
|
||||||
let signShift = 7 - (writePointer & 7)
|
|
||||||
bytes[byteIndex] = bytes[byteIndex] | (negationBit << signShift)
|
|
||||||
writePointer += 1
|
|
||||||
|
|
||||||
for i in 0..<8 {
|
|
||||||
let bit = (int8Value >> (7 - i)) & 1
|
|
||||||
let idx = writePointer >> 3
|
|
||||||
let shift = 7 - (writePointer & 7)
|
|
||||||
bytes[idx] = bytes[idx] | (bit << shift)
|
|
||||||
writePointer += 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func readInt8() -> Int {
|
func readInt8() -> Int {
|
||||||
guard readPointer + 9 <= bytes.count * 8 else {
|
Int(Int8(truncatingIfNeeded: readUInt8()))
|
||||||
readPointer = bytes.count * 8
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
var value = 0
|
|
||||||
let signShift = 7 - (readPointer & 7)
|
|
||||||
let negationBit = Int((bytes[readPointer >> 3] >> signShift) & 1)
|
|
||||||
readPointer += 1
|
|
||||||
|
|
||||||
for i in 0..<8 {
|
|
||||||
let shift = 7 - (readPointer & 7)
|
|
||||||
let bit = Int((bytes[readPointer >> 3] >> shift) & 1)
|
|
||||||
value = value | (bit << (7 - i))
|
|
||||||
readPointer += 1
|
|
||||||
}
|
|
||||||
|
|
||||||
return negationBit == 1 ? -value : value
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Int16 (2 × Int8)
|
// MARK: - UInt16 / Int16 (16 bits)
|
||||||
|
|
||||||
|
func writeUInt16(_ value: Int) {
|
||||||
|
let v = value & 0xFFFF
|
||||||
|
writeUInt8((v >> 8) & 0xFF)
|
||||||
|
writeUInt8(v & 0xFF)
|
||||||
|
}
|
||||||
|
|
||||||
|
func readUInt16() -> Int {
|
||||||
|
let hi = readUInt8()
|
||||||
|
let lo = readUInt8()
|
||||||
|
return (hi << 8) | lo
|
||||||
|
}
|
||||||
|
|
||||||
func writeInt16(_ value: Int) {
|
func writeInt16(_ value: Int) {
|
||||||
writeInt8(value >> 8)
|
writeUInt16(value)
|
||||||
writeInt8(value & 0xFF)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func readInt16() -> Int {
|
func readInt16() -> Int {
|
||||||
let high = readInt8() << 8
|
Int(Int16(truncatingIfNeeded: readUInt16()))
|
||||||
return high | readInt8()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Int32 (2 × Int16)
|
// MARK: - UInt32 / Int32 (32 bits)
|
||||||
|
|
||||||
|
func writeUInt32(_ value: Int) {
|
||||||
|
writeUInt8((value >> 24) & 0xFF)
|
||||||
|
writeUInt8((value >> 16) & 0xFF)
|
||||||
|
writeUInt8((value >> 8) & 0xFF)
|
||||||
|
writeUInt8(value & 0xFF)
|
||||||
|
}
|
||||||
|
|
||||||
|
func readUInt32() -> Int {
|
||||||
|
let b1 = readUInt8()
|
||||||
|
let b2 = readUInt8()
|
||||||
|
let b3 = readUInt8()
|
||||||
|
let b4 = readUInt8()
|
||||||
|
return (b1 << 24) | (b2 << 16) | (b3 << 8) | b4
|
||||||
|
}
|
||||||
|
|
||||||
func writeInt32(_ value: Int) {
|
func writeInt32(_ value: Int) {
|
||||||
writeInt16(value >> 16)
|
writeUInt32(value)
|
||||||
writeInt16(value & 0xFFFF)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func readInt32() -> Int {
|
func readInt32() -> Int {
|
||||||
let high = readInt16() << 16
|
Int(Int32(truncatingIfNeeded: readUInt32()))
|
||||||
return high | readInt16()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Int64 (2 × Int32)
|
// MARK: - UInt64 / Int64 (64 bits)
|
||||||
|
|
||||||
func writeInt64(_ value: Int64) {
|
func writeUInt64(_ value: Int64) {
|
||||||
let high = Int((value >> 32) & 0xFFFFFFFF)
|
writeUInt8(Int((value >> 56) & 0xFF))
|
||||||
let low = Int(value & 0xFFFFFFFF)
|
writeUInt8(Int((value >> 48) & 0xFF))
|
||||||
writeInt32(high)
|
writeUInt8(Int((value >> 40) & 0xFF))
|
||||||
writeInt32(low)
|
writeUInt8(Int((value >> 32) & 0xFF))
|
||||||
|
writeUInt8(Int((value >> 24) & 0xFF))
|
||||||
|
writeUInt8(Int((value >> 16) & 0xFF))
|
||||||
|
writeUInt8(Int((value >> 8) & 0xFF))
|
||||||
|
writeUInt8(Int(value & 0xFF))
|
||||||
}
|
}
|
||||||
|
|
||||||
func readInt64() -> Int64 {
|
func readUInt64() -> Int64 {
|
||||||
let high = Int64(readInt32())
|
let high = Int64(readUInt32()) & 0xFFFFFFFF
|
||||||
let low = Int64(readInt32()) & 0xFFFFFFFF
|
let low = Int64(readUInt32()) & 0xFFFFFFFF
|
||||||
return (high << 32) | low
|
return (high << 32) | low
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - String (Int32 length + UTF-16 code units)
|
func writeInt64(_ value: Int64) {
|
||||||
|
writeUInt64(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func readInt64() -> Int64 {
|
||||||
|
readUInt64()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Float32
|
||||||
|
|
||||||
|
func writeFloat32(_ value: Float) {
|
||||||
|
writeInt32(Int(Int32(bitPattern: value.bitPattern)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func readFloat32() -> Float {
|
||||||
|
Float(bitPattern: UInt32(bitPattern: Int32(truncatingIfNeeded: readInt32())))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - String (UInt32 length + UInt16 chars)
|
||||||
|
|
||||||
func writeString(_ value: String) {
|
func writeString(_ value: String) {
|
||||||
let utf16Units = Array(value.utf16)
|
let utf16Units = Array(value.utf16)
|
||||||
let requiredBits = 36 + utf16Units.count * 18
|
let length = utf16Units.count
|
||||||
ensureCapacityForUpcomingBits(requiredBits)
|
writeUInt32(length)
|
||||||
writeInt32(utf16Units.count)
|
|
||||||
|
guard length > 0 else { return }
|
||||||
|
|
||||||
|
ensureCapacityForUpcomingBits(length * 16)
|
||||||
for codeUnit in utf16Units {
|
for codeUnit in utf16Units {
|
||||||
writeInt16(Int(codeUnit))
|
writeUInt16(Int(codeUnit))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func readString() -> String {
|
func readString() -> String {
|
||||||
let length = readInt32()
|
let length = readUInt32()
|
||||||
let bitsAvailable = bytes.count * 8 - readPointer
|
guard length > 0 else { return "" }
|
||||||
let bytesAvailable = max(bitsAvailable / 8, 0)
|
|
||||||
if length < 0 || (length * 2) > bytesAvailable {
|
let requiredBits = length * 16
|
||||||
return ""
|
guard requiredBits <= remainingBits() else { return "" }
|
||||||
}
|
|
||||||
|
|
||||||
var codeUnits = [UInt16]()
|
var codeUnits = [UInt16]()
|
||||||
codeUnits.reserveCapacity(length)
|
codeUnits.reserveCapacity(length)
|
||||||
for _ in 0..<length {
|
for _ in 0..<length {
|
||||||
codeUnits.append(UInt16(truncatingIfNeeded: readInt16()))
|
codeUnits.append(UInt16(truncatingIfNeeded: readUInt16()))
|
||||||
}
|
}
|
||||||
return String(decoding: codeUnits, as: UTF16.self)
|
return String(decoding: codeUnits, as: UTF16.self)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Bytes (Int32 length + raw Int8s)
|
// MARK: - Bytes (UInt32 length + raw bytes)
|
||||||
|
|
||||||
func writeBytes(_ value: Data) {
|
func writeBytes(_ value: Data) {
|
||||||
let requiredBits = 36 + value.count * 9
|
let length = value.count
|
||||||
ensureCapacityForUpcomingBits(requiredBits)
|
writeUInt32(length)
|
||||||
writeInt32(value.count)
|
|
||||||
|
guard length > 0 else { return }
|
||||||
|
|
||||||
|
ensureCapacityForUpcomingBits(length * 8)
|
||||||
|
|
||||||
|
// Fast path: byte-aligned
|
||||||
|
if (writePointer & 7) == 0 {
|
||||||
|
let byteIndex = writePointer >> 3
|
||||||
|
for (i, byte) in value.enumerated() {
|
||||||
|
bytes[byteIndex + i] = byte
|
||||||
|
}
|
||||||
|
writePointer += length << 3
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
for byte in value {
|
for byte in value {
|
||||||
writeInt8(Int(byte))
|
writeUInt8(Int(byte))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func readBytes() -> Data {
|
func readBytes() -> Data {
|
||||||
let length = readInt32()
|
let length = readUInt32()
|
||||||
guard length >= 0 else {
|
guard length > 0 else { return Data() }
|
||||||
return Data()
|
|
||||||
}
|
|
||||||
|
|
||||||
let bitsAvailable = bytes.count * 8 - readPointer
|
let requiredBits = length * 8
|
||||||
let bytesAvailable = max(bitsAvailable / 9, 0)
|
guard requiredBits <= remainingBits() else { return Data() }
|
||||||
guard length <= bytesAvailable else {
|
|
||||||
return Data()
|
// Fast path: byte-aligned
|
||||||
|
if (readPointer & 7) == 0 {
|
||||||
|
let byteIndex = readPointer >> 3
|
||||||
|
let result = Data(bytes[byteIndex..<(byteIndex + length)])
|
||||||
|
readPointer += length << 3
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = Data(capacity: length)
|
var result = Data(capacity: length)
|
||||||
for _ in 0..<length {
|
for _ in 0..<length {
|
||||||
result.append(UInt8(truncatingIfNeeded: readInt8()))
|
result.append(UInt8(truncatingIfNeeded: readUInt8()))
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
|
private func remainingBits() -> Int {
|
||||||
|
writePointer - readPointer
|
||||||
|
}
|
||||||
|
|
||||||
|
private func writeBits(_ value: Int, count: Int) {
|
||||||
|
guard count > 0 else { return }
|
||||||
|
ensureCapacityForUpcomingBits(count)
|
||||||
|
|
||||||
|
for i in stride(from: count - 1, through: 0, by: -1) {
|
||||||
|
let bit = UInt8((value >> i) & 1)
|
||||||
|
let byteIndex = writePointer >> 3
|
||||||
|
let shift = 7 - (writePointer & 7)
|
||||||
|
if bit == 1 {
|
||||||
|
bytes[byteIndex] |= (1 << shift)
|
||||||
|
} else {
|
||||||
|
bytes[byteIndex] &= ~(1 << shift)
|
||||||
|
}
|
||||||
|
writePointer += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func readBits(_ count: Int) -> Int {
|
||||||
|
guard count > 0, remainingBits() >= count else { return 0 }
|
||||||
|
|
||||||
|
var value = 0
|
||||||
|
for _ in 0..<count {
|
||||||
|
let byteIndex = readPointer >> 3
|
||||||
|
let shift = 7 - (readPointer & 7)
|
||||||
|
let bit = Int((bytes[byteIndex] >> shift) & 1)
|
||||||
|
value = (value << 1) | bit
|
||||||
|
readPointer += 1
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
private func ensureCapacityForUpcomingBits(_ bitCount: Int) {
|
private func ensureCapacityForUpcomingBits(_ bitCount: Int) {
|
||||||
guard bitCount > 0 else { return }
|
guard bitCount > 0 else { return }
|
||||||
let lastBitIndex = writePointer + bitCount - 1
|
let lastBitIndex = writePointer + bitCount - 1
|
||||||
ensureCapacity(lastBitIndex >> 3)
|
ensureCapacity(lastBitIndex >> 3)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func ensureCapacity(_ index: Int) {
|
private func ensureCapacity(_ byteIndex: Int) {
|
||||||
if bytes.count <= index {
|
let requiredSize = byteIndex + 1
|
||||||
bytes.append(contentsOf: repeatElement(0, count: index - bytes.count + 1))
|
guard requiredSize > bytes.count else { return }
|
||||||
|
|
||||||
|
var newSize = bytes.isEmpty ? 32 : bytes.count
|
||||||
|
while newSize < requiredSize {
|
||||||
|
newSize <<= 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bytes.append(contentsOf: repeatElement(0 as UInt8, count: newSize - bytes.count))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,16 +71,16 @@ final class TransportManager: @unchecked Sendable {
|
|||||||
// MARK: - Upload
|
// MARK: - Upload
|
||||||
|
|
||||||
/// Uploads file content to the transport server.
|
/// Uploads file content to the transport server.
|
||||||
/// Desktop parity: `TransportProvider.tsx` `uploadFile()`.
|
/// Desktop parity: `usePrepareAttachment.ts` `uploadFile()`.
|
||||||
///
|
///
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - id: Unique file identifier (used as filename in multipart).
|
/// - id: Unique file identifier (used as filename in multipart).
|
||||||
/// - content: Raw file content to upload.
|
/// - content: Raw file content to upload.
|
||||||
/// - Returns: Server-assigned tag for later download.
|
/// - Returns: `(tag, server)` — server-assigned tag and the transport server URL used.
|
||||||
/// Android parity: retry with exponential backoff (1s, 2s, 4s) on upload failure.
|
/// Android parity: retry with exponential backoff (1s, 2s, 4s) on upload failure.
|
||||||
private static let maxUploadRetries = 3
|
private static let maxUploadRetries = 3
|
||||||
|
|
||||||
func uploadFile(id: String, content: Data) async throws -> String {
|
func uploadFile(id: String, content: Data) async throws -> (tag: String, server: String) {
|
||||||
guard let serverUrl = await MainActor.run(body: { transportServer }) else {
|
guard let serverUrl = await MainActor.run(body: { transportServer }) else {
|
||||||
throw TransportError.noTransportServer
|
throw TransportError.noTransportServer
|
||||||
}
|
}
|
||||||
@@ -125,8 +125,8 @@ final class TransportManager: @unchecked Sendable {
|
|||||||
throw TransportError.missingTag
|
throw TransportError.missingTag
|
||||||
}
|
}
|
||||||
|
|
||||||
Self.logger.info("Upload complete: id=\(id), tag=\(tag)")
|
Self.logger.info("Upload complete: id=\(id), tag=\(tag), server=\(serverUrl)")
|
||||||
return tag
|
return (tag: tag, server: serverUrl)
|
||||||
} catch {
|
} catch {
|
||||||
lastError = error
|
lastError = error
|
||||||
if attempt < Self.maxUploadRetries - 1 {
|
if attempt < Self.maxUploadRetries - 1 {
|
||||||
@@ -141,14 +141,22 @@ final class TransportManager: @unchecked Sendable {
|
|||||||
|
|
||||||
// MARK: - Download
|
// MARK: - Download
|
||||||
|
|
||||||
/// Downloads file content from the transport server.
|
/// Downloads file content from a transport server.
|
||||||
/// Desktop parity: `TransportProvider.tsx` `downloadFile()`.
|
/// Desktop parity: `useAttachment.ts` `downloadFile(id, tag, server)`.
|
||||||
///
|
///
|
||||||
/// - Parameter tag: Server-assigned file tag from upload response.
|
/// - Parameters:
|
||||||
|
/// - tag: Server-assigned file tag from upload response.
|
||||||
|
/// - server: Per-attachment transport server URL. Falls back to global transport if empty/nil.
|
||||||
/// - Returns: Raw file content.
|
/// - Returns: Raw file content.
|
||||||
func downloadFile(tag: String) async throws -> Data {
|
func downloadFile(tag: String, server: String? = nil) async throws -> Data {
|
||||||
guard let serverUrl = await MainActor.run(body: { transportServer }) else {
|
let serverUrl: String
|
||||||
throw TransportError.noTransportServer
|
if let explicit = server, !explicit.isEmpty {
|
||||||
|
serverUrl = explicit
|
||||||
|
} else {
|
||||||
|
guard let global = await MainActor.run(body: { transportServer }) else {
|
||||||
|
throw TransportError.noTransportServer
|
||||||
|
}
|
||||||
|
serverUrl = global
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let url = URL(string: "\(serverUrl)/d/\(tag)") else {
|
guard let url = URL(string: "\(serverUrl)/d/\(tag)") else {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
protocol AttachmentFlowTransporting {
|
protocol AttachmentFlowTransporting {
|
||||||
func uploadFile(id: String, content: Data) async throws -> String
|
func uploadFile(id: String, content: Data) async throws -> (tag: String, server: String)
|
||||||
func downloadFile(tag: String) async throws -> Data
|
func downloadFile(tag: String, server: String?) async throws -> Data
|
||||||
}
|
}
|
||||||
|
|
||||||
protocol PacketFlowSending {
|
protocol PacketFlowSending {
|
||||||
@@ -19,12 +19,12 @@ protocol SearchResultDispatching {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct LiveAttachmentFlowTransport: AttachmentFlowTransporting {
|
struct LiveAttachmentFlowTransport: AttachmentFlowTransporting {
|
||||||
func uploadFile(id: String, content: Data) async throws -> String {
|
func uploadFile(id: String, content: Data) async throws -> (tag: String, server: String) {
|
||||||
try await TransportManager.shared.uploadFile(id: id, content: content)
|
try await TransportManager.shared.uploadFile(id: id, content: content)
|
||||||
}
|
}
|
||||||
|
|
||||||
func downloadFile(tag: String) async throws -> Data {
|
func downloadFile(tag: String, server: String?) async throws -> Data {
|
||||||
try await TransportManager.shared.downloadFile(tag: tag)
|
try await TransportManager.shared.downloadFile(tag: tag, server: server)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -409,9 +409,9 @@ final class SessionManager {
|
|||||||
DialogRepository.shared.updateDialogFromMessages(opponentKey: packet.toPublicKey)
|
DialogRepository.shared.updateDialogFromMessages(opponentKey: packet.toPublicKey)
|
||||||
|
|
||||||
// Upload encrypted blob to transport server in background (desktop: uploadFile)
|
// Upload encrypted blob to transport server in background (desktop: uploadFile)
|
||||||
let tag: String
|
let upload: (tag: String, server: String)
|
||||||
do {
|
do {
|
||||||
tag = try await attachmentFlowTransport.uploadFile(
|
upload = try await attachmentFlowTransport.uploadFile(
|
||||||
id: attachmentId,
|
id: attachmentId,
|
||||||
content: Data(encryptedBlob.utf8)
|
content: Data(encryptedBlob.utf8)
|
||||||
)
|
)
|
||||||
@@ -424,13 +424,15 @@ final class SessionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update preview with CDN tag (tag::blurhash)
|
// Update preview with CDN tag (tag::blurhash)
|
||||||
let preview = "\(tag)::\(blurhash)"
|
let preview = "\(upload.tag)::\(blurhash)"
|
||||||
packet.attachments = [
|
packet.attachments = [
|
||||||
MessageAttachment(
|
MessageAttachment(
|
||||||
id: attachmentId,
|
id: attachmentId,
|
||||||
preview: preview,
|
preview: preview,
|
||||||
blob: "", // Desktop parity: blob cleared after upload
|
blob: "", // Desktop parity: blob cleared after upload
|
||||||
type: .avatar
|
type: .avatar,
|
||||||
|
transportTag: upload.tag,
|
||||||
|
transportServer: upload.server
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -442,14 +444,14 @@ final class SessionManager {
|
|||||||
DialogRepository.shared.updateDeliveryStatus(messageId: messageId, opponentKey: toPublicKey, status: .delivered)
|
DialogRepository.shared.updateDeliveryStatus(messageId: messageId, opponentKey: toPublicKey, status: .delivered)
|
||||||
// Send to server for multi-device sync (unlike text Saved Messages)
|
// Send to server for multi-device sync (unlike text Saved Messages)
|
||||||
packetFlowSender.sendPacket(packet)
|
packetFlowSender.sendPacket(packet)
|
||||||
Self.logger.info("📤 Avatar synced to Saved Messages (multi-device) tag=\(tag)")
|
Self.logger.info("📤 Avatar synced to Saved Messages (multi-device) tag=\(upload.tag)")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
packetFlowSender.sendPacket(packet)
|
packetFlowSender.sendPacket(packet)
|
||||||
registerOutgoingRetry(for: packet)
|
registerOutgoingRetry(for: packet)
|
||||||
MessageRepository.shared.persistNow()
|
MessageRepository.shared.persistNow()
|
||||||
Self.logger.info("📤 Avatar sent to \(toPublicKey.prefix(12))… tag=\(tag)")
|
Self.logger.info("📤 Avatar sent to \(toPublicKey.prefix(12))… tag=\(upload.tag)")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sends a message with image/file attachments.
|
/// Sends a message with image/file attachments.
|
||||||
@@ -662,23 +664,27 @@ final class SessionManager {
|
|||||||
// ── Phase 2: Upload in background, then send packet ──
|
// ── Phase 2: Upload in background, then send packet ──
|
||||||
let flowTransport = attachmentFlowTransport
|
let flowTransport = attachmentFlowTransport
|
||||||
let messageAttachments: [MessageAttachment] = try await withThrowingTaskGroup(
|
let messageAttachments: [MessageAttachment] = try await withThrowingTaskGroup(
|
||||||
of: (Int, String).self
|
of: (Int, String, String).self
|
||||||
) { group in
|
) { group in
|
||||||
for (index, item) in encryptedAttachments.enumerated() {
|
for (index, item) in encryptedAttachments.enumerated() {
|
||||||
group.addTask {
|
group.addTask {
|
||||||
let tag = try await flowTransport.uploadFile(
|
let result = try await flowTransport.uploadFile(
|
||||||
id: item.original.id, content: item.encryptedData
|
id: item.original.id, content: item.encryptedData
|
||||||
)
|
)
|
||||||
return (index, tag)
|
return (index, result.tag, result.server)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var tags = [Int: String]()
|
var uploads = [Int: (tag: String, server: String)]()
|
||||||
for try await (index, tag) in group { tags[index] = tag }
|
for try await (index, tag, server) in group { uploads[index] = (tag, server) }
|
||||||
return encryptedAttachments.enumerated().map { index, item in
|
return encryptedAttachments.enumerated().map { index, item in
|
||||||
let tag = tags[index] ?? ""
|
let upload = uploads[index] ?? (tag: "", server: "")
|
||||||
let preview = AttachmentPreviewCodec.compose(downloadTag: tag, payload: item.preview)
|
let preview = AttachmentPreviewCodec.compose(downloadTag: upload.tag, payload: item.preview)
|
||||||
Self.logger.info("📤 Attachment uploaded: type=\(String(describing: item.original.type)), tag=\(tag)")
|
Self.logger.info("📤 Attachment uploaded: type=\(String(describing: item.original.type)), tag=\(upload.tag), server=\(upload.server)")
|
||||||
return MessageAttachment(id: item.original.id, preview: preview, blob: "", type: item.original.type)
|
return MessageAttachment(
|
||||||
|
id: item.original.id, preview: preview, blob: "",
|
||||||
|
type: item.original.type,
|
||||||
|
transportTag: upload.tag, transportServer: upload.server
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -789,7 +795,7 @@ final class SessionManager {
|
|||||||
Self.logger.debug("📤 Forward re-upload: \(originalId) → \(newAttId) (\(jpegData.count) bytes JPEG, \(encryptedBlob.count) encrypted)")
|
Self.logger.debug("📤 Forward re-upload: \(originalId) → \(newAttId) (\(jpegData.count) bytes JPEG, \(encryptedBlob.count) encrypted)")
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
let tag = try await attachmentFlowTransport.uploadFile(
|
let upload = try await attachmentFlowTransport.uploadFile(
|
||||||
id: newAttId,
|
id: newAttId,
|
||||||
content: Data(encryptedBlob.utf8)
|
content: Data(encryptedBlob.utf8)
|
||||||
)
|
)
|
||||||
@@ -799,7 +805,7 @@ final class SessionManager {
|
|||||||
.flatMap { $0.attachments }
|
.flatMap { $0.attachments }
|
||||||
.first(where: { $0.id == originalId })?.preview ?? ""
|
.first(where: { $0.id == originalId })?.preview ?? ""
|
||||||
let blurhash = AttachmentPreviewCodec.blurHash(from: originalPreview)
|
let blurhash = AttachmentPreviewCodec.blurHash(from: originalPreview)
|
||||||
let newPreview = AttachmentPreviewCodec.compose(downloadTag: tag, payload: blurhash)
|
let newPreview = AttachmentPreviewCodec.compose(downloadTag: upload.tag, payload: blurhash)
|
||||||
attachmentIdMap[originalId] = (newAttId, newPreview)
|
attachmentIdMap[originalId] = (newAttId, newPreview)
|
||||||
|
|
||||||
// Cache locally under new ID for ForwardedImagePreviewCell
|
// Cache locally under new ID for ForwardedImagePreviewCell
|
||||||
@@ -808,7 +814,7 @@ final class SessionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
Self.logger.debug("📤 Forward re-upload OK: \(newAttId) tag=\(tag) preview=\(newPreview)")
|
Self.logger.debug("📤 Forward re-upload OK: \(newAttId) tag=\(upload.tag) preview=\(newPreview)")
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -831,7 +837,7 @@ final class SessionManager {
|
|||||||
Self.logger.debug("📤 Forward file re-upload: \(originalId) → \(newAttId) (\(fileInfo.data.count) bytes, \(fileInfo.fileName))")
|
Self.logger.debug("📤 Forward file re-upload: \(originalId) → \(newAttId) (\(fileInfo.data.count) bytes, \(fileInfo.fileName))")
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
let tag = try await attachmentFlowTransport.uploadFile(
|
let upload = try await attachmentFlowTransport.uploadFile(
|
||||||
id: newAttId,
|
id: newAttId,
|
||||||
content: Data(encryptedBlob.utf8)
|
content: Data(encryptedBlob.utf8)
|
||||||
)
|
)
|
||||||
@@ -846,11 +852,11 @@ final class SessionManager {
|
|||||||
fallbackFileSize: fileInfo.data.count
|
fallbackFileSize: fileInfo.data.count
|
||||||
)
|
)
|
||||||
let fileMeta = "\(filePreview.fileSize)::\(filePreview.fileName)"
|
let fileMeta = "\(filePreview.fileSize)::\(filePreview.fileName)"
|
||||||
let newPreview = AttachmentPreviewCodec.compose(downloadTag: tag, payload: fileMeta)
|
let newPreview = AttachmentPreviewCodec.compose(downloadTag: upload.tag, payload: fileMeta)
|
||||||
attachmentIdMap[originalId] = (newAttId, newPreview)
|
attachmentIdMap[originalId] = (newAttId, newPreview)
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
Self.logger.debug("📤 Forward file re-upload OK: \(newAttId) tag=\(tag) preview=\(newPreview)")
|
Self.logger.debug("📤 Forward file re-upload OK: \(newAttId) tag=\(upload.tag) preview=\(newPreview)")
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -233,7 +233,7 @@ struct MessageAvatarView: View {
|
|||||||
private func downloadAvatar() {
|
private func downloadAvatar() {
|
||||||
guard !isDownloading else { return }
|
guard !isDownloading else { return }
|
||||||
|
|
||||||
let tag = extractTag(from: attachment.preview)
|
let tag = attachment.effectiveDownloadTag
|
||||||
guard !tag.isEmpty else {
|
guard !tag.isEmpty else {
|
||||||
downloadError = true
|
downloadError = true
|
||||||
return
|
return
|
||||||
@@ -247,9 +247,10 @@ struct MessageAvatarView: View {
|
|||||||
isDownloading = true
|
isDownloading = true
|
||||||
downloadError = false
|
downloadError = false
|
||||||
|
|
||||||
|
let server = attachment.transportServer
|
||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
let encryptedData = try await TransportManager.shared.downloadFile(tag: tag)
|
let encryptedData = try await TransportManager.shared.downloadFile(tag: tag, server: server)
|
||||||
let encryptedString = String(decoding: encryptedData, as: UTF8.self)
|
let encryptedString = String(decoding: encryptedData, as: UTF8.self)
|
||||||
|
|
||||||
let passwords = MessageCrypto.attachmentPasswordCandidates(from: storedPassword)
|
let passwords = MessageCrypto.attachmentPasswordCandidates(from: storedPassword)
|
||||||
|
|||||||
@@ -98,7 +98,10 @@ struct MessageFileView: View {
|
|||||||
|
|
||||||
private var fileName: String { fileMetadata.name }
|
private var fileName: String { fileMetadata.name }
|
||||||
private var fileSize: Int { fileMetadata.size }
|
private var fileSize: Int { fileMetadata.size }
|
||||||
private var fileTag: String { fileMetadata.tag }
|
private var fileTag: String {
|
||||||
|
let effective = attachment.effectiveDownloadTag
|
||||||
|
return effective.isEmpty ? fileMetadata.tag : effective
|
||||||
|
}
|
||||||
|
|
||||||
private var formattedFileSize: String {
|
private var formattedFileSize: String {
|
||||||
let bytes = fileSize
|
let bytes = fileSize
|
||||||
@@ -140,7 +143,7 @@ struct MessageFileView: View {
|
|||||||
|
|
||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
let encryptedData = try await TransportManager.shared.downloadFile(tag: fileTag)
|
let encryptedData = try await TransportManager.shared.downloadFile(tag: fileTag, server: attachment.transportServer)
|
||||||
let encryptedString = String(decoding: encryptedData, as: UTF8.self)
|
let encryptedString = String(decoding: encryptedData, as: UTF8.self)
|
||||||
|
|
||||||
let passwords = MessageCrypto.attachmentPasswordCandidates(from: storedPassword)
|
let passwords = MessageCrypto.attachmentPasswordCandidates(from: storedPassword)
|
||||||
|
|||||||
@@ -251,7 +251,7 @@ struct MessageImageView: View {
|
|||||||
private func downloadImage() {
|
private func downloadImage() {
|
||||||
guard !isDownloading, image == nil else { return }
|
guard !isDownloading, image == nil else { return }
|
||||||
|
|
||||||
let tag = extractTag(from: attachment.preview)
|
let tag = attachment.effectiveDownloadTag
|
||||||
guard !tag.isEmpty else {
|
guard !tag.isEmpty else {
|
||||||
downloadError = true
|
downloadError = true
|
||||||
return
|
return
|
||||||
@@ -265,9 +265,10 @@ struct MessageImageView: View {
|
|||||||
isDownloading = true
|
isDownloading = true
|
||||||
downloadError = false
|
downloadError = false
|
||||||
|
|
||||||
|
let server = attachment.transportServer
|
||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
let encryptedData = try await TransportManager.shared.downloadFile(tag: tag)
|
let encryptedData = try await TransportManager.shared.downloadFile(tag: tag, server: server)
|
||||||
let encryptedString = String(decoding: encryptedData, as: UTF8.self)
|
let encryptedString = String(decoding: encryptedData, as: UTF8.self)
|
||||||
|
|
||||||
let passwords = MessageCrypto.attachmentPasswordCandidates(from: storedPassword)
|
let passwords = MessageCrypto.attachmentPasswordCandidates(from: storedPassword)
|
||||||
|
|||||||
@@ -901,10 +901,11 @@ final class NativeMessageCell: UICollectionViewCell, UIContextMenuInteractionDel
|
|||||||
/// Downloads avatar from CDN, decrypts, caches to disk, and returns the image.
|
/// Downloads avatar from CDN, decrypts, caches to disk, and returns the image.
|
||||||
/// Shared logic with `MessageAvatarView.downloadAvatar()`.
|
/// Shared logic with `MessageAvatarView.downloadAvatar()`.
|
||||||
private static func downloadAndCacheAvatar(
|
private static func downloadAndCacheAvatar(
|
||||||
tag: String, attachmentId: String, storedPassword: String, senderKey: String
|
tag: String, attachmentId: String, storedPassword: String, senderKey: String,
|
||||||
|
server: String = ""
|
||||||
) async -> UIImage? {
|
) async -> UIImage? {
|
||||||
do {
|
do {
|
||||||
let encryptedData = try await TransportManager.shared.downloadFile(tag: tag)
|
let encryptedData = try await TransportManager.shared.downloadFile(tag: tag, server: server)
|
||||||
let encryptedString = String(decoding: encryptedData, as: UTF8.self)
|
let encryptedString = String(decoding: encryptedData, as: UTF8.self)
|
||||||
|
|
||||||
let passwords = MessageCrypto.attachmentPasswordCandidates(from: storedPassword)
|
let passwords = MessageCrypto.attachmentPasswordCandidates(from: storedPassword)
|
||||||
@@ -1109,7 +1110,7 @@ final class NativeMessageCell: UICollectionViewCell, UIContextMenuInteractionDel
|
|||||||
// Already downloaded?
|
// Already downloaded?
|
||||||
if AttachmentCache.shared.cachedImage(forAttachmentId: id) != nil { return }
|
if AttachmentCache.shared.cachedImage(forAttachmentId: id) != nil { return }
|
||||||
// Download from CDN
|
// Download from CDN
|
||||||
let tag = AttachmentPreviewCodec.downloadTag(from: avatarAtt.preview)
|
let tag = avatarAtt.effectiveDownloadTag
|
||||||
guard !tag.isEmpty else { return }
|
guard !tag.isEmpty else { return }
|
||||||
guard let password = message.attachmentPassword, !password.isEmpty else { return }
|
guard let password = message.attachmentPassword, !password.isEmpty else { return }
|
||||||
|
|
||||||
@@ -1118,10 +1119,12 @@ final class NativeMessageCell: UICollectionViewCell, UIContextMenuInteractionDel
|
|||||||
|
|
||||||
let messageId = message.id
|
let messageId = message.id
|
||||||
let senderKey = message.fromPublicKey
|
let senderKey = message.fromPublicKey
|
||||||
|
let server = avatarAtt.transportServer
|
||||||
Task.detached(priority: .userInitiated) {
|
Task.detached(priority: .userInitiated) {
|
||||||
let downloaded = await Self.downloadAndCacheAvatar(
|
let downloaded = await Self.downloadAndCacheAvatar(
|
||||||
tag: tag, attachmentId: id,
|
tag: tag, attachmentId: id,
|
||||||
storedPassword: password, senderKey: senderKey
|
storedPassword: password, senderKey: senderKey,
|
||||||
|
server: server
|
||||||
)
|
)
|
||||||
await MainActor.run { [weak self] in
|
await MainActor.run { [weak self] in
|
||||||
guard let self, self.message?.id == messageId else { return }
|
guard let self, self.message?.id == messageId else { return }
|
||||||
@@ -1569,7 +1572,7 @@ final class NativeMessageCell: UICollectionViewCell, UIContextMenuInteractionDel
|
|||||||
|
|
||||||
private func downloadPhotoAttachment(attachment: MessageAttachment, message: ChatMessage) {
|
private func downloadPhotoAttachment(attachment: MessageAttachment, message: ChatMessage) {
|
||||||
if photoDownloadTasks[attachment.id] != nil { return }
|
if photoDownloadTasks[attachment.id] != nil { return }
|
||||||
let tag = Self.extractTag(from: attachment.preview)
|
let tag = attachment.effectiveDownloadTag
|
||||||
guard !tag.isEmpty,
|
guard !tag.isEmpty,
|
||||||
let storedPassword = message.attachmentPassword,
|
let storedPassword = message.attachmentPassword,
|
||||||
!storedPassword.isEmpty else {
|
!storedPassword.isEmpty else {
|
||||||
@@ -1593,9 +1596,10 @@ final class NativeMessageCell: UICollectionViewCell, UIContextMenuInteractionDel
|
|||||||
photoTileDownloadArrows[tileIndex].isHidden = true
|
photoTileDownloadArrows[tileIndex].isHidden = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let server = attachment.transportServer
|
||||||
photoDownloadTasks[attachmentId] = Task { [weak self] in
|
photoDownloadTasks[attachmentId] = Task { [weak self] in
|
||||||
do {
|
do {
|
||||||
let encryptedData = try await TransportManager.shared.downloadFile(tag: tag)
|
let encryptedData = try await TransportManager.shared.downloadFile(tag: tag, server: server)
|
||||||
let encryptedString = String(decoding: encryptedData, as: UTF8.self)
|
let encryptedString = String(decoding: encryptedData, as: UTF8.self)
|
||||||
let passwords = MessageCrypto.attachmentPasswordCandidates(from: storedPassword)
|
let passwords = MessageCrypto.attachmentPasswordCandidates(from: storedPassword)
|
||||||
let image = Self.decryptAndParseImage(encryptedString: encryptedString, passwords: passwords)
|
let image = Self.decryptAndParseImage(encryptedString: encryptedString, passwords: passwords)
|
||||||
|
|||||||
@@ -160,12 +160,13 @@ private final class MockAttachmentFlowTransport: AttachmentFlowTransporting {
|
|||||||
var tagsById: [String: String] = [:]
|
var tagsById: [String: String] = [:]
|
||||||
private(set) var uploadedIds: [String] = []
|
private(set) var uploadedIds: [String] = []
|
||||||
|
|
||||||
func uploadFile(id: String, content: Data) async throws -> String {
|
func uploadFile(id: String, content: Data) async throws -> (tag: String, server: String) {
|
||||||
uploadedIds.append(id)
|
uploadedIds.append(id)
|
||||||
return tagsById[id] ?? UUID().uuidString.lowercased()
|
let tag = tagsById[id] ?? UUID().uuidString.lowercased()
|
||||||
|
return (tag: tag, server: "https://mock-transport.test")
|
||||||
}
|
}
|
||||||
|
|
||||||
func downloadFile(tag: String) async throws -> Data {
|
func downloadFile(tag: String, server: String?) async throws -> Data {
|
||||||
Data()
|
Data()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user