TM-SGNL-iOS/Signal/ConversationView/CVAttachments.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

214 lines
7.4 KiB
Swift

//
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import AVFoundation
import Foundation
public import SignalServiceKit
// Represents a _playable_ audio attachment.
public class AudioAttachment {
public enum State: Equatable {
case attachmentStream(
attachmentStream: ReferencedAttachmentStream,
audioDurationSeconds: TimeInterval
)
case attachmentPointer(
attachmentPointer: ReferencedAttachmentTransitPointer,
transitTierDownloadState: AttachmentDownloadState
)
public static func == (lhs: AudioAttachment.State, rhs: AudioAttachment.State) -> Bool {
switch (lhs, rhs) {
case let (
.attachmentStream(lhsStream, lhsDuration),
.attachmentStream(rhsStream, rhsDuration)
):
return lhsStream.attachmentStream.id == rhsStream.attachmentStream.id
&& lhsStream.reference.hasSameOwner(as: rhsStream.reference)
&& lhsDuration == rhsDuration
case let (
.attachmentPointer(lhsPointer, lhsState),
.attachmentPointer(rhsPointer, rhsState)
):
return lhsPointer.attachment.id == rhsPointer.attachment.id
&& lhsPointer.reference.hasSameOwner(as: rhsPointer.reference)
&& lhsState == rhsState
case (.attachmentStream, _), (.attachmentPointer, _):
return false
}
}
}
public let state: State
public var sourceFilename: String? {
switch state {
case .attachmentStream(let attachmentStream, _):
return attachmentStream.reference.sourceFilename
case .attachmentPointer(let attachmentPointer, _):
return attachmentPointer.reference.sourceFilename
}
}
public let receivedAtDate: Date
public let owningMessage: TSMessage?
// Set at time of init. Value doesn't change even after download completes
// to ensure that conversation view diffing catches the need to redraw the cell
public let isDownloading: Bool
public init?(
attachmentStream: ReferencedAttachmentStream,
owningMessage: TSMessage?,
metadata: MediaMetadata?,
receivedAtDate: Date
) {
let audioDurationSeconds: TimeInterval
switch attachmentStream.attachmentStream.contentType {
case .audio(var duration, _):
// TODO: Remove & replace with a full fix to recompute the duration for invalid files.
if duration <= 0 {
duration = Self.cachedAudioDuration(forAttachment: attachmentStream.attachmentStream)
}
if duration <= 0 {
fallthrough
}
audioDurationSeconds = duration
default:
return nil
}
self.state = .attachmentStream(
attachmentStream: attachmentStream,
audioDurationSeconds: audioDurationSeconds
)
self.isDownloading = false
self.receivedAtDate = receivedAtDate
self.owningMessage = owningMessage
}
public init(
attachmentPointer: ReferencedAttachmentTransitPointer,
owningMessage: TSMessage?,
metadata: MediaMetadata?,
receivedAtDate: Date,
transitTierDownloadState: AttachmentDownloadState
) {
state = .attachmentPointer(
attachmentPointer: attachmentPointer,
transitTierDownloadState: transitTierDownloadState
)
switch transitTierDownloadState {
case .failed, .none:
isDownloading = false
case .enqueuedOrDownloading:
isDownloading = true
}
self.receivedAtDate = receivedAtDate
self.owningMessage = owningMessage
}
private static let cachedAttachmentDurations = AtomicDictionary<Int64, TimeInterval>([:], lock: .init())
private static func cachedAudioDuration(forAttachment attachmentStream: AttachmentStream) -> TimeInterval {
let attachmentId = attachmentStream.attachment.id
if let cachedDuration = cachedAttachmentDurations[attachmentId] {
return cachedDuration
}
let computedDuration: TimeInterval
do {
let asset = try attachmentStream.decryptedAVAsset()
computedDuration = asset.duration.seconds
} catch {
Logger.warn("Couldn't compute fallback duration: \(error)")
computedDuration = 0
}
cachedAttachmentDurations[attachmentId] = computedDuration
return computedDuration
}
}
extension AudioAttachment {
var isDownloaded: Bool { attachmentStream != nil }
public var attachment: Attachment {
switch state {
case .attachmentStream(let attachmentStream, _):
return attachmentStream.attachment
case .attachmentPointer(let attachmentPointer, _):
return attachmentPointer.attachment
}
}
public var attachmentStream: ReferencedAttachmentStream? {
switch state {
case .attachmentStream(let attachmentStream, _):
return attachmentStream
case .attachmentPointer:
return nil
}
}
public var attachmentPointer: ReferencedAttachmentTransitPointer? {
switch state {
case .attachmentStream:
return nil
case .attachmentPointer(let attachmentPointer, _):
return attachmentPointer
}
}
public var durationSeconds: TimeInterval {
switch state {
case .attachmentStream(_, let audioDurationSeconds):
return audioDurationSeconds
case .attachmentPointer:
return 0
}
}
public var isVoiceMessage: Bool {
{ () -> AttachmentReference.RenderingFlag in
switch state {
case .attachmentStream(let attachmentStream, _):
return attachmentStream.reference.renderingFlag
case .attachmentPointer(let attachmentPointer, _):
return attachmentPointer.reference.renderingFlag
}
}() == .voiceMessage
}
public func markOwningMessageAsViewed() -> Bool {
AssertIsOnMainThread()
guard let incomingMessage = owningMessage as? TSIncomingMessage, !incomingMessage.wasViewed else { return false }
SSKEnvironment.shared.databaseStorageRef.asyncWrite { tx in
let uniqueId = incomingMessage.uniqueId
guard
let latestMessage = TSIncomingMessage.anyFetchIncomingMessage(uniqueId: uniqueId, transaction: tx),
let latestThread = latestMessage.thread(tx: tx)
else {
return
}
let circumstance: OWSReceiptCircumstance = (
latestThread.hasPendingMessageRequest(transaction: tx)
? .onThisDeviceWhilePendingMessageRequest
: .onThisDevice
)
latestMessage.markAsViewed(
atTimestamp: Date.ows_millisecondTimestamp(),
thread: latestThread,
circumstance: circumstance,
transaction: tx
)
}
return true
}
}
extension AudioAttachment: Equatable {
public static func == (lhs: AudioAttachment, rhs: AudioAttachment) -> Bool {
lhs.state == rhs.state &&
lhs.owningMessage == rhs.owningMessage &&
lhs.isDownloading == rhs.isDownloading
}
}