TM-SGNL-iOS/SignalServiceKit/Calls/CallRecord/CallRecordMissedCallManager.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

328 lines
12 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
public protocol CallRecordMissedCallManager {
/// The number of unread missed calls.
func countUnreadMissedCalls(tx: DBReadTransaction) -> UInt
/// Marks all unread calls at and before the given timestamp as read.
///
/// - Parameter beforeTimestamp
/// A timestamp at and before which to mark calls as read. If this value is
/// `nil`, all calls are marked as read.
/// - Parameter sendSyncMessage
/// Whether a sync message should be sent as part of this operation. No sync
/// message is sent regardless of this value if no calls are actually marked
/// as read. The sync message will be of type
/// ``OutgoingCallLogEventSyncMessage/CallLogEvent/EventType/markedAsRead``.
func markUnreadCallsAsRead(
beforeTimestamp: UInt64?,
sendSyncMessage: Bool,
tx: DBWriteTransaction
)
/// Marks the given call and all unread calls before it in the same
/// conversation as the given call as read.
///
/// - Parameter beforeCallRecord
/// The call identifying the conversation and timestamp at and before which
/// to mark unread calls as read.
/// - Parameter sendSyncMessage
/// Whether a sync message should be sent as part of this operation. No sync
/// message is sent regardless of this value if no calls are actually marked
/// as read. The sync message will be of type
/// ``OutgoingCallLogEventSyncMessage/CallLogEvent/EventType/markedAsReadInConversation``.
func markUnreadCallsInConversationAsRead(
beforeCallRecord: CallRecord,
sendSyncMessage: Bool,
tx: DBWriteTransaction
)
}
class CallRecordMissedCallManagerImpl: CallRecordMissedCallManager {
private let callRecordConversationIdAdapter: CallRecordSyncMessageConversationIdAdapter
private let callRecordQuerier: CallRecordQuerier
private let callRecordStore: CallRecordStore
private let syncMessageSender: Shims.SyncMessageSender
private var logger: CallRecordLogger { .shared }
init(
callRecordConversationIdAdapter: CallRecordSyncMessageConversationIdAdapter,
callRecordQuerier: CallRecordQuerier,
callRecordStore: CallRecordStore,
syncMessageSender: Shims.SyncMessageSender
) {
self.callRecordConversationIdAdapter = callRecordConversationIdAdapter
self.callRecordStore = callRecordStore
self.callRecordQuerier = callRecordQuerier
self.syncMessageSender = syncMessageSender
}
// MARK: -
func countUnreadMissedCalls(tx: DBReadTransaction) -> UInt {
var unreadMissedCallCount: UInt = 0
for missedCallStatus in CallRecord.CallStatus.missedCalls {
guard let unreadMissedCallCursor = callRecordQuerier.fetchCursorForUnread(
callStatus: missedCallStatus,
ordering: .descending,
tx: tx
) else { continue }
do {
while let _ = try unreadMissedCallCursor.next() {
unreadMissedCallCount += 1
}
} catch {
owsFailDebug("Unexpectedly failed to iterate CallRecord cursor!")
continue
}
}
return unreadMissedCallCount
}
func markUnreadCallsAsRead(
beforeTimestamp: UInt64?,
sendSyncMessage: Bool,
tx: DBWriteTransaction
) {
let fetchOrdering = fetchOrdering(forBeforeTimestamp: beforeTimestamp)
let markedAsReadCount = _markUnreadCallsAsRead(
fetchOrdering: fetchOrdering,
threadRowId: nil,
tx: tx
)
guard markedAsReadCount > 0 else { return }
logger.info("Marked \(markedAsReadCount) calls as read.")
if sendSyncMessage {
/// When doing a bulk mark-as-read, we want to use the newest call
/// at or before the indicated timestamp (read or not) to populate
/// the sync message. So, we'll query for a single call, using the
/// same fetch ordering we used above.
let mostRecentCall: CallRecord? = try? callRecordQuerier.fetchCursor(
ordering: fetchOrdering, tx: tx
)?.next()
guard let mostRecentCall else {
owsFailDebug("Unexpectedly failed to get most-recent call after marking calls as read!")
return
}
sendMarkedCallsAsReadSyncMessage(
callRecord: mostRecentCall,
eventType: .markedAsRead,
tx: tx
)
}
}
func markUnreadCallsInConversationAsRead(
beforeCallRecord: CallRecord,
sendSyncMessage: Bool,
tx: DBWriteTransaction
) {
let threadRowId: Int64
switch beforeCallRecord.conversationId {
case .thread(let threadRowId2):
threadRowId = threadRowId2
case .callLink(_):
owsFailDebug("Can't mark call links as read within a conversation.")
return
}
let markedAsReadCount = _markUnreadCallsAsRead(
fetchOrdering: fetchOrdering(
forBeforeTimestamp: beforeCallRecord.callBeganTimestamp
),
threadRowId: threadRowId,
tx: tx
)
guard markedAsReadCount > 0 else { return }
logger.info("Marked \(markedAsReadCount) calls as read.")
if sendSyncMessage {
sendMarkedCallsAsReadSyncMessage(
callRecord: beforeCallRecord,
eventType: .markedAsReadInConversation,
tx: tx
)
}
}
/// Mark calls before or at the given timestamp as read, optionally
/// considering only calls with the given thread row ID.
private func _markUnreadCallsAsRead(
fetchOrdering: CallRecordQuerier.FetchOrdering,
threadRowId: Int64?,
tx: DBWriteTransaction
) -> UInt {
var markedAsReadCount: UInt = 0
for callStatus in CallRecord.CallStatus.allCases {
let unreadCallCursor: CallRecordCursor? = {
if let threadRowId {
return callRecordQuerier.fetchCursorForUnread(
threadRowId: threadRowId,
callStatus: callStatus,
ordering: fetchOrdering,
tx: tx
)
} else {
return callRecordQuerier.fetchCursorForUnread(
callStatus: callStatus,
ordering: fetchOrdering,
tx: tx
)
}
}()
guard let unreadCallCursor else { continue }
do {
let markedAsReadCountBefore = markedAsReadCount
while let unreadCallRecord = try unreadCallCursor.next() {
markedAsReadCount += 1
do {
try callRecordStore.markAsRead(
callRecord: unreadCallRecord, tx: tx
)
} catch let error {
owsFailBeta("Failed to update call record: \(error)")
}
}
owsAssertDebug(
markedAsReadCount == markedAsReadCountBefore || callStatus.isMissedCall,
"Unexpectedly had \(markedAsReadCount - markedAsReadCountBefore) unread calls that were not missed!"
)
} catch {
owsFailDebug("Unexpectedly failed to iterate CallRecord cursor!")
continue
}
}
return markedAsReadCount
}
/// Returns a fetch ordering appropriate for querying calls at or before the
/// given timestamp. If a `nil` timestamp, all calls will be queried.
private func fetchOrdering(
forBeforeTimestamp beforeTimestamp: UInt64?
) -> CallRecordQuerier.FetchOrdering {
if let beforeTimestamp {
/// Adjust the timestamp forward one second to catch calls at
/// this exact timestamp. That's relevant because when we send
/// this sync message, we do so with the timestamp of an actual
/// call and because we (try to) sync call timestamps across
/// devices, our copy of the call likely has the exact same
/// timestamp. Without adjusting, we'll skip that call!
return .descendingBefore(timestamp: beforeTimestamp + 1)
}
return .descending
}
/// Send a "marked calls as read" sync message with the given event type, so
/// our other devices can also mark the calls as read.
///
/// - Parameter callRecord
/// A call record whose timestamp and other parameters will populate the
/// sync message.
/// - Parameter eventType
/// The type of sync message to send.
private func sendMarkedCallsAsReadSyncMessage(
callRecord: CallRecord,
eventType: OutgoingCallLogEventSyncMessage.CallLogEvent.EventType,
tx: DBWriteTransaction
) {
let conversationId: Data
do {
conversationId = try callRecordConversationIdAdapter.getConversationId(callRecord: callRecord, tx: tx)
} catch {
owsFailDebug("\(error)")
return
}
syncMessageSender.sendCallLogEventSyncMessage(
eventType: eventType,
callId: callRecord.callId,
conversationId: conversationId,
timestamp: callRecord.callBeganTimestamp,
tx: tx
)
}
}
// MARK: - Mocks
extension CallRecordMissedCallManagerImpl {
enum Shims {
typealias SyncMessageSender = _CallRecordMissedCallManagerImpl_SyncMessageSender_Shim
}
enum Wrappers {
typealias SyncMessageSender = _CallRecordMissedCallManagerImpl_SyncMessageSender_Wrapper
}
}
protocol _CallRecordMissedCallManagerImpl_SyncMessageSender_Shim {
func sendCallLogEventSyncMessage(
eventType: OutgoingCallLogEventSyncMessage.CallLogEvent.EventType,
callId: UInt64,
conversationId: Data,
timestamp: UInt64,
tx: DBWriteTransaction
)
}
class _CallRecordMissedCallManagerImpl_SyncMessageSender_Wrapper: _CallRecordMissedCallManagerImpl_SyncMessageSender_Shim {
private let messageSenderJobQueue: MessageSenderJobQueue
init(_ messageSenderJobQueue: MessageSenderJobQueue) {
self.messageSenderJobQueue = messageSenderJobQueue
}
func sendCallLogEventSyncMessage(
eventType: OutgoingCallLogEventSyncMessage.CallLogEvent.EventType,
callId: UInt64,
conversationId: Data,
timestamp: UInt64,
tx: DBWriteTransaction
) {
let sdsTx = SDSDB.shimOnlyBridge(tx)
guard let localThread = TSContactThread.getOrCreateLocalThread(transaction: sdsTx) else {
return
}
let outgoingCallLogEventSyncMessage = OutgoingCallLogEventSyncMessage(
callLogEvent: OutgoingCallLogEventSyncMessage.CallLogEvent(
eventType: eventType,
callId: callId,
conversationId: conversationId,
timestamp: timestamp
),
thread: localThread,
tx: sdsTx
)
let preparedMessage = PreparedOutgoingMessage.preprepared(
transientMessageWithoutAttachments: outgoingCallLogEventSyncMessage
)
messageSenderJobQueue.add(
message: preparedMessage,
transaction: sdsTx
)
}
}