2
0
mirror of https://github.com/fork-maintainers/iceraven-browser synced 2024-11-03 23:15:31 +00:00

[fenix] EXP 2991: Add surface to messaging fml (https://github.com/mozilla-mobile/fenix/pull/28423)

* Move messaging fml to a separate file

* Add surface property to message data

* Get messages for just a single surface

* Add surface to messaging middleware

* ktlint

* Add tests for filtering by surface

* Add homescreen to default-browser message

* Move surface param to MessageActions instead of MessagingMiddleware

* Added computed property for surface to message

* ktlint

* Address reviewer comment

* Fixup tests

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
This commit is contained in:
jhugman 2023-01-17 15:21:20 +00:00 committed by GitHub
parent f22bdeef5d
commit 5ac1cd9b48
16 changed files with 358 additions and 294 deletions

View File

@ -207,7 +207,9 @@ class Components(private val context: Context) {
core.pocketStoriesService,
context.pocketStoriesSelectedCategoriesDataStore,
),
MessagingMiddleware(messagingStorage = analytics.messagingStorage),
MessagingMiddleware(
messagingStorage = analytics.messagingStorage,
),
MetricsMiddleware(metrics = analytics.metrics),
),
)

View File

@ -22,6 +22,7 @@ import org.mozilla.fenix.home.recentsyncedtabs.RecentSyncedTabState
import org.mozilla.fenix.home.recenttabs.RecentTab
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem
import org.mozilla.fenix.library.history.PendingDeletionHistory
import org.mozilla.fenix.nimbus.MessageSurfaceId
import org.mozilla.fenix.wallpapers.Wallpaper
/**
@ -137,7 +138,7 @@ sealed class AppAction : Action {
/**
* Evaluates if a new messages should be shown to users.
*/
object Evaluate : MessagingAction()
data class Evaluate(val surface: MessageSurfaceId) : MessagingAction()
/**
* Updates [MessagingState.messageToShow] with the given [message].
@ -147,7 +148,7 @@ sealed class AppAction : Action {
/**
* Updates [MessagingState.messageToShow] with the given [message].
*/
object ConsumeMessageToShow : MessagingAction()
data class ConsumeMessageToShow(val surface: MessageSurfaceId) : MessagingAction()
/**
* Updates [MessagingState.messages] with the given [messages].

View File

@ -5,6 +5,7 @@
package org.mozilla.fenix.gleanplumb
import org.mozilla.fenix.nimbus.MessageData
import org.mozilla.fenix.nimbus.MessageSurfaceId
import org.mozilla.fenix.nimbus.StyleData
/**
@ -33,6 +34,9 @@ data class Message(
val priority: Int
get() = style.priority
val surface: MessageSurfaceId
get() = data.surface
/**
* A data class that holds metadata that help to identify if a message should shown.
*

View File

@ -7,6 +7,7 @@ package org.mozilla.fenix.gleanplumb
import mozilla.components.support.base.feature.LifecycleAwareFeature
import org.mozilla.fenix.components.AppStore
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction
import org.mozilla.fenix.nimbus.MessageSurfaceId
/**
* A message observer that updates the provided.
@ -14,7 +15,7 @@ import org.mozilla.fenix.components.appstate.AppAction.MessagingAction
class MessagingFeature(val appStore: AppStore) : LifecycleAwareFeature {
override fun start() {
appStore.dispatch(MessagingAction.Evaluate)
appStore.dispatch(MessagingAction.Evaluate(MessageSurfaceId.HOMESCREEN))
}
override fun stop() = Unit

View File

@ -4,6 +4,8 @@
package org.mozilla.fenix.gleanplumb
import org.mozilla.fenix.nimbus.MessageSurfaceId
/**
* Represent all the state related to the Messaging framework.
* @param messages Indicates all the available messages.
@ -12,5 +14,5 @@ package org.mozilla.fenix.gleanplumb
*/
data class MessagingState(
val messages: List<Message> = emptyList(),
val messageToShow: Message? = null,
val messageToShow: Map<MessageSurfaceId, Message> = emptyMap(),
)

View File

@ -13,6 +13,7 @@ import org.mozilla.experiments.nimbus.GleanPlumbMessageHelper
import org.mozilla.experiments.nimbus.internal.FeatureHolder
import org.mozilla.experiments.nimbus.internal.NimbusException
import org.mozilla.fenix.nimbus.ControlMessageBehavior
import org.mozilla.fenix.nimbus.MessageSurfaceId
import org.mozilla.fenix.nimbus.Messaging
import org.mozilla.fenix.nimbus.StyleData
@ -63,34 +64,35 @@ class NimbusMessagingStorage(
val defaultStyle = StyleData()
val storageMetadata = metadataStorage.getMetadata()
return nimbusMessages.mapNotNull { (key, value) ->
val action = sanitizeAction(key, value.action, nimbusActions, value.isControl) ?: return@mapNotNull null
Message(
id = key,
data = value,
action = action,
style = nimbusStyles[value.style] ?: defaultStyle,
metadata = storageMetadata[key] ?: addMetadata(key),
triggers = sanitizeTriggers(key, value.trigger, nimbusTriggers)
?: return@mapNotNull null,
)
}.filter {
it.maxDisplayCount >= it.metadata.displayCount &&
!it.metadata.dismissed &&
!it.metadata.pressed
}.sortedByDescending {
it.style.priority
}
return nimbusMessages
.mapNotNull { (key, value) ->
val action = sanitizeAction(key, value.action, nimbusActions, value.isControl) ?: return@mapNotNull null
Message(
id = key,
data = value,
action = action,
style = nimbusStyles[value.style] ?: defaultStyle,
metadata = storageMetadata[key] ?: addMetadata(key),
triggers = sanitizeTriggers(key, value.trigger, nimbusTriggers)
?: return@mapNotNull null,
)
}.filter {
it.maxDisplayCount >= it.metadata.displayCount &&
!it.metadata.dismissed &&
!it.metadata.pressed
}.sortedByDescending {
it.style.priority
}
}
/**
* Returns the next higher priority message which all their triggers are true.
*/
fun getNextMessage(availableMessages: List<Message>): Message? {
fun getNextMessage(surface: MessageSurfaceId, availableMessages: List<Message>): Message? {
val jexlCache = HashMap<String, Boolean>()
val helper = gleanPlumb.createMessageHelper(customAttributes)
val message = availableMessages.firstOrNull {
isMessageEligible(it, helper, jexlCache)
surface == it.surface && isMessageEligible(it, helper, jexlCache)
} ?: return null
// Check this isn't an experimental message. If not, we can go ahead and return it.

View File

@ -44,12 +44,15 @@ class MessagingMiddleware(
}
is Evaluate -> {
val message = messagingStorage.getNextMessage(context.state.messaging.messages)
val message = messagingStorage.getNextMessage(
action.surface,
context.state.messaging.messages,
)
if (message != null) {
context.dispatch(UpdateMessageToShow(message))
onMessagedDisplayed(message, context)
} else {
context.dispatch(ConsumeMessageToShow)
context.dispatch(ConsumeMessageToShow(action.surface))
}
}
@ -135,8 +138,9 @@ class MessagingMiddleware(
context: AppStoreMiddlewareContext,
message: Message,
) {
if (context.state.messaging.messageToShow?.id == message.id) {
context.dispatch(ConsumeMessageToShow)
val current = context.state.messaging.messageToShow[message.surface]
if (current?.id == message.id) {
context.dispatch(ConsumeMessageToShow(message.surface))
}
}
@ -154,7 +158,7 @@ class MessagingMiddleware(
oldMessage: Message,
updatedMessage: Message,
): List<Message> {
val actualMessageToShow = context.state.messaging.messageToShow
val actualMessageToShow = context.state.messaging.messageToShow.get(updatedMessage.surface)
if (actualMessageToShow?.id == oldMessage.id) {
context.dispatch(UpdateMessageToShow(updatedMessage))

View File

@ -17,9 +17,11 @@ import org.mozilla.fenix.gleanplumb.MessagingState
internal object MessagingReducer {
fun reduce(state: AppState, action: AppAction.MessagingAction): AppState = when (action) {
is UpdateMessageToShow -> {
val messageToShow = state.messaging.messageToShow.toMutableMap()
messageToShow[action.message.surface] = action.message
state.copy(
messaging = state.messaging.copy(
messageToShow = action.message,
messageToShow = messageToShow,
),
)
}
@ -31,9 +33,11 @@ internal object MessagingReducer {
)
}
is ConsumeMessageToShow -> {
val messageToShow = state.messaging.messageToShow.toMutableMap()
messageToShow.remove(action.surface)
state.copy(
messaging = state.messaging.copy(
messageToShow = null,
messageToShow = messageToShow,
),
)
}

View File

@ -23,6 +23,7 @@ import org.mozilla.fenix.home.Mode
import org.mozilla.fenix.home.OnboardingState
import org.mozilla.fenix.home.recentbookmarks.RecentBookmark
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem
import org.mozilla.fenix.nimbus.MessageSurfaceId
import org.mozilla.fenix.onboarding.HomeCFRPresenter
import org.mozilla.fenix.utils.Settings
@ -167,7 +168,7 @@ private fun AppState.toAdapterList(settings: Settings): List<AdapterItem> = when
expandedCollections,
recentBookmarks,
showCollectionPlaceholder,
messaging.messageToShow,
messaging.messageToShow[MessageSurfaceId.HOMESCREEN],
shouldShowRecentTabs(settings),
shouldShowRecentSyncedTabs(settings),
recentHistory,

View File

@ -20,8 +20,6 @@ import mozilla.components.service.pocket.PocketStory.PocketSponsoredStory
import mozilla.components.service.pocket.PocketStory.PocketSponsoredStoryCaps
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertSame
import org.junit.Assert.assertTrue
import org.junit.Before
@ -34,6 +32,7 @@ import org.mozilla.fenix.components.appstate.AppState
import org.mozilla.fenix.components.appstate.filterOut
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getFilteredStories
import org.mozilla.fenix.gleanplumb.Message
import org.mozilla.fenix.home.CurrentMode
import org.mozilla.fenix.home.Mode
import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesCategory
@ -45,6 +44,8 @@ import org.mozilla.fenix.home.recenttabs.RecentTab
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryGroup
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryHighlight
import org.mozilla.fenix.nimbus.MessageData
import org.mozilla.fenix.nimbus.MessageSurfaceId
import org.mozilla.fenix.onboarding.FenixOnboarding
class AppStoreTest {
@ -113,11 +114,19 @@ class AppStoreTest {
@Test
fun `GIVEN a new value for messageToShow WHEN NimbusMessageChange is called THEN update the current value`() =
runTest {
assertNull(appStore.state.messaging.messageToShow)
assertTrue(appStore.state.messaging.messageToShow.isEmpty())
appStore.dispatch(UpdateMessageToShow(mockk())).join()
val message = Message(
"message",
MessageData(surface = MessageSurfaceId.HOMESCREEN),
"action",
mockk(),
emptyList(),
mockk(),
)
appStore.dispatch(UpdateMessageToShow(message)).join()
assertNotNull(appStore.state.messaging.messageToShow)
assertFalse(appStore.state.messaging.messageToShow.isEmpty())
}
@Test

View File

@ -12,6 +12,7 @@ import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.components.AppStore
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction
import org.mozilla.fenix.nimbus.MessageSurfaceId
class MessagingFeatureTest {
@OptIn(ExperimentalCoroutinesApi::class)
@ -25,6 +26,6 @@ class MessagingFeatureTest {
binding.start()
verify { appStore.dispatch(MessagingAction.Evaluate) }
verify { appStore.dispatch(MessagingAction.Evaluate(MessageSurfaceId.HOMESCREEN)) }
}
}

View File

@ -20,20 +20,19 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.experiments.nimbus.GleanPlumbInterface
import org.mozilla.experiments.nimbus.GleanPlumbMessageHelper
import org.mozilla.experiments.nimbus.NullVariables
import org.mozilla.experiments.nimbus.Res
import org.mozilla.experiments.nimbus.internal.FeatureHolder
import org.mozilla.experiments.nimbus.internal.NimbusException
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.nimbus.ControlMessageBehavior.SHOW_NEXT_MESSAGE
import org.mozilla.fenix.nimbus.MessageData
import org.mozilla.fenix.nimbus.MessageSurfaceId
import org.mozilla.fenix.nimbus.Messaging
import org.mozilla.fenix.nimbus.StyleData
@RunWith(FenixRobolectricTestRunner::class)
class NimbusMessagingStorageTest {
private val activity: HomeActivity = mockk(relaxed = true)
private val storageNimbus: NimbusMessagingStorage = mockk(relaxed = true)
private lateinit var storage: NimbusMessagingStorage
private lateinit var metadataStorage: MessageMetadataStorage
private lateinit var gleanPlumb: GleanPlumbInterface
@ -49,6 +48,7 @@ class NimbusMessagingStorageTest {
gleanPlumb = mockk(relaxed = true)
metadataStorage = mockk(relaxed = true)
malformedWasReported = false
NullVariables.instance.setContext(testContext)
messagingFeature = createMessagingFeature()
coEvery { metadataStorage.getMetadata() } returns mapOf("message-1" to Message.Metadata(id = "message-1"))
@ -63,11 +63,14 @@ class NimbusMessagingStorageTest {
}
@Test
fun `WHEN calling getMessages THEN provide a list of available messages`() = runTest {
val message = storage.getMessages().first()
fun `WHEN calling getMessages THEN provide a list of available messages for a given surface`() = runTest {
val homescreenMessage = storage.getMessages().first()
assertEquals("message-1", message.id)
assertEquals("message-1", message.metadata.id)
assertEquals("message-1", homescreenMessage.id)
assertEquals("message-1", homescreenMessage.metadata.id)
val notificationMessage = storage.getMessages().last()
assertEquals("message-2", notificationMessage.id)
}
@Test
@ -230,7 +233,7 @@ class NimbusMessagingStorageTest {
assertEquals("message-1", firstMessage.id)
assertEquals("message-1", firstMessage.metadata.id)
assertTrue(messages.size == 1)
assertTrue(messages.size == 2)
assertTrue(malformedWasReported)
}
@ -490,7 +493,7 @@ class NimbusMessagingStorageTest {
every { spiedStorage.isMessageEligible(any(), any()) } returns false
val result = spiedStorage.getNextMessage(listOf(message))
val result = spiedStorage.getNextMessage(MessageSurfaceId.HOMESCREEN, listOf(message))
assertNull(result)
}
@ -500,7 +503,7 @@ class NimbusMessagingStorageTest {
val spiedStorage = spyk(storage)
val message = Message(
"same-id",
mockk(relaxed = true),
createMessageData(surface = MessageSurfaceId.HOMESCREEN),
action = "action",
mockk(relaxed = true),
listOf("trigger"),
@ -510,7 +513,7 @@ class NimbusMessagingStorageTest {
every { spiedStorage.isMessageEligible(any(), any()) } returns true
every { spiedStorage.isMessageUnderExperiment(any(), any()) } returns false
val result = spiedStorage.getNextMessage(listOf(message))
val result = spiedStorage.getNextMessage(MessageSurfaceId.HOMESCREEN, listOf(message))
assertEquals(message.id, result!!.id)
}
@ -518,9 +521,7 @@ class NimbusMessagingStorageTest {
@Test
fun `GIVEN a message under experiment WHEN calling getNextMessage THEN call recordExposure`() {
val spiedStorage = spyk(storage)
val messageData: MessageData = mockk(relaxed = true)
every { messageData.isControl } returns false
val messageData: MessageData = createMessageData(isControl = false)
val message = Message(
"same-id",
@ -534,7 +535,7 @@ class NimbusMessagingStorageTest {
every { spiedStorage.isMessageEligible(any(), any()) } returns true
every { spiedStorage.isMessageUnderExperiment(any(), any()) } returns true
val result = spiedStorage.getNextMessage(listOf(message))
val result = spiedStorage.getNextMessage(MessageSurfaceId.HOMESCREEN, listOf(message))
verify { messagingFeature.recordExposure() }
assertEquals(message.id, result!!.id)
@ -543,12 +544,10 @@ class NimbusMessagingStorageTest {
@Test
fun `GIVEN a control message WHEN calling getNextMessage THEN return the next eligible message`() {
val spiedStorage = spyk(storage)
val messageData: MessageData = mockk(relaxed = true)
val controlMessageData: MessageData = mockk(relaxed = true)
val messageData: MessageData = createMessageData()
val controlMessageData: MessageData = createMessageData(isControl = true)
every { messageData.isControl } returns false
every { spiedStorage.getOnControlBehavior() } returns SHOW_NEXT_MESSAGE
every { controlMessageData.isControl } returns true
val message = Message(
"id",
@ -571,7 +570,10 @@ class NimbusMessagingStorageTest {
every { spiedStorage.isMessageEligible(any(), any()) } returns true
every { spiedStorage.isMessageUnderExperiment(any(), any()) } returns true
val result = spiedStorage.getNextMessage(listOf(controlMessage, message))
val result = spiedStorage.getNextMessage(
MessageSurfaceId.HOMESCREEN,
listOf(controlMessage, message),
)
verify { messagingFeature.recordExposure() }
assertEquals(message.id, result!!.id)
@ -581,33 +583,35 @@ class NimbusMessagingStorageTest {
action: String = "action-1",
style: String = "style-1",
triggers: List<String> = listOf("trigger-1"),
): MessageData {
val messageData1: MessageData = mockk(relaxed = true)
every { messageData1.action } returns action
every { messageData1.style } returns style
every { messageData1.trigger } returns triggers
return messageData1
}
surface: MessageSurfaceId = MessageSurfaceId.HOMESCREEN,
isControl: Boolean = false,
) = MessageData(
action = Res.string(action),
style = style,
trigger = triggers,
surface = surface,
isControl = isControl,
)
private fun createMessagingFeature(
triggers: Map<String, String> = mapOf("trigger-1" to "trigger-1-expression"),
styles: Map<String, StyleData> = mapOf("style-1" to createStyle()),
actions: Map<String, String> = mapOf("action-1" to "action-1-url"),
messages: Map<String, MessageData> = mapOf(
"message-1" to createMessageData(),
"malformed" to mockk(relaxed = true),
"message-1" to createMessageData(surface = MessageSurfaceId.HOMESCREEN),
"message-2" to createMessageData(surface = MessageSurfaceId.NOTIFICATION),
"malformed" to createMessageData(action = "malformed-action"),
),
): FeatureHolder<Messaging> {
val messagingFeature: FeatureHolder<Messaging> = mockk(relaxed = true)
messaging = mockk(relaxed = true)
every { messaging.triggers } returns triggers
every { messaging.styles } returns styles
every { messaging.actions } returns actions
every { messaging.messages } returns messages
messaging = Messaging(
actions = actions,
triggers = triggers,
messages = messages,
styles = styles,
)
every { messagingFeature.value() } returns messaging
return messagingFeature
}

View File

@ -37,6 +37,7 @@ import org.mozilla.fenix.gleanplumb.MessagingState
import org.mozilla.fenix.gleanplumb.NimbusMessagingStorage
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.nimbus.MessageData
import org.mozilla.fenix.nimbus.MessageSurfaceId
import org.mozilla.fenix.nimbus.StyleData
@RunWith(FenixRobolectricTestRunner::class)
@ -85,9 +86,9 @@ class MessagingMiddlewareTest {
every { messagingState.messages } returns emptyList()
every { appState.messaging } returns messagingState
every { middlewareContext.state } returns appState
every { messagingStorage.getNextMessage(any()) } returns message
every { messagingStorage.getNextMessage(MessageSurfaceId.HOMESCREEN, any()) } returns message
middleware.invoke(middlewareContext, {}, Evaluate)
middleware.invoke(middlewareContext, {}, Evaluate(MessageSurfaceId.HOMESCREEN))
verify { middlewareContext.dispatch(UpdateMessageToShow(message)) }
}
@ -218,7 +219,7 @@ class MessagingMiddlewareTest {
fun `WHEN consumeMessageToShowIfNeeded THEN consume the message`() = runTestOnMain {
val message = Message(
"control-id",
mockk(relaxed = true),
MessageData(),
action = "action",
mockk(relaxed = true),
listOf("trigger"),
@ -227,20 +228,20 @@ class MessagingMiddlewareTest {
val appState: AppState = mockk(relaxed = true)
val messagingState: MessagingState = mockk(relaxed = true)
every { messagingState.messageToShow } returns message
every { messagingState.messageToShow } returns mapOf(message.surface to message)
every { appState.messaging } returns messagingState
every { middlewareContext.state } returns appState
middleware.consumeMessageToShowIfNeeded(middlewareContext, message)
verify { middlewareContext.dispatch(ConsumeMessageToShow) }
verify { middlewareContext.dispatch(ConsumeMessageToShow(message.surface)) }
}
@Test
fun `WHEN updateMessage THEN update available messages`() = runTestOnMain {
val oldMessage = Message(
"oldMessage",
mockk(relaxed = true),
MessageData(),
action = "action",
mockk(relaxed = true),
listOf("trigger"),
@ -249,7 +250,7 @@ class MessagingMiddlewareTest {
val updatedMessage = Message(
"oldMessage",
mockk(relaxed = true),
MessageData(),
action = "action",
mockk(relaxed = true),
listOf("trigger"),
@ -261,7 +262,7 @@ class MessagingMiddlewareTest {
val appState: AppState = mockk(relaxed = true)
val messagingState: MessagingState = mockk(relaxed = true)
every { messagingState.messageToShow } returns oldMessage
every { messagingState.messageToShow } returns mapOf(oldMessage.surface to oldMessage)
every { appState.messaging } returns messagingState
every { middlewareContext.state } returns appState
every { spiedMiddleware.removeMessage(middlewareContext, oldMessage) } returns emptyList()

View File

@ -15,7 +15,11 @@ import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.UpdateMes
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.UpdateMessages
import org.mozilla.fenix.components.appstate.AppState
import org.mozilla.fenix.components.appstate.AppStoreReducer
import org.mozilla.fenix.gleanplumb.Message
import org.mozilla.fenix.gleanplumb.MessagingState
import org.mozilla.fenix.nimbus.MessageData
import org.mozilla.fenix.nimbus.MessageSurfaceId
import org.mozilla.fenix.nimbus.StyleData
class MessagingReducerTest {
@ -23,22 +27,34 @@ class MessagingReducerTest {
fun `GIVEN a new value for messageToShow WHEN UpdateMessageToShow is called THEN update the current value`() {
val initialState = AppState(
messaging = MessagingState(
messageToShow = null,
messageToShow = mapOf(),
),
)
val m = createMessage("message1")
var updatedState = MessagingReducer.reduce(
initialState,
UpdateMessageToShow(mockk()),
UpdateMessageToShow(m),
)
assertNotNull(updatedState.messaging.messageToShow)
assertNotNull(updatedState.messaging.messageToShow[m.surface])
updatedState = AppStoreReducer.reduce(updatedState, ConsumeMessageToShow)
updatedState = AppStoreReducer.reduce(updatedState, ConsumeMessageToShow(m.surface))
assertNull(updatedState.messaging.messageToShow)
assertNull(updatedState.messaging.messageToShow[m.surface])
}
private fun createMessage(id: String, action: String = "action-1", surface: MessageSurfaceId = MessageSurfaceId.HOMESCREEN): Message =
Message(
id = id,
data = MessageData(surface = surface),
action = action,
style = StyleData(),
triggers = listOf(),
metadata = Message.Metadata(id = id),
)
@Test
fun `GIVEN a new value for messages WHEN UpdateMessages is called THEN update the current value`() {
val initialState = AppState(

212
messaging.fml.yaml Normal file
View File

@ -0,0 +1,212 @@
---
features:
messaging:
description: |
Configuration for the messaging system.
In practice this is a set of growable lookup tables for the
message controller to piece together.
variables:
message-under-experiment:
description: Id or prefix of the message under experiment.
type: Option<String>
default: null
messages:
description: A growable collection of messages
type: Map<String, MessageData>
default: {}
triggers:
description: >
A collection of out the box trigger
expressions. Each entry maps to a
valid JEXL expression.
type: Map<String, String>
default: {}
styles:
description: >
A map of styles to configure message
appearance.
type: Map<String, StyleData>
default: {}
actions:
type: Map<String, String>
description: A growable map of action URLs.
default: {}
on-control:
type: ControlMessageBehavior
description: What should be displayed when a control message is selected.
default: show-next-message
notification-config:
description: Configuration of the notification worker for all notification messages.
type: NotificationConfig
default: {}
defaults:
- value:
triggers:
USER_RECENTLY_INSTALLED: days_since_install < 7
USER_RECENTLY_UPDATED: days_since_update < 7 && days_since_install != days_since_update
USER_TIER_ONE_COUNTRY: ('US' in locale || 'GB' in locale || 'CA' in locale || 'DE' in locale || 'FR' in locale)
USER_EN_SPEAKER: "'en' in locale"
USER_DE_SPEAKER: "'de' in locale"
USER_FR_SPEAKER: "'fr' in locale"
DEVICE_ANDROID: os == 'Android'
DEVICE_IOS: os == 'iOS'
ALWAYS: "true"
NEVER: "false"
I_AM_DEFAULT_BROWSER: "is_default_browser"
I_AM_NOT_DEFAULT_BROWSER: "is_default_browser == false"
USER_ESTABLISHED_INSTALL: "number_of_app_launches >=4"
actions:
ENABLE_PRIVATE_BROWSING: ://enable_private_browsing
INSTALL_SEARCH_WIDGET: ://install_search_widget
MAKE_DEFAULT_BROWSER: ://make_default_browser
VIEW_BOOKMARKS: ://urls_bookmarks
VIEW_COLLECTIONS: ://home_collections
VIEW_HISTORY: ://urls_history
VIEW_HOMESCREEN: ://home
OPEN_SETTINGS_ACCESSIBILITY: ://settings_accessibility
OPEN_SETTINGS_ADDON_MANAGER: ://settings_addon_manager
OPEN_SETTINGS_DELETE_BROWSING_DATA: ://settings_delete_browsing_data
OPEN_SETTINGS_LOGINS: ://settings_logins
OPEN_SETTINGS_NOTIFICATIONS: ://settings_notifications
OPEN_SETTINGS_PRIVACY: ://settings_privacy
OPEN_SETTINGS_SEARCH_ENGINE: ://settings_search_engine
OPEN_SETTINGS_TRACKING_PROTECTION: ://settings_tracking_protection
OPEN_SETTINGS_WALLPAPERS: ://settings_wallpapers
OPEN_SETTINGS: ://settings
TURN_ON_SYNC: ://turn_on_sync
styles:
DEFAULT:
priority: 50
max-display-count: 5
SURVEY:
priority: 55
max-display-count: 10
PERSISTENT:
priority: 50
max-display-count: 20
WARNING:
priority: 60
max-display-count: 10
URGENT:
priority: 100
max-display-count: 10
messages:
default-browser:
text: default_browser_experiment_card_text
surface: homescreen
action: "MAKE_DEFAULT_BROWSER"
trigger: [ "I_AM_NOT_DEFAULT_BROWSER","USER_ESTABLISHED_INSTALL" ]
style: "PERSISTENT"
button-label: preferences_set_as_default_browser
- channel: developer
value:
styles:
DEFAULT:
priority: 50
max-display-count: 100
EXPIRES_QUICKLY:
priority: 100
max-display-count: 1
notification-config:
polling-interval: 180 # 3 minutes
objects:
MessageData:
description: >
An object to describe a message. It uses human
readable strings to describe the triggers, action and
style of the message as well as the text of the message
and call to action.
fields:
action:
type: Text
description: >
A URL of a page or a deeplink.
This may have substitution variables in.
# This should never be defaulted.
default: empty_string
title:
type: Option<Text>
description: "The title text displayed to the user"
default: null
text:
type: Text
description: "The message text displayed to the user"
# This should never be defaulted.
default: empty_string
is-control:
type: Boolean
description: "Indicates if this message is the control message, if true shouldn't be displayed"
default: false
button-label:
type: Option<Text>
description: >
The text on the button. If no text
is present, the whole message is clickable.
default: null
style:
type: String
description: >
The style as described in a
`StyleData` from the styles table.
default: DEFAULT
surface:
description:
The surface identifier for this message.
type: MessageSurfaceId
default: homescreen
trigger:
type: List<String>
description: >
A list of strings corresponding to
targeting expressions. The message will be
shown if all expressions `true`.
default: []
StyleData:
description: >
A group of properities (predominantly visual) to
describe the style of the message.
fields:
priority:
type: Int
description: >
The importance of this message.
0 is not very important, 100 is very important.
default: 50
max-display-count:
type: Int
description: >
How many sessions will this message be shown to the user
before it is expired.
default: 5
NotificationConfig:
description: Attributes controlling the global configuration of notification messages.
fields:
polling-interval:
type: Int
description: >
How often, in seconds, the notification message worker will wake up and check for new
messages.
default: 3600
enums:
ControlMessageBehavior:
description: An enum to influence what should be displayed when a control message is selected.
variants:
show-next-message:
description: The next eligible message should be shown.
show-none:
description: The surface should show no message.
MessageSurfaceId:
description: The identity of a message surface
variants:
homescreen:
description: A banner in the homescreen.
notification:
description: A local notification in the background, like a push notification.

View File

@ -9,6 +9,8 @@ channels:
- beta
- nightly
- developer
includes:
- ./messaging.fml.yaml
features:
homescreen:
description: The homescreen that the user goes to when they press home or new tab.
@ -73,121 +75,6 @@ features:
- channel: developer
value:
enabled: true
messaging:
description: |
Configuration for the messaging system.
In practice this is a set of growable lookup tables for the
message controller to piece together.
variables:
message-under-experiment:
description: Id or prefix of the message under experiment.
type: Option<String>
default: null
messages:
description: A growable collection of messages
type: Map<String, MessageData>
default: {}
triggers:
description: >
A collection of out the box trigger
expressions. Each entry maps to a
valid JEXL expression.
type: Map<String, String>
default: {}
styles:
description: >
A map of styles to configure message
appearance.
type: Map<String, StyleData>
default: {}
actions:
type: Map<String, String>
description: A growable map of action URLs.
default: {}
on-control:
type: ControlMessageBehavior
description: What should be displayed when a control message is selected.
default: show-next-message
notification-config:
description: Configuration of the notification worker for all notification messages.
type: NotificationConfig
default: {}
defaults:
- value:
triggers:
USER_RECENTLY_INSTALLED: days_since_install < 7
USER_RECENTLY_UPDATED: days_since_update < 7 && days_since_install != days_since_update
USER_TIER_ONE_COUNTRY: ('US' in locale || 'GB' in locale || 'CA' in locale || 'DE' in locale || 'FR' in locale)
USER_EN_SPEAKER: "'en' in locale"
USER_DE_SPEAKER: "'de' in locale"
USER_FR_SPEAKER: "'fr' in locale"
DEVICE_ANDROID: os == 'Android'
DEVICE_IOS: os == 'iOS'
ALWAYS: "true"
NEVER: "false"
I_AM_DEFAULT_BROWSER: "is_default_browser"
I_AM_NOT_DEFAULT_BROWSER: "is_default_browser == false"
USER_ESTABLISHED_INSTALL: "number_of_app_launches >=4"
actions:
ENABLE_PRIVATE_BROWSING: ://enable_private_browsing
INSTALL_SEARCH_WIDGET: ://install_search_widget
MAKE_DEFAULT_BROWSER: ://make_default_browser
VIEW_BOOKMARKS: ://urls_bookmarks
VIEW_COLLECTIONS: ://home_collections
VIEW_HISTORY: ://urls_history
VIEW_HOMESCREEN: ://home
OPEN_SETTINGS_ACCESSIBILITY: ://settings_accessibility
OPEN_SETTINGS_ADDON_MANAGER: ://settings_addon_manager
OPEN_SETTINGS_DELETE_BROWSING_DATA: ://settings_delete_browsing_data
OPEN_SETTINGS_LOGINS: ://settings_logins
OPEN_SETTINGS_NOTIFICATIONS: ://settings_notifications
OPEN_SETTINGS_PRIVACY: ://settings_privacy
OPEN_SETTINGS_SEARCH_ENGINE: ://settings_search_engine
OPEN_SETTINGS_TRACKING_PROTECTION: ://settings_tracking_protection
OPEN_SETTINGS_WALLPAPERS: ://settings_wallpapers
OPEN_SETTINGS: ://settings
TURN_ON_SYNC: ://turn_on_sync
styles:
DEFAULT:
priority: 50
max-display-count: 5
SURVEY:
priority: 55
max-display-count: 10
PERSISTENT:
priority: 50
max-display-count: 20
WARNING:
priority: 60
max-display-count: 10
URGENT:
priority: 100
max-display-count: 10
messages:
default-browser:
text: default_browser_experiment_card_text
action: "MAKE_DEFAULT_BROWSER"
trigger: [ "I_AM_NOT_DEFAULT_BROWSER","USER_ESTABLISHED_INSTALL" ]
style: "PERSISTENT"
button-label: preferences_set_as_default_browser
- channel: developer
value:
styles:
DEFAULT:
priority: 50
max-display-count: 100
EXPIRES_QUICKLY:
priority: 100
max-display-count: 1
notification-config:
polling-interval: 180 # 3 minutes
mr2022:
description: Features for MR 2022.
variables:
@ -287,88 +174,9 @@ features:
default: true
types:
objects:
MessageData:
description: >
An object to describe a message. It uses human
readable strings to describe the triggers, action and
style of the message as well as the text of the message
and call to action.
fields:
action:
type: Text
description: >
A URL of a page or a deeplink.
This may have substitution variables in.
# This should never be defaulted.
default: empty_string
title:
type: Option<Text>
description: "The title text displayed to the user"
default: null
text:
type: Text
description: "The message text displayed to the user"
# This should never be defaulted.
default: empty_string
is-control:
type: Boolean
description: "Indicates if this message is the control message, if true shouldn't be displayed"
default: false
button-label:
type: Option<Text>
description: >
The text on the button. If no text
is present, the whole message is clickable.
default: null
style:
type: String
description: >
The style as described in a
`StyleData` from the styles table.
default: DEFAULT
trigger:
type: List<String>
description: >
A list of strings corresponding to
targeting expressions. The message will be
shown if all expressions `true`.
default: []
StyleData:
description: >
A group of properities (predominantly visual) to
describe the style of the message.
fields:
priority:
type: Int
description: >
The importance of this message.
0 is not very important, 100 is very important.
default: 50
max-display-count:
type: Int
description: >
How many sessions will this message be shown to the user
before it is expired.
default: 5
NotificationConfig:
description: Attributes controlling the global configuration of notification messages.
fields:
polling-interval:
type: Int
description: >
How often, in seconds, the notification message worker will wake up and check for new
messages.
default: 3600
objects: {}
enums:
ControlMessageBehavior:
description: An enum to influence what should be displayed when a control message is selected.
variants:
show-next-message:
description: The next eligible message should be shown.
show-none:
description: The surface should show no message.
HomeScreenSection:
description: The identifiers for the sections of the homescreen.
variants:
@ -384,15 +192,7 @@ types:
description: The pocket section. This should only be available in the US.
pocket-sponsored-stories:
description: Subsection of the Pocket homescreen section which shows sponsored stories.
MessageSurfaceId:
description: The identity of a message surface, used in the default browser experiments
variants:
app-menu-item:
description: An item in the default toolbar menu.
settings:
description: A setting in the settings screen.
homescreen-banner:
description: A banner in the homescreen.
MR2022Section:
description: The identifiers for the sections of the MR 2022.
variants: