Skip to content

Commit eeafed9

Browse files
authored
Merge branch 'develop' into feat/files-versioning
2 parents 7d1a9e3 + 7208713 commit eeafed9

88 files changed

Lines changed: 1082 additions & 3820 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Cartfile.resolved

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
1-
github "wireapp/cryptobox-ios" "v1.1.0_xcframework_arm64simulator"
21
github "wireapp/libPhoneNumber-iOS" "1.1"
32
github "wireapp/ocmock" "v3.4.3_Xcode14.3.1"

WireDomain/Sources/WireDomain/Components/ClientSessionComponent.swift

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -808,7 +808,7 @@ public final class ClientSessionComponent {
808808

809809
public lazy var workAgent: WorkAgent = .init(scheduler: PriorityOrderWorkItemScheduler())
810810

811-
public lazy var conversationUpdatesGenerator: ConversationUpdatesGeneratorProtocol = ConversationUpdatesGenerator(
811+
public lazy var conversationUpdatesGenerator: IncrementalGeneratorProtocol = ConversationUpdatesGenerator(
812812
repository: conversationRepository,
813813
context: syncContext,
814814
onConversationUpdated: { [weak self] workItem in
@@ -817,4 +817,29 @@ public final class ClientSessionComponent {
817817
}
818818
)
819819

820+
public lazy var commitPendingProposalsGenerator: LiveGeneratorProtocol = CommitPendingProposalsGenerator(
821+
repository: conversationRepository,
822+
mlsService: mlsService,
823+
context: syncContext,
824+
isMLSGroupBroken: { [weak self] groupID in
825+
self?.isMLSGroupBroken(groupID: groupID) == true
826+
},
827+
onCommitPendingProposals: { [weak self] workItem in
828+
829+
self?.workAgent.submitItem(workItem)
830+
}
831+
)
832+
833+
public lazy var generatorsDirectory = GeneratorsDirectory(
834+
generators: [
835+
conversationUpdatesGenerator,
836+
commitPendingProposalsGenerator
837+
],
838+
syncStatePublisher: syncStateSubject.eraseToAnyPublisher()
839+
)
840+
841+
private func isMLSGroupBroken(groupID: MLSGroupID) -> Bool {
842+
let brokenGroupIds = journal[.brokenMLSGroupIDs]
843+
return brokenGroupIds.contains(groupID.description)
844+
}
820845
}

WireDomain/Sources/WireDomain/Event Decryption/UpdateEventDecryptor.swift

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,6 @@ struct UpdateEventDecryptor: UpdateEventDecryptorProtocol {
8080

8181
var decryptedEvents = [UpdateEvent]()
8282
var brokenMLSGroupIDs = Set<String>()
83-
var shouldCommitPendingProposals = false
8483

8584
for event in eventEnvelope.events {
8685
logAttributes[.messageType] = event.name
@@ -120,7 +119,6 @@ struct UpdateEventDecryptor: UpdateEventDecryptorProtocol {
120119
"decrypting MLS add message event...",
121120
attributes: logAttributes
122121
)
123-
shouldCommitPendingProposals = true
124122

125123
do {
126124
let decryptedEventData = try await mlsMessageDecryptor.decryptedMessageAddEventData(
@@ -170,23 +168,9 @@ struct UpdateEventDecryptor: UpdateEventDecryptorProtocol {
170168
}
171169
}
172170

173-
if shouldCommitPendingProposals {
174-
Task.detached {
175-
// we don't need to wait for this, as it can take a while to finish
176-
// it should not block decryption
177-
await commitPendingProposalsIfNeeded()
178-
}
179-
}
180-
181171
return EventDecryptorResult(events: decryptedEvents, brokenMLSGroupIDs: brokenMLSGroupIDs)
182172
}
183173

184-
private func commitPendingProposalsIfNeeded() async {
185-
// MLSService will be nil when called from push notification service.
186-
// As we don't need to commit pending proposals in that case.
187-
await mlsService?.commitPendingProposalsIfNeeded()
188-
}
189-
190174
private func appendFailedToDecryptProteusMessage(
191175
eventData: ConversationProteusMessageAddEvent,
192176
error: ProteusError

WireDomain/Sources/WireDomain/Repositories/Conversations/LocalStore/ConversationLocalStore.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1302,6 +1302,15 @@ public final class ConversationLocalStore: ConversationLocalStoreProtocol {
13021302
}
13031303
}
13041304

1305+
public func execute(
1306+
identifier: MLSGroupID,
1307+
block: @escaping @Sendable (ZMConversation?, NSManagedObjectContext) -> Void
1308+
) async {
1309+
await context.perform { [context] in
1310+
let conversation = ZMConversation.fetch(with: identifier, in: context)
1311+
block(conversation, context)
1312+
}
1313+
}
13051314
}
13061315

13071316
// MARK: - Private helpers

WireDomain/Sources/WireDomain/Repositories/Conversations/Protocols/ConversationLocalStoreProtocol.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,4 +469,8 @@ public protocol ConversationLocalStoreProtocol {
469469

470470
func fetchServerTimeDelta() async -> TimeInterval
471471

472+
func execute(
473+
identifier: MLSGroupID,
474+
block: @escaping @Sendable (ZMConversation?, NSManagedObjectContext) -> Void
475+
) async
472476
}

WireDomain/Sources/WireDomain/Repositories/Conversations/Protocols/ConversationRepositoryProtocol.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,4 +192,10 @@ public protocol ConversationRepositoryProtocol: Sendable {
192192
conversationID: String
193193
) async throws -> String?
194194

195+
/// Checks if selfUser is still in a given conversation
196+
/// - Parameter groupID: mlsGroupID of the conversation
197+
/// - Returns: true if selfUser belongs to the conversation, false otherwise
198+
func isSelfAnActiveMember(
199+
in groupID: WireDataModel.MLSGroupID
200+
) async -> Bool
195201
}

WireDomain/Sources/WireDomain/Repositories/Conversations/Repository/ConversationRepository.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,16 @@ public final class ConversationRepository: ConversationRepositoryProtocol {
377377
await deleteMembership(for: removedUserIDs, time: date)
378378
}
379379

380+
public func isSelfAnActiveMember(
381+
in groupID: MLSGroupID
382+
) async -> Bool {
383+
nonisolated(unsafe) var isSelfAnActiveMember = false
384+
await conversationsLocalStore.execute(identifier: groupID) { conversation, _ in
385+
isSelfAnActiveMember = conversation?.isSelfAnActiveMember ?? false
386+
}
387+
return isSelfAnActiveMember
388+
}
389+
380390
// MARK: - Private
381391

382392
private func addSystemMessage(
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
//
2+
// Wire
3+
// Copyright (C) 2025 Wire Swiss GmbH
4+
//
5+
// This program is free software: you can redistribute it and/or modify
6+
// it under the terms of the GNU General Public License as published by
7+
// the Free Software Foundation, either version 3 of the License, or
8+
// (at your option) any later version.
9+
//
10+
// This program is distributed in the hope that it will be useful,
11+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
// GNU General Public License for more details.
14+
//
15+
// You should have received a copy of the GNU General Public License
16+
// along with this program. If not, see http://www.gnu.org/licenses/.
17+
//
18+
import Foundation
19+
import WireDataModel
20+
import WireLogging
21+
22+
public final class CommitPendingProposalsGenerator: NSObject, LiveGeneratorProtocol {
23+
24+
private let context: NSManagedObjectContext
25+
private var fetchedResultsController: NSFetchedResultsController<ZMConversation>?
26+
private let repository: ConversationRepositoryProtocol
27+
private let mlsService: MLSServiceInterface
28+
private let isMLSGroupBroken: (MLSGroupID) -> Bool
29+
private var onCommitPendingProposals: (CommitPendingProposalItem) -> Void
30+
31+
private var scheduledTasks: [QualifiedID: Task<Void, Never>] = [:]
32+
33+
init(
34+
repository: ConversationRepositoryProtocol,
35+
mlsService: MLSServiceInterface,
36+
context: NSManagedObjectContext,
37+
isMLSGroupBroken: @escaping (MLSGroupID) -> Bool,
38+
onCommitPendingProposals: @escaping (CommitPendingProposalItem) -> Void
39+
) {
40+
self.context = context
41+
self.onCommitPendingProposals = onCommitPendingProposals
42+
self.repository = repository
43+
self.mlsService = mlsService
44+
self.isMLSGroupBroken = isMLSGroupBroken
45+
super.init()
46+
}
47+
48+
public func start() async {
49+
if fetchedResultsController == nil {
50+
fetchedResultsController = createFetchedResultsController()
51+
fetchedResultsController?.delegate = self
52+
}
53+
54+
await context.perform {
55+
do {
56+
try self.fetchedResultsController?.performFetch()
57+
} catch {
58+
WireLogger.conversation.error("error fetching conversations: \(String(describing: error))")
59+
}
60+
61+
let conversations = self.fetchedResultsController?.fetchedObjects ?? []
62+
for conversation in conversations {
63+
self.scheduleCommitIfNeeded(for: conversation)
64+
}
65+
}
66+
}
67+
68+
public func stop() {
69+
fetchedResultsController = nil
70+
71+
// Cancel all scheduled commits
72+
for (_, task) in scheduledTasks {
73+
task.cancel()
74+
}
75+
scheduledTasks.removeAll()
76+
}
77+
78+
private func createFetchedResultsController() -> NSFetchedResultsController<ZMConversation> {
79+
let request = NSFetchRequest<ZMConversation>(entityName: ZMConversation.entityName())
80+
request.predicate = ZMConversation.commitPendingProposalDatePredicate()
81+
request.sortDescriptors = [ZMConversation.sortCommitPendingProsalsByDateAscending()]
82+
return NSFetchedResultsController(
83+
fetchRequest: request,
84+
managedObjectContext: context,
85+
sectionNameKeyPath: nil,
86+
cacheName: nil
87+
)
88+
}
89+
90+
private func scheduleCommitIfNeeded(for conversation: ZMConversation) {
91+
guard
92+
let conversationID = conversation.qualifiedID,
93+
let timestamp = conversation.commitPendingProposalDate,
94+
let mlsGroupID = conversation.mlsGroupID,
95+
conversation.isSelfAnActiveMember,
96+
!isMLSGroupBroken(mlsGroupID)
97+
else {
98+
// If the conversation no longer qualifies, cancel any existing schedule.
99+
if let id = conversation.qualifiedID {
100+
scheduledTasks[id]?.cancel()
101+
scheduledTasks[id] = nil
102+
}
103+
return
104+
}
105+
106+
// Reschedule (cancel previous if any)
107+
scheduledTasks[conversationID]?.cancel()
108+
109+
// we create a task that will generate a workItem in time because we don't want to block the WorkAgent from
110+
// executing other workItems
111+
let task = Task { [repository, mlsService, onCommitPendingProposals] in
112+
113+
let delay = timestamp.timeIntervalSinceNow
114+
if delay > 0 {
115+
do { try await Task.sleep(for: .seconds(delay)) } catch { return } // cancelled
116+
}
117+
118+
// Re-check membership right before enqueuing the actual work item
119+
let stillMember = await repository.isSelfAnActiveMember(in: mlsGroupID)
120+
guard stillMember else { return }
121+
122+
// Enqueue parent group item
123+
onCommitPendingProposals(
124+
CommitPendingProposalItem(
125+
repository: repository,
126+
conversationID: conversationID,
127+
groupID: mlsGroupID,
128+
mlsService: mlsService
129+
)
130+
)
131+
132+
// Enqueue subconversation item if any
133+
if let subgroupID = await mlsService.conferenceSubconversation(parentGroupID: mlsGroupID) {
134+
onCommitPendingProposals(
135+
CommitPendingProposalItem(
136+
repository: repository,
137+
conversationID: conversationID,
138+
groupID: subgroupID,
139+
mlsService: mlsService
140+
)
141+
)
142+
}
143+
}
144+
145+
scheduledTasks[conversationID] = task
146+
147+
WireLogger.workAgent.debug(
148+
"scheduled commit pending proposal work-item",
149+
attributes: [.mlsGroupID: mlsGroupID.safeForLoggingDescription]
150+
)
151+
}
152+
}
153+
154+
// MARK: - NSFetchedResultsControllerDelegate
155+
156+
extension CommitPendingProposalsGenerator: NSFetchedResultsControllerDelegate {
157+
158+
public func controller(
159+
_ controller: NSFetchedResultsController<NSFetchRequestResult>,
160+
didChange anObject: Any,
161+
at indexPath: IndexPath?,
162+
for type: NSFetchedResultsChangeType,
163+
newIndexPath: IndexPath?
164+
) {
165+
guard let conversation = anObject as? ZMConversation else {
166+
fatal("unexpected object, expected ZMConversation")
167+
}
168+
169+
switch type {
170+
case .insert, .update:
171+
// run on the context queue to safely read properties
172+
Task { [context] in
173+
await context.perform {
174+
self.scheduleCommitIfNeeded(for: conversation)
175+
}
176+
}
177+
178+
case .move, .delete:
179+
// Best effort cancel if we can identify it
180+
if let id = conversation.qualifiedID {
181+
scheduledTasks[id]?.cancel()
182+
scheduledTasks[id] = nil
183+
}
184+
185+
@unknown default:
186+
break
187+
}
188+
}
189+
}

WireDomain/Sources/WireDomain/Synchronization/ConversationUpdatesGenerator.swift

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,7 @@ import Foundation
1919
import WireDataModel
2020
import WireLogging
2121

22-
/// sourcery: AutoMockable
23-
public protocol ConversationUpdatesGeneratorProtocol {
24-
func start() async
25-
func stop()
26-
}
27-
28-
public final class ConversationUpdatesGenerator: NSObject, ConversationUpdatesGeneratorProtocol {
22+
public final class ConversationUpdatesGenerator: NSObject, IncrementalGeneratorProtocol {
2923

3024
private let context: NSManagedObjectContext
3125
private var fetchedResultsController: NSFetchedResultsController<ZMConversation>?
@@ -46,7 +40,7 @@ public final class ConversationUpdatesGenerator: NSObject, ConversationUpdatesGe
4640
/// Starts monitoring and triggers pulls for any needingToBeUpdatedFromBackend conversations.
4741
public func start() async {
4842
if fetchedResultsController == nil {
49-
fetchedResultsController = createFetchRequestController()
43+
fetchedResultsController = createFetchedResultsController()
5044
fetchedResultsController?.delegate = self
5145
}
5246
await context.perform {
@@ -73,7 +67,7 @@ public final class ConversationUpdatesGenerator: NSObject, ConversationUpdatesGe
7367
fetchedResultsController = nil
7468
}
7569

76-
private func createFetchRequestController() -> NSFetchedResultsController<ZMConversation> {
70+
private func createFetchedResultsController() -> NSFetchedResultsController<ZMConversation> {
7771
let request = NSFetchRequest<ZMConversation>(entityName: ZMConversation.entityName())
7872
request.predicate = NSPredicate.all(of: [
7973
ZMConversation.predicateForNeedingToBeUpdatedFromBackend(),
@@ -87,7 +81,6 @@ public final class ConversationUpdatesGenerator: NSObject, ConversationUpdatesGe
8781
sectionNameKeyPath: nil,
8882
cacheName: nil
8983
)
90-
9184
}
9285
}
9386

0 commit comments

Comments
 (0)