Display statuses if NIP05 succeeds.

This commit is contained in:
Vitor Pamplona
2023-08-24 15:57:18 -04:00
parent fe6fb8470d
commit b3bb85cd53
4 changed files with 149 additions and 28 deletions
@@ -15,7 +15,9 @@ import com.vitorpamplona.quartz.encoders.decodePublicKeyAsHexOrNull
import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.events.* import com.vitorpamplona.quartz.events.*
import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.TimeUtils
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableSet import kotlinx.collections.immutable.toImmutableSet
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
@@ -343,7 +345,27 @@ object LocalCache {
private fun consume(event: PinListEvent) { consumeBaseReplaceable(event) } private fun consume(event: PinListEvent) { consumeBaseReplaceable(event) }
private fun consume(event: RelaySetEvent) { consumeBaseReplaceable(event) } private fun consume(event: RelaySetEvent) { consumeBaseReplaceable(event) }
private fun consume(event: AudioTrackEvent) { consumeBaseReplaceable(event) } private fun consume(event: AudioTrackEvent) { consumeBaseReplaceable(event) }
private fun consume(event: StatusEvent, relay: Relay?) { consumeBaseReplaceable(event) } private fun consume(event: StatusEvent, relay: Relay?) {
val version = getOrCreateNote(event.id)
val note = getOrCreateAddressableNote(event.address())
val author = getOrCreateUser(event.pubKey)
if (version.event == null) {
version.loadEvent(event, author, emptyList())
version.moveAllReferencesTo(note)
}
// Already processed this event.
if (note.event?.id() == event.id()) return
if (event.createdAt > (note.createdAt() ?: 0)) {
note.loadEvent(event, author, emptyList())
author.liveSet?.statuses?.invalidateData()
refreshObservers(note)
}
}
fun consume(event: BadgeDefinitionEvent) { consumeBaseReplaceable(event) } fun consume(event: BadgeDefinitionEvent) { consumeBaseReplaceable(event) }
@@ -1127,6 +1149,18 @@ object LocalCache {
} }
} }
suspend fun findStatusesForUser(user: User): ImmutableList<AddressableNote> {
checkNotInMainThread()
return addressables.filter {
val noteEvent = it.value.event
(noteEvent is StatusEvent && noteEvent.pubKey == user.pubkeyHex && !noteEvent.isExpired() && noteEvent.content.isNotBlank())
}.values
.sortedWith(compareBy({ it.event?.expiration() ?: it.event?.createdAt() }, { it.idHex }))
.reversed()
.toImmutableList()
}
fun cleanObservers() { fun cleanObservers() {
notes.forEach { notes.forEach {
it.value.clearLive() it.value.clearLive()
@@ -392,11 +392,16 @@ class UserLiveSet(u: User) {
val metadata: UserLiveData = UserLiveData(u) val metadata: UserLiveData = UserLiveData(u)
val zaps: UserLiveData = UserLiveData(u) val zaps: UserLiveData = UserLiveData(u)
val bookmarks: UserLiveData = UserLiveData(u) val bookmarks: UserLiveData = UserLiveData(u)
val statuses: UserLiveData = UserLiveData(u)
val profilePictureChanges = metadata.map { val profilePictureChanges = metadata.map {
it.user.profilePicture() it.user.profilePicture()
}.distinctUntilChanged() }.distinctUntilChanged()
val nip05Changes = metadata.map {
it.user.nip05()
}.distinctUntilChanged()
val userMetadataInfo = metadata.map { val userMetadataInfo = metadata.map {
it.user.info it.user.info
}.distinctUntilChanged() }.distinctUntilChanged()
@@ -5,38 +5,48 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.ClickableText import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.Icon
import androidx.compose.material.LocalTextStyle import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.map import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.Nip05Verifier import com.vitorpamplona.amethyst.service.Nip05Verifier
import com.vitorpamplona.amethyst.ui.note.LoadAnyAddressableNote
import com.vitorpamplona.amethyst.ui.note.NIP05CheckingIcon import com.vitorpamplona.amethyst.ui.note.NIP05CheckingIcon
import com.vitorpamplona.amethyst.ui.note.NIP05FailedVerification import com.vitorpamplona.amethyst.ui.note.NIP05FailedVerification
import com.vitorpamplona.amethyst.ui.note.NIP05VerifiedIcon import com.vitorpamplona.amethyst.ui.note.NIP05VerifiedIcon
import com.vitorpamplona.amethyst.ui.theme.Font14SP
import com.vitorpamplona.amethyst.ui.theme.NIP05IconSize import com.vitorpamplona.amethyst.ui.theme.NIP05IconSize
import com.vitorpamplona.amethyst.ui.theme.Size16Modifier import com.vitorpamplona.amethyst.ui.theme.Size16Modifier
import com.vitorpamplona.amethyst.ui.theme.Size18Modifier
import com.vitorpamplona.amethyst.ui.theme.nip05 import com.vitorpamplona.amethyst.ui.theme.nip05
import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.quartz.events.AddressableEvent
import com.vitorpamplona.quartz.events.UserMetadata import com.vitorpamplona.quartz.events.UserMetadata
import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.TimeUtils
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.time.Duration.Companion.seconds
@Composable @Composable
fun nip05VerificationAsAState(userMetadata: UserMetadata, pubkeyHex: String): MutableState<Boolean?> { fun nip05VerificationAsAState(userMetadata: UserMetadata, pubkeyHex: String): MutableState<Boolean?> {
@@ -94,12 +104,7 @@ fun nip05VerificationAsAState(userMetadata: UserMetadata, pubkeyHex: String): Mu
@Composable @Composable
fun ObserveDisplayNip05Status(baseNote: Note, columnModifier: Modifier = Modifier) { fun ObserveDisplayNip05Status(baseNote: Note, columnModifier: Modifier = Modifier) {
val noteState by baseNote.live().metadata.observeAsState() val author by baseNote.live().authorChanges.observeAsState()
val author by remember(noteState) {
derivedStateOf {
noteState?.note?.author
}
}
author?.let { author?.let {
ObserveDisplayNip05Status(it, columnModifier) ObserveDisplayNip05Status(it, columnModifier)
@@ -108,35 +113,88 @@ fun ObserveDisplayNip05Status(baseNote: Note, columnModifier: Modifier = Modifie
@Composable @Composable
fun ObserveDisplayNip05Status(baseUser: User, columnModifier: Modifier = Modifier) { fun ObserveDisplayNip05Status(baseUser: User, columnModifier: Modifier = Modifier) {
val nip05 by baseUser.live().metadata.map { val nip05 by baseUser.live().nip05Changes.observeAsState(baseUser.nip05())
it.user.nip05()
}.observeAsState(baseUser.nip05())
Crossfade(targetState = nip05, modifier = columnModifier) { LoadAnyAddressableNote(baseUser) { statuses ->
if (it != null) { Crossfade(targetState = nip05, modifier = columnModifier, label = "ObserveDisplayNip05StatusCrossfade") {
DisplayNIP05Line(it, baseUser, columnModifier) if (it != null) {
} else { VerifyAndDisplayNIP05OrStatusLine(it, statuses, baseUser, columnModifier)
Text( } else {
text = baseUser.pubkeyDisplayHex(), Text(
color = MaterialTheme.colors.placeholderText, text = baseUser.pubkeyDisplayHex(),
maxLines = 1, color = MaterialTheme.colors.placeholderText,
overflow = TextOverflow.Ellipsis, maxLines = 1,
modifier = columnModifier overflow = TextOverflow.Ellipsis,
) modifier = columnModifier
)
}
} }
} }
} }
@Composable @Composable
private fun DisplayNIP05Line(nip05: String, baseUser: User, columnModifier: Modifier = Modifier) { private fun VerifyAndDisplayNIP05OrStatusLine(
nip05: String,
statuses: ImmutableList<AddressableNote>,
baseUser: User,
columnModifier: Modifier = Modifier
) {
var displayStatusOrNIP05 by remember {
mutableStateOf<Int>(0)
}
val nip05Verified = nip05VerificationAsAState(baseUser.info!!, baseUser.pubkeyHex)
Column(modifier = columnModifier) { Column(modifier = columnModifier) {
val nip05Verified = nip05VerificationAsAState(baseUser.info!!, baseUser.pubkeyHex) Crossfade(targetState = displayStatusOrNIP05, label = "NIP05StatusRotationCrossfade") {
Crossfade(targetState = nip05Verified) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
DisplayNIP05(nip05, it) if (statuses.isEmpty()) {
DisplayNIP05(nip05, nip05Verified)
} else if (nip05Verified.value != true || it < 0 || it >= statuses.size) {
DisplayNIP05(nip05, nip05Verified)
} else {
DisplayStatus(statuses[it])
}
} }
} }
} }
if (statuses.size > 1) {
LaunchedEffect(Unit) {
while (true) {
delay(10.seconds)
displayStatusOrNIP05 = ((displayStatusOrNIP05 + 1) % (statuses.size + 1))
}
}
}
}
@Composable
fun DisplayStatus(addressableNote: AddressableNote) {
val noteState by addressableNote.live().metadata.observeAsState()
val content = remember(noteState) { addressableNote.event?.content() ?: "" }
val type = remember(noteState) {
(addressableNote.event as? AddressableEvent)?.dTag() ?: ""
}
when (type) {
"music" -> Icon(
painter = painterResource(id = R.drawable.tunestr),
null,
modifier = Size18Modifier,
tint = MaterialTheme.colors.placeholderText
)
else -> {}
}
Text(
text = content,
fontSize = Font14SP,
color = MaterialTheme.colors.placeholderText,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
} }
@Composable @Composable
@@ -1052,7 +1052,7 @@ private fun NoteBody(
) )
} }
Spacer(modifier = StdVertSpacer) Spacer(modifier = Modifier.height(3.dp))
if (!makeItShort) { if (!makeItShort) {
ReplyRow( ReplyRow(
@@ -2506,6 +2506,30 @@ fun SecondUserInfoRow(
} }
} }
@Composable
fun LoadAnyAddressableNote(
user: User,
content: @Composable (ImmutableList<AddressableNote>) -> Unit
) {
var statuses: ImmutableList<AddressableNote> by remember {
mutableStateOf(persistentListOf())
}
val userStatus = user.live().statuses.observeAsState()
LaunchedEffect(key1 = userStatus) {
launch(Dispatchers.IO) {
val myUser = userStatus.value?.user ?: return@launch
val newStatuses = LocalCache.findStatusesForUser(myUser)
if (!equalImmutableLists(statuses, newStatuses)) {
statuses = newStatuses
}
}
}
content(statuses)
}
@Composable @Composable
fun DisplayLocation(geohash: String, nav: (String) -> Unit) { fun DisplayLocation(geohash: String, nav: (String) -> Unit) {
val context = LocalContext.current val context = LocalContext.current