diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt index 620997127..2acdcfaff 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt @@ -6,9 +6,12 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.ClickableText import androidx.compose.material.Icon +import androidx.compose.material.IconButton import androidx.compose.material.LocalTextStyle import androidx.compose.material.MaterialTheme 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.LaunchedEffect 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.User 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.NIP05CheckingIcon import com.vitorpamplona.amethyst.ui.note.NIP05FailedVerification 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.NIP05IconSize +import com.vitorpamplona.amethyst.ui.theme.Size15Modifier import com.vitorpamplona.amethyst.ui.theme.Size16Modifier 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.placeholderText import com.vitorpamplona.quartz.events.AddressableEvent @@ -105,21 +113,31 @@ fun nip05VerificationAsAState(userMetadata: UserMetadata, pubkeyHex: String): Mu } @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() author?.let { - ObserveDisplayNip05Status(it, columnModifier) + ObserveDisplayNip05Status(it, columnModifier, accountViewModel, nav) } } @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()) LoadStatuses(baseUser) { statuses -> 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?, statuses: ImmutableList, baseUser: User, - columnModifier: Modifier = Modifier + columnModifier: Modifier = Modifier, + accountViewModel: AccountViewModel, + nav: (String) -> Unit ) { Column(modifier = columnModifier) { Row(verticalAlignment = Alignment.CenterVertically) { @@ -139,13 +159,13 @@ private fun VerifyAndDisplayNIP05OrStatusLine( if (nip05Verified.value != true) { DisplayNIP05(nip05, nip05Verified) } else if (!statuses.isEmpty()) { - RotateStatuses(statuses) + RotateStatuses(statuses, accountViewModel, nav) } else { DisplayNIP05(nip05, nip05Verified) } } else { if (!statuses.isEmpty()) { - RotateStatuses(statuses) + RotateStatuses(statuses, accountViewModel, nav) } else { DisplayUsersNpub(baseUser.pubkeyDisplayHex()) } @@ -155,12 +175,16 @@ private fun VerifyAndDisplayNIP05OrStatusLine( } @Composable -fun RotateStatuses(statuses: ImmutableList) { +fun RotateStatuses( + statuses: ImmutableList, + accountViewModel: AccountViewModel, + nav: (String) -> Unit +) { var indexToDisplay by remember { mutableIntStateOf(0) } - DisplayStatus(statuses[indexToDisplay]) + DisplayStatus(statuses[indexToDisplay], accountViewModel, nav) if (statuses.size > 1) { LaunchedEffect(Unit) { @@ -184,13 +208,26 @@ fun DisplayUsersNpub(npub: String) { } @Composable -fun DisplayStatus(addressableNote: AddressableNote) { +fun DisplayStatus( + addressableNote: AddressableNote, + accountViewModel: AccountViewModel, + nav: (String) -> Unit +) { val noteState by addressableNote.live().metadata.observeAsState() val content = remember(noteState) { addressableNote.event?.content() ?: "" } val type = remember(noteState) { (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) { "music" -> Icon( @@ -209,6 +246,63 @@ fun DisplayStatus(addressableNote: AddressableNote) { maxLines = 1, 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index dc856596c..c5e2971a5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -2487,7 +2487,7 @@ fun SecondUserInfoRow( val noteAuthor = remember { note.author } ?: return Row(verticalAlignment = CenterVertically, modifier = UserNameMaxRowHeight) { - ObserveDisplayNip05Status(noteAuthor, remember { Modifier.weight(1f) }) + ObserveDisplayNip05Status(noteAuthor, remember { Modifier.weight(1f) }, accountViewModel, nav) val geo = remember { noteEvent.getGeoHash() } if (geo != null) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt index 580c14d5b..a6b38f399 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt @@ -328,7 +328,7 @@ fun NoteMaster( } Row(verticalAlignment = Alignment.CenterVertically) { - ObserveDisplayNip05Status(baseNote, remember { Modifier.weight(1f) }) + ObserveDisplayNip05Status(baseNote, remember { Modifier.weight(1f) }, accountViewModel, nav) val geo = remember { noteEvent.getGeoHash() } if (geo != null) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt index 059c5b624..eed164283 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt @@ -504,7 +504,7 @@ fun ChatroomHeader( Column(modifier = Modifier.padding(start = 10.dp)) { UsernameDisplay(baseUser) - ObserveDisplayNip05Status(baseUser) + ObserveDisplayNip05Status(baseUser, accountViewModel = accountViewModel, nav = nav) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt index 3970c99d2..1ce0efd78 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt @@ -374,7 +374,9 @@ private fun RenderAuthorInformation( Row(verticalAlignment = Alignment.CenterVertically) { ObserveDisplayNip05Status( remember { note.author!! }, - remember { Modifier.weight(1f) } + remember { Modifier.weight(1f) }, + accountViewModel, + nav = nav ) } Row( diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt index 7e102bf12..7026382df 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt @@ -69,6 +69,16 @@ open class Event( 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 isSensitive() = tags.any { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/EventInterface.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/EventInterface.kt index e8cec5744..61ba9dbc6 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/EventInterface.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/EventInterface.kt @@ -68,6 +68,11 @@ interface EventInterface { fun taggedEvents(): List fun taggedUrls(): List + fun firstTaggedAddress(): ATag? + fun firstTaggedUser(): HexKey? + fun firstTaggedEvent(): HexKey? + fun firstTaggedUrl(): String? + fun taggedEmojis(): List fun matchTag1With(text: String): Boolean fun isExpired(): Boolean diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/StatusEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/StatusEvent.kt index c326a6f47..325f4edf0 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/StatusEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/StatusEvent.kt @@ -44,10 +44,11 @@ class StatusEvent( privateKey: ByteArray, createdAt: Long = TimeUtils.now() ): StatusEvent { + val tags = event.tags.plus(listOf(listOf("r", "http://amethyst.social"))) 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) - return StatusEvent(id.toHexKey(), pubKey, createdAt, event.tags, newStatus, sig.toHexKey()) + return StatusEvent(id.toHexKey(), pubKey, createdAt, tags, newStatus, sig.toHexKey()) } } }