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 new file mode 100644 index 000000000..98ea3a5b5 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt @@ -0,0 +1,193 @@ +package com.vitorpamplona.amethyst.ui.components + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.text.ClickableText +import androidx.compose.material.Icon +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.Downloading +import androidx.compose.material.icons.filled.Report +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.lnurl.Nip05Verifier +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.model.UserMetadata +import com.vitorpamplona.amethyst.ui.theme.Nip05 +import java.util.Date + +@Composable +fun nip05VerificationAsAState(user: UserMetadata, pubkeyHex: String): State { + var nip05Verified = remember { mutableStateOf(null) } + + LaunchedEffect(key1 = user) { + user.nip05?.ifBlank { null }?.let { nip05 -> + val now = Date().time / 1000 + if ((user.nip05LastVerificationTime ?: 0) > (now - 60 * 60)) { // 1hour + nip05Verified.value = user.nip05Verified + } else { + Nip05Verifier().verifyNip05( + nip05, + onSuccess = { + // Marks user as verified + if (it == pubkeyHex) { + user.nip05Verified = true + user.nip05LastVerificationTime = now + nip05Verified.value = true + } else { + user.nip05Verified = false + user.nip05LastVerificationTime = 0 + nip05Verified.value = false + } + }, + onError = { + user.nip05LastVerificationTime = 0 + user.nip05Verified = false + nip05Verified.value = false + } + ) + } + } + } + + return nip05Verified +} + +@Composable +fun ObserveDisplayNip05Status(baseNote: Note) { + val noteState by baseNote.live().metadata.observeAsState() + val note = noteState?.note ?: return + + val author = note.author + if (author != null) + ObserveDisplayNip05Status(author) +} + +@Composable +fun ObserveDisplayNip05Status(baseUser: User) { + val userState by baseUser.live().metadata.observeAsState() + val user = userState?.user ?: return + + val uri = LocalUriHandler.current + + user.nip05()?.let { nip05 -> + if (nip05.split("@").size == 2) { + val nip05Verified by nip05VerificationAsAState(user.info!!, user.pubkeyHex) + Row(verticalAlignment = Alignment.CenterVertically) { + if (nip05.split("@")[0] != "_") + Text( + text = AnnotatedString(nip05.split("@")[0]), + color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + + if (nip05Verified == null) { + Icon( + tint = Color.Yellow, + imageVector = Icons.Default.Downloading, + contentDescription = "Downloading", + modifier = Modifier + .size(14.dp) + .padding(top = 1.dp) + ) + } else if (nip05Verified == true) { + Icon( + painter = painterResource(R.drawable.ic_verified), + "NIP-05 Verified", + tint = Nip05.copy(0.52f), + modifier = Modifier + .size(14.dp) + .padding(top = 1.dp) + ) + } else { + Icon( + tint = Color.Red, + imageVector = Icons.Default.Report, + contentDescription = "Invalid Nip05", + modifier = Modifier + .size(14.dp) + .padding(top = 1.dp) + ) + } + + ClickableText( + text = AnnotatedString(nip05.split("@")[1]), + onClick = { nip05.let { runCatching { uri.openUri("https://${it.split("@")[1]}") } } }, + style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary.copy(0.52f)), + maxLines = 1, + overflow = TextOverflow.Visible + ) + } + } + } +} + +@Composable +fun DisplayNip05ProfileStatus(user: User) { + val uri = LocalUriHandler.current + + user.nip05()?.let { nip05 -> + if (nip05.split("@").size == 2) { + val nip05Verified by nip05VerificationAsAState(user.info!!, user.pubkeyHex) + Row(verticalAlignment = Alignment.CenterVertically) { + if (nip05Verified == null) { + Icon( + tint = Color.Yellow, + imageVector = Icons.Default.Downloading, + contentDescription = "Downloading", + modifier = Modifier.size(16.dp) + ) + } else if (nip05Verified == true) { + Icon( + painter = painterResource(R.drawable.ic_verified), + "NIP-05 Verified", + tint = Nip05, + modifier = Modifier.size(16.dp) + ) + } else { + Icon( + tint = Color.Red, + imageVector = Icons.Default.Report, + contentDescription = "Invalid Nip05", + modifier = Modifier.size(16.dp) + ) + } + + Text( + text = AnnotatedString(nip05.split("@")[0] + "@"), + modifier = Modifier.padding(top = 1.dp, bottom = 1.dp, start = 5.dp), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + + ClickableText( + text = AnnotatedString(nip05.split("@")[1]), + onClick = { nip05.let { runCatching { uri.openUri("https://${it.split("@")[1]}") } } }, + style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary), + modifier = Modifier.padding(top = 1.dp, bottom = 1.dp), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + } +} \ No newline at end of file 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 f9a828b90..2bee74a65 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 @@ -40,8 +40,7 @@ import com.vitorpamplona.amethyst.service.model.RepostEvent import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy import com.vitorpamplona.amethyst.ui.components.ResizeImage import com.vitorpamplona.amethyst.ui.components.TranslateableRichTextViewer -import com.vitorpamplona.amethyst.ui.screen.loggedIn.DisplayNip05Status -import com.vitorpamplona.amethyst.ui.screen.loggedIn.ObserveDisplayNip05Status +import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.Following import nostr.postr.events.TextNoteEvent 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 5be205180..451071a98 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 @@ -46,10 +46,8 @@ import com.vitorpamplona.amethyst.ui.note.NoteDropDownMenu import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay import com.vitorpamplona.amethyst.ui.note.ReactionsRow import com.vitorpamplona.amethyst.ui.note.timeAgo -import com.vitorpamplona.amethyst.ui.note.timeAgoLong import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.DisplayNip05Status -import com.vitorpamplona.amethyst.ui.screen.loggedIn.ObserveDisplayNip05Status +import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status @Composable fun ThreadFeedView(noteId: String, viewModel: FeedViewModel, accountViewModel: AccountViewModel, navController: NavController) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt index a858bd18e..7853b597e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt @@ -1,8 +1,5 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn -import android.graphics.Rect -import android.view.ViewTreeObserver -import android.widget.Toast import androidx.compose.foundation.* import androidx.compose.foundation.gestures.scrollBy import androidx.compose.foundation.layout.* @@ -12,12 +9,10 @@ import androidx.compose.foundation.text.ClickableText import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Bolt -import androidx.compose.material.icons.filled.Downloading import androidx.compose.material.icons.filled.EditNote import androidx.compose.material.icons.filled.Key import androidx.compose.material.icons.filled.Link import androidx.compose.material.icons.filled.MoreVert -import androidx.compose.material.icons.filled.Report import androidx.compose.material.icons.filled.Share import androidx.compose.runtime.* import androidx.compose.runtime.livedata.observeAsState @@ -35,13 +30,10 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalUriHandler -import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.platform.UriHandler import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -54,16 +46,14 @@ import com.google.accompanist.pager.HorizontalPager import com.google.accompanist.pager.pagerTabIndicatorOffset import com.google.accompanist.pager.rememberPagerState import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.lnurl.Nip05Verifier import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.UserMetadata import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource import com.vitorpamplona.amethyst.service.model.ReportEvent import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy import com.vitorpamplona.amethyst.ui.components.ResizeImage import com.vitorpamplona.amethyst.ui.actions.NewUserMetadataView +import com.vitorpamplona.amethyst.ui.components.DisplayNip05ProfileStatus import com.vitorpamplona.amethyst.ui.components.InvoiceRequest import com.vitorpamplona.amethyst.ui.dal.UserProfileFollowersFeedFilter import com.vitorpamplona.amethyst.ui.dal.UserProfileFollowsFeedFilter @@ -71,7 +61,6 @@ import com.vitorpamplona.amethyst.ui.dal.UserProfileConversationsFeedFilter import com.vitorpamplona.amethyst.ui.dal.UserProfileNewThreadFeedFilter import com.vitorpamplona.amethyst.ui.dal.UserProfileReportsFeedFilter import com.vitorpamplona.amethyst.ui.dal.UserProfileZapsFeedFilter -import com.vitorpamplona.amethyst.ui.navigation.Keyboard import com.vitorpamplona.amethyst.ui.note.UserPicture import com.vitorpamplona.amethyst.ui.note.showAmount import com.vitorpamplona.amethyst.ui.screen.FeedView @@ -85,10 +74,7 @@ import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileZapsFeedViewModel import com.vitorpamplona.amethyst.ui.screen.RelayFeedView import com.vitorpamplona.amethyst.ui.screen.RelayFeedViewModel import com.vitorpamplona.amethyst.ui.screen.UserFeedView -import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange -import com.vitorpamplona.amethyst.ui.theme.Nip05 -import java.util.Date import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import nostr.postr.toNsec @@ -367,44 +353,6 @@ private fun ProfileHeader( } } - -@Composable -fun nip05VerificationAsAState(user: UserMetadata, pubkeyHex: String): State { - var nip05Verified = remember { mutableStateOf(null) } - - LaunchedEffect(key1 = user) { - user.nip05?.ifBlank { null }?.let { nip05 -> - val now = Date().time / 1000 - if ((user.nip05LastVerificationTime ?: 0) > (now - 60*60)) { // 1hour - nip05Verified.value = user.nip05Verified - } else { - Nip05Verifier().verifyNip05( - nip05, - onSuccess = { - // Marks user as verified - if (it == pubkeyHex) { - user.nip05Verified = true - user.nip05LastVerificationTime = now - nip05Verified.value = true - } else { - user.nip05Verified = false - user.nip05LastVerificationTime = 0 - nip05Verified.value = false - } - }, - onError = { - user.nip05LastVerificationTime = 0 - user.nip05Verified = false - nip05Verified.value = false - } - ) - } - } - } - - return nip05Verified -} - @Composable private fun DrawAdditionalInfo(baseUser: User, account: Account) { val userState by baseUser.live().metadata.observeAsState() @@ -430,7 +378,7 @@ private fun DrawAdditionalInfo(baseUser: User, account: Account) { } } - DisplayNip05Status(user) + DisplayNip05ProfileStatus(user) val website = user.info?.website if (!website.isNullOrEmpty()) { @@ -492,127 +440,6 @@ private fun DrawAdditionalInfo(baseUser: User, account: Account) { } } -@Composable -fun ObserveDisplayNip05Status(baseNote: Note) { - val noteState by baseNote.live().metadata.observeAsState() - val note = noteState?.note ?: return - - val author = note.author - if (author != null) - ObserveDisplayNip05Status(author) -} - -@Composable -fun ObserveDisplayNip05Status(baseUser: User) { - val userState by baseUser.live().metadata.observeAsState() - val user = userState?.user ?: return - - val uri = LocalUriHandler.current - - user.nip05()?.let { nip05 -> - if (nip05.split("@").size == 2) { - val nip05Verified by nip05VerificationAsAState(user.info!!, user.pubkeyHex) - Row(verticalAlignment = Alignment.CenterVertically) { - if (nip05.split("@")[0] != "_") - Text( - text = AnnotatedString(nip05.split("@")[0]), - color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f), - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - - if (nip05Verified == null) { - Icon( - tint = Color.Yellow, - imageVector = Icons.Default.Downloading, - contentDescription = "Downloading", - modifier = Modifier - .size(14.dp) - .padding(top = 1.dp) - ) - } else if (nip05Verified == true) { - Icon( - painter = painterResource(R.drawable.ic_verified), - "NIP-05 Verified", - tint = Nip05.copy(0.52f), - modifier = Modifier - .size(14.dp) - .padding(top = 1.dp) - ) - } else { - Icon( - tint = Color.Red, - imageVector = Icons.Default.Report, - contentDescription = "Invalid Nip05", - modifier = Modifier - .size(14.dp) - .padding(top = 1.dp) - ) - } - - ClickableText( - text = AnnotatedString(nip05.split("@")[1]), - onClick = { nip05.let { runCatching { uri.openUri("https://${it.split("@")[1]}") } } }, - style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary.copy(0.52f)), - maxLines = 1, - overflow = TextOverflow.Visible - ) - } - } - } -} - -@Composable -fun DisplayNip05Status(user: User) { - val uri = LocalUriHandler.current - - user.nip05()?.let { nip05 -> - if (nip05.split("@").size == 2) { - val nip05Verified by nip05VerificationAsAState(user.info!!, user.pubkeyHex) - Row(verticalAlignment = Alignment.CenterVertically) { - if (nip05Verified == null) { - Icon( - tint = Color.Yellow, - imageVector = Icons.Default.Downloading, - contentDescription = "Downloading", - modifier = Modifier.size(16.dp) - ) - } else if (nip05Verified == true) { - Icon( - painter = painterResource(R.drawable.ic_verified), - "NIP-05 Verified", - tint = Nip05, - modifier = Modifier.size(16.dp) - ) - } else { - Icon( - tint = Color.Red, - imageVector = Icons.Default.Report, - contentDescription = "Invalid Nip05", - modifier = Modifier.size(16.dp) - ) - } - - Text( - text = AnnotatedString(nip05.split("@")[0] + "@"), - modifier = Modifier.padding(top = 1.dp, bottom = 1.dp, start = 5.dp), - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - - ClickableText( - text = AnnotatedString(nip05.split("@")[1]), - onClick = { nip05.let { runCatching { uri.openUri("https://${it.split("@")[1]}") } } }, - style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary), - modifier = Modifier.padding(top = 1.dp, bottom = 1.dp), - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } - } - } -} - @Composable private fun DrawBanner(baseUser: User) { val userState by baseUser.live().metadata.observeAsState()