Skip to content

Conversation

@anikiki
Copy link
Contributor

@anikiki anikiki commented Dec 16, 2025

Task/Issue URL: https://app.asana.com/1/137249556945/project/1200581511062568/task/1212304890091987?focus=true

Description

Added support for standard remote messages in the modal surface activity, which previously only supported cards list messages. This change allows both types of messages to be displayed in a modal context.

Key changes:

  • Created a common RemoteMessageDismissListener interface to handle message dismissal for both message types
  • Updated RemoteMessageView to support the dismiss listener
  • Modified ModalSurfaceViewModel to handle non-cards list messages
  • Added RemoteMessageView to the modal surface layout

Steps to test this PR

Standard Remote Messages in Modal Surface

  • Verify that standard remote messages can be displayed in the modal surface
  • Confirm that dismissing a standard remote message triggers the dismiss listener
  • Check that both cards list and standard remote messages work correctly in the modal surface

Dismiss Behavior

  • Verify that the dismiss listener is called when a message is dismissed
  • Confirm that the activity closes properly after message dismissal

NO UI changes


Note

Modal surface can now display standard remote messages in addition to cards list, using a shared dismiss listener with updated view models and layout.

  • UI / Activity:
    • Modal surface (ModalSurfaceActivity) now renders either CardsListRemoteMessageView or RemoteMessageView, toggling toolbar visibility accordingly.
    • Layout activity_modal_surface.xml updated to include toolbar and RemoteMessageView container.
  • Views:
    • RemoteMessageView: adds RemoteMessageDismissListener and emits onDismiss when message is dismissed; wiring for commands unchanged.
    • CardsListRemoteMessageView: replaces local listener with shared RemoteMessageDismissListener and removes inner interface.
    • New RemoteMessageDismissListener interface shared across views.
  • ViewModels:
    • ModalSurfaceViewModel: refactors ViewState to sealed classes (CardsList, Message); handles back navigation (including cards list dismiss pixel) and dismissal uniformly.
    • RemoteMessageViewModel: adds dismissed flag in ViewState; simplifies active message stream (no surface filtering) and tracks new/dismissed transitions.
  • Tests:
    • Update tests for new sealed ViewState, dismiss behavior, and support for modal-surface messages in RemoteMessageViewModel.

Written by Cursor Bugbot for commit 4aa7c92. This will update automatically on new commits. Configure here.

Copy link
Contributor Author

anikiki commented Dec 16, 2025

@anikiki anikiki changed the title Refactor remote message views to use a common dismiss listener and support modal display. [IN PROGRESS][Android] "What’s New" promo message: Make the ModalSurfaceActivity handle any remote message we support Dec 16, 2025
@anikiki anikiki changed the title [IN PROGRESS][Android] "What’s New" promo message: Make the ModalSurfaceActivity handle any remote message we support [Android] "What’s New" promo message: Make the ModalSurfaceActivity handle any remote message we support Dec 16, 2025
@anikiki anikiki requested a review from Copilot December 16, 2025 18:36
@anikiki anikiki marked this pull request as ready for review December 16, 2025 18:38
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR extends the ModalSurfaceActivity to support standard remote messages in addition to the previously supported cards list messages. The changes introduce a common dismiss listener interface, update the view model to use sealed classes for different view states, and add RemoteMessageView to the modal surface layout.

Key changes:

  • Created RemoteMessageDismissListener interface to unify dismissal handling across message types
  • Refactored ModalSurfaceViewModel.ViewState from a data class to a sealed class with CardsList and Message variants
  • Removed Surface.NEW_TAB_PAGE filtering from RemoteMessageViewModel to allow modal surface usage

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
RemoteMessageDismissListener.kt Introduces common interface for message dismissal listeners
ModalSurfaceViewModel.kt Refactors ViewState to sealed class and adds Message state handling
ModalSurfaceActivity.kt Implements RemoteMessageDismissListener and adds RemoteMessageView support with toolbar
CardsListRemoteMessageView.kt Updates listener type to use common RemoteMessageDismissListener interface
RemoteMessageView.kt Adds dismiss listener support and triggers callback on message dismissal
RemoteMessageViewModel.kt Removes Surface.NEW_TAB_PAGE filter and adds dismissed state tracking
activity_modal_surface.xml Changes root to LinearLayout and adds RemoteMessageView alongside CardsListRemoteMessageView
ModalSurfaceViewModelTest.kt Updates tests for sealed class ViewState structure
RemoteMessageViewModelTest.kt Updates test to verify modal surface messages are displayed
Comments suppressed due to low confidence (1)

remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/newtab/RemoteMessageViewModel.kt:121

  • The flow has redundant dispatcher switching with flowOn(dispatchers.io()) at line 102 and flowOn(dispatchers.main()) at line 120. Since the outer coroutine is already launched on dispatchers.io(), and the withContext at line 104 already switches to dispatchers.main() for the critical section, the flowOn operators are unnecessary and add confusion. The withContext call is sufficient to handle the dispatcher switching needed for this flow.
        viewModelScope.launch(dispatchers.io()) {
            remoteMessagingModel.getActiveMessages()
                .flowOn(dispatchers.io())
                .onEach { message ->
                    withContext(dispatchers.main()) {
                        val newMessage = message?.id != lastRemoteMessageSeen?.id
                        val dismissed = newMessage && message == null && lastRemoteMessageSeen != null
                        if (newMessage) {
                            lastRemoteMessageSeen = message
                        }

                        _viewState.emit(
                            viewState.value.copy(
                                message = message,
                                newMessage = newMessage,
                                dismissed = dismissed,
                            ),
                        )
                    }
                }
                .flowOn(dispatchers.main())
                .launchIn(viewModelScope)

Comment on lines +17 to +20
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:orientation="vertical">
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

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

Changing the root layout from FrameLayout to LinearLayout with orientation=vertical adds unnecessary layout complexity. Since the toolbar is initially gone and the FrameLayout takes match_parent for height, a FrameLayout root with the toolbar as the first child would achieve the same result with simpler layout hierarchy and potentially better performance.

Copilot uses AI. Check for mistakes.
Comment on lines +54 to +57
private fun setupToolbar() {
setSupportActionBar(binding.includeToolbar.toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

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

The toolbar is set up unconditionally in onCreate, including setting the action bar and enabling the home/up button, but the toolbar is only shown for ViewState.Message. This means the action bar is configured even when it's not displayed (for CardsList views). While not causing a runtime error, this could lead to unexpected behavior or resource waste. Consider conditionally setting up the action bar only when the toolbar will be visible, or at least document why this setup is necessary even when hidden.

Copilot uses AI. Check for mistakes.
binding.cardsListRemoteMessageView.show()
}
is ModalSurfaceViewModel.ViewState.Message -> {
// RemoteMessageView fetches the active message from RemoteMessageModel, so no messageId is needed
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

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

The comment states that RemoteMessageView fetches the active message from RemoteMessageModel, but this is misleading. RemoteMessageView uses RemoteMessageViewModel which fetches from RemoteMessageModel.getActiveMessages(). More importantly, the comment doesn't explain that the messageId from the activity params is not used because the view relies on the ViewModel to fetch whatever is currently active, which could potentially differ from the intended message if the active message changes between when the modal was triggered and when the view loads.

Copilot uses AI. Check for mistakes.
Comment on lines 100 to 102
viewModelScope.launch(dispatchers.io()) {
remoteMessagingModel.getActiveMessages()
.map { message ->
if (message?.surfaces?.contains(Surface.NEW_TAB_PAGE) == true) message else null
}
.flowOn(dispatchers.io())
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

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

The removal of Surface filtering causes RemoteMessageView to display messages regardless of their intended surface. RemoteMessageView is used in two contexts: (1) New Tab Page via RemoteMessageNewTabSectionPlugin where it should only show Surface.NEW_TAB_PAGE messages, and (2) Modal Surface via ModalSurfaceActivity where it should show Surface.MODAL messages. Without filtering, a MODAL surface message could appear on the new tab page, or vice versa. The ViewModel needs to accept a surface parameter or the filtering needs to be applied based on the usage context to ensure messages are only shown on their intended surfaces.

Copilot uses AI. Check for mistakes.
Comment on lines +140 to +141
if (viewState.dismissed) {
listener?.onDismiss()
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

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

The dismiss listener is called when viewState.dismissed is true, but there's no documentation explaining when this happens or what the expected behavior is for consumers. Consider adding a KDoc comment explaining that the listener is invoked when a previously displayed message is removed from the active messages flow, so implementers understand when to expect this callback.

Copilot uses AI. Check for mistakes.
.map { message ->
if (message?.surfaces?.contains(Surface.NEW_TAB_PAGE) == true) message else null
}
.flowOn(dispatchers.io())
Copy link

Choose a reason for hiding this comment

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

Bug: Removing surface filter shows modal messages on wrong surface

The Surface.NEW_TAB_PAGE filter was removed from RemoteMessageViewModel, but this view model is still used by RemoteMessageNewTabSectionPlugin to display messages on the New Tab Page. When a message is active with only Surface.MODAL in its surfaces list, it will now incorrectly appear on the New Tab Page. The RemoteMessageNewTabSectionPlugin.isUserEnabled() method also has no surface filtering—it just checks if any message exists. This change was needed to support modal messages, but it breaks the New Tab Page behavior by showing messages that aren't intended for that surface.


Please tell me if this was useful or not with a 👍 or 👎.

Fix in Cursor Fix in Web

viewModelScope.launch {
_command.send(Command.DismissMessage)
}
}
Copy link

Choose a reason for hiding this comment

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

Bug: Standard messages not dismissed on back press in modal

When the user presses back on a standard message (ViewState.Message) in the modal surface, the code only sends Command.DismissMessage to close the activity, but doesn't actually dismiss the message in the repository. In contrast, for ViewState.CardsList, cardsListPixelHelper.dismissCardsListMessage() is called which both fires a pixel AND calls remoteMessagingRepository.dismissMessage(). This inconsistency means standard messages will persist and reappear every time the modal surface is opened, while cards list messages are properly dismissed. The standard message handling needs to call remoteMessagingRepository.dismissMessage() or equivalent.


Please tell me if this was useful or not with a 👍 or 👎.

Fix in Cursor Fix in Web

@anikiki
Copy link
Contributor Author

anikiki commented Dec 18, 2025

Closing this PR as we won't support other remote messages on MODAL yet.

@anikiki anikiki closed this Dec 18, 2025
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.

1 participant