Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 26 additions & 70 deletions Convos/Conversation Detail/ConversationViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class ConversationViewModel {
private let consentWriter: any ConversationConsentWriterProtocol
private let localStateWriter: any ConversationLocalStateWriterProtocol
private let metadataWriter: any ConversationMetadataWriterProtocol
private let explosionWriter: any ConversationExplosionWriterProtocol
private let explosionCoordinator: ExplosionCoordinator
private let reactionWriter: any ReactionWriterProtocol
private let conversationRepository: any ConversationRepositoryProtocol
private let messagesListRepository: any MessagesListRepositoryProtocol
Expand Down Expand Up @@ -210,7 +210,7 @@ class ConversationViewModel {
!messageText.isEmpty || selectedAttachmentImage != nil
}
private(set) var isSendingPhoto: Bool = false
var explodeState: ExplodeState = .ready
var explodeState: ExplodeState { explosionCoordinator.state }

var presentingConversationSettings: Bool = false
var presentingProfileSettings: Bool = false
Expand Down Expand Up @@ -289,12 +289,8 @@ class ConversationViewModel {
@ObservationIgnored
private var loadConversationImageTask: Task<Void, Never>?

@ObservationIgnored
private var explodeTask: Task<Void, Never>?

deinit {
loadConversationImageTask?.cancel()
explodeTask?.cancel()
assistantJoinTask?.cancel()
}

Expand Down Expand Up @@ -352,7 +348,9 @@ class ConversationViewModel {
self.consentWriter = conversationStateManager.conversationConsentWriter
self.localStateWriter = conversationStateManager.conversationLocalStateWriter
self.metadataWriter = conversationStateManager.conversationMetadataWriter
self.explosionWriter = messagingService.conversationExplosionWriter()
self.explosionCoordinator = ExplosionCoordinator(
explosionWriter: messagingService.conversationExplosionWriter()
)
self.reactionWriter = messagingService.reactionWriter()

let myProfileWriter = conversationStateManager.myProfileWriter
Expand Down Expand Up @@ -418,7 +416,9 @@ class ConversationViewModel {
self.consentWriter = conversationStateManager.conversationConsentWriter
self.localStateWriter = conversationStateManager.conversationLocalStateWriter
self.metadataWriter = conversationStateManager.conversationMetadataWriter
self.explosionWriter = messagingService.conversationExplosionWriter()
self.explosionCoordinator = ExplosionCoordinator(
explosionWriter: messagingService.conversationExplosionWriter()
)
self.reactionWriter = messagingService.reactionWriter()

let myProfileWriter = conversationStateManager.myProfileWriter
Expand Down Expand Up @@ -1092,78 +1092,34 @@ extension ConversationViewModel {
extension ConversationViewModel {
func explodeConvo() {
guard canRemoveMembers else { return }
guard explodeState.isReady || explodeState.isError || explodeState.isScheduled else { return }

explodeState = .exploding

explodeTask?.cancel()
explodeTask = Task { [weak self] in
explosionCoordinator.explode(
conversationId: conversation.id,
memberInboxIds: conversation.members.map { $0.profile.inboxId },
displayName: conversation.displayName
) { [weak self] in
guard let self else { return }
self.presentingConversationSettings = false

do {
let memberIds = conversation.members.map { $0.profile.inboxId }
try await explosionWriter.explodeConversation(
conversationId: conversation.id,
memberInboxIds: memberIds
)

self.presentingConversationSettings = false
self.explodeState = .exploded

await UNUserNotificationCenter.current().addExplosionNotification(
conversationId: conversation.id,
displayName: conversation.displayName
)

NotificationCenter.default.post(
name: .conversationExpired,
object: nil,
userInfo: ["conversationId": self.conversation.id]
)
self.conversation.postLeftConversationNotification()
Log.info("Explode complete, inbox deletion triggered")
} catch {
Log.error("Error exploding convo: \(error.localizedDescription)")
self.explodeState = .error("Explode failed")
try? await Task.sleep(for: .seconds(2))
guard !Task.isCancelled else { return }
self.explodeState = .ready
}
NotificationCenter.default.post(
name: .conversationExpired,
object: nil,
userInfo: ["conversationId": self.conversation.id]
)
self.conversation.postLeftConversationNotification()
}
}

func scheduleExplosion(at expiresAt: Date) {
guard canRemoveMembers else { return }
// Intentionally excludes .isScheduled — rescheduling is not supported
guard explodeState.isReady || explodeState.isError else { return }

if expiresAt <= Date() {
explodeConvo()
return
}

explodeState = .exploding

explodeTask?.cancel()
explodeTask = Task { [weak self] in
guard let self else { return }

do {
try await explosionWriter.scheduleExplosion(
conversationId: conversation.id,
expiresAt: expiresAt
)

self.explodeState = .scheduled(expiresAt)
Log.info("Explosion scheduled for \(expiresAt)")
} catch {
Log.error("Error scheduling explosion: \(error.localizedDescription)")
self.explodeState = .error("Schedule failed")
try? await Task.sleep(for: .seconds(2))
guard !Task.isCancelled else { return }
self.explodeState = .ready
explosionCoordinator.scheduleExplosion(
conversationId: conversation.id,
expiresAt: expiresAt,
onImmediateExplosion: { [weak self] in
self?.explodeConvo()
}
}
)
}
}

Expand Down
98 changes: 98 additions & 0 deletions Convos/Conversation Detail/ExplosionCoordinator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import ConvosCore
import Foundation
import Observation
import UserNotifications

@MainActor
@Observable
final class ExplosionCoordinator {
private let explosionWriter: any ConversationExplosionWriterProtocol

private(set) var state: ExplodeState = .ready

@ObservationIgnored
private var explodeTask: Task<Void, Never>?

init(explosionWriter: any ConversationExplosionWriterProtocol) {
self.explosionWriter = explosionWriter
}

deinit {
explodeTask?.cancel()
}

func explode(
conversationId: String,
memberInboxIds: [String],
displayName: String,
onExploded: @escaping () -> Void
) {
guard state.isReady || state.isError || state.isScheduled else { return }

state = .exploding

explodeTask?.cancel()
explodeTask = Task { [weak self] in
guard let self else { return }

do {
try await explosionWriter.explodeConversation(
conversationId: conversationId,
memberInboxIds: memberInboxIds
)

self.state = .exploded

await UNUserNotificationCenter.current().addExplosionNotification(
conversationId: conversationId,
displayName: displayName
)

onExploded()
Log.info("Explode complete, inbox deletion triggered")
} catch {
Log.error("Error exploding convo: \(error.localizedDescription)")
self.state = .error("Explode failed")
try? await Task.sleep(for: .seconds(2))
guard !Task.isCancelled else { return }
self.state = .ready
}
}
}

func scheduleExplosion(
conversationId: String,
expiresAt: Date,
onImmediateExplosion: () -> Void
) {
guard state.isReady || state.isError else { return }

if expiresAt <= Date() {
onImmediateExplosion()
return
}

state = .exploding

explodeTask?.cancel()
explodeTask = Task { [weak self] in
guard let self else { return }

do {
try await explosionWriter.scheduleExplosion(
conversationId: conversationId,
expiresAt: expiresAt
)

self.state = .scheduled(expiresAt)
Log.info("Explosion scheduled for \(expiresAt)")
} catch {
Log.error("Error scheduling explosion: \(error.localizedDescription)")
self.state = .error("Schedule failed")
try? await Task.sleep(for: .seconds(2))
guard !Task.isCancelled else { return }
self.state = .ready
}
}
}
}
Loading