diff --git a/app/build.gradle b/app/build.gradle index 3ed45fcfe..a441e0f38 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -93,6 +93,9 @@ dependencies { // Json Serialization implementation 'com.fasterxml.jackson.module:jackson-module-kotlin:2.14.2' + // Robohash for Avatars + implementation group: 'com.github.vitorpamplona', name: 'android-robohash', version: 'master-SNAPSHOT', ext: 'aar' + // link preview implementation 'tw.com.oneup.www:Baha-UrlPreview:1.0.1' implementation 'androidx.security:security-crypto-ktx:1.1.0-alpha04' diff --git a/app/src/main/java/com/vitorpamplona/amethyst/RoboHashCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/RoboHashCache.kt new file mode 100644 index 000000000..fe2976e80 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/RoboHashCache.kt @@ -0,0 +1,22 @@ +package com.vitorpamplona.amethyst + +import android.content.Context +import android.graphics.Bitmap +import android.util.LruCache +import java.util.UUID +import name.neuhalfen.projects.android.robohash.RoboHash + +object RoboHashCache { + + lateinit var robots: RoboHash + + fun get(context: Context, hash: String): Bitmap { + if (!this::robots.isInitialized) { + robots = RoboHash(context) + robots.useCache(LruCache(100)); + } + + return robots.imageForHandle(robots.calculateHandleFromUUID(UUID.nameUUIDFromBytes(hash.toByteArray()))) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt index 343f49f7b..df56d0f60 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt @@ -46,6 +46,7 @@ import coil.Coil import coil.compose.AsyncImage import coil.compose.rememberAsyncImagePainter import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.RoboHashCache import com.vitorpamplona.amethyst.service.NostrAccountDataSource import com.vitorpamplona.amethyst.service.NostrChannelDataSource import com.vitorpamplona.amethyst.service.NostrChatRoomDataSource @@ -82,7 +83,7 @@ fun MainTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) val account = accountState?.account ?: return val accountUserState by account.userProfile().liveMetadata.observeAsState() - val accountUser = accountUserState?.user + val accountUser = accountUserState?.user ?: return val relayViewModel: RelayPoolViewModel = viewModel { RelayPoolViewModel() } val connectedRelaysLiveData by relayViewModel.connectedRelaysLiveData.observeAsState() @@ -91,6 +92,7 @@ fun MainTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) val coroutineScope = rememberCoroutineScope() val context = LocalContext.current + val ctx = LocalContext.current.applicationContext var wantsToEditRelays by remember { mutableStateOf(false) @@ -196,8 +198,10 @@ fun MainTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) modifier = Modifier ) { AsyncImage( - model = accountUser?.profilePicture() ?: "https://robohash.org/ohno.png", - placeholder = rememberAsyncImagePainter("https://robohash.org/${accountUser?.pubkeyHex}.png"), + model = accountUser.profilePicture(), + placeholder = rememberAsyncImagePainter(RoboHashCache.get(ctx, accountUser.pubkeyHex)), + fallback = rememberAsyncImagePainter(RoboHashCache.get(ctx, accountUser.pubkeyHex)), + error = rememberAsyncImagePainter(RoboHashCache.get(ctx, accountUser.pubkeyHex)), contentDescription = "Profile Image", modifier = Modifier .width(34.dp) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt index 92c65b7af..b123ce781 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt @@ -71,6 +71,8 @@ import com.google.zxing.qrcode.encoder.ByteMatrix import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.PathFillType +import androidx.compose.ui.platform.LocalContext +import com.vitorpamplona.amethyst.RoboHashCache @Composable fun DrawerContent(navController: NavHostController, @@ -124,6 +126,8 @@ fun ProfileContent(baseAccountUser: User, modifier: Modifier = Modifier, scaffol val accountUserFollowsState by baseAccountUser.liveFollows.observeAsState() val accountUserFollows = accountUserFollowsState?.user ?: return + val ctx = LocalContext.current.applicationContext + Box { val banner = accountUser.info.banner if (banner != null && banner.isNotBlank()) { @@ -150,7 +154,9 @@ fun ProfileContent(baseAccountUser: User, modifier: Modifier = Modifier, scaffol AsyncImage( model = accountUser.profilePicture(), contentDescription = "Profile Image", - placeholder = rememberAsyncImagePainter("https://robohash.org/${accountUser.pubkeyHex}.png"), + placeholder = rememberAsyncImagePainter(RoboHashCache.get(ctx, accountUser.pubkeyHex)), + fallback = rememberAsyncImagePainter(RoboHashCache.get(ctx, accountUser.pubkeyHex)), + error = rememberAsyncImagePainter(RoboHashCache.get(ctx, accountUser.pubkeyHex)), modifier = Modifier .width(100.dp) .height(100.dp) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomCompose.kt index 14e5d4af4..e261eafd6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomCompose.kt @@ -151,7 +151,7 @@ fun ChannelName( AsyncImage( model = channelPicture, placeholder = channelPicturePlaceholder, - contentDescription = "Profile Image", + contentDescription = "Channel Image", modifier = Modifier .width(55.dp) .height(55.dp) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt index 8276f5d47..a13433e28 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt @@ -53,6 +53,7 @@ import coil.compose.AsyncImage import coil.compose.rememberAsyncImagePainter import com.google.accompanist.flowlayout.FlowRow import com.vitorpamplona.amethyst.NotificationCache +import com.vitorpamplona.amethyst.RoboHashCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent @@ -151,7 +152,7 @@ fun ChatroomMessageCompose(baseNote: Note, routeForLastRead: String?, innerQuote ) { val authorState by note.author!!.liveMetadata.observeAsState() - val author = authorState?.user + val author = authorState?.user!! if (innerQuote || author != accountUser && note.event is ChannelMessageEvent) { Row( @@ -160,8 +161,10 @@ fun ChatroomMessageCompose(baseNote: Note, routeForLastRead: String?, innerQuote modifier = Modifier.padding(top = 5.dp) ) { AsyncImage( - model = author?.profilePicture(), - placeholder = rememberAsyncImagePainter("https://robohash.org/${author?.pubkeyHex}.png"), + model = author.profilePicture(), + placeholder = rememberAsyncImagePainter(RoboHashCache.get(context, author.pubkeyHex)), + fallback = rememberAsyncImagePainter(RoboHashCache.get(context, author.pubkeyHex)), + error = rememberAsyncImagePainter(RoboHashCache.get(context, author.pubkeyHex)), contentDescription = "Profile Image", modifier = Modifier .width(25.dp) @@ -277,6 +280,7 @@ private fun RelayBadges(baseNote: Note) { val relaysToDisplay = if (expanded) noteRelays else noteRelays.take(3) val uri = LocalUriHandler.current + val ctx = LocalContext.current.applicationContext FlowRow(Modifier.padding(start = 10.dp)) { relaysToDisplay.forEach { @@ -284,9 +288,9 @@ private fun RelayBadges(baseNote: Note) { Box(Modifier.size(15.dp).padding(1.dp)) { AsyncImage( model = "https://${url}/favicon.ico", - placeholder = rememberAsyncImagePainter("https://robohash.org/$url.png"), - fallback = rememberAsyncImagePainter("https://robohash.org/$url.png"), - error = rememberAsyncImagePainter("https://robohash.org/$url.png"), + placeholder = rememberAsyncImagePainter(RoboHashCache.get(ctx, url)), + fallback = rememberAsyncImagePainter(RoboHashCache.get(ctx, url)), + error = rememberAsyncImagePainter(RoboHashCache.get(ctx, url)), contentDescription = "Relay Icon", colorFilter = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(0f) }), modifier = Modifier 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 1213fabcf..c05b0bff3 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 @@ -57,6 +57,7 @@ import coil.compose.rememberAsyncImagePainter import com.google.accompanist.flowlayout.FlowRow import com.vitorpamplona.amethyst.NotificationCache import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.RoboHashCache import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User @@ -306,6 +307,7 @@ private fun RelayBadges(baseNote: Note) { val relaysToDisplay = if (expanded) noteRelays else noteRelays.take(3) val uri = LocalUriHandler.current + val ctx = LocalContext.current.applicationContext FlowRow(Modifier.padding(top = 10.dp, start = 5.dp, end = 4.dp)) { relaysToDisplay.forEach { @@ -313,9 +315,9 @@ private fun RelayBadges(baseNote: Note) { Box(Modifier.size(15.dp).padding(1.dp)) { AsyncImage( model = "https://${url}/favicon.ico", - placeholder = rememberAsyncImagePainter("https://robohash.org/$url.png"), - fallback = rememberAsyncImagePainter("https://robohash.org/$url.png"), - error = rememberAsyncImagePainter("https://robohash.org/$url.png"), + placeholder = rememberAsyncImagePainter(RoboHashCache.get(ctx, url)), + fallback = rememberAsyncImagePainter(RoboHashCache.get(ctx, url)), + error = rememberAsyncImagePainter(RoboHashCache.get(ctx, url)), contentDescription = "Relay Icon", colorFilter = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(0f) }), modifier = Modifier @@ -373,15 +375,16 @@ fun NoteAuthorPicture( val author = note.author + val ctx = LocalContext.current.applicationContext + Box( Modifier .width(size) .height(size)) { if (author == null) { AsyncImage( - model = "https://robohash.org/ohno.png", + model = rememberAsyncImagePainter(RoboHashCache.get(ctx, "ohno")), contentDescription = "Unknown Author", - placeholder = rememberAsyncImagePainter("https://robohash.org/ohno.png"), modifier = pictureModifier .fillMaxSize(1f) .clip(shape = CircleShape) @@ -417,6 +420,8 @@ fun UserPicture( val userState by baseUser.liveMetadata.observeAsState() val user = userState?.user ?: return + val ctx = LocalContext.current.applicationContext + Box( Modifier .width(size) @@ -425,9 +430,9 @@ fun UserPicture( AsyncImage( model = user.profilePicture(), contentDescription = "Profile Image", - placeholder = rememberAsyncImagePainter("https://robohash.org/${user.pubkeyHex}.png"), - fallback = rememberAsyncImagePainter("https://robohash.org/${user.pubkeyHex}.png"), - error = rememberAsyncImagePainter("https://robohash.org/${user.pubkeyHex}.png"), + placeholder = rememberAsyncImagePainter(RoboHashCache.get(ctx, user.pubkeyHex)), + fallback = rememberAsyncImagePainter(RoboHashCache.get(ctx, user.pubkeyHex)), + error = rememberAsyncImagePainter(RoboHashCache.get(ctx, user.pubkeyHex)), modifier = pictureModifier .fillMaxSize(1f) .clip(shape = CircleShape) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/ShowQRDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/ShowQRDialog.kt index a3cf6b34b..87a8b6536 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/ShowQRDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/ShowQRDialog.kt @@ -27,6 +27,7 @@ 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.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -34,6 +35,7 @@ import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import coil.compose.AsyncImage import coil.compose.rememberAsyncImagePainter +import com.vitorpamplona.amethyst.RoboHashCache import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.actions.CloseButton import com.vitorpamplona.amethyst.ui.qrcode.QrCodeScanner @@ -43,6 +45,8 @@ import nostr.postr.toNpub fun ShowQRDialog(user: User, onScan: (String) -> Unit, onClose: () -> Unit) { var presenting by remember { mutableStateOf(true) } + val ctx = LocalContext.current.applicationContext + Dialog( onDismissRequest = onClose, properties = DialogProperties(usePlatformDefaultWidth = false) @@ -76,9 +80,11 @@ fun ShowQRDialog(user: User, onScan: (String) -> Unit, onClose: () -> Unit) { Column(modifier = Modifier.fillMaxWidth()) { Row(horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth()) { AsyncImage( - model = user.profilePicture() ?: "https://robohash.org/ohno.png", + model = user.profilePicture(), + placeholder = rememberAsyncImagePainter(RoboHashCache.get(ctx, user.pubkeyHex)), + fallback = rememberAsyncImagePainter(RoboHashCache.get(ctx, user.pubkeyHex)), + error = rememberAsyncImagePainter(RoboHashCache.get(ctx, user.pubkeyHex)), contentDescription = "Profile Image", - placeholder = rememberAsyncImagePainter("https://robohash.org/${user.pubkeyHex}.png"), modifier = Modifier .width(100.dp) .height(100.dp) 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 f918d30de..a3a7d2a20 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 @@ -124,6 +124,8 @@ fun ChatroomScreen(userId: String?, accountViewModel: AccountViewModel, navContr @Composable fun ChatroomHeader(baseUser: User, accountViewModel: AccountViewModel, navController: NavController) { + val ctx = LocalContext.current.applicationContext + Column(modifier = Modifier.clickable( onClick = { navController.navigate("User/${baseUser.pubkeyHex}") } ) @@ -132,11 +134,13 @@ fun ChatroomHeader(baseUser: User, accountViewModel: AccountViewModel, navContro Row(verticalAlignment = Alignment.CenterVertically) { val authorState by baseUser.liveMetadata.observeAsState() - val author = authorState?.user + val author = authorState?.user!! AsyncImage( - model = author?.profilePicture(), - placeholder = rememberAsyncImagePainter("https://robohash.org/${author?.pubkeyHex}.png"), + model = author.profilePicture(), + placeholder = rememberAsyncImagePainter(RoboHashCache.get(ctx, author.pubkeyHex)), + fallback = rememberAsyncImagePainter(RoboHashCache.get(ctx, author.pubkeyHex)), + error = rememberAsyncImagePainter(RoboHashCache.get(ctx, author.pubkeyHex)), contentDescription = "Profile Image", modifier = Modifier .width(35.dp)