diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index 1ac3961c0..bf38e942f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -229,7 +229,7 @@ fun AppNavigation( content = { it.arguments?.getString("id")?.let { id -> NIP90ContentDiscoveryScreen( - dvmPublicKey = id, + appDefinitionEventId = id, accountViewModel = accountViewModel, nav = nav, ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt index 39566b0fa..7a79c116c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt @@ -34,8 +34,10 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ExpandMore @@ -60,11 +62,13 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider @@ -73,6 +77,7 @@ import androidx.lifecycle.map import androidx.lifecycle.viewModelScope import androidx.navigation.NavBackStackEntry import coil.Coil +import coil.compose.AsyncImage import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.AddressableNote @@ -97,6 +102,7 @@ import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource import com.vitorpamplona.amethyst.service.NostrVideoDataSource import com.vitorpamplona.amethyst.service.relays.Client import com.vitorpamplona.amethyst.service.relays.RelayPool +import com.vitorpamplona.amethyst.ui.components.LoadNote import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.note.AmethystIcon import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon @@ -106,6 +112,7 @@ import com.vitorpamplona.amethyst.ui.note.LoadChannel import com.vitorpamplona.amethyst.ui.note.LoadCityName import com.vitorpamplona.amethyst.ui.note.LoadUser import com.vitorpamplona.amethyst.ui.note.NonClickableUserPictures +import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture import com.vitorpamplona.amethyst.ui.note.SearchIcon import com.vitorpamplona.amethyst.ui.note.UserCompose import com.vitorpamplona.amethyst.ui.note.UsernameDisplay @@ -122,6 +129,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.LongRoomHeader import com.vitorpamplona.amethyst.ui.screen.loggedIn.RoomNameOnlyDisplay import com.vitorpamplona.amethyst.ui.screen.loggedIn.ShortChannelHeader import com.vitorpamplona.amethyst.ui.screen.loggedIn.SpinnerSelectionDialog +import com.vitorpamplona.amethyst.ui.screen.loggedIn.observeAppDefinition import com.vitorpamplona.amethyst.ui.theme.BottomTopHeight import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer @@ -297,26 +305,34 @@ private fun RoomTopBar( @Composable private fun DvmTopBar( - id: String, + appDefinitionId: String, accountViewModel: AccountViewModel, nav: (String) -> Unit, navPopBack: () -> Unit, ) { FlexibleTopBarWithBackButton( title = { - LoadUser(baseUserHex = id, accountViewModel) { baseUser -> - if (baseUser != null) { - ClickableUserPicture( - baseUser = baseUser, - accountViewModel = accountViewModel, - size = Size34dp, - ) + LoadNote(baseNoteHex = appDefinitionId, accountViewModel = accountViewModel) { appDefinitionNote -> + if (appDefinitionNote != null) { + val card = observeAppDefinition(appDefinitionNote) + + card.cover?.let { + AsyncImage( + model = it, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier.size(Size34dp).clip(shape = CircleShape), + ) + } ?: run { NoteAuthorPicture(baseNote = appDefinitionNote, size = Size34dp, accountViewModel = accountViewModel) } Spacer(modifier = DoubleHorzSpacer) - UsernameDisplay(baseUser, Modifier.weight(1f), fontWeight = FontWeight.Normal) - } else { - Spacer(BottomTopHeight) + Text( + text = card.name, + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) } } }, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt index 4a1ae3cf4..68e658035 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt @@ -80,6 +80,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.EndedFlag import com.vitorpamplona.amethyst.ui.screen.loggedIn.LiveFlag import com.vitorpamplona.amethyst.ui.screen.loggedIn.OfflineFlag import com.vitorpamplona.amethyst.ui.screen.loggedIn.ScheduledFlag +import com.vitorpamplona.amethyst.ui.screen.loggedIn.observeAppDefinition import com.vitorpamplona.amethyst.ui.screen.loggedIn.showAmountAxis import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.ui.theme.HalfPadding @@ -342,7 +343,9 @@ fun InnerRenderClassifiedsThumb( note: Note, ) { Box( - Modifier.fillMaxWidth().aspectRatio(1f), + Modifier + .fillMaxWidth() + .aspectRatio(1f), contentAlignment = BottomStart, ) { card.image?.let { @@ -355,7 +358,10 @@ fun InnerRenderClassifiedsThumb( } ?: run { DisplayAuthorBanner(note) } Row( - Modifier.fillMaxWidth().background(Color.Black.copy(0.6f)).padding(Size5dp), + Modifier + .fillMaxWidth() + .background(Color.Black.copy(0.6f)) + .padding(Size5dp), horizontalArrangement = Arrangement.SpaceBetween, ) { card.title?.let { @@ -451,14 +457,20 @@ fun RenderLiveActivityThumb( ) { Box( contentAlignment = TopEnd, - modifier = Modifier.aspectRatio(ratio = 16f / 9f).fillMaxWidth(), + modifier = + Modifier + .aspectRatio(ratio = 16f / 9f) + .fillMaxWidth(), ) { card.cover?.let { AsyncImage( model = it, contentDescription = null, contentScale = ContentScale.Crop, - modifier = Modifier.fillMaxSize().clip(QuoteBorder), + modifier = + Modifier + .fillMaxSize() + .clip(QuoteBorder), ) } ?: run { DisplayAuthorBanner(baseNote) } @@ -494,7 +506,9 @@ fun RenderLiveActivityThumb( LoadParticipants(card.participants, baseNote, accountViewModel) { participantUsers -> Box( - Modifier.padding(10.dp).align(BottomStart), + Modifier + .padding(10.dp) + .align(BottomStart), ) { if (participantUsers.isNotEmpty()) { Gallery(participantUsers, accountViewModel) @@ -572,7 +586,10 @@ fun RenderCommunitiesThumb( model = it, contentDescription = null, contentScale = ContentScale.Crop, - modifier = Modifier.fillMaxSize().clip(QuoteBorder), + modifier = + Modifier + .fillMaxSize() + .clip(QuoteBorder), ) } } ?: run { DisplayAuthorBanner(baseNote) } @@ -742,29 +759,7 @@ fun RenderContentDVMThumb( accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { - val noteEvent = baseNote.event as? AppDefinitionEvent ?: return - - val card by - baseNote - .live() - .metadata - .map { - val noteEvent = it.note.event as? AppDefinitionEvent - - DVMCard( - name = noteEvent?.appMetaData()?.name ?: "", - description = noteEvent?.appMetaData()?.about ?: "", - cover = noteEvent?.appMetaData()?.image?.ifBlank { null }, - ) - } - .distinctUntilChanged() - .observeAsState( - DVMCard( - name = noteEvent.appMetaData()?.name ?: "", - description = noteEvent.appMetaData()?.about ?: "", - cover = noteEvent.appMetaData()?.image?.ifBlank { null }, - ), - ) + val card = observeAppDefinition(appDefinitionNote = baseNote) LeftPictureLayout( onImage = { @@ -774,7 +769,10 @@ fun RenderContentDVMThumb( model = it, contentDescription = null, contentScale = ContentScale.Crop, - modifier = Modifier.fillMaxSize().clip(QuoteBorder), + modifier = + Modifier + .fillMaxSize() + .clip(QuoteBorder), ) } } ?: run { DisplayAuthorBanner(baseNote) } @@ -788,7 +786,7 @@ fun RenderContentDVMThumb( modifier = Modifier.weight(1f), ) - Spacer(modifier = StdHorzSpacer) + Spacer(modifier = StdVertSpacer) Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = RowColSpacing, @@ -895,7 +893,10 @@ fun RenderChannelThumb( model = it, contentDescription = null, contentScale = ContentScale.Crop, - modifier = Modifier.fillMaxSize().clip(QuoteBorder), + modifier = + Modifier + .fillMaxSize() + .clip(QuoteBorder), ) } ?: run { DisplayAuthorBanner(baseNote) } }, @@ -970,6 +971,11 @@ fun Gallery( @Composable fun DisplayAuthorBanner(note: Note) { WatchAuthor(note) { - BannerImage(it, Modifier.fillMaxSize().clip(QuoteBorder)) + BannerImage( + it, + Modifier + .fillMaxSize() + .clip(QuoteBorder), + ) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/WatchNoteEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/WatchNoteEvent.kt index 021896dea..06dc89eb7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/WatchNoteEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/WatchNoteEvent.kt @@ -38,6 +38,33 @@ fun WatchNoteEvent( accountViewModel: AccountViewModel, modifier: Modifier = Modifier, onNoteEventFound: @Composable () -> Unit, +) { + WatchNoteEvent( + baseNote, + onNoteEventFound, + onBlank = { + LongPressToQuickAction( + baseNote = baseNote, + accountViewModel = accountViewModel, + ) { showPopup -> + BlankNote( + remember { + modifier.combinedClickable( + onClick = {}, + onLongClick = showPopup, + ) + }, + ) + } + }, + ) +} + +@Composable +fun WatchNoteEvent( + baseNote: Note, + onNoteEventFound: @Composable () -> Unit, + onBlank: @Composable () -> Unit, ) { if (baseNote.event != null) { onNoteEventFound() @@ -49,19 +76,7 @@ fun WatchNoteEvent( if (it) { onNoteEventFound() } else { - LongPressToQuickAction( - baseNote = baseNote, - accountViewModel = accountViewModel, - ) { showPopup -> - BlankNote( - remember { - modifier.combinedClickable( - onClick = {}, - onLongClick = showPopup, - ) - }, - ) - } + onBlank() } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt index c18547b5e..5808e515b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt @@ -26,6 +26,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -36,45 +37,77 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.distinctUntilChanged +import androidx.lifecycle.map import androidx.lifecycle.viewmodel.compose.viewModel +import coil.compose.AsyncImage import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture -import com.vitorpamplona.amethyst.ui.note.LoadUser -import com.vitorpamplona.amethyst.ui.note.UsernameDisplay +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.ui.components.LoadNote +import com.vitorpamplona.amethyst.ui.note.DVMCard +import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture +import com.vitorpamplona.amethyst.ui.note.WatchNoteEvent import com.vitorpamplona.amethyst.ui.screen.FeedEmpty import com.vitorpamplona.amethyst.ui.screen.NostrNIP90ContentDiscoveryFeedViewModel import com.vitorpamplona.amethyst.ui.screen.RefresheableBox import com.vitorpamplona.amethyst.ui.screen.RenderFeedState import com.vitorpamplona.amethyst.ui.screen.SaveableFeedState import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer +import com.vitorpamplona.amethyst.ui.theme.QuoteBorder import com.vitorpamplona.amethyst.ui.theme.Size75dp -import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.events.AppDefinitionEvent import com.vitorpamplona.quartz.events.NIP90ContentDiscoveryResponseEvent import com.vitorpamplona.quartz.events.NIP90StatusEvent @Composable fun NIP90ContentDiscoveryScreen( - dvmPublicKey: String, + appDefinitionEventId: String, accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { + LoadNote(baseNoteHex = appDefinitionEventId, accountViewModel = accountViewModel) { + it?.let { baseNote -> + WatchNoteEvent( + baseNote, + onNoteEventFound = { + NIP90ContentDiscoveryScreen(baseNote, accountViewModel, nav) + }, + onBlank = { + FeedEmptywithStatus(baseNote, stringResource(R.string.dvm_looking_for_app), accountViewModel, nav) + }, + ) + } + } +} + +@Composable +fun NIP90ContentDiscoveryScreen( + appDefinition: Note, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val noteAuthor = appDefinition.author ?: return + var requestEventID by - remember(dvmPublicKey) { + remember(appDefinition) { mutableStateOf(null) } val onRefresh = { - accountViewModel.requestDVMContentDiscovery(dvmPublicKey) { + accountViewModel.requestDVMContentDiscovery(noteAuthor.pubkeyHex) { requestEventID = it } } - LaunchedEffect(key1 = dvmPublicKey) { + LaunchedEffect(key1 = appDefinition) { onRefresh() } @@ -84,7 +117,7 @@ fun NIP90ContentDiscoveryScreen( val myRequestEventID = requestEventID if (myRequestEventID != null) { ObserverContentDiscoveryResponse( - dvmPublicKey, + appDefinition, myRequestEventID, onRefresh, accountViewModel, @@ -92,19 +125,20 @@ fun NIP90ContentDiscoveryScreen( ) } else { // TODO: Make a good splash screen with loading animation for this DVM. - FeedEmptywithStatus(dvmPublicKey, stringResource(R.string.dvm_requesting_job), accountViewModel, nav) + FeedEmptywithStatus(appDefinition, stringResource(R.string.dvm_requesting_job), accountViewModel, nav) } } } @Composable fun ObserverContentDiscoveryResponse( - dvmPublicKey: String, + appDefinition: Note, dvmRequestId: Note, onRefresh: () -> Unit, accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { + val noteAuthor = appDefinition.author ?: return val updateFiltersFromRelays = dvmRequestId.live().metadata.observeAsState() val resultFlow = @@ -116,7 +150,7 @@ fun ObserverContentDiscoveryResponse( if (latestResponse != null) { PrepareViewContentDiscoveryModels( - dvmPublicKey, + noteAuthor, dvmRequestId.idHex, onRefresh, accountViewModel, @@ -124,7 +158,7 @@ fun ObserverContentDiscoveryResponse( ) } else { ObserverDvmStatusResponse( - dvmPublicKey, + appDefinition, dvmRequestId.idHex, accountViewModel, nav, @@ -134,7 +168,7 @@ fun ObserverContentDiscoveryResponse( @Composable fun ObserverDvmStatusResponse( - dvmPublicKey: String, + appDefinition: Note, dvmRequestId: String, accountViewModel: AccountViewModel, nav: (String) -> Unit, @@ -149,17 +183,17 @@ fun ObserverDvmStatusResponse( if (latestStatus != null) { // TODO: Make a good splash screen with loading animation for this DVM. latestStatus?.let { - FeedEmptywithStatus(dvmPublicKey, it.content(), accountViewModel, nav) + FeedEmptywithStatus(appDefinition, it.content(), accountViewModel, nav) } } else { // TODO: Make a good splash screen with loading animation for this DVM. - FeedEmptywithStatus(dvmPublicKey, stringResource(R.string.dvm_waiting_status), accountViewModel, nav) + FeedEmptywithStatus(appDefinition, stringResource(R.string.dvm_waiting_status), accountViewModel, nav) } } @Composable fun PrepareViewContentDiscoveryModels( - dvmPublicKey: String, + dvm: User, dvmRequestId: String, onRefresh: () -> Unit, accountViewModel: AccountViewModel, @@ -167,8 +201,8 @@ fun PrepareViewContentDiscoveryModels( ) { val resultFeedViewModel: NostrNIP90ContentDiscoveryFeedViewModel = viewModel( - key = "NostrNIP90ContentDiscoveryFeedViewModel$dvmPublicKey$dvmRequestId", - factory = NostrNIP90ContentDiscoveryFeedViewModel.Factory(accountViewModel.account, dvmkey = dvmPublicKey, requestid = dvmRequestId), + key = "NostrNIP90ContentDiscoveryFeedViewModel${dvm.pubkeyHex}$dvmRequestId", + factory = NostrNIP90ContentDiscoveryFeedViewModel.Factory(accountViewModel.account, dvmkey = dvm.pubkeyHex, requestid = dvmRequestId), ) LaunchedEffect(key1 = dvmRequestId) { @@ -210,7 +244,7 @@ fun RenderNostrNIP90ContentDiscoveryScreen( @Composable fun FeedEmptywithStatus( - pubkey: HexKey, + appDefinitionNote: Note, status: String, accountViewModel: AccountViewModel, nav: (String) -> Unit, @@ -222,22 +256,62 @@ fun FeedEmptywithStatus( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, ) { - LoadUser(baseUserHex = pubkey, accountViewModel = accountViewModel) { baseUser -> - if (baseUser != null) { - ClickableUserPicture( - baseUser = baseUser, - accountViewModel = accountViewModel, - size = Size75dp, - ) + val card = observeAppDefinition(appDefinitionNote) - Spacer(modifier = DoubleVertSpacer) + card.cover?.let { + AsyncImage( + model = it, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier.size(Size75dp).clip(QuoteBorder), + ) + } ?: run { NoteAuthorPicture(appDefinitionNote, nav, accountViewModel, Size75dp) } - UsernameDisplay(baseUser, Modifier, fontWeight = FontWeight.Normal) + Spacer(modifier = DoubleVertSpacer) - Spacer(modifier = DoubleVertSpacer) - } - } + Text( + text = card.name, + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + + Spacer(modifier = DoubleVertSpacer) Text(status) } } + +@Composable +fun observeAppDefinition(appDefinitionNote: Note): DVMCard { + val noteEvent = + appDefinitionNote.event as? AppDefinitionEvent ?: return DVMCard( + name = "", + description = "", + cover = null, + ) + + val card by + appDefinitionNote + .live() + .metadata + .map { + val noteEvent = it.note.event as? AppDefinitionEvent + + DVMCard( + name = noteEvent?.appMetaData()?.name ?: "", + description = noteEvent?.appMetaData()?.about ?: "", + cover = noteEvent?.appMetaData()?.image?.ifBlank { null }, + ) + } + .distinctUntilChanged() + .observeAsState( + DVMCard( + name = noteEvent.appMetaData()?.name ?: "", + description = noteEvent.appMetaData()?.about ?: "", + cover = noteEvent.appMetaData()?.image?.ifBlank { null }, + ), + ) + + return card +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 84cf4a04e..3ed3959e0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -847,6 +847,7 @@ From Msg + Looking for Application Job Requested, waiting for a reply Requesting Job from DVM