Displays links in the Status if they are present.

This commit is contained in:
Vitor Pamplona
2023-08-24 17:58:34 -04:00
parent 5b596d2a56
commit b57dd98059
8 changed files with 128 additions and 16 deletions
@@ -6,9 +6,12 @@ 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.Icon
import androidx.compose.material.IconButton
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.material.icons.Icons
import androidx.compose.material.icons.filled.OpenInNew
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
@@ -31,14 +34,19 @@ 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.LoadAddressableNote
import com.vitorpamplona.amethyst.ui.note.LoadStatuses import com.vitorpamplona.amethyst.ui.note.LoadStatuses
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.note.routeFor
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.Font14SP 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.Size15Modifier
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.Size18Modifier
import com.vitorpamplona.amethyst.ui.theme.lessImportantLink
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.AddressableEvent
@@ -105,21 +113,31 @@ fun nip05VerificationAsAState(userMetadata: UserMetadata, pubkeyHex: String): Mu
} }
@Composable @Composable
fun ObserveDisplayNip05Status(baseNote: Note, columnModifier: Modifier = Modifier) { fun ObserveDisplayNip05Status(
baseNote: Note,
columnModifier: Modifier = Modifier,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val author by baseNote.live().authorChanges.observeAsState() val author by baseNote.live().authorChanges.observeAsState()
author?.let { author?.let {
ObserveDisplayNip05Status(it, columnModifier) ObserveDisplayNip05Status(it, columnModifier, accountViewModel, nav)
} }
} }
@Composable @Composable
fun ObserveDisplayNip05Status(baseUser: User, columnModifier: Modifier = Modifier) { fun ObserveDisplayNip05Status(
baseUser: User,
columnModifier: Modifier = Modifier,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val nip05 by baseUser.live().nip05Changes.observeAsState(baseUser.nip05()) val nip05 by baseUser.live().nip05Changes.observeAsState(baseUser.nip05())
LoadStatuses(baseUser) { statuses -> LoadStatuses(baseUser) { statuses ->
Crossfade(targetState = nip05, modifier = columnModifier, label = "ObserveDisplayNip05StatusCrossfade") { Crossfade(targetState = nip05, modifier = columnModifier, label = "ObserveDisplayNip05StatusCrossfade") {
VerifyAndDisplayNIP05OrStatusLine(it, statuses, baseUser, columnModifier) VerifyAndDisplayNIP05OrStatusLine(it, statuses, baseUser, columnModifier, accountViewModel, nav)
} }
} }
} }
@@ -129,7 +147,9 @@ private fun VerifyAndDisplayNIP05OrStatusLine(
nip05: String?, nip05: String?,
statuses: ImmutableList<AddressableNote>, statuses: ImmutableList<AddressableNote>,
baseUser: User, baseUser: User,
columnModifier: Modifier = Modifier columnModifier: Modifier = Modifier,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) { ) {
Column(modifier = columnModifier) { Column(modifier = columnModifier) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
@@ -139,13 +159,13 @@ private fun VerifyAndDisplayNIP05OrStatusLine(
if (nip05Verified.value != true) { if (nip05Verified.value != true) {
DisplayNIP05(nip05, nip05Verified) DisplayNIP05(nip05, nip05Verified)
} else if (!statuses.isEmpty()) { } else if (!statuses.isEmpty()) {
RotateStatuses(statuses) RotateStatuses(statuses, accountViewModel, nav)
} else { } else {
DisplayNIP05(nip05, nip05Verified) DisplayNIP05(nip05, nip05Verified)
} }
} else { } else {
if (!statuses.isEmpty()) { if (!statuses.isEmpty()) {
RotateStatuses(statuses) RotateStatuses(statuses, accountViewModel, nav)
} else { } else {
DisplayUsersNpub(baseUser.pubkeyDisplayHex()) DisplayUsersNpub(baseUser.pubkeyDisplayHex())
} }
@@ -155,12 +175,16 @@ private fun VerifyAndDisplayNIP05OrStatusLine(
} }
@Composable @Composable
fun RotateStatuses(statuses: ImmutableList<AddressableNote>) { fun RotateStatuses(
statuses: ImmutableList<AddressableNote>,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
var indexToDisplay by remember { var indexToDisplay by remember {
mutableIntStateOf(0) mutableIntStateOf(0)
} }
DisplayStatus(statuses[indexToDisplay]) DisplayStatus(statuses[indexToDisplay], accountViewModel, nav)
if (statuses.size > 1) { if (statuses.size > 1) {
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
@@ -184,13 +208,26 @@ fun DisplayUsersNpub(npub: String) {
} }
@Composable @Composable
fun DisplayStatus(addressableNote: AddressableNote) { fun DisplayStatus(
addressableNote: AddressableNote,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val noteState by addressableNote.live().metadata.observeAsState() val noteState by addressableNote.live().metadata.observeAsState()
val content = remember(noteState) { addressableNote.event?.content() ?: "" } val content = remember(noteState) { addressableNote.event?.content() ?: "" }
val type = remember(noteState) { val type = remember(noteState) {
(addressableNote.event as? AddressableEvent)?.dTag() ?: "" (addressableNote.event as? AddressableEvent)?.dTag() ?: ""
} }
val url = remember(noteState) {
addressableNote.event?.firstTaggedUrl()?.ifBlank { null }
}
val nostrATag = remember(noteState) {
addressableNote.event?.firstTaggedAddress()
}
val nostrHexID = remember(noteState) {
addressableNote.event?.firstTaggedEvent()?.ifBlank { null }
}
when (type) { when (type) {
"music" -> Icon( "music" -> Icon(
@@ -209,6 +246,63 @@ fun DisplayStatus(addressableNote: AddressableNote) {
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
if (url != null) {
val uri = LocalUriHandler.current
IconButton(
modifier = Size15Modifier,
onClick = { runCatching { uri.openUri(url.trim()) } }
) {
Icon(
imageVector = Icons.Default.OpenInNew,
null,
modifier = Size15Modifier,
tint = MaterialTheme.colors.lessImportantLink
)
}
} else if (nostrATag != null) {
LoadAddressableNote(nostrATag) { note ->
if (note != null) {
IconButton(
modifier = Size15Modifier,
onClick = {
routeFor(
note,
accountViewModel.userProfile()
)?.let { nav(it) }
}
) {
Icon(
imageVector = Icons.Default.OpenInNew,
null,
modifier = Size15Modifier,
tint = MaterialTheme.colors.lessImportantLink
)
}
}
}
} else if (nostrHexID != null) {
LoadNote(baseNoteHex = nostrHexID) {
if (it != null) {
IconButton(
modifier = Size15Modifier,
onClick = {
routeFor(
it,
accountViewModel.userProfile()
)?.let { nav(it) }
}
) {
Icon(
imageVector = Icons.Default.OpenInNew,
null,
modifier = Size15Modifier,
tint = MaterialTheme.colors.lessImportantLink
)
}
}
}
}
} }
@Composable @Composable
@@ -2487,7 +2487,7 @@ fun SecondUserInfoRow(
val noteAuthor = remember { note.author } ?: return val noteAuthor = remember { note.author } ?: return
Row(verticalAlignment = CenterVertically, modifier = UserNameMaxRowHeight) { Row(verticalAlignment = CenterVertically, modifier = UserNameMaxRowHeight) {
ObserveDisplayNip05Status(noteAuthor, remember { Modifier.weight(1f) }) ObserveDisplayNip05Status(noteAuthor, remember { Modifier.weight(1f) }, accountViewModel, nav)
val geo = remember { noteEvent.getGeoHash() } val geo = remember { noteEvent.getGeoHash() }
if (geo != null) { if (geo != null) {
@@ -328,7 +328,7 @@ fun NoteMaster(
} }
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
ObserveDisplayNip05Status(baseNote, remember { Modifier.weight(1f) }) ObserveDisplayNip05Status(baseNote, remember { Modifier.weight(1f) }, accountViewModel, nav)
val geo = remember { noteEvent.getGeoHash() } val geo = remember { noteEvent.getGeoHash() }
if (geo != null) { if (geo != null) {
@@ -504,7 +504,7 @@ fun ChatroomHeader(
Column(modifier = Modifier.padding(start = 10.dp)) { Column(modifier = Modifier.padding(start = 10.dp)) {
UsernameDisplay(baseUser) UsernameDisplay(baseUser)
ObserveDisplayNip05Status(baseUser) ObserveDisplayNip05Status(baseUser, accountViewModel = accountViewModel, nav = nav)
} }
} }
} }
@@ -374,7 +374,9 @@ private fun RenderAuthorInformation(
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
ObserveDisplayNip05Status( ObserveDisplayNip05Status(
remember { note.author!! }, remember { note.author!! },
remember { Modifier.weight(1f) } remember { Modifier.weight(1f) },
accountViewModel,
nav = nav
) )
} }
Row( Row(
@@ -69,6 +69,16 @@ open class Event(
override fun taggedUrls() = tags.filter { it.size > 1 && it[0] == "r" }.map { it[1] } override fun taggedUrls() = tags.filter { it.size > 1 && it[0] == "r" }.map { it[1] }
override fun firstTaggedUser() = tags.firstOrNull { it.size > 1 && it[0] == "p" }?.let { it[1] }
override fun firstTaggedEvent() = tags.firstOrNull { it.size > 1 && it[0] == "e" }?.let { it[1] }
override fun firstTaggedUrl() = tags.firstOrNull { it.size > 1 && it[0] == "r" }?.let { it[1] }
override fun firstTaggedAddress() = tags.firstOrNull { it.size > 1 && it[0] == "r" }?.let {
val aTagValue = it[1]
val relay = it.getOrNull(2)
ATag.parse(aTagValue, relay)
}
override fun taggedEmojis() = tags.filter { it.size > 2 && it[0] == "emoji" }.map { EmojiUrl(it[1], it[2]) } override fun taggedEmojis() = tags.filter { it.size > 2 && it[0] == "emoji" }.map { EmojiUrl(it[1], it[2]) }
override fun isSensitive() = tags.any { override fun isSensitive() = tags.any {
@@ -68,6 +68,11 @@ interface EventInterface {
fun taggedEvents(): List<HexKey> fun taggedEvents(): List<HexKey>
fun taggedUrls(): List<String> fun taggedUrls(): List<String>
fun firstTaggedAddress(): ATag?
fun firstTaggedUser(): HexKey?
fun firstTaggedEvent(): HexKey?
fun firstTaggedUrl(): String?
fun taggedEmojis(): List<EmojiUrl> fun taggedEmojis(): List<EmojiUrl>
fun matchTag1With(text: String): Boolean fun matchTag1With(text: String): Boolean
fun isExpired(): Boolean fun isExpired(): Boolean
@@ -44,10 +44,11 @@ class StatusEvent(
privateKey: ByteArray, privateKey: ByteArray,
createdAt: Long = TimeUtils.now() createdAt: Long = TimeUtils.now()
): StatusEvent { ): StatusEvent {
val tags = event.tags.plus(listOf(listOf("r", "http://amethyst.social")))
val pubKey = event.pubKey() val pubKey = event.pubKey()
val id = generateId(pubKey, createdAt, kind, event.tags, newStatus) val id = generateId(pubKey, createdAt, kind, tags, newStatus)
val sig = CryptoUtils.sign(id, privateKey) val sig = CryptoUtils.sign(id, privateKey)
return StatusEvent(id.toHexKey(), pubKey, createdAt, event.tags, newStatus, sig.toHexKey()) return StatusEvent(id.toHexKey(), pubKey, createdAt, tags, newStatus, sig.toHexKey())
} }
} }
} }