Migrates DVM route from pubkey to AppDefinition eventId and makes sure NFC reading will load the AppDefinition itself before rendering the rest of the screen.

This commit is contained in:
Vitor Pamplona 2024-05-17 18:06:20 -04:00
parent d6988ad4e1
commit 72c6e93524
6 changed files with 202 additions and 90 deletions

View File

@ -229,7 +229,7 @@ fun AppNavigation(
content = {
it.arguments?.getString("id")?.let { id ->
NIP90ContentDiscoveryScreen(
dvmPublicKey = id,
appDefinitionEventId = id,
accountViewModel = accountViewModel,
nav = nav,
)

View File

@ -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,
)
}
}
},

View File

@ -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),
)
}
}

View File

@ -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()
}
}
}

View File

@ -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<Note?>(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
}

View File

@ -847,6 +847,7 @@
<string name="load_from_text">From Msg</string>
<string name="dvm_looking_for_app">Looking for Application</string>
<string name="dvm_waiting_status">Job Requested, waiting for a reply</string>
<string name="dvm_requesting_job">Requesting Job from DVM</string>
</resources>