Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Blaze: Ad target languages #10641

Merged
merged 10 commits into from
Jan 30, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.os.Parcelable
import com.woocommerce.android.tools.SelectedSite
import com.woocommerce.android.ui.products.ProductDetailRepository
import com.woocommerce.android.util.TimezoneProvider
import kotlinx.coroutines.flow.map
import kotlinx.parcelize.Parcelize
import org.wordpress.android.fluxc.persistence.blaze.BlazeCampaignsDao.BlazeAdSuggestionEntity
import org.wordpress.android.fluxc.store.blaze.BlazeCampaignsStore
Expand All @@ -26,6 +27,11 @@ class BlazeRepository @Inject constructor(
const val ONE_DAY_IN_MILLIS = 1000 * 60 * 60 * 24
}

fun observeLanguages() = blazeCampaignsStore.observeBlazeTargetingLanguages()
.map { it.map { language -> Language(language.id, language.name) } }

suspend fun fetchLanguages() = blazeCampaignsStore.fetchBlazeTargetingLanguages()

suspend fun getMostRecentCampaign() = blazeCampaignsStore.getMostRecentBlazeCampaign(selectedSite.get())

suspend fun getAdSuggestions(productId: Long): List<AiSuggestionForAd>? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import com.woocommerce.android.ui.blaze.creation.ad.BlazeCampaignCreationEditAdF
import com.woocommerce.android.ui.blaze.creation.ad.BlazeCampaignCreationEditAdViewModel.EditAdResult
import com.woocommerce.android.ui.blaze.creation.preview.BlazeCampaignCreationPreviewViewModel.NavigateToBudgetScreen
import com.woocommerce.android.ui.blaze.creation.preview.BlazeCampaignCreationPreviewViewModel.NavigateToEditAdScreen
import com.woocommerce.android.ui.blaze.creation.preview.BlazeCampaignCreationPreviewViewModel.NavigateToTargetSelectionScreen
import com.woocommerce.android.ui.blaze.creation.targets.BlazeCampaignTargetSelectionFragment
import com.woocommerce.android.ui.blaze.creation.targets.BlazeCampaignTargetSelectionViewModel.TargetSelectionResult
import com.woocommerce.android.ui.compose.composeView
import com.woocommerce.android.ui.main.AppBarStatus
import com.woocommerce.android.viewmodel.MultiLiveEvent.Event.Exit
Expand Down Expand Up @@ -54,6 +57,13 @@ class BlazeCampaignCreationPreviewFragment : BaseFragment() {
event.campaignImageUrl
)
)
is NavigateToTargetSelectionScreen -> findNavController().navigateSafely(
BlazeCampaignCreationPreviewFragmentDirections
.actionBlazeCampaignCreationPreviewFragmentToBlazeCampaignTargetSelectionFragment(
event.targetType,
event.selectedIds.toTypedArray()
)
)
}
}
}
Expand All @@ -62,5 +72,8 @@ class BlazeCampaignCreationPreviewFragment : BaseFragment() {
handleResult<EditAdResult>(BlazeCampaignCreationEditAdFragment.EDIT_AD_RESULT) {
viewModel.onAdUpdated(it.tagline, it.description, it.campaignImageUrl)
}
handleResult<TargetSelectionResult>(BlazeCampaignTargetSelectionFragment.BLAZE_TARGET_SELECTION_RESULT) {
viewModel.onTargetSelectionUpdated(it.targetType, it.selectedIds)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import com.woocommerce.android.R.string
import com.woocommerce.android.extensions.combine
import com.woocommerce.android.extensions.formatToMMMdd
import com.woocommerce.android.ui.blaze.BlazeRepository
import com.woocommerce.android.ui.blaze.BlazeRepository.Budget
Expand All @@ -16,13 +15,16 @@ import com.woocommerce.android.ui.blaze.BlazeRepository.Language
import com.woocommerce.android.ui.blaze.BlazeRepository.Location
import com.woocommerce.android.ui.blaze.creation.preview.BlazeCampaignCreationPreviewViewModel.AdDetailsUi.AdDetails
import com.woocommerce.android.ui.blaze.creation.preview.BlazeCampaignCreationPreviewViewModel.AdDetailsUi.Loading
import com.woocommerce.android.ui.blaze.creation.targets.BlazeTargetType
import com.woocommerce.android.ui.blaze.creation.targets.BlazeTargetType.LANGUAGE
import com.woocommerce.android.util.CurrencyFormatter
import com.woocommerce.android.viewmodel.MultiLiveEvent
import com.woocommerce.android.viewmodel.ResourceProvider
import com.woocommerce.android.viewmodel.ScopedViewModel
import com.woocommerce.android.viewmodel.getStateFlow
import com.woocommerce.android.viewmodel.navArgs
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
Expand All @@ -41,27 +43,29 @@ class BlazeCampaignCreationPreviewViewModel @Inject constructor(

private val adDetails = savedStateHandle.getStateFlow<AdDetailsUi>(viewModelScope, Loading)
private val budget = savedStateHandle.getStateFlow(viewModelScope, getDefaultBudget())
private val selectedLanguages = savedStateHandle.getStateFlow<List<Language>>(viewModelScope, emptyList())
private val selectedDevices = savedStateHandle.getStateFlow<List<Device>>(viewModelScope, emptyList())
private val selectedInterests = savedStateHandle.getStateFlow<List<Interest>>(viewModelScope, emptyList())
private val selectedLocations = savedStateHandle.getStateFlow<List<Location>>(viewModelScope, emptyList())
private val languages = blazeRepository.observeLanguages()
private val selectedLanguages = savedStateHandle.getStateFlow<List<String>>(viewModelScope, emptyList())

val viewState = combine(
adDetails,
budget,
selectedLanguages,
selectedDevices,
selectedInterests,
selectedLocations
) { adDetails, budget, languages, devices, interests, locations ->
languages,
selectedLanguages
Comment on lines +52 to +53
Copy link
Contributor

Choose a reason for hiding this comment

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

Minor np, Wdyt about having a single flow for languages and add a new boolean field to the Language model to save the isSelected state. I feel with this approach of 2 different flows one for all the values and one for the selected ones we are going to have an explosion of flows once we add the rest of the campaign details.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hm.. 🤔 I'm not convinced this would be cleaner, in the end. We'd save 4 flows overall but updating the selection and returning results would result in copying the entire lists and mapping, like this:

languages.update { items -> items.map { it.copy(isSelected = it.language.code in selectedIds) } } } instead of selectedLanguages.update { selectedIds }.

Let's discuss this further, I don't feel too strongly about this.. 🙂

) { adDetails, budget, languages, selectedLanguages ->
CampaignPreviewUiState(
adDetails = adDetails,
campaignDetails = campaign.toCampaignDetailsUi(budget, languages, devices, locations, interests)
campaignDetails = campaign.toCampaignDetailsUi(
budget,
languages.filter { it.code in selectedLanguages },
emptyList(),
emptyList(),
emptyList()
)
)
}.asLiveData()

init {
loadSuggestions()
loadData()
}

fun onBackPressed() {
Expand Down Expand Up @@ -92,8 +96,18 @@ class BlazeCampaignCreationPreviewViewModel @Inject constructor(
}
}

private fun loadSuggestions() {
fun onTargetSelectionUpdated(targetType: BlazeTargetType, selectedIds: List<String>) {
launch {
when (targetType) {
LANGUAGE -> selectedLanguages.update { selectedIds }
else -> Unit
}
}
}

private fun loadData() {
launch {
blazeRepository.fetchLanguages()
blazeRepository.getAdSuggestions(navArgs.productId).let { suggestions ->
adDetails.update {
AdDetails(
Expand All @@ -111,8 +125,8 @@ class BlazeCampaignCreationPreviewViewModel @Inject constructor(
budget: Budget,
languages: List<Language>,
devices: List<Device>,
locations: List<Location>,
interests: List<Interest>
interests: List<Interest>,
locations: List<Location>
) = CampaignDetailsUi(
budget = CampaignDetailItemUi(
displayTitle = resourceProvider.getString(string.blaze_campaign_preview_details_budget),
Expand All @@ -124,13 +138,15 @@ class BlazeCampaignCreationPreviewViewModel @Inject constructor(
displayTitle = resourceProvider.getString(string.blaze_campaign_preview_details_language),
displayValue = languages.joinToString { it.name }
.ifEmpty { resourceProvider.getString(string.blaze_campaign_preview_target_default_value) },
onItemSelected = { /* TODO Add language selection */ },
onItemSelected = {
triggerEvent(NavigateToTargetSelectionScreen(LANGUAGE, languages.map { it.code }))
},
),
CampaignDetailItemUi(
displayTitle = resourceProvider.getString(string.blaze_campaign_preview_details_devices),
displayValue = devices.joinToString { it.name }
.ifEmpty { resourceProvider.getString(string.blaze_campaign_preview_target_default_value) },
onItemSelected = { /* TODO Add devices selection */ },
onItemSelected = { /* TODO Add device selection */ },
),
CampaignDetailItemUi(
displayTitle = resourceProvider.getString(string.blaze_campaign_preview_details_location),
Expand Down Expand Up @@ -213,4 +229,8 @@ class BlazeCampaignCreationPreviewViewModel @Inject constructor(
)

object NavigateToBudgetScreen : MultiLiveEvent.Event()
data class NavigateToTargetSelectionScreen(
val targetType: BlazeTargetType,
val selectedIds: List<String>
) : MultiLiveEvent.Event()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.woocommerce.android.ui.blaze.creation.targets

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import com.woocommerce.android.extensions.navigateBackWithResult
import com.woocommerce.android.ui.base.BaseFragment
import com.woocommerce.android.ui.compose.composeView
import com.woocommerce.android.ui.main.AppBarStatus
import com.woocommerce.android.viewmodel.MultiLiveEvent
import com.woocommerce.android.viewmodel.MultiLiveEvent.Event.ExitWithResult
import dagger.hilt.android.AndroidEntryPoint

@AndroidEntryPoint
class BlazeCampaignTargetSelectionFragment : BaseFragment() {
companion object {
const val BLAZE_TARGET_SELECTION_RESULT = "blaze_target_selection_result"
}

override val activityAppBarStatus: AppBarStatus
get() = AppBarStatus.Hidden

val viewModel: BlazeCampaignTargetSelectionViewModel by viewModels()

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return composeView {
BlazeCampaignTargetSelectionScreen(viewModel)
}
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupObservers()
}

private fun setupObservers() {
viewModel.event.observe(viewLifecycleOwner) { event ->
when (event) {
is MultiLiveEvent.Event.Exit -> findNavController().popBackStack()
is ExitWithResult<*> -> navigateBackWithResult(BLAZE_TARGET_SELECTION_RESULT, event.data)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package com.woocommerce.android.ui.blaze.creation.targets

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.padding
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold
import androidx.compose.material.icons.Icons.Filled
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.runtime.Composable
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.woocommerce.android.R.string
import com.woocommerce.android.ui.blaze.creation.targets.BlazeCampaignTargetSelectionViewModel.TargetItem
import com.woocommerce.android.ui.blaze.creation.targets.BlazeCampaignTargetSelectionViewModel.ViewState
import com.woocommerce.android.ui.compose.component.MultiSelectAllItemsButton
import com.woocommerce.android.ui.compose.component.MultiSelectList
import com.woocommerce.android.ui.compose.component.Toolbar
import com.woocommerce.android.ui.compose.preview.LightDarkThemePreviews

@Composable
fun BlazeCampaignTargetSelectionScreen(viewModel: BlazeCampaignTargetSelectionViewModel) {
viewModel.viewState.observeAsState().value?.let { state ->
TargetSelectionScreen(
state = state,
onBackPressed = viewModel::onBackPressed,
onSaveTapped = viewModel::onSaveTapped,
onItemTapped = viewModel::onItemTapped,
onAllButtonTapped = viewModel::onAllButtonTapped
)
}
}

@Composable
private fun TargetSelectionScreen(
state: ViewState,
onBackPressed: () -> Unit,
onSaveTapped: () -> Unit,
onItemTapped: (TargetItem) -> Unit,
onAllButtonTapped: () -> Unit
) {
Scaffold(
topBar = {
Toolbar(
title = state.title,
onNavigationButtonClick = onBackPressed,
navigationIcon = Filled.ArrowBack,
actionButtonText = stringResource(id = string.save).uppercase(),
onActionButtonClick = onSaveTapped
)
},
modifier = Modifier.background(MaterialTheme.colors.surface)
) { paddingValues ->
MultiSelectList(
items = state.items,
selectedItems = state.selectedItems,
itemFormatter = { value },
onItemToggled = onItemTapped,
allItemsButton = MultiSelectAllItemsButton(
text = stringResource(id = string.blaze_campaign_preview_target_default_value),
onClicked = onAllButtonTapped
),
modifier = Modifier
.background(MaterialTheme.colors.surface)
.padding(paddingValues)
)
}
}

@LightDarkThemePreviews
@Composable
fun PreviewTargetSelectionScreen() {
TargetSelectionScreen(
state = ViewState(
items = listOf(
TargetItem("1", "Item 1"),
TargetItem("2", "Item 2"),
TargetItem("3", "Item 3"),
TargetItem("4", "Item 4"),
TargetItem("5", "Item 5"),
TargetItem("6", "Item 6"),
TargetItem("7", "Item 7"),
TargetItem("8", "Item 8"),
TargetItem("9", "Item 9")
),
selectedItems = listOf(
TargetItem("4", "Item 4"),
TargetItem("5", "Item 5"),
TargetItem("8", "Item 8"),
TargetItem("9", "Item 9")
),
title = "Title"
),
onBackPressed = { /*TODO*/ },
onSaveTapped = { /*TODO*/ },
onItemTapped = {},
onAllButtonTapped = {}
)
}
Loading
Loading