Skip to content

fix: stabilize message media loading [WPB-18914]#4687

Open
Garzas wants to merge 3 commits intodevelopfrom
fix/stabilize-message-media-loading
Open

fix: stabilize message media loading [WPB-18914]#4687
Garzas wants to merge 3 commits intodevelopfrom
fix/stabilize-message-media-loading

Conversation

@Garzas
Copy link
Copy Markdown
Contributor

@Garzas Garzas commented Apr 1, 2026


PR Submission Checklist for internal contributors

  • The PR Title

    • conforms to the style of semantic commits messages¹ supported in Wire's Github Workflow²
    • contains a reference JIRA issue number like SQPIT-764
    • answers the question: If merged, this PR will: ... ³
  • The PR Description

    • is free of optional paragraphs and you have filled the relevant parts to the best of your ability

What's new in this PR?

Issues

  • Opening a conversation with a large backlog of missed messages could leave the message list in an unstable loading state.
  • Instead of showing a single loading phase, the conversation rendered messages progressively one by one, with many per-message loading indicators visible in the list.
  • Media loading inside the conversation was tightly coupled to the message list read model, which also increased the risk of visual churn while messages were being loaded.

Causes (Optional)

  • The conversation paging query depended on local asset cache data through MessageDetailsView, so writes to the Asset table could invalidate the message list while the conversation was loading.
  • Local asset paths were propagated through the message read model, even though they represent local runtime/cache state rather than stable message data.
  • Some media UI flows relied on local asset path data being present directly in the message model instead of resolving it on demand for the specific item.

Solutions

  • Removed the dependency between conversation message paging and local asset cache writes by detaching MessageDetailsView from the Asset table and adding a forward DB migration.
  • Removed local asset path from the message read model and stopped treating it as part of the main message state.
  • Switched media components to resolve local asset paths on demand for the specific message item instead of loading them through the whole conversation list model.
  • Updated image/video/audio-related flows so they no longer rely on local asset path being embedded in the message read model.
  • Fixed follow-up media regressions caused by this refactor, including item flicker/blinking and concurrent temporary file collisions during asset downloads.

@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud bot commented Apr 1, 2026

Quality Gate Failed Quality Gate failed

Failed conditions
9.9% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

audioMessagePlayer.preloadAudioMessage(args.conversationId, args.messageId)
try {
// calls preload to initially fetch the audio asset data to be ready and schedule waves mask generation if needed
audioMessagePlayer.preloadAudioMessage(args.conversationId, args.messageId)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ConversationAudioMessagePlayer is a singleton, wouldn't be better to have this logic of checking and not preloading the same audio message again inside this singleton player instead of having this not so pretty approach here with keeping a map in companion object?

.firstOrNull { it != null } // wait for the first non-null value
.let { state = state.copy(wavesMask = it) }
?.let {
cachedWavesMasks[args.key] = it
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Waves masks are being generated only once and then are stored in the DB, so there isn't much overhead here. fetchWavesMask waits until the waves mask is generated, or just takes the value from DB if it's already there.

I don't understand this idea of having cachedWavesMasks - fetchWavesMask method is still executed, even if we initially already have a wave mask in AudioMessageState, so there's no advantage.

hiltViewModelScoped<AssetLocalPathViewModelImpl, AssetLocalPathViewModel, AssetLocalPathArgs>(
AssetLocalPathArgs(message.conversationId, message.header.messageId)
)
var rememberedAssetDataPath by rememberSaveable(message.header.messageId) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm not sure if I get that but isn't it redundant?
I mean, viewModel.localAssetPath is already a composable state, and it's stored in a ViewModel so it survives when the screen recomposes, why then do we need also rememberSaveable variable here?

}

private companion object {
val cachedLocalAssetPaths = ConcurrentHashMap<String, String>()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm still not convinced whether it's a good approach. I think I would rather prefer having some in-memory cache in kalium, like we do with FlowCache for instance, because this approach honestly feels like an anti-pattern, it breaks the separation of concerns, mixes logic layer with storage and makes it so that the data doesn't flow the way it should so it can also make it harder to test.

Comment on lines +76 to 81
if (configuration is SwipeableMessageConfiguration.NotSwipeable) {
content()
return
}

SwipeableBox(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I fixed some excessive recompositions some time ago: #4398
This change will actually introduce a regression and make it so that these unwanted recompositions start happening again, because when the message's state changes, then message.isSwipeable changes and depending on that we will again either wrap the content in SwipeableMessageBox or not which causes the whole message’s content to recompose.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants