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

MOBILE-188/Features such as recommend,review in profile screen #366

Merged
Merged
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.listenbrainz.android.ui.navigation

import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.navigation.NavController
Expand All @@ -20,6 +21,7 @@ fun AppNavigation(
navController: NavController = rememberNavController(),
scrollRequestState: Boolean,
onScrollToTop: (suspend () -> Unit) -> Unit,
snackbarState : SnackbarHostState
) {
NavHost(
navController = navController as NavHostController,
Expand All @@ -38,7 +40,8 @@ fun AppNavigation(
composable(route = AppNavigationItem.Profile.route){
ProfileScreen(
onScrollToTop = onScrollToTop,
scrollRequestState = scrollRequestState
scrollRequestState = scrollRequestState,
snackbarState = snackbarState
)
}
composable(route = AppNavigationItem.Settings.route){
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.listenbrainz.android.ui.screens.listens

import android.os.Bundle
import android.widget.Toast
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
Expand All @@ -11,25 +13,46 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.platform.UriHandler
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel
import kotlinx.coroutines.launch
import org.listenbrainz.android.model.Listen
import org.listenbrainz.android.model.Metadata
import org.listenbrainz.android.model.TrackMetadata
import org.listenbrainz.android.model.feed.ReviewEntityType
import org.listenbrainz.android.ui.components.ListenCardSmall
import org.listenbrainz.android.ui.components.LoadingAnimation
import org.listenbrainz.android.ui.components.dialogs.Dialog
import org.listenbrainz.android.ui.components.dialogs.PersonalRecommendationDialog
import org.listenbrainz.android.ui.components.dialogs.PinDialog
import org.listenbrainz.android.ui.components.dialogs.ReviewDialog
import org.listenbrainz.android.ui.components.dialogs.rememberDialogsState
import org.listenbrainz.android.ui.screens.feed.FeedUiState
import org.listenbrainz.android.ui.screens.feed.SocialDropdown
import org.listenbrainz.android.ui.screens.profile.UserData
import org.listenbrainz.android.ui.screens.settings.PreferencesUiState
import org.listenbrainz.android.ui.theme.ListenBrainzTheme
import org.listenbrainz.android.util.Constants
import org.listenbrainz.android.util.Utils
import org.listenbrainz.android.viewmodel.FeedViewModel
import org.listenbrainz.android.viewmodel.ListensViewModel
import org.listenbrainz.android.viewmodel.SocialViewModel

Expand All @@ -39,6 +62,7 @@ fun ListensScreen(
socialViewModel: SocialViewModel = hiltViewModel(),
scrollRequestState: Boolean,
onScrollToTop: (suspend () -> Unit) -> Unit,
snackbarState : SnackbarHostState
) {

val uiState by viewModel.uiState.collectAsState()
Expand All @@ -60,24 +84,50 @@ fun ListensScreen(
},
playListen = {
socialViewModel.playListen(it)
}
},
snackbarState = snackbarState
)
}

private enum class ListenDialogBundleKeys {
PAGE,
EVENT_INDEX;
companion object {
fun listenDialogBundle(page: Int, eventIndex: Int): Bundle {
return Bundle().apply {
putInt(PAGE.name, page)
putInt(EVENT_INDEX.name, eventIndex)
}
}
}
}



@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ListensScreen(
scrollRequestState: Boolean,
onScrollToTop: (suspend () -> Unit) -> Unit,
socialViewModel: SocialViewModel = hiltViewModel(),
pranavkonidena marked this conversation as resolved.
Show resolved Hide resolved
feedViewModel: FeedViewModel = hiltViewModel(),
uiState: ListensUiState,
preferencesUiState: PreferencesUiState,
updateNotificationServicePermissionStatus: () -> Unit,
validateUserToken: suspend (String) -> Boolean,
setToken: (String) -> Unit,
playListen: (TrackMetadata) -> Unit
playListen: (TrackMetadata) -> Unit,
uriHandler: UriHandler = LocalUriHandler.current,
snackbarState: SnackbarHostState
) {
val listState = rememberLazyListState()
val dropdownItemIndex: MutableState<Int?> = rememberSaveable {
mutableStateOf(null)
}
val dialogsState = rememberDialogsState()
val feedUiState by feedViewModel.uiState.collectAsState()
val scope = rememberCoroutineScope()


// Scroll to the top when shouldScrollToTop becomes true
LaunchedEffect(scrollRequestState) {
Expand Down Expand Up @@ -134,7 +184,8 @@ fun ListensScreen(
}
}

items(items = uiState.listens) { listen ->
itemsIndexed(items = uiState.listens) { index , listen ->
val metadata = Metadata(trackMetadata = listen.trackMetadata)
ListenCardSmall(
modifier = Modifier.padding(
horizontal = ListenBrainzTheme.paddings.horizontal,
Expand All @@ -145,12 +196,80 @@ fun ListensScreen(
coverArtUrl = Utils.getCoverArtUrl(
caaReleaseMbid = listen.trackMetadata.mbidMapping?.caaReleaseMbid,
caaId = listen.trackMetadata.mbidMapping?.caaId
)
),
dropDown = {
SocialDropdown(
isExpanded = dropdownItemIndex.value == index,
onDismiss = {
dropdownItemIndex.value = null
},
metadata = metadata,
onRecommend = {
try {
socialViewModel.recommend(metadata)
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is wrong, we do not propagate errors to the UI right now so this will always result in a successful toast. If you want, you can propagate the error via Resource class and then check if the response was successful then return error. We can ditch errorFlow for this as well.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Remember its okay to change the core code if you feel its right. At most I will say is "This is wrong" and you will learn a thing or two. Great work overall.

scope.launch {
snackbarState.showSnackbar(Constants.Strings.RECOMMENDATION_GREETING)
}
}
catch (e : Error) {
scope.launch {
snackbarState.showSnackbar(Constants.Strings.ERROR_MESSAGE)
}
}
dropdownItemIndex.value = null
},
onPersonallyRecommend = {
dialogsState.activateDialog(Dialog.PERSONAL_RECOMMENDATION , ListenDialogBundleKeys.listenDialogBundle(0, index))
dropdownItemIndex.value = null
},
onReview = {
dialogsState.activateDialog(Dialog.REVIEW , ListenDialogBundleKeys.listenDialogBundle(0, index))
dropdownItemIndex.value = null
},
onPin = {
dialogsState.activateDialog(Dialog.PIN , ListenDialogBundleKeys.listenDialogBundle(0, index))
dropdownItemIndex.value = null
},
onOpenInMusicBrainz = {
try {
uriHandler.openUri("https://musicbrainz.org/recording/${metadata.trackMetadata?.mbidMapping?.recordingMbid}")
}
catch(e : Error) {
scope.launch {
snackbarState.showSnackbar(Constants.Strings.ERROR_MESSAGE)
}
}
dropdownItemIndex.value = null
}

)
},
enableDropdownIcon = true,
onDropdownIconClick = {
dropdownItemIndex.value = index
}
) {
playListen(listen.trackMetadata)
}
}
}
Dialogs(
deactivateDialog = {
dialogsState.deactivateDialog()
},
currentDialog = dialogsState.currentDialog,
currentIndex = dialogsState.metadata?.getInt(ListenDialogBundleKeys.EVENT_INDEX.name),
listens = uiState.listens,
onPin = {metadata, blurbContent -> socialViewModel.pin(metadata , blurbContent)},
searchUsers = {
query -> feedViewModel.searchUser(query)
},
feedUiState = feedUiState,
isCritiqueBrainzLinked = { feedViewModel.isCritiqueBrainzLinked() },
onReview = {type, blurbContent, rating, locale, metadata -> socialViewModel.review(metadata , type , blurbContent , rating , locale) },
onPersonallyRecommend = {metadata, users, blurbContent -> socialViewModel.personallyRecommend(metadata, users, blurbContent)},
snackbarState = snackbarState
)

// Loading Animation
AnimatedVisibility(
Expand All @@ -164,6 +283,87 @@ fun ListensScreen(
}
}

@Composable
private fun Dialogs(
deactivateDialog: () -> Unit,
currentDialog: Dialog,
feedUiState: FeedUiState,
currentIndex : Int?,
listens : List<Listen>,
onPin : (metadata: Metadata , blurbContent : String) -> Unit,
searchUsers : (String) -> Unit,
isCritiqueBrainzLinked: suspend () -> Boolean?,
onReview : (type: ReviewEntityType, blurbContent: String, rating: Int?, locale: String , metadata : Metadata) -> Unit,
onPersonallyRecommend : (metadata : Metadata , users : List<String> , blurbContent : String) -> Unit,
snackbarState: SnackbarHostState
) {
val scope = rememberCoroutineScope()
when (currentDialog) {
Dialog.NONE -> Unit
Dialog.PIN -> {
PinDialog(trackName = listens[currentIndex!!].trackMetadata.trackName, artistName = listens[currentIndex].trackMetadata.artistName, onDismiss = deactivateDialog, onSubmit = {
blurbContent -> try {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same here as well. Please take care everywhere you've done this.

onPin(Metadata(trackMetadata = listens[currentIndex].trackMetadata) , blurbContent)
scope.launch {
snackbarState.showSnackbar(Constants.Strings.PIN_GREETING)
}
} catch (e : Error) {
scope.launch {
snackbarState.showSnackbar(Constants.Strings.ERROR_MESSAGE)
}
}
})
}
Dialog.PERSONAL_RECOMMENDATION -> {
PersonalRecommendationDialog(
trackName = listens[currentIndex!!].trackMetadata.trackName,
onDismiss = deactivateDialog,
searchResult = feedUiState.searchResult,
searchUsers = searchUsers,
onSubmit = {
users, blurbContent -> try {
onPersonallyRecommend(
Metadata(trackMetadata = listens[currentIndex].trackMetadata),
users,
blurbContent
)
scope.launch {
snackbarState.showSnackbar(Constants.Strings.PERSONAL_RECOMMENDATION_GREETING)
}
}
catch (e : Error) {
scope.launch {
snackbarState.showSnackbar(Constants.Strings.ERROR_MESSAGE)
}
}
}
)
}
Dialog.REVIEW -> {
ReviewDialog(
trackName = listens[currentIndex!!].trackMetadata.trackName,
artistName = listens[currentIndex].trackMetadata.artistName,
releaseName = listens[currentIndex].trackMetadata.releaseName,
onDismiss = deactivateDialog,
isCritiqueBrainzLinked = isCritiqueBrainzLinked,
onSubmit = {
type, blurbContent, rating, locale -> try {
onReview(type, blurbContent, rating, locale , Metadata(trackMetadata = listens[currentIndex].trackMetadata))
scope.launch {
snackbarState.showSnackbar(Constants.Strings.REVIEW_GREETING)
}
}
catch (e : Error) {
scope.launch {
snackbarState.showSnackbar(Constants.Strings.ERROR_MESSAGE)
}
}
}
)
}
}
}

@Preview
@Composable
fun ListensScreenPreview() {
Expand All @@ -175,6 +375,7 @@ fun ListensScreenPreview() {
preferencesUiState = PreferencesUiState(),
validateUserToken = { true },
setToken = {},
playListen = {}
playListen = {},
snackbarState = SnackbarHostState()
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ class MainActivity : ComponentActivity() {
val backdropScaffoldState =
rememberBackdropScaffoldState(initialValue = BackdropValue.Revealed)
var scrollToTopState by remember { mutableStateOf(false) }
val snackbarState = SnackbarHostState()
val snackbarState = remember { SnackbarHostState() }
val searchBarState = rememberSearchBarState()
val scope = rememberCoroutineScope()

Expand Down Expand Up @@ -183,7 +183,8 @@ class MainActivity : ComponentActivity() {
scrollToTopState = false
}
}
}
},
snackbarState = snackbarState
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
Expand Down Expand Up @@ -44,7 +45,8 @@ fun ProfileScreen(
context: Context = LocalContext.current,
viewModel: ProfileViewModel = hiltViewModel(),
scrollRequestState: Boolean,
onScrollToTop: (suspend () -> Unit) -> Unit
onScrollToTop: (suspend () -> Unit) -> Unit,
snackbarState : SnackbarHostState
) {
val scrollState = rememberScrollState()

Expand All @@ -61,7 +63,8 @@ fun ProfileScreen(
STATUS_LOGGED_IN -> {
ListensScreen(
onScrollToTop = onScrollToTop,
scrollRequestState = scrollRequestState
scrollRequestState = scrollRequestState,
snackbarState = snackbarState
)
}
else -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ class YearInMusic23Activity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val yim23ViewModel : Yim23ViewModel by viewModels()
val socialViewModel : SocialViewModel by viewModels()
val networkConnectivityViewModel: NetworkConnectivityViewModel =
ViewModelProvider(this)[NetworkConnectivityViewModelImpl::class.java]

Expand All @@ -37,8 +36,7 @@ class YearInMusic23Activity : ComponentActivity() {
Toast.LENGTH_LONG).show()
finish()
}
Yim23Navigation(yimViewModel = yim23ViewModel, socialViewModel = socialViewModel
, networkConnectivityViewModel = networkConnectivityViewModel, activity = this)
Yim23Navigation(yimViewModel = yim23ViewModel ,networkConnectivityViewModel = networkConnectivityViewModel, activity = this)
}
}
}
Loading
Loading