Revert "For #24220 and #24223: Connect GleanPlumb messages with the new tab ui card. Co-authored-by: Jonathan Almeida <jalmeida@mozilla.com>"
This reverts commit f953c5ec
This commit is contained in:
parent
febff55459
commit
cc9e91809b
|
@ -109,10 +109,4 @@ object FeatureFlags {
|
|||
* Enables the Unified Search feature.
|
||||
*/
|
||||
val unifiedSearchFeature = Config.channel.isNightlyOrDebug
|
||||
|
||||
/**
|
||||
* Enables receiving from the messaging framework.
|
||||
*/
|
||||
@Suppress("MayBeConst")
|
||||
val messagingFeature = false
|
||||
}
|
||||
|
|
|
@ -66,7 +66,6 @@ import org.mozilla.fenix.GleanMetrics.SearchDefaultEngine
|
|||
import org.mozilla.fenix.GleanMetrics.TopSites
|
||||
import org.mozilla.fenix.components.Components
|
||||
import org.mozilla.fenix.components.Core
|
||||
import org.mozilla.fenix.components.appstate.AppAction
|
||||
import org.mozilla.fenix.components.metrics.MetricServiceType
|
||||
import org.mozilla.fenix.components.metrics.MozillaProductDetector
|
||||
import org.mozilla.fenix.components.toolbar.ToolbarPosition
|
||||
|
@ -156,9 +155,6 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
|
|||
GlobalScope.launch(Dispatchers.IO) {
|
||||
setStartupMetrics(store, settings())
|
||||
}
|
||||
if (FeatureFlags.messagingFeature && settings().isExperimentationEnabled) {
|
||||
components.appStore.dispatch(AppAction.MessagingAction.Restore)
|
||||
}
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
|
|
|
@ -1019,22 +1019,18 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
|
|||
}
|
||||
}
|
||||
|
||||
fun processIntent(intent: Intent): Boolean {
|
||||
return externalSourceIntentProcessors.any {
|
||||
it.process(
|
||||
intent,
|
||||
navHost.navController,
|
||||
this.intent
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
internal fun getSettings(): Settings = settings()
|
||||
|
||||
private fun shouldNavigateToBrowserOnColdStart(savedInstanceState: Bundle?): Boolean {
|
||||
return isActivityColdStarted(intent, savedInstanceState) &&
|
||||
!processIntent(intent)
|
||||
!externalSourceIntentProcessors.any {
|
||||
it.process(
|
||||
intent,
|
||||
navHost.navController,
|
||||
this.intent
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -25,8 +25,6 @@ import org.mozilla.fenix.components.metrics.GleanMetricsService
|
|||
import org.mozilla.fenix.components.metrics.MetricController
|
||||
import org.mozilla.fenix.experiments.createNimbus
|
||||
import org.mozilla.fenix.ext.settings
|
||||
import org.mozilla.fenix.gleanplumb.KeyPairMessageMetadataStorage
|
||||
import org.mozilla.fenix.gleanplumb.NimbusMessagingStorage
|
||||
import org.mozilla.fenix.nimbus.FxNimbus
|
||||
import org.mozilla.fenix.perf.lazyMonitored
|
||||
import org.mozilla.geckoview.BuildConfig.MOZ_APP_BUILDID
|
||||
|
@ -126,15 +124,6 @@ class Analytics(
|
|||
FxNimbus.api = api
|
||||
}
|
||||
}
|
||||
|
||||
val messagingStorage by lazyMonitored {
|
||||
NimbusMessagingStorage(
|
||||
context = context,
|
||||
metadataStorage = KeyPairMessageMetadataStorage(),
|
||||
gleanPlumb = experiments,
|
||||
messagingFeature = FxNimbus.features.messaging,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isSentryEnabled() = !BuildConfig.SENTRY_TOKEN.isNullOrEmpty()
|
||||
|
|
|
@ -32,7 +32,6 @@ import org.mozilla.fenix.ext.asRecentTabs
|
|||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.ext.filterState
|
||||
import org.mozilla.fenix.ext.settings
|
||||
import org.mozilla.fenix.gleanplumb.state.MessagingMiddleware
|
||||
import org.mozilla.fenix.ext.sort
|
||||
import org.mozilla.fenix.home.PocketUpdatesMiddleware
|
||||
import org.mozilla.fenix.home.blocklist.BlocklistHandler
|
||||
|
@ -194,7 +193,6 @@ class Components(private val context: Context) {
|
|||
val appStartReasonProvider by lazyMonitored { AppStartReasonProvider() }
|
||||
val startupActivityLog by lazyMonitored { StartupActivityLog() }
|
||||
val startupStateProvider by lazyMonitored { StartupStateProvider(startupActivityLog, appStartReasonProvider) }
|
||||
|
||||
val appStore by lazyMonitored {
|
||||
val blocklistHandler = BlocklistHandler(settings)
|
||||
|
||||
|
@ -205,6 +203,7 @@ class Components(private val context: Context) {
|
|||
topSites = core.topSitesStorage.cachedTopSites.sort(),
|
||||
recentBookmarks = emptyList(),
|
||||
showCollectionPlaceholder = settings.showCollectionsPlaceholderOnHome,
|
||||
showSetAsDefaultBrowserCard = settings.shouldShowSetAsDefaultBrowserCard(),
|
||||
// Provide an initial state for recent tabs to prevent re-rendering on the home screen.
|
||||
// This will otherwise cause a visual jump as the section gets rendered from no state
|
||||
// to some state.
|
||||
|
@ -220,8 +219,7 @@ class Components(private val context: Context) {
|
|||
PocketUpdatesMiddleware(
|
||||
core.pocketStoriesService,
|
||||
context.pocketStoriesSelectedCategoriesDataStore
|
||||
),
|
||||
MessagingMiddleware(messagingStorage = analytics.messagingStorage)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -16,8 +16,6 @@ import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesSelectedCategory
|
|||
import org.mozilla.fenix.home.recentbookmarks.RecentBookmark
|
||||
import org.mozilla.fenix.home.recenttabs.RecentTab
|
||||
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem
|
||||
import org.mozilla.fenix.gleanplumb.Message
|
||||
import org.mozilla.fenix.gleanplumb.MessagingState
|
||||
|
||||
/**
|
||||
* [Action] implementation related to [AppStore].
|
||||
|
@ -64,47 +62,7 @@ sealed class AppAction : Action {
|
|||
) : AppAction()
|
||||
object RemoveCollectionsPlaceholder : AppAction()
|
||||
/**
|
||||
* [Action]s related to interactions with the Messaging Framework.
|
||||
* [Action] implementation related to remove the DefaultBrowserCard.
|
||||
*/
|
||||
sealed class MessagingAction : AppAction() {
|
||||
/**
|
||||
* Restores the [Message] state from the storage.
|
||||
*/
|
||||
object Restore : MessagingAction()
|
||||
|
||||
/**
|
||||
* Evaluates if a new messages should be shown to users.
|
||||
*/
|
||||
object Evaluate : MessagingAction()
|
||||
|
||||
/**
|
||||
* Updates [MessagingState.messageToShow] with the given [message].
|
||||
*/
|
||||
data class UpdateMessageToShow(val message: Message) : MessagingAction()
|
||||
|
||||
/**
|
||||
* Updates [MessagingState.messageToShow] with the given [message].
|
||||
*/
|
||||
object ConsumeMessageToShow : MessagingAction()
|
||||
|
||||
/**
|
||||
* Updates [MessagingState.messages] with the given [messages].
|
||||
*/
|
||||
data class UpdateMessages(val messages: List<Message>) : MessagingAction()
|
||||
|
||||
/**
|
||||
* Indicates the given [message] was clicked.
|
||||
*/
|
||||
data class MessageClicked(val message: Message) : MessagingAction()
|
||||
|
||||
/**
|
||||
* Indicates the given [message] was shown.
|
||||
*/
|
||||
data class MessageDisplayed(val message: Message) : MessagingAction()
|
||||
|
||||
/**
|
||||
* Indicates the given [message] was dismissed.
|
||||
*/
|
||||
data class MessageDismissed(val message: Message) : MessagingAction()
|
||||
}
|
||||
object RemoveSetDefaultBrowserCard : AppAction()
|
||||
}
|
||||
|
|
|
@ -17,7 +17,6 @@ import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesSelectedCategory
|
|||
import org.mozilla.fenix.home.recentbookmarks.RecentBookmark
|
||||
import org.mozilla.fenix.home.recenttabs.RecentTab
|
||||
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem
|
||||
import org.mozilla.fenix.gleanplumb.MessagingState
|
||||
|
||||
/**
|
||||
* Value type that represents the state of the tabs tray.
|
||||
|
@ -31,12 +30,12 @@ import org.mozilla.fenix.gleanplumb.MessagingState
|
|||
* @property mode The state of the [HomeFragment] UI.
|
||||
* @property topSites The list of [TopSite] in the [HomeFragment].
|
||||
* @property showCollectionPlaceholder If true, shows a placeholder when there are no collections.
|
||||
* @property showSetAsDefaultBrowserCard If true, shows the default browser card
|
||||
* @property recentTabs The list of recent [RecentTab] in the [HomeFragment].
|
||||
* @property recentBookmarks The list of recently saved [BookmarkNode]s to show on the [HomeFragment].
|
||||
* @property recentHistory The list of [RecentlyVisitedItem]s.
|
||||
* @property pocketStories The list of currently shown [PocketRecommendedStory]s.
|
||||
* @property pocketStoriesCategories All [PocketRecommendedStory] categories.
|
||||
* @property messaging State related messages.
|
||||
* Also serves as an in memory cache of all stories mapped by category allowing for quick stories filtering.
|
||||
*/
|
||||
data class AppState(
|
||||
|
@ -47,11 +46,11 @@ data class AppState(
|
|||
val mode: Mode = Mode.Normal,
|
||||
val topSites: List<TopSite> = emptyList(),
|
||||
val showCollectionPlaceholder: Boolean = false,
|
||||
val showSetAsDefaultBrowserCard: Boolean = false,
|
||||
val recentTabs: List<RecentTab> = emptyList(),
|
||||
val recentBookmarks: List<RecentBookmark> = emptyList(),
|
||||
val recentHistory: List<RecentlyVisitedItem> = emptyList(),
|
||||
val pocketStories: List<PocketRecommendedStory> = emptyList(),
|
||||
val pocketStoriesCategories: List<PocketRecommendedStoriesCategory> = emptyList(),
|
||||
val pocketStoriesCategoriesSelections: List<PocketRecommendedStoriesSelectedCategory> = emptyList(),
|
||||
val messaging: MessagingState = MessagingState(),
|
||||
val pocketStoriesCategoriesSelections: List<PocketRecommendedStoriesSelectedCategory> = emptyList()
|
||||
) : State
|
||||
|
|
|
@ -14,7 +14,6 @@ import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesSelectedCategory
|
|||
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.gleanplumb.state.MessagingReducer
|
||||
|
||||
/**
|
||||
* Reducer for [AppStore].
|
||||
|
@ -31,8 +30,6 @@ internal object AppStoreReducer {
|
|||
is AppAction.RemoveAllNonFatalCrashes ->
|
||||
state.copy(nonFatalCrashes = emptyList())
|
||||
|
||||
is AppAction.MessagingAction -> MessagingReducer.reduce(state, action)
|
||||
|
||||
is AppAction.Change -> state.copy(
|
||||
collections = action.collections,
|
||||
mode = action.mode,
|
||||
|
@ -63,6 +60,7 @@ internal object AppStoreReducer {
|
|||
is AppAction.RemoveCollectionsPlaceholder -> {
|
||||
state.copy(showCollectionPlaceholder = false)
|
||||
}
|
||||
is AppAction.RemoveSetDefaultBrowserCard -> state.copy(showSetAsDefaultBrowserCard = false)
|
||||
is AppAction.RecentTabsChange -> {
|
||||
val recentSearchGroup = action.recentTabs.find { it is RecentTab.SearchGroup } as RecentTab.SearchGroup?
|
||||
state.copy(
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.gleanplumb
|
||||
|
||||
import android.content.Context
|
||||
import org.json.JSONObject
|
||||
import org.mozilla.fenix.utils.BrowsersCache
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import java.util.Calendar
|
||||
|
||||
/**
|
||||
* Custom attributes that the messaging framework will use to evaluate if message is eligible
|
||||
* to be shown.
|
||||
*/
|
||||
object CustomAttributeProvider {
|
||||
private val formatter = SimpleDateFormat("yyyy-MM-dd", Locale.US)
|
||||
|
||||
/**
|
||||
* Returns a [JSONObject] that contains all the custom attributes, evaluated when the function
|
||||
* was called.
|
||||
*/
|
||||
fun getCustomAttributes(context: Context): JSONObject {
|
||||
val now = Calendar.getInstance()
|
||||
return JSONObject(
|
||||
mapOf(
|
||||
"is_default_browser" to BrowsersCache.all(context).isDefaultBrowser,
|
||||
"date_string" to formatter.format(now.time)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.gleanplumb
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.core.net.toUri
|
||||
import org.mozilla.fenix.BuildConfig
|
||||
import org.mozilla.fenix.HomeActivity
|
||||
import org.mozilla.fenix.components.AppStore
|
||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.MessageClicked
|
||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.MessageDismissed
|
||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.MessageDisplayed
|
||||
|
||||
/**
|
||||
* Handles default interactions with the ui of GleanPlumb messages.
|
||||
*/
|
||||
class DefaultMessageController(
|
||||
private val appStore: AppStore,
|
||||
private val messagingStorage: NimbusMessagingStorage,
|
||||
private val homeActivity: HomeActivity
|
||||
) : MessageController {
|
||||
|
||||
override fun onMessagePressed(message: Message) {
|
||||
// Report telemetry event
|
||||
// This will be covered on https://github.com/mozilla-mobile/fenix/issues/24224
|
||||
val action = messagingStorage.getMessageAction(message)
|
||||
handleAction(action)
|
||||
appStore.dispatch(MessageClicked(message))
|
||||
}
|
||||
|
||||
override fun onMessageDismissed(message: Message) {
|
||||
// Report telemetry event
|
||||
// This will be covered on https://github.com/mozilla-mobile/fenix/issues/24224
|
||||
appStore.dispatch(MessageDismissed(message))
|
||||
}
|
||||
|
||||
override fun onMessageDisplayed(message: Message) {
|
||||
// Report telemetry event
|
||||
// This will be covered on https://github.com/mozilla-mobile/fenix/issues/24224
|
||||
appStore.dispatch(MessageDisplayed(message))
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
internal fun handleAction(action: String): Intent {
|
||||
val partialAction = if (action.startsWith("http", ignoreCase = true)) {
|
||||
"://open?url=${Uri.encode(action)}"
|
||||
} else {
|
||||
action
|
||||
}
|
||||
val intent =
|
||||
Intent(Intent.ACTION_VIEW, "${BuildConfig.DEEP_LINK_SCHEME}$partialAction".toUri())
|
||||
homeActivity.processIntent(intent)
|
||||
|
||||
return intent
|
||||
}
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.gleanplumb
|
||||
|
||||
/* Dummy implementation until we provide full implementation.
|
||||
* This will covered on https://github.com/mozilla-mobile/fenix/issues/24222
|
||||
* */
|
||||
class KeyPairMessageMetadataStorage : MessageMetadataStorage {
|
||||
override fun getMetadata(): List<Message.Metadata> {
|
||||
return listOf(
|
||||
Message.Metadata(
|
||||
id = "eu-tracking-protection-for-ireland",
|
||||
displayCount = 0,
|
||||
pressed = false,
|
||||
dismissed = false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun addMetadata(metadata: Message.Metadata): Message.Metadata {
|
||||
return metadata
|
||||
}
|
||||
|
||||
@SuppressWarnings("EmptyFunctionBlock")
|
||||
override fun updateMetadata(metadata: Message.Metadata) {
|
||||
}
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.gleanplumb
|
||||
|
||||
import org.mozilla.fenix.nimbus.MessageData
|
||||
import org.mozilla.fenix.nimbus.StyleData
|
||||
|
||||
/**
|
||||
* A data class that holds a representation of GleanPlum message from Nimbus.
|
||||
*
|
||||
* @param id identifies a message as unique.
|
||||
* @param data Data information provided from Nimbus.
|
||||
* @param action A strings that represents which action should be performed
|
||||
* after a message is clicked.
|
||||
* @param style Indicates how a message should be styled.
|
||||
* @param triggers A list of strings corresponding to targeting expressions. The message
|
||||
* will be shown if all expressions `true`.
|
||||
* @param metadata Metadata that help to identify if a message should shown.
|
||||
*/
|
||||
data class Message(
|
||||
val id: String,
|
||||
val data: MessageData,
|
||||
val action: String,
|
||||
val style: StyleData,
|
||||
val triggers: List<String>,
|
||||
val metadata: Metadata
|
||||
) {
|
||||
val maxDisplayCount: Int
|
||||
get() = style.maxDisplayCount
|
||||
|
||||
val priority: Int
|
||||
get() = style.priority
|
||||
|
||||
/**
|
||||
* A data class that holds metadata that help to identify if a message should shown.
|
||||
*
|
||||
* @param id identifies a message as unique.
|
||||
* @param displayCount Indicates how many times a message is displayed.
|
||||
* @param pressed Indicates if a message has been clicked.
|
||||
* @param dismissed Indicates if a message has been closed.
|
||||
*/
|
||||
data class Metadata(
|
||||
val id: String,
|
||||
val displayCount: Int = 0,
|
||||
val pressed: Boolean = false,
|
||||
val dismissed: Boolean = false
|
||||
)
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.gleanplumb
|
||||
|
||||
/**
|
||||
* Controls all the interactions with a [Message].
|
||||
*/
|
||||
interface MessageController {
|
||||
/**
|
||||
* Indicates the provided [message] was pressed by a user.
|
||||
*/
|
||||
fun onMessagePressed(message: Message)
|
||||
|
||||
/**
|
||||
* Indicates the provided [message] was dismissed by a user.
|
||||
*/
|
||||
fun onMessageDismissed(message: Message)
|
||||
|
||||
/**
|
||||
* Indicates the provided [message] was displayed to a user.
|
||||
*/
|
||||
fun onMessageDisplayed(message: Message)
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.gleanplumb
|
||||
|
||||
interface MessageMetadataStorage {
|
||||
/**
|
||||
* Provide all the message metadata saved in the storage.
|
||||
*/
|
||||
fun getMetadata(): List<Message.Metadata>
|
||||
|
||||
/**
|
||||
* Given a [metadata] add the message metadata on the storage.
|
||||
* @return the added message on the [MessageMetadataStorage]
|
||||
*/
|
||||
fun addMetadata(metadata: Message.Metadata): Message.Metadata
|
||||
|
||||
/**
|
||||
* Given a [metadata] update the message metadata on the storage.
|
||||
*/
|
||||
fun updateMetadata(metadata: Message.Metadata)
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.gleanplumb
|
||||
|
||||
import mozilla.components.support.base.feature.LifecycleAwareFeature
|
||||
import org.mozilla.fenix.FeatureFlags
|
||||
import org.mozilla.fenix.components.AppStore
|
||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction
|
||||
|
||||
/**
|
||||
* A message observer that updates the provided.
|
||||
*/
|
||||
class MessagingFeature(val store: AppStore) : LifecycleAwareFeature {
|
||||
|
||||
override fun start() {
|
||||
if (FeatureFlags.messagingFeature) {
|
||||
store.dispatch(MessagingAction.Evaluate)
|
||||
}
|
||||
}
|
||||
|
||||
override fun stop() = Unit
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.gleanplumb
|
||||
|
||||
/**
|
||||
* Represent all the state related to the Messaging framework.
|
||||
* @param messages Indicates all the available messages.
|
||||
* @param messageToShow Indicates the message that should be shown to users,
|
||||
* if it is null means there is not message that is eligible to be shown to users.
|
||||
*/
|
||||
data class MessagingState(
|
||||
val messages: List<Message> = emptyList(),
|
||||
val messageToShow: Message? = null
|
||||
)
|
|
@ -1,174 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.gleanplumb
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import mozilla.components.support.base.log.logger.Logger
|
||||
import org.json.JSONObject
|
||||
import org.mozilla.experiments.nimbus.GleanPlumbInterface
|
||||
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.Messaging
|
||||
import org.mozilla.fenix.nimbus.StyleData
|
||||
|
||||
/**
|
||||
* Provides messages from [messagingFeature] and combine with the metadata store on [metadataStorage].
|
||||
*/
|
||||
class NimbusMessagingStorage(
|
||||
private val context: Context,
|
||||
private val metadataStorage: MessageMetadataStorage,
|
||||
private val gleanPlumb: GleanPlumbInterface,
|
||||
private val messagingFeature: FeatureHolder<Messaging>
|
||||
) {
|
||||
private val logger = Logger("MessagingStorage")
|
||||
private val nimbusFeature = messagingFeature.value()
|
||||
private val customAttributes: JSONObject
|
||||
get() = JSONObject()
|
||||
|
||||
/**
|
||||
* Returns a list of available messages descending sorted by their priority.
|
||||
*/
|
||||
fun getMessages(): List<Message> {
|
||||
val nimbusTriggers = nimbusFeature.triggers
|
||||
val nimbusStyles = nimbusFeature.styles
|
||||
val nimbusActions = nimbusFeature.actions
|
||||
|
||||
val nimbusMessages = nimbusFeature.messages
|
||||
val defaultStyle = StyleData(context)
|
||||
val storageMetadata = metadataStorage.getMetadata().associateBy {
|
||||
it.id
|
||||
}
|
||||
|
||||
return nimbusMessages.mapNotNull { (key, value) ->
|
||||
val action = sanitizeAction(value.action, nimbusActions) ?: return@mapNotNull null
|
||||
Message(
|
||||
id = key,
|
||||
data = value,
|
||||
action = action,
|
||||
style = nimbusStyles[value.style] ?: defaultStyle,
|
||||
metadata = storageMetadata[key] ?: addMetadata(key),
|
||||
triggers = sanitizeTriggers(value.trigger, nimbusTriggers) ?: return@mapNotNull null
|
||||
)
|
||||
}.filter {
|
||||
it.data.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? {
|
||||
val helper = gleanPlumb.createMessageHelper(customAttributes)
|
||||
var message = availableMessages.firstOrNull {
|
||||
isMessageEligible(it, helper)
|
||||
} ?: return null
|
||||
|
||||
if (isMessageUnderExperiment(message, nimbusFeature.messageUnderExperiment)) {
|
||||
messagingFeature.recordExposure()
|
||||
|
||||
if (message.data.isControl) {
|
||||
message = availableMessages.firstOrNull {
|
||||
!it.data.isControl && isMessageEligible(it, helper)
|
||||
} ?: return null
|
||||
}
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a valid action for the provided [message].
|
||||
*/
|
||||
fun getMessageAction(message: Message): String {
|
||||
val helper = gleanPlumb.createMessageHelper(customAttributes)
|
||||
val uuid = helper.getUuid(message.action)
|
||||
|
||||
return helper.stringFormat(message.action, uuid)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updated the provided [metadata] in the storage.
|
||||
*/
|
||||
fun updateMetadata(metadata: Message.Metadata) {
|
||||
metadataStorage.updateMetadata(metadata)
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
internal fun sanitizeAction(
|
||||
unsafeAction: String,
|
||||
nimbusActions: Map<String, String>
|
||||
): String? {
|
||||
return if (unsafeAction.startsWith("http")) {
|
||||
unsafeAction
|
||||
} else {
|
||||
val safeAction = nimbusActions[unsafeAction]
|
||||
if (safeAction.isNullOrBlank() || safeAction.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
safeAction
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
internal fun sanitizeTriggers(
|
||||
unsafeTriggers: List<String>,
|
||||
nimbusTriggers: Map<String, String>
|
||||
): List<String>? {
|
||||
return unsafeTriggers.map {
|
||||
val safeTrigger = nimbusTriggers[it]
|
||||
if (safeTrigger.isNullOrBlank() || safeTrigger.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
safeTrigger
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
internal fun isMessageUnderExperiment(message: Message, expression: String?): Boolean {
|
||||
return when {
|
||||
expression.isNullOrBlank() -> {
|
||||
false
|
||||
}
|
||||
expression.endsWith("-") -> {
|
||||
message.id.startsWith(expression)
|
||||
}
|
||||
else -> {
|
||||
message.id == expression
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
internal fun isMessageEligible(
|
||||
message: Message,
|
||||
helper: GleanPlumbMessageHelper
|
||||
): Boolean {
|
||||
return message.triggers.all { condition ->
|
||||
try {
|
||||
helper.evalJexl(condition)
|
||||
} catch (e: NimbusException.EvaluationException) {
|
||||
// Report to glean as malformed message
|
||||
// Will be addressed on https://github.com/mozilla-mobile/fenix/issues/24224
|
||||
logger.info("Unable to evaluate $condition")
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun addMetadata(id: String): Message.Metadata {
|
||||
// This will be improve on https://github.com/mozilla-mobile/fenix/issues/24222
|
||||
return metadataStorage.addMetadata(
|
||||
Message.Metadata(
|
||||
id = id,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,139 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.gleanplumb.state
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import mozilla.components.lib.state.Middleware
|
||||
import mozilla.components.lib.state.MiddlewareContext
|
||||
import org.mozilla.fenix.components.appstate.AppAction
|
||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.ConsumeMessageToShow
|
||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.Evaluate
|
||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.MessageClicked
|
||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.MessageDismissed
|
||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.MessageDisplayed
|
||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.Restore
|
||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.UpdateMessageToShow
|
||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.UpdateMessages
|
||||
import org.mozilla.fenix.components.appstate.AppState
|
||||
import org.mozilla.fenix.gleanplumb.Message
|
||||
import org.mozilla.fenix.gleanplumb.NimbusMessagingStorage
|
||||
|
||||
typealias AppStoreMiddlewareContext = MiddlewareContext<AppState, AppAction>
|
||||
|
||||
class MessagingMiddleware(
|
||||
private val messagingStorage: NimbusMessagingStorage
|
||||
) : Middleware<AppState, AppAction> {
|
||||
|
||||
override fun invoke(
|
||||
context: AppStoreMiddlewareContext,
|
||||
next: (AppAction) -> Unit,
|
||||
action: AppAction
|
||||
) {
|
||||
when (action) {
|
||||
is Restore -> {
|
||||
val messages = messagingStorage.getMessages()
|
||||
|
||||
context.dispatch(UpdateMessages(messages))
|
||||
}
|
||||
|
||||
is Evaluate -> {
|
||||
val message = messagingStorage.getNextMessage(context.state.messaging.messages)
|
||||
if (message != null) {
|
||||
context.dispatch(UpdateMessageToShow(message))
|
||||
} else {
|
||||
context.dispatch(ConsumeMessageToShow)
|
||||
}
|
||||
}
|
||||
|
||||
is MessageClicked -> onMessageClicked(action.message, context)
|
||||
|
||||
is MessageDismissed -> onMessageDismissed(context, action.message)
|
||||
|
||||
is MessageDisplayed -> onMessagedDisplayed(action.message, context)
|
||||
}
|
||||
next(action)
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
internal fun onMessagedDisplayed(
|
||||
oldMessage: Message,
|
||||
context: AppStoreMiddlewareContext
|
||||
) {
|
||||
val newMetadata = oldMessage.metadata.copy(
|
||||
displayCount = oldMessage.metadata.displayCount + 1
|
||||
)
|
||||
val newMessage = oldMessage.copy(
|
||||
metadata = newMetadata
|
||||
)
|
||||
val newMessages = if (newMetadata.displayCount < oldMessage.maxDisplayCount) {
|
||||
updateMessage(context, oldMessage, newMessage)
|
||||
} else {
|
||||
consumeMessageToShowIfNeeded(context, oldMessage)
|
||||
removeMessage(context, oldMessage)
|
||||
}
|
||||
context.dispatch(UpdateMessages(newMessages))
|
||||
messagingStorage.updateMetadata(newMetadata)
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
internal fun onMessageDismissed(
|
||||
context: AppStoreMiddlewareContext,
|
||||
message: Message
|
||||
) {
|
||||
val newMessages = removeMessage(context, message)
|
||||
val updatedMetadata = message.metadata.copy(dismissed = true)
|
||||
|
||||
messagingStorage.updateMetadata(updatedMetadata)
|
||||
context.dispatch(UpdateMessages(newMessages))
|
||||
consumeMessageToShowIfNeeded(context, message)
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
internal fun onMessageClicked(
|
||||
message: Message,
|
||||
context: AppStoreMiddlewareContext
|
||||
) {
|
||||
// Update Nimbus storage.
|
||||
val updatedMetadata = message.metadata.copy(pressed = true)
|
||||
messagingStorage.updateMetadata(updatedMetadata)
|
||||
|
||||
// Update app state.
|
||||
val newMessages = removeMessage(context, message)
|
||||
context.dispatch(UpdateMessages(newMessages))
|
||||
consumeMessageToShowIfNeeded(context, message)
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
internal fun consumeMessageToShowIfNeeded(
|
||||
context: AppStoreMiddlewareContext,
|
||||
message: Message
|
||||
) {
|
||||
if (context.state.messaging.messageToShow?.id == message.id) {
|
||||
context.dispatch(ConsumeMessageToShow)
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
internal fun removeMessage(
|
||||
context: AppStoreMiddlewareContext,
|
||||
message: Message
|
||||
): List<Message> {
|
||||
return context.state.messaging.messages.filter { it.id != message.id }
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
internal fun updateMessage(
|
||||
context: AppStoreMiddlewareContext,
|
||||
oldMessage: Message,
|
||||
updatedMessage: Message
|
||||
): List<Message> {
|
||||
val actualMessageToShow = context.state.messaging.messageToShow
|
||||
|
||||
if (actualMessageToShow?.id == oldMessage.id) {
|
||||
context.dispatch(UpdateMessageToShow(updatedMessage))
|
||||
}
|
||||
return removeMessage(context, oldMessage) + updatedMessage
|
||||
}
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.gleanplumb.state
|
||||
|
||||
import org.mozilla.fenix.components.appstate.AppAction
|
||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.ConsumeMessageToShow
|
||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.UpdateMessageToShow
|
||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.UpdateMessages
|
||||
import org.mozilla.fenix.components.appstate.AppState
|
||||
import org.mozilla.fenix.gleanplumb.MessagingState
|
||||
|
||||
/**
|
||||
* Reducer for [MessagingState].
|
||||
*/
|
||||
internal object MessagingReducer {
|
||||
fun reduce(state: AppState, action: AppAction.MessagingAction): AppState = when (action) {
|
||||
is UpdateMessageToShow -> {
|
||||
state.copy(
|
||||
messaging = state.messaging.copy(
|
||||
messageToShow = action.message
|
||||
)
|
||||
)
|
||||
}
|
||||
is UpdateMessages -> {
|
||||
state.copy(
|
||||
messaging = state.messaging.copy(
|
||||
messages = action.messages
|
||||
)
|
||||
)
|
||||
}
|
||||
is ConsumeMessageToShow -> {
|
||||
state.copy(
|
||||
messaging = state.messaging.copy(
|
||||
messageToShow = null
|
||||
)
|
||||
)
|
||||
}
|
||||
else -> state
|
||||
}
|
||||
}
|
|
@ -97,8 +97,6 @@ import org.mozilla.fenix.ext.nav
|
|||
import org.mozilla.fenix.ext.requireComponents
|
||||
import org.mozilla.fenix.ext.runIfFragmentIsAttached
|
||||
import org.mozilla.fenix.ext.settings
|
||||
import org.mozilla.fenix.gleanplumb.DefaultMessageController
|
||||
import org.mozilla.fenix.gleanplumb.MessagingFeature
|
||||
import org.mozilla.fenix.home.mozonline.showPrivacyPopWindow
|
||||
import org.mozilla.fenix.home.pocket.DefaultPocketStoriesController
|
||||
import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesCategory
|
||||
|
@ -174,7 +172,6 @@ class HomeFragment : Fragment() {
|
|||
private lateinit var currentMode: CurrentMode
|
||||
|
||||
private val topSitesFeature = ViewBoundFeatureWrapper<TopSitesFeature>()
|
||||
private val messagingFeature = ViewBoundFeatureWrapper<MessagingFeature>()
|
||||
private val recentTabsListFeature = ViewBoundFeatureWrapper<RecentTabsListFeature>()
|
||||
private val recentBookmarksFeature = ViewBoundFeatureWrapper<RecentBookmarksFeature>()
|
||||
private val historyMetadataFeature = ViewBoundFeatureWrapper<RecentVisitsFeature>()
|
||||
|
@ -242,16 +239,6 @@ class HomeFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
|
||||
if (requireContext().settings().isExperimentationEnabled) {
|
||||
messagingFeature.set(
|
||||
feature = MessagingFeature(
|
||||
store = requireComponents.appStore,
|
||||
),
|
||||
owner = viewLifecycleOwner,
|
||||
view = binding.root
|
||||
)
|
||||
}
|
||||
|
||||
if (requireContext().settings().showTopSitesFeature) {
|
||||
topSitesFeature.set(
|
||||
feature = TopSitesFeature(
|
||||
|
@ -311,11 +298,6 @@ class HomeFragment : Fragment() {
|
|||
settings = components.settings,
|
||||
engine = components.core.engine,
|
||||
metrics = components.analytics.metrics,
|
||||
messageController = DefaultMessageController(
|
||||
appStore = components.appStore,
|
||||
messagingStorage = components.analytics.messagingStorage,
|
||||
homeActivity = activity
|
||||
),
|
||||
store = store,
|
||||
tabCollectionStorage = components.core.tabCollectionStorage,
|
||||
addTabUseCase = components.useCases.tabsUseCases.addTab,
|
||||
|
|
|
@ -18,7 +18,6 @@ import mozilla.components.feature.top.sites.TopSite
|
|||
import mozilla.components.ui.widgets.WidgetSiteItemView
|
||||
import org.mozilla.fenix.components.AppStore
|
||||
import org.mozilla.fenix.components.Components
|
||||
import org.mozilla.fenix.gleanplumb.Message
|
||||
import org.mozilla.fenix.home.BottomSpacerViewHolder
|
||||
import org.mozilla.fenix.home.TopPlaceholderViewHolder
|
||||
import org.mozilla.fenix.home.pocket.PocketCategoriesViewHolder
|
||||
|
@ -36,7 +35,7 @@ import org.mozilla.fenix.home.sessioncontrol.viewholders.CustomizeHomeButtonView
|
|||
import org.mozilla.fenix.home.sessioncontrol.viewholders.NoCollectionsMessageViewHolder
|
||||
import org.mozilla.fenix.home.sessioncontrol.viewholders.PrivateBrowsingDescriptionViewHolder
|
||||
import org.mozilla.fenix.home.sessioncontrol.viewholders.TabInCollectionViewHolder
|
||||
import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.MessageCardViewHolder
|
||||
import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.ExperimentDefaultBrowserCardViewHolder
|
||||
import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingFinishViewHolder
|
||||
import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingHeaderViewHolder
|
||||
import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingManualSignInViewHolder
|
||||
|
@ -143,12 +142,10 @@ sealed class AdapterItem(@LayoutRes val viewType: Int) {
|
|||
|
||||
object OnboardingManualSignIn : AdapterItem(OnboardingManualSignInViewHolder.LAYOUT_ID)
|
||||
|
||||
data class NimbusMessageCard(
|
||||
val message: Message
|
||||
) : AdapterItem(MessageCardViewHolder.LAYOUT_ID) {
|
||||
override fun sameAs(other: AdapterItem) =
|
||||
other is NimbusMessageCard && message.id == other.message.id
|
||||
}
|
||||
/**
|
||||
* AdapterItem for the default browser card.
|
||||
*/
|
||||
object ExperimentDefaultBrowserCard : AdapterItem(ExperimentDefaultBrowserCardViewHolder.LAYOUT_ID)
|
||||
|
||||
object OnboardingThemePicker : AdapterItem(OnboardingThemePickerViewHolder.LAYOUT_ID)
|
||||
object OnboardingTrackingProtection :
|
||||
|
@ -308,7 +305,7 @@ class SessionControlAdapter(
|
|||
OnboardingToolbarPositionPickerViewHolder.LAYOUT_ID -> OnboardingToolbarPositionPickerViewHolder(
|
||||
view
|
||||
)
|
||||
MessageCardViewHolder.LAYOUT_ID -> MessageCardViewHolder(view, interactor)
|
||||
ExperimentDefaultBrowserCardViewHolder.LAYOUT_ID -> ExperimentDefaultBrowserCardViewHolder(view, interactor)
|
||||
BottomSpacerViewHolder.LAYOUT_ID -> BottomSpacerViewHolder(view)
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
|
@ -369,9 +366,6 @@ class SessionControlAdapter(
|
|||
is TopSitePagerViewHolder -> {
|
||||
holder.bind((item as AdapterItem.TopSitePager).topSites)
|
||||
}
|
||||
is MessageCardViewHolder -> {
|
||||
holder.bind((item as AdapterItem.NimbusMessageCard).message)
|
||||
}
|
||||
is CollectionViewHolder -> {
|
||||
val (collection, expanded) = item as AdapterItem.CollectionItem
|
||||
holder.bindSession(collection, expanded)
|
||||
|
|
|
@ -48,9 +48,8 @@ import org.mozilla.fenix.components.metrics.MetricsUtils
|
|||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.ext.metrics
|
||||
import org.mozilla.fenix.ext.nav
|
||||
import org.mozilla.fenix.ext.openSetDefaultBrowserOption
|
||||
import org.mozilla.fenix.ext.settings
|
||||
import org.mozilla.fenix.gleanplumb.Message
|
||||
import org.mozilla.fenix.gleanplumb.MessageController
|
||||
import org.mozilla.fenix.home.HomeFragment
|
||||
import org.mozilla.fenix.home.HomeFragmentDirections
|
||||
import org.mozilla.fenix.home.Mode
|
||||
|
@ -176,19 +175,14 @@ interface SessionControlController {
|
|||
fun handleMenuOpened()
|
||||
|
||||
/**
|
||||
* @see [MessageCardInteractor.onMessageClicked]
|
||||
* @see [ExperimentCardInteractor.onSetDefaultBrowserClicked]
|
||||
*/
|
||||
fun handleMessageClicked(message: Message)
|
||||
fun handleSetDefaultBrowser()
|
||||
|
||||
/**
|
||||
* @see [MessageCardInteractor.onMessageClosedClicked]
|
||||
* @see [ExperimentCardInteractor.onCloseExperimentCardClicked]
|
||||
*/
|
||||
fun handleMessageClosed(message: Message)
|
||||
|
||||
/**
|
||||
* @see [MessageCardInteractor.onMessageDisplayed]
|
||||
*/
|
||||
fun handleMessageDisplayed(message: Message)
|
||||
fun handleCloseExperimentCard()
|
||||
|
||||
/**
|
||||
* @see [TabSessionInteractor.onPrivateModeButtonClicked]
|
||||
|
@ -211,13 +205,12 @@ interface SessionControlController {
|
|||
fun handleReportSessionMetrics(state: AppState)
|
||||
}
|
||||
|
||||
@Suppress("TooManyFunctions", "LargeClass")
|
||||
@Suppress("TooManyFunctions", "LargeClass", "LongParameterList")
|
||||
class DefaultSessionControlController(
|
||||
private val activity: HomeActivity,
|
||||
private val settings: Settings,
|
||||
private val engine: Engine,
|
||||
private val metrics: MetricController,
|
||||
private val messageController: MessageController,
|
||||
private val store: BrowserStore,
|
||||
private val tabCollectionStorage: TabCollectionStorage,
|
||||
private val addTabUseCase: TabsUseCases.AddNewTabUseCase,
|
||||
|
@ -615,16 +608,14 @@ class DefaultSessionControlController(
|
|||
navController.nav(R.id.homeFragment, directions)
|
||||
}
|
||||
|
||||
override fun handleMessageClicked(message: Message) {
|
||||
messageController.onMessagePressed(message)
|
||||
override fun handleSetDefaultBrowser() {
|
||||
settings.userDismissedExperimentCard = true
|
||||
activity.openSetDefaultBrowserOption()
|
||||
}
|
||||
|
||||
override fun handleMessageClosed(message: Message) {
|
||||
messageController.onMessageDismissed(message)
|
||||
}
|
||||
|
||||
override fun handleMessageDisplayed(message: Message) {
|
||||
messageController.onMessageDisplayed(message)
|
||||
override fun handleCloseExperimentCard() {
|
||||
settings.userDismissedExperimentCard = true
|
||||
appStore.dispatch(AppAction.RemoveSetDefaultBrowserCard)
|
||||
}
|
||||
|
||||
override fun handlePrivateModeButtonClicked(
|
||||
|
|
|
@ -10,7 +10,6 @@ import mozilla.components.feature.top.sites.TopSite
|
|||
import mozilla.components.service.pocket.PocketRecommendedStory
|
||||
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
|
||||
import org.mozilla.fenix.components.appstate.AppState
|
||||
import org.mozilla.fenix.gleanplumb.Message
|
||||
import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesCategory
|
||||
import org.mozilla.fenix.home.pocket.PocketStoriesController
|
||||
import org.mozilla.fenix.home.pocket.PocketStoriesInteractor
|
||||
|
@ -226,21 +225,19 @@ interface TopSiteInteractor {
|
|||
fun onTopSiteMenuOpened()
|
||||
}
|
||||
|
||||
interface MessageCardInteractor {
|
||||
/**
|
||||
* Interface for interactions with the default browser card.
|
||||
*/
|
||||
interface ExperimentCardInteractor {
|
||||
/**
|
||||
* Called when a [Message]'s button is clicked
|
||||
* Called when set default browser button is clicked
|
||||
*/
|
||||
fun onMessageClicked(message: Message)
|
||||
fun onSetDefaultBrowserClicked()
|
||||
|
||||
/**
|
||||
* Called when close button on a [Message] card.
|
||||
* Called when close button on experiment card
|
||||
*/
|
||||
fun onMessageClosedClicked(message: Message)
|
||||
|
||||
/**
|
||||
* Called when close button on a [Message] card.
|
||||
*/
|
||||
fun onMessageDisplayed(message: Message)
|
||||
fun onCloseExperimentCardClicked()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -261,7 +258,7 @@ class SessionControlInteractor(
|
|||
TopSiteInteractor,
|
||||
TabSessionInteractor,
|
||||
ToolbarInteractor,
|
||||
MessageCardInteractor,
|
||||
ExperimentCardInteractor,
|
||||
RecentTabInteractor,
|
||||
RecentBookmarksInteractor,
|
||||
RecentVisitsInteractor,
|
||||
|
@ -368,6 +365,14 @@ class SessionControlInteractor(
|
|||
controller.handleMenuOpened()
|
||||
}
|
||||
|
||||
override fun onSetDefaultBrowserClicked() {
|
||||
controller.handleSetDefaultBrowser()
|
||||
}
|
||||
|
||||
override fun onCloseExperimentCardClicked() {
|
||||
controller.handleCloseExperimentCard()
|
||||
}
|
||||
|
||||
override fun onRecentTabClicked(tabId: String) {
|
||||
recentTabController.handleRecentTabClicked(tabId)
|
||||
}
|
||||
|
@ -445,16 +450,4 @@ class SessionControlInteractor(
|
|||
override fun reportSessionMetrics(state: AppState) {
|
||||
controller.handleReportSessionMetrics(state)
|
||||
}
|
||||
|
||||
override fun onMessageClicked(message: Message) {
|
||||
controller.handleMessageClicked(message)
|
||||
}
|
||||
|
||||
override fun onMessageClosedClicked(message: Message) {
|
||||
controller.handleMessageClosed(message)
|
||||
}
|
||||
|
||||
override fun onMessageDisplayed(message: Message) {
|
||||
controller.handleMessageDisplayed(message)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,6 @@ import mozilla.components.feature.top.sites.TopSite
|
|||
import mozilla.components.service.pocket.PocketRecommendedStory
|
||||
import org.mozilla.fenix.components.AppStore
|
||||
import org.mozilla.fenix.components.appstate.AppState
|
||||
import org.mozilla.fenix.gleanplumb.Message
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.ext.settings
|
||||
import org.mozilla.fenix.home.Mode
|
||||
|
@ -37,7 +36,7 @@ internal fun normalModeAdapterItems(
|
|||
expandedCollections: Set<Long>,
|
||||
recentBookmarks: List<RecentBookmark>,
|
||||
showCollectionsPlaceholder: Boolean,
|
||||
nimbusMessageCard: Message? = null,
|
||||
showSetAsDefaultBrowserCard: Boolean,
|
||||
recentTabs: List<RecentTab>,
|
||||
recentVisits: List<RecentlyVisitedItem>,
|
||||
pocketStories: List<PocketRecommendedStory>
|
||||
|
@ -48,8 +47,8 @@ internal fun normalModeAdapterItems(
|
|||
// Add a synchronous, unconditional and invisible placeholder so home is anchored to the top when created.
|
||||
items.add(AdapterItem.TopPlaceholderItem)
|
||||
|
||||
nimbusMessageCard?.let {
|
||||
items.add(AdapterItem.NimbusMessageCard(it))
|
||||
if (showSetAsDefaultBrowserCard) {
|
||||
items.add(AdapterItem.ExperimentDefaultBrowserCard)
|
||||
}
|
||||
|
||||
if (settings.showTopSitesFeature && topSites.isNotEmpty()) {
|
||||
|
@ -158,7 +157,7 @@ private fun AppState.toAdapterList(settings: Settings): List<AdapterItem> = when
|
|||
expandedCollections,
|
||||
recentBookmarks,
|
||||
showCollectionPlaceholder,
|
||||
messaging.messageToShow,
|
||||
showSetAsDefaultBrowserCard,
|
||||
recentTabs,
|
||||
recentHistory,
|
||||
pocketStories
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.databinding.ExperimentDefaultBrowserBinding
|
||||
import org.mozilla.fenix.ext.increaseTapArea
|
||||
import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor
|
||||
|
||||
/**
|
||||
* View holder for the default browser card.
|
||||
*/
|
||||
class ExperimentDefaultBrowserCardViewHolder(
|
||||
view: View,
|
||||
private val interactor: SessionControlInteractor
|
||||
) : RecyclerView.ViewHolder(view) {
|
||||
|
||||
init {
|
||||
val binding = ExperimentDefaultBrowserBinding.bind(view)
|
||||
binding.setDefaultBrowser.setOnClickListener {
|
||||
interactor.onSetDefaultBrowserClicked()
|
||||
}
|
||||
|
||||
binding.close.apply {
|
||||
increaseTapArea(CLOSE_BUTTON_EXTRA_DPS)
|
||||
setOnClickListener {
|
||||
interactor.onCloseExperimentCardClicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
internal const val LAYOUT_ID = R.layout.experiment_default_browser
|
||||
private const val CLOSE_BUTTON_EXTRA_DPS = 38
|
||||
}
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding
|
||||
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.databinding.NimbusMessageCardBinding
|
||||
import org.mozilla.fenix.ext.increaseTapArea
|
||||
import org.mozilla.fenix.gleanplumb.Message
|
||||
import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor
|
||||
|
||||
class MessageCardViewHolder(
|
||||
view: View,
|
||||
private val interactor: SessionControlInteractor
|
||||
) : RecyclerView.ViewHolder(view) {
|
||||
|
||||
fun bind(message: Message) {
|
||||
val binding = NimbusMessageCardBinding.bind(itemView)
|
||||
|
||||
if (message.data.title.isNullOrBlank()) {
|
||||
binding.titleText.isVisible = false
|
||||
} else {
|
||||
binding.titleText.text = message.data.title
|
||||
}
|
||||
|
||||
binding.descriptionText.text = message.data.text
|
||||
|
||||
if (message.data.buttonLabel.isNullOrBlank()) {
|
||||
binding.messageButton.isVisible = false
|
||||
binding.experimentCard.setOnClickListener {
|
||||
interactor.onMessageClicked(message)
|
||||
}
|
||||
} else {
|
||||
binding.messageButton.text = message.data.buttonLabel
|
||||
binding.messageButton.setOnClickListener {
|
||||
interactor.onMessageClicked(message)
|
||||
}
|
||||
}
|
||||
|
||||
binding.close.apply {
|
||||
increaseTapArea(CLOSE_BUTTON_EXTRA_DPS)
|
||||
setOnClickListener {
|
||||
interactor.onMessageClosedClicked(message)
|
||||
}
|
||||
}
|
||||
interactor.onMessageDisplayed(message)
|
||||
}
|
||||
|
||||
companion object {
|
||||
internal const val LAYOUT_ID = R.layout.nimbus_message_card
|
||||
private const val CLOSE_BUTTON_EXTRA_DPS = 38
|
||||
}
|
||||
}
|
|
@ -71,6 +71,7 @@ class Settings(private val appContext: Context) : PreferencesHolder {
|
|||
private const val ALLOWED_INT = 2
|
||||
private const val CFR_COUNT_CONDITION_FOCUS_INSTALLED = 1
|
||||
private const val CFR_COUNT_CONDITION_FOCUS_NOT_INSTALLED = 3
|
||||
private const val APP_LAUNCHES_TO_SHOW_DEFAULT_BROWSER_CARD = 3
|
||||
private const val INACTIVE_TAB_MINIMUM_TO_SHOW_AUTO_CLOSE_DIALOG = 20
|
||||
|
||||
const val FOUR_HOURS_MS = 60 * 60 * 4 * 1000L
|
||||
|
@ -311,6 +312,24 @@ class Settings(private val appContext: Context) : PreferencesHolder {
|
|||
default = false
|
||||
)
|
||||
|
||||
/**
|
||||
* Shows if the user has chosen to close the set default browser experiment card
|
||||
* on home screen or has clicked the set as default browser button.
|
||||
*/
|
||||
var userDismissedExperimentCard by booleanPreference(
|
||||
appContext.getPreferenceKey(R.string.pref_key_experiment_card_home),
|
||||
default = false
|
||||
)
|
||||
|
||||
/**
|
||||
* Shows if the set default browser experiment card should be shown on home screen.
|
||||
*/
|
||||
fun shouldShowSetAsDefaultBrowserCard(): Boolean {
|
||||
return isDefaultBrowserMessageLocation(MessageSurfaceId.HOMESCREEN_BANNER) &&
|
||||
!userDismissedExperimentCard &&
|
||||
numberOfAppLaunches > APP_LAUNCHES_TO_SHOW_DEFAULT_BROWSER_CARD
|
||||
}
|
||||
|
||||
private val defaultBrowserFeature: DefaultBrowserMessage by lazy {
|
||||
FxNimbus.features.defaultBrowserMessage.value()
|
||||
}
|
||||
|
@ -1209,7 +1228,7 @@ class Settings(private val appContext: Context) : PreferencesHolder {
|
|||
)
|
||||
|
||||
private val homescreenSections: Map<HomeScreenSection, Boolean> by lazy {
|
||||
FxNimbus.features.homescreen.value(appContext).sectionsEnabled
|
||||
FxNimbus.features.homescreen.value().sectionsEnabled
|
||||
}
|
||||
|
||||
var historyMetadataUIFeature by lazyFeatureFlagPreference(
|
||||
|
|
|
@ -10,18 +10,6 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/home_item_horizontal_margin">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title_text"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="10dp"
|
||||
tools:text="Title"
|
||||
android:textAppearance="@style/Header16TextStyle"
|
||||
app:layout_constraintBottom_toTopOf="@id/description_text"
|
||||
app:layout_constraintEnd_toStartOf="@id/close"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/close"
|
||||
android:layout_width="10dp"
|
||||
|
@ -38,15 +26,16 @@
|
|||
android:id="@+id/description_text"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@string/default_browser_experiment_card_text"
|
||||
android:textAppearance="@style/Body14TextStyle"
|
||||
app:layout_constraintBottom_toTopOf="@id/message_button"
|
||||
app:layout_constraintBottom_toTopOf="@id/set_default_browser"
|
||||
app:layout_constraintEnd_toStartOf="@id/close"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/message_button"
|
||||
android:id="@+id/set_default_browser"
|
||||
style="@style/PositiveButton"
|
||||
android:layout_height="36dp"
|
||||
android:background="@drawable/rounded_button_background"
|
|
@ -276,6 +276,9 @@
|
|||
|
||||
<string name="pref_key_open_next_tab_desktop_mode" translatable="false">pref_key_open_next_tab_desktop_mode</string>
|
||||
|
||||
<!-- Set default browser experiment card-->
|
||||
<string name="pref_key_experiment_card_home" translatable="false">pref_key_experiment_card_home</string>
|
||||
|
||||
<!-- Secret Info Setting Keys -->
|
||||
<string name="pref_key_secret_debug_info" translatable="false">pref_key_secret_debug_info</string>
|
||||
|
||||
|
|
|
@ -16,8 +16,6 @@ import mozilla.components.service.fxa.manager.FxaAccountManager
|
|||
import mozilla.components.service.pocket.PocketRecommendedStory
|
||||
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
|
||||
|
@ -25,7 +23,6 @@ import org.junit.Test
|
|||
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
|
||||
import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
|
||||
import org.mozilla.fenix.components.appstate.AppAction
|
||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.UpdateMessageToShow
|
||||
import org.mozilla.fenix.components.appstate.AppState
|
||||
import org.mozilla.fenix.components.appstate.filterOut
|
||||
import org.mozilla.fenix.ext.components
|
||||
|
@ -74,6 +71,7 @@ class AppStoreTest {
|
|||
mode = currentMode.getCurrentMode(),
|
||||
topSites = emptyList(),
|
||||
showCollectionPlaceholder = true,
|
||||
showSetAsDefaultBrowserCard = true,
|
||||
recentTabs = emptyList()
|
||||
)
|
||||
|
||||
|
@ -94,16 +92,6 @@ class AppStoreTest {
|
|||
assertEquals(Mode.Normal, appStore.state.mode)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN a new value for messageToShow WHEN NimbusMessageChange is called THEN update the current value`() =
|
||||
runBlocking {
|
||||
assertNull(appStore.state.messaging.messageToShow)
|
||||
|
||||
appStore.dispatch(UpdateMessageToShow(mockk())).join()
|
||||
|
||||
assertNotNull(appStore.state.messaging.messageToShow)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Test changing the collections in AppStore`() = runBlocking {
|
||||
assertEquals(0, appStore.state.collections.size)
|
||||
|
|
|
@ -1,109 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.gleanplumb
|
||||
|
||||
import android.net.Uri
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.spyk
|
||||
import io.mockk.verify
|
||||
import mozilla.components.support.test.robolectric.testContext
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mozilla.fenix.BuildConfig
|
||||
import org.mozilla.fenix.HomeActivity
|
||||
import org.mozilla.fenix.components.AppStore
|
||||
import org.mozilla.fenix.components.appstate.AppAction
|
||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.MessageClicked
|
||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.MessageDisplayed
|
||||
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
||||
import org.mozilla.fenix.nimbus.MessageData
|
||||
|
||||
@RunWith(FenixRobolectricTestRunner::class)
|
||||
class DefaultMessageControllerTest {
|
||||
|
||||
private val activity: HomeActivity = mockk(relaxed = true)
|
||||
private val storageNimbus: NimbusMessagingStorage = mockk(relaxed = true)
|
||||
private lateinit var controller: DefaultMessageController
|
||||
private val store: AppStore = mockk(relaxed = true)
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
controller = DefaultMessageController(
|
||||
messagingStorage = storageNimbus,
|
||||
appStore = store,
|
||||
homeActivity = activity
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN calling onMessagePressed THEN update the store and handle the action`() {
|
||||
val customController = spyk(controller)
|
||||
every { customController.handleAction(any()) } returns mockk()
|
||||
|
||||
val message = mockMessage()
|
||||
|
||||
customController.onMessagePressed(message)
|
||||
|
||||
verify { customController.handleAction(any()) }
|
||||
verify { store.dispatch(MessageClicked(message)) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN an URL WHEN calling handleAction THEN process the intent with an open uri`() {
|
||||
val intent = controller.handleAction("http://mozilla.org")
|
||||
|
||||
verify { activity.processIntent(any()) }
|
||||
|
||||
val encodedUrl = Uri.encode("http://mozilla.org")
|
||||
assertEquals(
|
||||
"${BuildConfig.DEEP_LINK_SCHEME}://open?url=$encodedUrl",
|
||||
intent.data.toString()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN an deeplink WHEN calling handleAction THEN process the intent with an deeplink uri`() {
|
||||
val intent = controller.handleAction("://settings_privacy")
|
||||
|
||||
verify { activity.processIntent(any()) }
|
||||
|
||||
assertEquals("${BuildConfig.DEEP_LINK_SCHEME}://settings_privacy", intent.data.toString())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN calling onMessageDismissed THEN report to the messageManager`() {
|
||||
val message = mockMessage()
|
||||
|
||||
controller.onMessageDismissed(message)
|
||||
|
||||
verify { store.dispatch(AppAction.MessagingAction.MessageDismissed(message)) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN calling onMessageDisplayed THEN report to the messageManager`() {
|
||||
val message = mockMessage()
|
||||
|
||||
controller.onMessageDisplayed(message)
|
||||
|
||||
verify { store.dispatch(MessageDisplayed(message)) }
|
||||
}
|
||||
|
||||
private fun mockMessage() = Message(
|
||||
id = "id",
|
||||
data = MessageData(_context = testContext),
|
||||
style = mockk(),
|
||||
action = "action",
|
||||
triggers = emptyList(),
|
||||
metadata = Message.Metadata(
|
||||
id = "id",
|
||||
displayCount = 0,
|
||||
pressed = false,
|
||||
dismissed = false
|
||||
)
|
||||
)
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.gleanplumb
|
||||
|
||||
import io.mockk.every
|
||||
import io.mockk.mockkObject
|
||||
import io.mockk.spyk
|
||||
import io.mockk.unmockkObject
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import mozilla.components.support.test.libstate.ext.waitUntilIdle
|
||||
import mozilla.components.support.test.mock
|
||||
import mozilla.components.support.test.rule.MainCoroutineRule
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.mozilla.fenix.Config
|
||||
import org.mozilla.fenix.FeatureFlags
|
||||
import org.mozilla.fenix.components.AppStore
|
||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction
|
||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.UpdateMessageToShow
|
||||
|
||||
class MessagingFeatureTest {
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@get:Rule
|
||||
val coroutinesTestRule = MainCoroutineRule()
|
||||
|
||||
@Test
|
||||
fun `WHEN start is called THEN evaluate messages`() {
|
||||
val store: AppStore = spyk(AppStore())
|
||||
val binding = MessagingFeature(store)
|
||||
|
||||
mockkObject(FeatureFlags)
|
||||
every { FeatureFlags.messagingFeature } returns true
|
||||
|
||||
binding.start()
|
||||
|
||||
store.dispatch(UpdateMessageToShow(mock()))
|
||||
store.waitUntilIdle()
|
||||
|
||||
verify { store.dispatch(MessagingAction.Evaluate) }
|
||||
|
||||
unmockkObject(Config)
|
||||
}
|
||||
}
|
|
@ -1,487 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.gleanplumb
|
||||
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.spyk
|
||||
import io.mockk.verify
|
||||
import mozilla.components.support.test.mock
|
||||
import mozilla.components.support.test.robolectric.testContext
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
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.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.MessageData
|
||||
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
|
||||
private lateinit var messagingFeature: FeatureHolder<Messaging>
|
||||
private lateinit var messaging: Messaging
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
gleanPlumb = mockk(relaxed = true)
|
||||
metadataStorage = mockk(relaxed = true)
|
||||
|
||||
messagingFeature = createMessagingFeature()
|
||||
|
||||
every { metadataStorage.getMetadata() } returns listOf(Message.Metadata(id = "message-1"))
|
||||
|
||||
storage = NimbusMessagingStorage(
|
||||
testContext,
|
||||
metadataStorage,
|
||||
gleanPlumb,
|
||||
messagingFeature
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN calling getMessages THEN provide a list of available messages`() {
|
||||
val message = storage.getMessages().first()
|
||||
|
||||
assertEquals("message-1", message.id)
|
||||
assertEquals("message-1", message.metadata.id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN calling getMessages THEN provide a list of sorted messages by priority`() {
|
||||
val messages = mapOf(
|
||||
"low-message" to createMessageData(style = "low-priority"),
|
||||
"high-message" to createMessageData(style = "high-priority"),
|
||||
"medium-message" to createMessageData(style = "medium-priority"),
|
||||
)
|
||||
val styles = mapOf(
|
||||
"high-priority" to createStyle(priority = 100),
|
||||
"medium-priority" to createStyle(priority = 50),
|
||||
"low-priority" to createStyle(priority = 1)
|
||||
)
|
||||
val metadataStorage: MessageMetadataStorage = mockk(relaxed = true)
|
||||
val messagingFeature = createMessagingFeature(
|
||||
styles = styles,
|
||||
messages = messages
|
||||
)
|
||||
|
||||
every { metadataStorage.getMetadata() } returns listOf(Message.Metadata(id = "message-1"))
|
||||
|
||||
val storage = NimbusMessagingStorage(
|
||||
testContext,
|
||||
metadataStorage,
|
||||
gleanPlumb,
|
||||
messagingFeature
|
||||
)
|
||||
|
||||
val results = storage.getMessages()
|
||||
|
||||
assertEquals("high-message", results[0].id)
|
||||
assertEquals("medium-message", results[1].id)
|
||||
assertEquals("low-message", results[2].id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN pressed message WHEN calling getMessages THEN filter out the pressed message`() {
|
||||
val metadataList = listOf(
|
||||
Message.Metadata(id = "pressed-message", pressed = true),
|
||||
Message.Metadata(id = "normal-message", pressed = false)
|
||||
)
|
||||
val messages = mapOf(
|
||||
"pressed-message" to createMessageData(style = "high-priority"),
|
||||
"normal-message" to createMessageData(style = "high-priority"),
|
||||
)
|
||||
val styles = mapOf(
|
||||
"high-priority" to createStyle(priority = 100),
|
||||
)
|
||||
val metadataStorage: MessageMetadataStorage = mockk(relaxed = true)
|
||||
val messagingFeature = createMessagingFeature(
|
||||
styles = styles,
|
||||
messages = messages
|
||||
)
|
||||
|
||||
every { metadataStorage.getMetadata() } returns metadataList
|
||||
|
||||
val storage = NimbusMessagingStorage(
|
||||
testContext,
|
||||
metadataStorage,
|
||||
gleanPlumb,
|
||||
messagingFeature
|
||||
)
|
||||
|
||||
val results = storage.getMessages()
|
||||
|
||||
assertEquals(1, results.size)
|
||||
assertEquals("normal-message", results[0].id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN dismissed message WHEN calling getMessages THEN filter out the dismissed message`() {
|
||||
val metadataList = listOf(
|
||||
Message.Metadata(id = "dismissed-message", dismissed = true),
|
||||
Message.Metadata(id = "normal-message", dismissed = false)
|
||||
)
|
||||
val messages = mapOf(
|
||||
"dismissed-message" to createMessageData(style = "high-priority"),
|
||||
"normal-message" to createMessageData(style = "high-priority"),
|
||||
)
|
||||
val styles = mapOf(
|
||||
"high-priority" to createStyle(priority = 100),
|
||||
)
|
||||
val metadataStorage: MessageMetadataStorage = mockk(relaxed = true)
|
||||
val messagingFeature = createMessagingFeature(
|
||||
styles = styles,
|
||||
messages = messages
|
||||
)
|
||||
|
||||
every { metadataStorage.getMetadata() } returns metadataList
|
||||
|
||||
val storage = NimbusMessagingStorage(
|
||||
testContext,
|
||||
metadataStorage,
|
||||
gleanPlumb,
|
||||
messagingFeature
|
||||
)
|
||||
|
||||
val results = storage.getMessages()
|
||||
|
||||
assertEquals(1, results.size)
|
||||
assertEquals("normal-message", results[0].id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN a message that the maxDisplayCount WHEN calling getMessages THEN filter out the message`() {
|
||||
val metadataList = listOf(
|
||||
Message.Metadata(id = "shown-many-times-message", displayCount = 10),
|
||||
Message.Metadata(id = "normal-message", displayCount = 0)
|
||||
)
|
||||
val messages = mapOf(
|
||||
"shown-many-times-message" to createMessageData(
|
||||
style = "high-priority",
|
||||
maxDisplayCount = 2
|
||||
),
|
||||
"normal-message" to createMessageData(style = "high-priority"),
|
||||
)
|
||||
val styles = mapOf(
|
||||
"high-priority" to createStyle(priority = 100),
|
||||
)
|
||||
val metadataStorage: MessageMetadataStorage = mockk(relaxed = true)
|
||||
val messagingFeature = createMessagingFeature(
|
||||
styles = styles,
|
||||
messages = messages
|
||||
)
|
||||
|
||||
every { metadataStorage.getMetadata() } returns metadataList
|
||||
|
||||
val storage = NimbusMessagingStorage(
|
||||
testContext,
|
||||
metadataStorage,
|
||||
gleanPlumb,
|
||||
messagingFeature
|
||||
)
|
||||
|
||||
val results = storage.getMessages()
|
||||
|
||||
assertEquals(1, results.size)
|
||||
assertEquals("normal-message", results[0].id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN a malformed message WHEN calling getMessages THEN provide a list of messages ignoring the malformed one`() {
|
||||
val messages = storage.getMessages()
|
||||
val firstMessage = messages.first()
|
||||
|
||||
assertEquals("message-1", firstMessage.id)
|
||||
assertEquals("message-1", firstMessage.metadata.id)
|
||||
assertTrue(messages.size == 1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN a malformed action WHEN calling sanitizeAction THEN return null`() {
|
||||
val actionsMap = mapOf("action-1" to "action-1-url")
|
||||
|
||||
val notFoundAction = storage.sanitizeAction("no-found-action", actionsMap)
|
||||
val emptyAction = storage.sanitizeAction("", actionsMap)
|
||||
val blankAction = storage.sanitizeAction(" ", actionsMap)
|
||||
|
||||
assertNull(notFoundAction)
|
||||
assertNull(emptyAction)
|
||||
assertNull(blankAction)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN calling updateMetadata THEN delegate to metadataStorage`() {
|
||||
|
||||
storage.updateMetadata(mockk())
|
||||
|
||||
verify { metadataStorage.updateMetadata(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN a valid action WHEN calling sanitizeAction THEN return the action`() {
|
||||
val actionsMap = mapOf("action-1" to "action-1-url")
|
||||
|
||||
val validAction = storage.sanitizeAction("action-1", actionsMap)
|
||||
|
||||
assertEquals("action-1-url", validAction)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN a trigger action WHEN calling sanitizeTriggers THEN return null`() {
|
||||
val triggersMap = mapOf("trigger-1" to "trigger-1-expression")
|
||||
|
||||
val notFoundTrigger = storage.sanitizeTriggers(listOf("no-found-trigger"), triggersMap)
|
||||
val emptyTrigger = storage.sanitizeTriggers(listOf(""), triggersMap)
|
||||
val blankTrigger = storage.sanitizeTriggers(listOf(" "), triggersMap)
|
||||
|
||||
assertNull(notFoundTrigger)
|
||||
assertNull(emptyTrigger)
|
||||
assertNull(blankTrigger)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN a valid trigger WHEN calling sanitizeAction THEN return the trigger`() {
|
||||
val triggersMap = mapOf("trigger-1" to "trigger-1-expression")
|
||||
|
||||
val validTrigger = storage.sanitizeTriggers(listOf("trigger-1"), triggersMap)
|
||||
|
||||
assertEquals(listOf("trigger-1-expression"), validTrigger)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN a null or black expression WHEN calling isMessageUnderExperiment THEN return false`() {
|
||||
val message = Message(
|
||||
"id", mockk(),
|
||||
action = "action",
|
||||
mock(),
|
||||
emptyList(),
|
||||
Message.Metadata("id")
|
||||
)
|
||||
|
||||
val result = storage.isMessageUnderExperiment(message, null)
|
||||
|
||||
assertFalse(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN messages id that ends with - WHEN calling isMessageUnderExperiment THEN return true`() {
|
||||
val message = Message(
|
||||
"end-", mockk(),
|
||||
action = "action",
|
||||
mock(),
|
||||
emptyList(),
|
||||
Message.Metadata("end-")
|
||||
)
|
||||
|
||||
val result = storage.isMessageUnderExperiment(message, "end-")
|
||||
|
||||
assertTrue(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN message under experiment WHEN calling isMessageUnderExperiment THEN return true`() {
|
||||
val message = Message(
|
||||
"same-id", mockk(),
|
||||
action = "action",
|
||||
mock(),
|
||||
emptyList(),
|
||||
Message.Metadata("same-id")
|
||||
)
|
||||
|
||||
val result = storage.isMessageUnderExperiment(message, "same-id")
|
||||
|
||||
assertTrue(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN an eligible message WHEN calling isMessageEligible THEN return true`() {
|
||||
val helper: GleanPlumbMessageHelper = mockk(relaxed = true)
|
||||
val message = Message(
|
||||
"same-id", mockk(),
|
||||
action = "action",
|
||||
mock(),
|
||||
listOf("trigger"),
|
||||
Message.Metadata("same-id")
|
||||
)
|
||||
|
||||
every { helper.evalJexl(any()) } returns true
|
||||
|
||||
val result = storage.isMessageEligible(message, helper)
|
||||
|
||||
assertTrue(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN a malformed trigger WHEN calling isMessageEligible THEN return false`() {
|
||||
val helper: GleanPlumbMessageHelper = mockk(relaxed = true)
|
||||
val message = Message(
|
||||
"same-id", mockk(),
|
||||
action = "action",
|
||||
mock(),
|
||||
listOf("trigger"),
|
||||
Message.Metadata("same-id")
|
||||
)
|
||||
|
||||
every { helper.evalJexl(any()) } throws NimbusException.EvaluationException("")
|
||||
|
||||
val result = storage.isMessageEligible(message, helper)
|
||||
|
||||
assertFalse(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN none available messages are eligible WHEN calling getNextMessage THEN return null`() {
|
||||
val spiedStorage = spyk(storage)
|
||||
val message = Message(
|
||||
"same-id", mockk(),
|
||||
action = "action",
|
||||
mock(),
|
||||
listOf("trigger"),
|
||||
Message.Metadata("same-id")
|
||||
)
|
||||
|
||||
every { spiedStorage.isMessageEligible(any(), any()) } returns false
|
||||
|
||||
val result = spiedStorage.getNextMessage(listOf(message))
|
||||
|
||||
assertNull(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN an eligible message WHEN calling getNextMessage THEN return the message`() {
|
||||
val spiedStorage = spyk(storage)
|
||||
val message = Message(
|
||||
"same-id", mockk(),
|
||||
action = "action",
|
||||
mock(),
|
||||
listOf("trigger"),
|
||||
Message.Metadata("same-id")
|
||||
)
|
||||
|
||||
every { spiedStorage.isMessageEligible(any(), any()) } returns true
|
||||
every { spiedStorage.isMessageUnderExperiment(any(), any()) } returns false
|
||||
|
||||
val result = spiedStorage.getNextMessage(listOf(message))
|
||||
|
||||
assertEquals(message.id, result!!.id)
|
||||
}
|
||||
|
||||
@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 message = Message(
|
||||
"same-id",
|
||||
messageData,
|
||||
action = "action",
|
||||
mockk(relaxed = true),
|
||||
listOf("trigger"),
|
||||
Message.Metadata("same-id")
|
||||
)
|
||||
|
||||
every { spiedStorage.isMessageEligible(any(), any()) } returns true
|
||||
every { spiedStorage.isMessageUnderExperiment(any(), any()) } returns true
|
||||
|
||||
val result = spiedStorage.getNextMessage(listOf(message))
|
||||
|
||||
verify { messagingFeature.recordExposure() }
|
||||
assertEquals(message.id, result!!.id)
|
||||
}
|
||||
|
||||
@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)
|
||||
|
||||
every { messageData.isControl } returns false
|
||||
every { controlMessageData.isControl } returns true
|
||||
|
||||
val message = Message(
|
||||
"id",
|
||||
messageData,
|
||||
action = "action",
|
||||
mockk(relaxed = true),
|
||||
listOf("trigger"),
|
||||
Message.Metadata("same-id")
|
||||
)
|
||||
|
||||
val controlMessage = Message(
|
||||
"control-id",
|
||||
controlMessageData,
|
||||
action = "action",
|
||||
mockk(relaxed = true),
|
||||
listOf("trigger"),
|
||||
Message.Metadata("same-id")
|
||||
)
|
||||
|
||||
every { spiedStorage.isMessageEligible(any(), any()) } returns true
|
||||
every { spiedStorage.isMessageUnderExperiment(any(), any()) } returns true
|
||||
|
||||
val result = spiedStorage.getNextMessage(listOf(controlMessage, message))
|
||||
|
||||
verify { messagingFeature.recordExposure() }
|
||||
assertEquals(message.id, result!!.id)
|
||||
}
|
||||
|
||||
private fun createMessageData(
|
||||
action: String = "action-1",
|
||||
style: String = "style-1",
|
||||
triggers: List<String> = listOf("trigger-1"),
|
||||
maxDisplayCount: Int = 5
|
||||
): MessageData {
|
||||
val messageData1: MessageData = mockk(relaxed = true)
|
||||
every { messageData1.action } returns action
|
||||
every { messageData1.style } returns style
|
||||
every { messageData1.trigger } returns triggers
|
||||
every { messageData1.maxDisplayCount } returns maxDisplayCount
|
||||
return messageData1
|
||||
}
|
||||
|
||||
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)
|
||||
),
|
||||
): 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
|
||||
|
||||
every { messagingFeature.value() } returns messaging
|
||||
return messagingFeature
|
||||
}
|
||||
|
||||
private fun createStyle(priority: Int = 1): StyleData {
|
||||
val style1: StyleData = mockk(relaxed = true)
|
||||
every { style1.priority } returns priority
|
||||
return style1
|
||||
}
|
||||
}
|
|
@ -1,327 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.gleanplumb.state
|
||||
|
||||
import io.mockk.Runs
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.spyk
|
||||
import io.mockk.verify
|
||||
import mozilla.components.lib.state.MiddlewareContext
|
||||
import mozilla.components.service.glean.testing.GleanTestRule
|
||||
import mozilla.components.support.test.robolectric.testContext
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mozilla.fenix.components.AppStore
|
||||
import org.mozilla.fenix.components.appstate.AppAction
|
||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.ConsumeMessageToShow
|
||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.Evaluate
|
||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.MessageClicked
|
||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.MessageDismissed
|
||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.MessageDisplayed
|
||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.Restore
|
||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.UpdateMessageToShow
|
||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.UpdateMessages
|
||||
import org.mozilla.fenix.components.appstate.AppState
|
||||
import org.mozilla.fenix.gleanplumb.Message
|
||||
import org.mozilla.fenix.gleanplumb.MessagingState
|
||||
import org.mozilla.fenix.gleanplumb.NimbusMessagingStorage
|
||||
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
||||
import org.mozilla.fenix.nimbus.MessageData
|
||||
|
||||
@RunWith(FenixRobolectricTestRunner::class)
|
||||
class MessagingMiddlewareTest {
|
||||
|
||||
private lateinit var store: AppStore
|
||||
private lateinit var middleware: MessagingMiddleware
|
||||
private lateinit var messagingStorage: NimbusMessagingStorage
|
||||
private lateinit var middlewareContext: MiddlewareContext<AppState, AppAction>
|
||||
|
||||
@get:Rule
|
||||
val gleanTestRule = GleanTestRule(testContext)
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
messagingStorage = mockk(relaxed = true)
|
||||
middlewareContext = mockk(relaxed = true)
|
||||
middleware = MessagingMiddleware(
|
||||
messagingStorage
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN Restore THEN get messages from the storage and UpdateMessages`() {
|
||||
val messages: List<Message> = emptyList()
|
||||
|
||||
every { messagingStorage.getMessages() } returns messages
|
||||
|
||||
middleware.invoke(middlewareContext, {}, Restore)
|
||||
|
||||
verify { middlewareContext.dispatch(UpdateMessages(messages)) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN Restore THEN getNextMessage from the storage and UpdateMessageToShow`() {
|
||||
val message: Message = mockk(relaxed = true)
|
||||
val appState: AppState = mockk(relaxed = true)
|
||||
val messagingState: MessagingState = mockk(relaxed = true)
|
||||
|
||||
every { messagingState.messages } returns emptyList()
|
||||
every { appState.messaging } returns messagingState
|
||||
every { middlewareContext.state } returns appState
|
||||
every { messagingStorage.getNextMessage(any()) } returns message
|
||||
|
||||
middleware.invoke(middlewareContext, {}, Evaluate)
|
||||
|
||||
verify { middlewareContext.dispatch(UpdateMessageToShow(message)) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN MessageClicked THEN update storage`() {
|
||||
val message = Message(
|
||||
"control-id",
|
||||
mockk(relaxed = true),
|
||||
action = "action",
|
||||
mockk(relaxed = true),
|
||||
listOf("trigger"),
|
||||
Message.Metadata("same-id")
|
||||
)
|
||||
val appState: AppState = mockk(relaxed = true)
|
||||
val messagingState: MessagingState = mockk(relaxed = true)
|
||||
|
||||
every { messagingState.messages } returns emptyList()
|
||||
every { appState.messaging } returns messagingState
|
||||
every { middlewareContext.state } returns appState
|
||||
|
||||
middleware.invoke(middlewareContext, {}, MessageClicked(message))
|
||||
|
||||
verify { messagingStorage.updateMetadata(message.metadata.copy(pressed = true)) }
|
||||
verify { middlewareContext.dispatch(UpdateMessages(emptyList())) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN MessageDismissed THEN update storage`() {
|
||||
val message = Message(
|
||||
"control-id",
|
||||
mockk(relaxed = true),
|
||||
action = "action",
|
||||
mockk(relaxed = true),
|
||||
listOf("trigger"),
|
||||
Message.Metadata("same-id")
|
||||
)
|
||||
val appState: AppState = mockk(relaxed = true)
|
||||
val messagingState: MessagingState = mockk(relaxed = true)
|
||||
|
||||
every { messagingState.messages } returns emptyList()
|
||||
every { appState.messaging } returns messagingState
|
||||
every { middlewareContext.state } returns appState
|
||||
|
||||
middleware.invoke(
|
||||
middlewareContext, {},
|
||||
MessageDismissed(message)
|
||||
)
|
||||
|
||||
verify { messagingStorage.updateMetadata(message.metadata.copy(dismissed = true)) }
|
||||
verify { middlewareContext.dispatch(UpdateMessages(emptyList())) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN MessageDisplayed THEN update storage`() {
|
||||
val message = Message(
|
||||
"control-id",
|
||||
mockk(relaxed = true),
|
||||
action = "action",
|
||||
mockk(relaxed = true),
|
||||
listOf("trigger"),
|
||||
Message.Metadata("same-id")
|
||||
)
|
||||
val appState: AppState = mockk(relaxed = true)
|
||||
val messagingState: MessagingState = mockk(relaxed = true)
|
||||
|
||||
every { messagingState.messages } returns emptyList()
|
||||
every { appState.messaging } returns messagingState
|
||||
every { middlewareContext.state } returns appState
|
||||
|
||||
middleware.invoke(
|
||||
middlewareContext, {},
|
||||
MessageDisplayed(message)
|
||||
)
|
||||
|
||||
verify { messagingStorage.updateMetadata(message.metadata.copy(displayCount = 1)) }
|
||||
verify { middlewareContext.dispatch(UpdateMessages(emptyList())) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN onMessageDismissed THEN updateMetadata,removeMessage , UpdateMessages and removeMessageToShowIfNeeded`() {
|
||||
val message = Message(
|
||||
"control-id",
|
||||
mockk(relaxed = true),
|
||||
action = "action",
|
||||
mockk(relaxed = true),
|
||||
listOf("trigger"),
|
||||
Message.Metadata("same-id")
|
||||
)
|
||||
|
||||
val spiedMiddleware = spyk(middleware)
|
||||
|
||||
every { spiedMiddleware.removeMessage(middlewareContext, message) } returns emptyList()
|
||||
every { spiedMiddleware.consumeMessageToShowIfNeeded(middlewareContext, message) } just Runs
|
||||
|
||||
spiedMiddleware.onMessageDismissed(middlewareContext, message)
|
||||
|
||||
verify { messagingStorage.updateMetadata(message.metadata.copy(dismissed = true)) }
|
||||
verify { middlewareContext.dispatch(UpdateMessages(emptyList())) }
|
||||
verify { spiedMiddleware.removeMessage(middlewareContext, message) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN removeMessage THEN remove the message`() {
|
||||
val message = Message(
|
||||
"control-id",
|
||||
mockk(relaxed = true),
|
||||
action = "action",
|
||||
mockk(relaxed = true),
|
||||
listOf("trigger"),
|
||||
Message.Metadata("same-id")
|
||||
)
|
||||
val messages = listOf(message)
|
||||
val appState: AppState = mockk(relaxed = true)
|
||||
val messagingState: MessagingState = mockk(relaxed = true)
|
||||
|
||||
every { messagingState.messages } returns messages
|
||||
every { appState.messaging } returns messagingState
|
||||
every { middlewareContext.state } returns appState
|
||||
|
||||
val results = middleware.removeMessage(middlewareContext, message)
|
||||
|
||||
assertTrue(results.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN consumeMessageToShowIfNeeded THEN consume the message`() {
|
||||
val message = Message(
|
||||
"control-id",
|
||||
mockk(relaxed = true),
|
||||
action = "action",
|
||||
mockk(relaxed = true),
|
||||
listOf("trigger"),
|
||||
Message.Metadata("same-id")
|
||||
)
|
||||
val appState: AppState = mockk(relaxed = true)
|
||||
val messagingState: MessagingState = mockk(relaxed = true)
|
||||
|
||||
every { messagingState.messageToShow } returns message
|
||||
every { appState.messaging } returns messagingState
|
||||
every { middlewareContext.state } returns appState
|
||||
|
||||
middleware.consumeMessageToShowIfNeeded(middlewareContext, message)
|
||||
|
||||
verify { middlewareContext.dispatch(ConsumeMessageToShow) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN updateMessage THEN update available messages`() {
|
||||
val oldMessage = Message(
|
||||
"oldMessage",
|
||||
mockk(relaxed = true),
|
||||
action = "action",
|
||||
mockk(relaxed = true),
|
||||
listOf("trigger"),
|
||||
Message.Metadata("same-id", pressed = false)
|
||||
)
|
||||
|
||||
val updatedMessage = Message(
|
||||
"oldMessage",
|
||||
mockk(relaxed = true),
|
||||
action = "action",
|
||||
mockk(relaxed = true),
|
||||
listOf("trigger"),
|
||||
Message.Metadata("same-id", pressed = true)
|
||||
)
|
||||
|
||||
val spiedMiddleware = spyk(middleware)
|
||||
|
||||
val appState: AppState = mockk(relaxed = true)
|
||||
val messagingState: MessagingState = mockk(relaxed = true)
|
||||
|
||||
every { messagingState.messageToShow } returns oldMessage
|
||||
every { appState.messaging } returns messagingState
|
||||
every { middlewareContext.state } returns appState
|
||||
every { spiedMiddleware.removeMessage(middlewareContext, oldMessage) } returns emptyList()
|
||||
|
||||
val results = spiedMiddleware.updateMessage(middlewareContext, oldMessage, updatedMessage)
|
||||
|
||||
verify { middlewareContext.dispatch(UpdateMessageToShow(updatedMessage)) }
|
||||
verify { spiedMiddleware.removeMessage(middlewareContext, oldMessage) }
|
||||
|
||||
assertTrue(results.size == 1)
|
||||
assertTrue(results.first().metadata.pressed)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN a message with that not surpassed the maxDisplayCount WHEN onMessagedDisplayed THEN update the available messages and the updateMetadata`() {
|
||||
val oldMessageData: MessageData = mockk(relaxed = true)
|
||||
val oldMessage = Message(
|
||||
"oldMessage",
|
||||
oldMessageData,
|
||||
action = "action",
|
||||
mockk(relaxed = true),
|
||||
listOf("trigger"),
|
||||
Message.Metadata("same-id", displayCount = 0)
|
||||
)
|
||||
val updatedMessage = oldMessage.copy(metadata = oldMessage.metadata.copy(displayCount = 1))
|
||||
val spiedMiddleware = spyk(middleware)
|
||||
|
||||
every { oldMessageData.maxDisplayCount } returns 2
|
||||
every {
|
||||
spiedMiddleware.updateMessage(
|
||||
middlewareContext,
|
||||
oldMessage,
|
||||
updatedMessage
|
||||
)
|
||||
} returns emptyList()
|
||||
|
||||
spiedMiddleware.onMessagedDisplayed(oldMessage, middlewareContext)
|
||||
|
||||
verify { spiedMiddleware.updateMessage(middlewareContext, oldMessage, updatedMessage) }
|
||||
verify { middlewareContext.dispatch(UpdateMessages(emptyList())) }
|
||||
verify { messagingStorage.updateMetadata(updatedMessage.metadata) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN a message with that surpassed the maxDisplayCount WHEN onMessagedDisplayed THEN remove the message and consume it`() {
|
||||
val oldMessageData: MessageData = mockk(relaxed = true)
|
||||
val oldMessage = Message(
|
||||
"oldMessage",
|
||||
oldMessageData,
|
||||
action = "action",
|
||||
mockk(relaxed = true),
|
||||
listOf("trigger"),
|
||||
Message.Metadata("same-id", displayCount = 0)
|
||||
)
|
||||
val updatedMessage = oldMessage.copy(metadata = oldMessage.metadata.copy(displayCount = 1))
|
||||
val spiedMiddleware = spyk(middleware)
|
||||
|
||||
every { oldMessageData.maxDisplayCount } returns 1
|
||||
every {
|
||||
spiedMiddleware.consumeMessageToShowIfNeeded(
|
||||
middlewareContext,
|
||||
oldMessage
|
||||
)
|
||||
} just Runs
|
||||
every { spiedMiddleware.removeMessage(middlewareContext, oldMessage) } returns emptyList()
|
||||
|
||||
spiedMiddleware.onMessagedDisplayed(oldMessage, middlewareContext)
|
||||
|
||||
verify { spiedMiddleware.consumeMessageToShowIfNeeded(middlewareContext, oldMessage) }
|
||||
verify { spiedMiddleware.removeMessage(middlewareContext, oldMessage) }
|
||||
verify { middlewareContext.dispatch(UpdateMessages(emptyList())) }
|
||||
verify { messagingStorage.updateMetadata(updatedMessage.metadata) }
|
||||
}
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.gleanplumb.state
|
||||
|
||||
import io.mockk.mockk
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.ConsumeMessageToShow
|
||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.UpdateMessageToShow
|
||||
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.MessagingState
|
||||
|
||||
class MessagingReducerTest {
|
||||
|
||||
@Test
|
||||
fun `GIVEN a new value for messageToShow WHEN UpdateMessageToShow is called THEN update the current value`() {
|
||||
val initialState = AppState(
|
||||
messaging = MessagingState(
|
||||
messageToShow = null
|
||||
)
|
||||
)
|
||||
|
||||
var updatedState = MessagingReducer.reduce(
|
||||
initialState,
|
||||
UpdateMessageToShow(mockk())
|
||||
)
|
||||
|
||||
assertNotNull(updatedState.messaging.messageToShow)
|
||||
|
||||
updatedState = AppStoreReducer.reduce(updatedState, ConsumeMessageToShow)
|
||||
|
||||
assertNull(updatedState.messaging.messageToShow)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN a new value for messages WHEN UpdateMessages is called THEN update the current value`() {
|
||||
val initialState = AppState(
|
||||
messaging = MessagingState(
|
||||
messages = emptyList()
|
||||
)
|
||||
)
|
||||
|
||||
var updatedState = MessagingReducer.reduce(
|
||||
initialState,
|
||||
UpdateMessages(listOf(mockk()))
|
||||
)
|
||||
|
||||
assertFalse(updatedState.messaging.messages.isEmpty())
|
||||
|
||||
updatedState = AppStoreReducer.reduce(updatedState, UpdateMessages(emptyList()))
|
||||
|
||||
assertTrue(updatedState.messaging.messages.isEmpty())
|
||||
}
|
||||
}
|
|
@ -63,8 +63,6 @@ import org.mozilla.fenix.components.metrics.Event.PerformedSearch.EngineSource
|
|||
import org.mozilla.fenix.components.metrics.MetricController
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.ext.settings
|
||||
import org.mozilla.fenix.gleanplumb.Message
|
||||
import org.mozilla.fenix.gleanplumb.MessageController
|
||||
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
||||
import org.mozilla.fenix.home.recentbookmarks.RecentBookmark
|
||||
import org.mozilla.fenix.home.recenttabs.RecentTab
|
||||
|
@ -86,7 +84,6 @@ class DefaultSessionControlControllerTest {
|
|||
private val appStore: AppStore = mockk(relaxed = true)
|
||||
private val navController: NavController = mockk(relaxed = true)
|
||||
private val metrics: MetricController = mockk(relaxed = true)
|
||||
private val messageController: MessageController = mockk(relaxed = true)
|
||||
private val engine: Engine = mockk(relaxed = true)
|
||||
private val tabCollectionStorage: TabCollectionStorage = mockk(relaxed = true)
|
||||
private val tabsUseCases: TabsUseCases = mockk(relaxed = true)
|
||||
|
@ -140,6 +137,7 @@ class DefaultSessionControlControllerTest {
|
|||
mode = Mode.Normal,
|
||||
topSites = emptyList(),
|
||||
showCollectionPlaceholder = true,
|
||||
showSetAsDefaultBrowserCard = true,
|
||||
recentTabs = emptyList(),
|
||||
recentBookmarks = emptyList()
|
||||
)
|
||||
|
@ -1122,26 +1120,6 @@ class DefaultSessionControlControllerTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN handleMessageClicked,handleMessageClosed and handleMessageDisplayed are called THEN delegate to messageController`() {
|
||||
val controller = createController()
|
||||
val message = mockk<Message>()
|
||||
|
||||
controller.handleMessageClicked(message)
|
||||
controller.handleMessageClosed(message)
|
||||
controller.handleMessageDisplayed(message)
|
||||
|
||||
verify {
|
||||
messageController.onMessagePressed(message)
|
||||
}
|
||||
verify {
|
||||
messageController.onMessageDismissed(message)
|
||||
}
|
||||
verify {
|
||||
messageController.onMessageDisplayed(message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createController(
|
||||
hideOnboarding: () -> Unit = { },
|
||||
registerCollectionStorageObserver: () -> Unit = { },
|
||||
|
@ -1154,7 +1132,6 @@ class DefaultSessionControlControllerTest {
|
|||
engine = engine,
|
||||
metrics = metrics,
|
||||
store = store,
|
||||
messageController = messageController,
|
||||
tabCollectionStorage = tabCollectionStorage,
|
||||
addTabUseCase = tabsUseCases.addTab,
|
||||
restoreUseCase = mockk(relaxed = true),
|
||||
|
|
|
@ -19,7 +19,6 @@ import org.junit.Test
|
|||
import org.junit.runner.RunWith
|
||||
import org.mozilla.fenix.components.appstate.AppState
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.gleanplumb.Message
|
||||
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
||||
import org.mozilla.fenix.home.recentbookmarks.RecentBookmark
|
||||
import org.mozilla.fenix.home.recenttabs.RecentTab
|
||||
|
@ -156,7 +155,7 @@ class SessionControlViewTest {
|
|||
expandedCollections,
|
||||
recentBookmarks,
|
||||
false,
|
||||
null,
|
||||
false,
|
||||
recentTabs,
|
||||
historyMetadata,
|
||||
pocketArticles
|
||||
|
@ -168,40 +167,6 @@ class SessionControlViewTest {
|
|||
assertTrue(results[3] is AdapterItem.CustomizeHomeButton)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN a nimbusMessageCard WHEN normalModeAdapterItems is called THEN add a NimbusMessageCard`() {
|
||||
val settings: Settings = mockk()
|
||||
val topSites = emptyList<TopSite>()
|
||||
val collections = emptyList<TabCollection>()
|
||||
val expandedCollections = emptySet<Long>()
|
||||
val recentBookmarks = listOf(RecentBookmark())
|
||||
val recentTabs = emptyList<RecentTab.Tab>()
|
||||
val historyMetadata = emptyList<RecentHistoryGroup>()
|
||||
val pocketArticles = emptyList<PocketRecommendedStory>()
|
||||
val nimbusMessageCard: Message = mockk()
|
||||
|
||||
every { settings.showTopSitesFeature } returns true
|
||||
every { settings.showRecentTabsFeature } returns true
|
||||
every { settings.showRecentBookmarksFeature } returns true
|
||||
every { settings.historyMetadataUIFeature } returns true
|
||||
every { settings.showPocketRecommendationsFeature } returns true
|
||||
|
||||
val results = normalModeAdapterItems(
|
||||
settings,
|
||||
topSites,
|
||||
collections,
|
||||
expandedCollections,
|
||||
recentBookmarks,
|
||||
false,
|
||||
nimbusMessageCard,
|
||||
recentTabs,
|
||||
historyMetadata,
|
||||
pocketArticles
|
||||
)
|
||||
|
||||
assertTrue(results.contains(AdapterItem.NimbusMessageCard(nimbusMessageCard)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN recent tabs WHEN normalModeAdapterItems is called THEN add a customize home button`() {
|
||||
val settings: Settings = mockk()
|
||||
|
@ -226,7 +191,7 @@ class SessionControlViewTest {
|
|||
expandedCollections,
|
||||
recentBookmarks,
|
||||
false,
|
||||
null,
|
||||
false,
|
||||
recentTabs,
|
||||
historyMetadata,
|
||||
pocketArticles
|
||||
|
@ -262,7 +227,7 @@ class SessionControlViewTest {
|
|||
expandedCollections,
|
||||
recentBookmarks,
|
||||
false,
|
||||
null,
|
||||
false,
|
||||
recentTabs,
|
||||
historyMetadata,
|
||||
pocketArticles
|
||||
|
@ -298,7 +263,7 @@ class SessionControlViewTest {
|
|||
expandedCollections,
|
||||
recentBookmarks,
|
||||
false,
|
||||
null,
|
||||
false,
|
||||
recentTabs,
|
||||
historyMetadata,
|
||||
pocketArticles
|
||||
|
@ -335,7 +300,7 @@ class SessionControlViewTest {
|
|||
expandedCollections,
|
||||
recentBookmarks,
|
||||
false,
|
||||
null,
|
||||
false,
|
||||
recentTabs,
|
||||
historyMetadata,
|
||||
pocketArticles
|
||||
|
@ -371,7 +336,7 @@ class SessionControlViewTest {
|
|||
expandedCollections,
|
||||
recentBookmarks,
|
||||
false,
|
||||
null,
|
||||
false,
|
||||
recentTabs,
|
||||
historyMetadata,
|
||||
pocketArticles
|
||||
|
|
268
nimbus.fml.yaml
268
nimbus.fml.yaml
|
@ -70,253 +70,31 @@ features:
|
|||
description: Where is the message to be put.
|
||||
type: Option<MessageSurfaceId>
|
||||
default: null
|
||||
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
|
||||
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"
|
||||
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
|
||||
PERSISTENT:
|
||||
priority: 50
|
||||
max-display-count: 20
|
||||
WARNING:
|
||||
priority: 60
|
||||
max-display-count: 10
|
||||
URGENT:
|
||||
priority: 100
|
||||
max-display-count: 10
|
||||
|
||||
- channel: developer
|
||||
value:
|
||||
styles:
|
||||
DEFAULT:
|
||||
priority: 50
|
||||
max-display-count: 100
|
||||
EXPIRES_QUICKLY:
|
||||
priority: 100
|
||||
max-display-count: 1
|
||||
- channel: developer
|
||||
value: {
|
||||
"messages": {
|
||||
"my-viewpoint-survey": {
|
||||
"title": "Message tile",
|
||||
"text": "Love Firefox? Fill in our survey!",
|
||||
"action": "https://surveyprovider.com/survey-id/{uuid}",
|
||||
"trigger": [ "ALWAYS" ],
|
||||
"style": "DEFAULT",
|
||||
"button-label": "Go to the survey"
|
||||
}
|
||||
}
|
||||
}
|
||||
- channel: developer
|
||||
value: {
|
||||
"messages": {
|
||||
"private-tabs-auto-close": {
|
||||
"action": "OPEN_SETTINGS",
|
||||
"text": "Sharing your phone? Autoclosing private tabs is for you!",
|
||||
"trigger": [
|
||||
"USER_RECENTLY_INSTALLED"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
"message-under-experiment": "private-tabs-auto-close"
|
||||
}
|
||||
- channel: developer
|
||||
value: {
|
||||
"triggers": {
|
||||
"USER_IE_COUNTRY": "'IE' in locale"
|
||||
},
|
||||
|
||||
"styles": {
|
||||
"irish-green": {
|
||||
"priority": 50
|
||||
}
|
||||
},
|
||||
|
||||
"messages": {
|
||||
"eu-tracking-protection-for-ireland": {
|
||||
"action": "OPEN_SETTINGS",
|
||||
"text": "GDPR has you covered. Firefox has GDPR covered",
|
||||
"style": "irish-green",
|
||||
"trigger": [
|
||||
"NEW_USER",
|
||||
"USER_IE_COUNTRY"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
"message-under-experiment": "eu-tracking-protection-for-"
|
||||
}
|
||||
|
||||
|
||||
|
||||
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
|
||||
|
||||
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:
|
||||
top-sites:
|
||||
description: The frecency and pinned sites.
|
||||
recently-saved:
|
||||
description: The sites the user has bookmarked recently.
|
||||
jump-back-in:
|
||||
description: The tabs the user was looking immediately before being interrupted.
|
||||
recent-explorations:
|
||||
description: The tab groups
|
||||
pocket:
|
||||
description: The pocket section. This should only be available in the US.
|
||||
contile-top-sites:
|
||||
description: The sponsored shortcuts in the homescreen.
|
||||
description: The identifiers for the sections of the homescreen.
|
||||
variants:
|
||||
top-sites:
|
||||
description: The frecency and pinned sites.
|
||||
recently-saved:
|
||||
description: The sites the user has bookmarked recently.
|
||||
jump-back-in:
|
||||
description: The tabs the user was looking immediately before being interrupted.
|
||||
recent-explorations:
|
||||
description: The tab groups
|
||||
pocket:
|
||||
description: The pocket section. This should only be available in the US.
|
||||
contile-top-sites:
|
||||
description: The sponsored shortcuts in the homescreen.
|
||||
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.
|
||||
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.
|
||||
|
||||
|
|
Loading…
Reference in New Issue