diff --git a/app/src/main/java/com/kickstarter/features/pledgedprojectsoverview/ui/PledgedProjectsOverviewActivity.kt b/app/src/main/java/com/kickstarter/features/pledgedprojectsoverview/ui/PledgedProjectsOverviewActivity.kt index b8cd9919de..a29f37d47f 100644 --- a/app/src/main/java/com/kickstarter/features/pledgedprojectsoverview/ui/PledgedProjectsOverviewActivity.kt +++ b/app/src/main/java/com/kickstarter/features/pledgedprojectsoverview/ui/PledgedProjectsOverviewActivity.kt @@ -8,17 +8,24 @@ import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.SnackbarHostState +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.lifecycleScope import androidx.paging.compose.collectAsLazyPagingItems import com.kickstarter.features.pledgedprojectsoverview.viewmodel.PledgedProjectsOverviewViewModel +import com.kickstarter.libs.MessagePreviousScreenType import com.kickstarter.libs.utils.TransitionUtils import com.kickstarter.libs.utils.extensions.getEnvironment import com.kickstarter.libs.utils.extensions.isDarkModeEnabled import com.kickstarter.ui.SharedPreferenceKey import com.kickstarter.ui.activities.AppThemes import com.kickstarter.ui.compose.designsystem.KickstarterApp +import com.kickstarter.ui.extensions.startCreatorMessageActivity import com.kickstarter.ui.extensions.transition +import kotlinx.coroutines.launch class PledgedProjectsOverviewActivity : AppCompatActivity() { @@ -39,6 +46,7 @@ class PledgedProjectsOverviewActivity : AppCompatActivity() { val darkModeEnabled = this.isDarkModeEnabled(env = env) val lazyListState = rememberLazyListState() + val snackbarHostState = remember { SnackbarHostState() } val ppoCardPagingSource = viewModel.ppoCardsState.collectAsLazyPagingItems() val totalAlerts = viewModel.totalAlertsState.collectAsStateWithLifecycle() @@ -60,11 +68,26 @@ class PledgedProjectsOverviewActivity : AppCompatActivity() { modifier = Modifier, onBackPressed = { onBackPressedDispatcher.onBackPressed() }, lazyColumnListState = lazyListState, + errorSnackBarHostState = snackbarHostState, ppoCards = ppoCardPagingSource, - totalAlerts = totalAlerts.value + totalAlerts = totalAlerts.value, + onSendMessageClick = { projectName -> viewModel.onMessageCreatorClicked(projectName) } ) } + LaunchedEffect(Unit) { + viewModel.projectFlow + .collect { + startCreatorMessageActivity(it, previousScreen = MessagePreviousScreenType.PLEDGED_PROJECTS_OVERVIEW) + } + } + + viewModel.provideSnackbarMessage { + lifecycleScope.launch { + snackbarHostState.showSnackbar(getString(it)) + } + } + onBackPressedDispatcher.addCallback(object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { finish() diff --git a/app/src/main/java/com/kickstarter/features/pledgedprojectsoverview/ui/PledgedProjectsOverviewScreen.kt b/app/src/main/java/com/kickstarter/features/pledgedprojectsoverview/ui/PledgedProjectsOverviewScreen.kt index 7f957032a0..80e0f845f0 100644 --- a/app/src/main/java/com/kickstarter/features/pledgedprojectsoverview/ui/PledgedProjectsOverviewScreen.kt +++ b/app/src/main/java/com/kickstarter/features/pledgedprojectsoverview/ui/PledgedProjectsOverviewScreen.kt @@ -12,6 +12,8 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.Scaffold +import androidx.compose.material.SnackbarHost +import androidx.compose.material.SnackbarHostState import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -47,7 +49,9 @@ private fun PledgedProjectsOverviewScreenPreview() { lazyColumnListState = rememberLazyListState(), ppoCards = ppoCardList, totalAlerts = 10, - onBackPressed = {} + onBackPressed = {}, + onSendMessageClick = {}, + errorSnackBarHostState = SnackbarHostState() ) } } @@ -58,14 +62,21 @@ fun PledgedProjectsOverviewScreen( modifier: Modifier, onBackPressed: () -> Unit, lazyColumnListState: LazyListState, + errorSnackBarHostState: SnackbarHostState, ppoCards: LazyPagingItems, - totalAlerts: Int = 0 + totalAlerts: Int = 0, + onSendMessageClick: (projectName: String) -> Unit ) { Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { Scaffold( + snackbarHost = { + SnackbarHost( + hostState = errorSnackBarHostState + ) + }, modifier = modifier, topBar = { TopToolBar( @@ -113,7 +124,7 @@ fun PledgedProjectsOverviewScreen( imageUrl = it.imageUrl, imageContentDescription = it.imageContentDescription, creatorName = it.creatorName, - sendAMessageClickAction = { }, + sendAMessageClickAction = { onSendMessageClick(it.projectSlug) }, shippingAddress = it.shippingAddress, showBadge = it.showBadge, onActionButtonClicked = { }, @@ -140,6 +151,7 @@ data class PPOCardDataMock( val viewType: PPOCardViewType = PPOCardViewType.FIX_PAYMENT, val onCardClick: () -> Unit = { }, val projectName: String = "This is a project name", + val projectSlug: String = "", val pledgeAmount: String = "$14.00", val imageUrl: String = "", val imageContentDescription: String = "", diff --git a/app/src/main/java/com/kickstarter/features/pledgedprojectsoverview/viewmodel/PledgedProjectsOverviewViewModel.kt b/app/src/main/java/com/kickstarter/features/pledgedprojectsoverview/viewmodel/PledgedProjectsOverviewViewModel.kt index 3c493457ed..cbc11f85d6 100644 --- a/app/src/main/java/com/kickstarter/features/pledgedprojectsoverview/viewmodel/PledgedProjectsOverviewViewModel.kt +++ b/app/src/main/java/com/kickstarter/features/pledgedprojectsoverview/viewmodel/PledgedProjectsOverviewViewModel.kt @@ -2,25 +2,70 @@ package com.kickstarter.features.pledgedprojectsoverview.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope import androidx.paging.PagingData +import com.kickstarter.R import com.kickstarter.features.pledgedprojectsoverview.ui.PPOCardDataMock import com.kickstarter.libs.Environment +import com.kickstarter.models.Project +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.shareIn +import kotlinx.coroutines.launch +import kotlinx.coroutines.rx2.asFlow class PledgedProjectsOverviewViewModel(environment: Environment) : ViewModel() { private val ppoCards = MutableStateFlow>(PagingData.empty()) private val totalAlerts = MutableStateFlow(0) + private var mutableProjectFlow = MutableSharedFlow() + private var snackbarMessage: (stringID: Int) -> Unit = {} + private val apolloClient = requireNotNull(environment.apolloClientV2()) val ppoCardsState: StateFlow> = ppoCards.asStateFlow() val totalAlertsState: StateFlow = totalAlerts.asStateFlow() + val projectFlow: SharedFlow + get() = mutableProjectFlow + .asSharedFlow() + .shareIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(), + ) + class Factory(private val environment: Environment) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { return PledgedProjectsOverviewViewModel(environment) as T } } + + fun provideSnackbarMessage(snackBarMessage: (Int) -> Unit) { + this.snackbarMessage = snackBarMessage + } + + fun onMessageCreatorClicked(projectName: String) { + viewModelScope.launch { + apolloClient.getProject( + slug = projectName, + ) + .asFlow() + .onStart { + // TODO emit loading ui state + }.map { project -> + mutableProjectFlow.emit(project) + }.catch { + snackbarMessage.invoke(R.string.Something_went_wrong_please_try_again) + }.collect() + } + } } diff --git a/app/src/main/java/com/kickstarter/libs/MessagePreviousScreenType.kt b/app/src/main/java/com/kickstarter/libs/MessagePreviousScreenType.kt index b12e361fd0..b23e6eafb4 100644 --- a/app/src/main/java/com/kickstarter/libs/MessagePreviousScreenType.kt +++ b/app/src/main/java/com/kickstarter/libs/MessagePreviousScreenType.kt @@ -13,5 +13,6 @@ enum class MessagePreviousScreenType(val trackingString: String) { CREATOR_BIO_MODAL("creator_bio_modal"), MESSAGES("messages"), PROJECT_PAGE("project_page"), - PUSH("push") + PUSH("push"), + PLEDGED_PROJECTS_OVERVIEW("pledged_projects_overview") } diff --git a/app/src/main/java/com/kickstarter/ui/extensions/ActivityExt.kt b/app/src/main/java/com/kickstarter/ui/extensions/ActivityExt.kt index 722a4c3bea..f88419fc0d 100644 --- a/app/src/main/java/com/kickstarter/ui/extensions/ActivityExt.kt +++ b/app/src/main/java/com/kickstarter/ui/extensions/ActivityExt.kt @@ -16,6 +16,7 @@ import com.google.android.play.core.review.ReviewInfo import com.google.android.play.core.review.ReviewManagerFactory import com.kickstarter.R import com.kickstarter.libs.Environment +import com.kickstarter.libs.MessagePreviousScreenType import com.kickstarter.libs.RefTag import com.kickstarter.libs.utils.Secrets import com.kickstarter.libs.utils.TransitionUtils @@ -36,6 +37,7 @@ import com.kickstarter.ui.IntentKey import com.kickstarter.ui.activities.DisclaimerItems import com.kickstarter.ui.activities.HelpActivity import com.kickstarter.ui.activities.LoginToutActivity +import com.kickstarter.ui.activities.MessagesActivity import com.kickstarter.ui.data.PledgeData import com.kickstarter.ui.data.PledgeReason import com.kickstarter.ui.data.ProjectData @@ -270,3 +272,12 @@ fun Activity.startDisclaimerChromeTab(disclaimerItem: DisclaimerItems, environme ChromeTabsHelperActivity.openCustomTab(this, UrlUtils.baseCustomTabsIntent(this), uri, fallback) } + +fun Activity.startCreatorMessageActivity(project: Project, previousScreen: MessagePreviousScreenType) { + startActivity( + Intent(this, MessagesActivity::class.java) + .putExtra(IntentKey.MESSAGE_SCREEN_SOURCE_CONTEXT, previousScreen) + .putExtra(IntentKey.PROJECT, project) + .putExtra(IntentKey.BACKING, project.backing()) + ) +} diff --git a/app/src/test/java/com/kickstarter/features/pledgedprojectsoverview/ui/PledgedProjectsOverviewScreenTest.kt b/app/src/test/java/com/kickstarter/features/pledgedprojectsoverview/ui/PledgedProjectsOverviewScreenTest.kt index acb7f6725c..7f82dc9d4d 100644 --- a/app/src/test/java/com/kickstarter/features/pledgedprojectsoverview/ui/PledgedProjectsOverviewScreenTest.kt +++ b/app/src/test/java/com/kickstarter/features/pledgedprojectsoverview/ui/PledgedProjectsOverviewScreenTest.kt @@ -1,6 +1,7 @@ package com.kickstarter.features.pledgedprojectsoverview.ui import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.SnackbarHostState import androidx.compose.ui.Modifier import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick @@ -29,7 +30,9 @@ class PledgedProjectsOverviewScreenTest : KSRobolectricTestCase() { modifier = Modifier, onBackPressed = { backClickedCount++ }, lazyColumnListState = rememberLazyListState(), - ppoCards = ppoCardList + ppoCards = ppoCardList, + errorSnackBarHostState = SnackbarHostState(), + onSendMessageClick = {} ) } } diff --git a/app/src/test/java/com/kickstarter/features/pledgedprojectsoverview/viewmodel/PledgedProjectsOverviewViewModelTest.kt b/app/src/test/java/com/kickstarter/features/pledgedprojectsoverview/viewmodel/PledgedProjectsOverviewViewModelTest.kt new file mode 100644 index 0000000000..869176dcf9 --- /dev/null +++ b/app/src/test/java/com/kickstarter/features/pledgedprojectsoverview/viewmodel/PledgedProjectsOverviewViewModelTest.kt @@ -0,0 +1,74 @@ +package com.kickstarter.features.pledgedprojectsoverview.viewmodel + +import com.kickstarter.KSRobolectricTestCase +import com.kickstarter.R +import com.kickstarter.mock.factories.ProjectFactory +import com.kickstarter.mock.services.MockApolloClientV2 +import com.kickstarter.models.Project +import io.reactivex.Observable +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +class PledgedProjectsOverviewViewModelTest : KSRobolectricTestCase() { + + private lateinit var viewModel: PledgedProjectsOverviewViewModel + + @Before + fun setUpEnvrionment() { + viewModel = PledgedProjectsOverviewViewModel.Factory(environment = environment()) + .create(PledgedProjectsOverviewViewModel::class.java) + } + + @Test + fun `emits_project_when_message_creator_called`() = + runTest { + val projectState = mutableListOf() + + val project = ProjectFactory.successfulProject() + viewModel = PledgedProjectsOverviewViewModel.Factory( + environment = environment().toBuilder() + .apolloClientV2(object : MockApolloClientV2() { + override fun getProject(slug: String): Observable { + return Observable.just(project) + } + }).build() + ).create(PledgedProjectsOverviewViewModel::class.java) + + backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { + viewModel.projectFlow.toList(projectState) + } + viewModel.onMessageCreatorClicked("test_project_slug") + + assertEquals( + projectState.last(), + project + ) + } + + @Test + fun `emits_error_when_message_creator_called`() = + runTest { + var snackbarAction = 0 + viewModel = PledgedProjectsOverviewViewModel.Factory( + environment = environment().toBuilder() + .apolloClientV2(object : MockApolloClientV2() { + override fun getProject(slug: String): Observable { + return Observable.error(Throwable("error")) + } + }).build() + ).create(PledgedProjectsOverviewViewModel::class.java) + + viewModel.provideSnackbarMessage { snackbarAction = it } + viewModel.onMessageCreatorClicked("test_project_slug") + + // Should equal error string id + assertEquals( + snackbarAction, + R.string.Something_went_wrong_please_try_again + ) + } +}