Skip to content

Conversation

@netbe
Copy link
Collaborator

@netbe netbe commented Oct 27, 2025

BugWPB-20123 [iOS] Invisible groups, channels and 1:1 chats with functioning push notifications

Issue

Sometimes conversations are not shown within the app. Some conversations were found not updated with data from backend for example having a invalid groupType hence not displayed.

Right now, the ConversationsRequestStrategy with the updateSync (KeyPathObjectSync) sync the conversation with the backend when the NeedsToBeUpdatedFromBackend property is set to true, but it does not wait for the request to finish to turn off the flag, so if the request fails or the app crashes, we'll never retry the request.

Solution:

  • Using WorkAgent in feat: introduce WorkAgent - WPB-20123 #3800, we remove old KeyPathObjectSync
  • ConversationUpdatesGenerator creates UpdateConversationItem workItems for the WorkAgent.
  • remove pullConversation from the mlsWelcomeMessage event processing so processing events are without extra network calls.

Other changes:

  • add logs for processing events

Testing

  1. Create a group
  2. See ticket being created and executed

Checklist

  • Title contains a reference JIRA issue number like [WPB-XXX].
  • Description is filled and free of optional paragraphs.
  • Adds/updates automated tests.

UI accessibility checklist

If your PR includes UI changes, please utilize this checklist:

  • Make sure you use the API for UI elements that support large fonts.
  • All colors are taken from WireDesign.ColorTheme or constructed using WireDesign.BaseColorPalette.
  • New UI elements have Accessibility strings for VoiceOver.

@netbe netbe force-pushed the fix/invisible-groups-WPB-20123 branch from d95d9ff to 3b5f27c Compare October 30, 2025 21:14
@netbe netbe changed the base branch from release/cycle-4.8 to release/cycle-4.9 October 30, 2025 21:15
@netbe netbe changed the base branch from release/cycle-4.9 to feat/work-agent October 30, 2025 21:25
public func start() async {
fetchedResultsController.delegate = self
do {
try fetchedResultsController.performFetch()
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this class monitors the coredata change, and create ticket for scheduler

Comment on lines 1683 to 1688
var epoch: Int64 = 0
if let context, let conversation = fetchConversationInfo(
with: groupID,
in: context
)?.conversation {
epoch = Int64(conversation.epoch)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this part getting the epoch outside the CoreData context was failing on debug build

func performIncrementalSync() async throws {

Task {
await conversationsMonitor.start()
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we start generating the conversation sync with backend tickets only after initial sync is finished, in order to avoid having conversations syncing twice (in Worker and in InitialSync)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: if the monitor is inside the worker, we could instead tell the workers to "start" and "stop" work. In this case, the conversation worker would start and stop monitoring. We could even add these start stop methods as optional protocol methods on Worker, then flip a flag on the work agent isAcceptingTickets or isOpen, which would then inform all its workers to start or stop. What do you think?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so workers would start when WorkAgent start to run, right?

in the case of conversations, I don't know if that would solve the issue where initialSync pulling conversations would create conversations with needsToBeUpdatedFromBackend = true and trigger the worker.

  1. Did you mean that we should move setting needsToBeUpdatedFromBackend to true or false within the Worker?

}()

extension InitiateResetMLSConversationUseCase: ResetBrokenMLSConversationDelegate {
public lazy var conversationMonitor: ConversationUpdatesGeneratorProtocol = ConversationUpdatesGenerator(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: if we say that the worker is responsible for keeping local conversations in sync with the backend, then I would expect the worker to be capable of monitoring the database and raising tickets autonomously. In which case, the worker retains the monitor, the monitor simply informs a delegate or via callback that there are objects that it detected, and the worker creates the ticket.

func performIncrementalSync() async throws {

Task {
await conversationsMonitor.start()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: if the monitor is inside the worker, we could instead tell the workers to "start" and "stop" work. In this case, the conversation worker would start and stop monitoring. We could even add these start stop methods as optional protocol methods on Worker, then flip a flag on the work agent isAcceptingTickets or isOpen, which would then inform all its workers to start or stop. What do you think?

Base automatically changed from feat/work-agent to develop November 4, 2025 15:05
@netbe netbe force-pushed the fix/invisible-groups-WPB-20123 branch from 5361b25 to 99138f9 Compare November 5, 2025 11:28
@netbe netbe changed the base branch from develop to release/cycle-4.10 November 5, 2025 11:28
/// Cancel the work item.

func cancel() async
// func cancel() async
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@johnxnguyen this is not implemented, I wonder if we keep it

@netbe netbe marked this pull request as ready for review November 5, 2025 11:32
@github-actions
Copy link
Contributor

github-actions bot commented Nov 5, 2025

Test Results

   21 files  1 225 suites   12m 57s ⏱️
9 362 tests 9 333 ✅ 28 💤 1 ❌
9 375 runs  9 347 ✅ 28 💤 0 ❌

For more details on these failures, see this check.

Results for commit 2524cb0.

♻️ This comment has been updated with latest results.

@netbe netbe requested a review from johnxnguyen November 6, 2025 13:06
Copy link
Contributor

@samwyndham samwyndham left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. I've just left a few comments

self.conversationID = conversationID
}

func start() async throws {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question (out of scope): @netbe @johnxnguyen Am I right that the way a WorkAgent has changed since we discussed it last Friday? The work now belongs to a WorkItem and not some other actor?

There were a few things that were nice about the old approach:

  1. Less need to pass dependencies to the place that the work item is created.
  2. Possibility to persist work item across app runs.

Anyway this is out of scope for this PR.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes that's basically what change

Comment on lines +27 to +32
var id = UUID()
var priority: WorkItemPriority {
.medium
}

var conversationID: WireNetwork.QualifiedID
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Why are id and conversationID publicly mutable?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no reason, true I'll make it a let

for type: NSFetchedResultsChangeType,
newIndexPath: IndexPath?
) {
guard let conversation = anObject as? ZMConversation else { return }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Crash if this fails. The fetched results controller is configured in this file so we know that anObject right?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants