From b3f2a032959697517e2a4241bae2fa6ac2412b1c Mon Sep 17 00:00:00 2001 From: maxmoney21m Date: Tue, 7 Mar 2023 23:45:40 +0800 Subject: [PATCH 1/7] Add RoboHash SVG generator --- .../ui/components/RoboHashAsyncImage.kt | 333 ++++++++++++++++++ 1 file changed, 333 insertions(+) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/components/RoboHashAsyncImage.kt diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RoboHashAsyncImage.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RoboHashAsyncImage.kt new file mode 100644 index 000000000..0b06e3f38 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RoboHashAsyncImage.kt @@ -0,0 +1,333 @@ +package com.vitorpamplona.amethyst.ui.components + +import android.content.Context +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.DefaultAlpha +import androidx.compose.ui.graphics.FilterQuality +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import coil.compose.AsyncImage +import coil.compose.AsyncImagePainter +import coil.request.ImageRequest +import java.nio.ByteBuffer +import java.security.MessageDigest + +private fun toHex(color: Color): String { + val argb = color.toArgb() + val rgb = argb and 0x00FFFFFF // Mask out the alpha channel + return String.format("#%06X", rgb) +} + +private val sha256: MessageDigest = MessageDigest.getInstance("SHA-256") + +private fun byteMod(byte: Byte, modulo: Int): Int { + val ub = byte.toUByte().toInt() + return ub % modulo +} + +private fun bytesToRGB(b1: Byte, b2: Byte, b3: Byte): Color { + return Color(b1.toUByte().toInt(), b2.toUByte().toInt(), b3.toUByte().toInt()) +} + +fun generateRoboHashSvg(msg: String): String { + val hash = sha256.digest(msg.toByteArray()) + val hashHex = hash.joinToString(separator = "") {b -> "%02x".format(b)} + val bgColor1 = bytesToRGB(hash[0], hash[1], hash[2]) + val bgColor2 = bytesToRGB(hash[3], hash[4], hash[5]) + val fgColor = bytesToRGB(hash[6], hash[7], hash[8]) + val bgIndex = byteMod(hash[9], 8) + val bodyIndex = byteMod(hash[10], 10) + val faceIndex = byteMod(hash[11], 10) + val eyesIndex = byteMod(hash[12], 10) + val mouthIndex = byteMod(hash[13], 10) + val accIndex = byteMod(hash[14], 10) + val background = backgrounds[bgIndex] + val body = bodies[bodyIndex] + val face = faces[faceIndex] + val eye = eyes[eyesIndex] + val mouth = mouths[mouthIndex] + val accessory = accessories[accIndex] + + return """ + + + + + RoboHash $hashHex + ${background}${body.paths}${face.paths}${eye.paths}${mouth.paths}${accessory.paths} + + """.trimIndent() +} + +fun roboHashImageRequest(context: Context, message: String): ImageRequest { + return ImageRequest + .Builder(context) + .data(ByteBuffer.wrap( + generateRoboHashSvg(message).toByteArray() + )) + .build() +} + +@Composable +fun RoboHashAsyncImage( + message: String, + modifier: Modifier = Modifier, + contentDescription: String? = null, + transform: (AsyncImagePainter.State) -> AsyncImagePainter.State = AsyncImagePainter.DefaultTransform, + onState: ((AsyncImagePainter.State) -> Unit)? = null, + alignment: Alignment = Alignment.Center, + contentScale: ContentScale = ContentScale.Fit, + alpha: Float = DefaultAlpha, + colorFilter: ColorFilter? = null, + filterQuality: FilterQuality = DrawScope.DefaultFilterQuality, +) { + AsyncImage( + model = roboHashImageRequest(LocalContext.current, message), + contentDescription = contentDescription, + modifier = modifier, + transform = transform, + onState = onState, + alignment = alignment, + contentScale = contentScale, + alpha = alpha, + colorFilter = colorFilter, + filterQuality = filterQuality, + ) +} + +private data class Part(val style: String, val paths: String) + +private val backgrounds: List = listOf( + """""", + """""", + """""", + """""", + """""", + """""", + """""", + """""", +) + +private val accessories: List = listOf( + Part( + """.cls-00-2{fill:none;}.cls-00-2,.cls-00-3,.cls-00-4{stroke:#000;}.cls-00-2,.cls-00-3{stroke-linecap:round;stroke-linejoin:round;}.cls-00-3{fill-opacity:0.4;stroke-width:0.75px;}.cls-00-4{fill-opacity:0.2;stroke-miterlimit:10;stroke-width:0.5px;}""", + """""" + ), + Part( + """.cls-01-2{fill:#fff;fill-opacity:0.2;}.cls-01-3,.cls-01-4{fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-01-4{stroke-width:0.75px;}""", + """""" + ), + Part( + """.cls-02-2{fill:#be1e2d;}.cls-02-11,.cls-02-2,.cls-02-5,.cls-02-6,.cls-02-7,.cls-02-9{stroke:#000;}.cls-02-11,.cls-02-2,.cls-02-5,.cls-02-9{stroke-miterlimit:10;}.cls-02-3{fill:#561317;}.cls-02-4{fill:#ed293b;}.cls-02-11,.cls-02-5,.cls-02-7{fill:none;}.cls-02-5,.cls-02-6{stroke-width:0.75px;}.cls-02-6{fill:#fff;}.cls-02-6,.cls-02-8{fill-opacity:0.2;}.cls-02-6,.cls-02-7{stroke-linecap:round;stroke-linejoin:round;}.cls-02-9{fill:#e6e7e8;}.cls-02-10{fill:#d0d2d3;}""", + """""" + ), + Part( + """.cls-03-2,.cls-03-3,.cls-03-8{fill:#fff;}.cls-03-2,.cls-03-3,.cls-03-7{fill-opacity:0.2;}.cls-03-2,.cls-03-4,.cls-03-5{stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-03-2{stroke-width:0.75px;}.cls-03-4{fill:none;}.cls-03-5{fill:#ec1c24;}.cls-03-6{fill-opacity:0.1;}.cls-03-8{fill-opacity:0.4;}""", + """""" + ), + Part( + """.cls-04-2,.cls-04-3{fill:none;stroke-linecap:round;stroke-linejoin:round;}.cls-04-2,.cls-04-3,.cls-04-5{stroke:#000;}.cls-04-3,.cls-04-5{stroke-width:0.75px;}.cls-04-4,.cls-04-6{fill:#fff;}.cls-04-4{fill-opacity:0.4;}.cls-04-5{fill:#ec1c24;stroke-miterlimit:10;}.cls-04-6,.cls-04-7{fill-opacity:0.2;}""", + """""" + ), + Part( + """.cls-05-2{fill:#fff;fill-opacity:0.4;}.cls-05-2,.cls-05-3{stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-05-3{fill:none;stroke-width:0.75px;}""", + """""" + ), + Part( + """.cls-06-2,.cls-06-4{fill-opacity:0.2;}.cls-06-3,.cls-06-6{fill:none;}.cls-06-3,.cls-06-5,.cls-06-6{stroke:#000;}.cls-06-3,.cls-06-5{stroke-miterlimit:10;}.cls-06-4{fill:#fff;}.cls-06-5{fill:#ec1c24;stroke-width:0.75px;}.cls-06-6{stroke-linecap:round;stroke-linejoin:round;}""", + """""" + ), + Part( + """.cls-07-10,.cls-07-2,.cls-07-4,.cls-07-8,.cls-07-9{fill:none;}.cls-07-10,.cls-07-2,.cls-07-3,.cls-07-4,.cls-07-5,.cls-07-6,.cls-07-8,.cls-07-9{stroke:#000;}.cls-07-2,.cls-07-3,.cls-07-4,.cls-07-5,.cls-07-6,.cls-07-8{stroke-linecap:round;}.cls-07-2,.cls-07-3,.cls-07-4,.cls-07-5,.cls-07-6,.cls-07-8,.cls-07-9{stroke-linejoin:round;}.cls-07-3,.cls-07-5{fill:#fff;}.cls-07-3,.cls-07-5,.cls-07-6,.cls-07-7{fill-opacity:0.2;}.cls-07-3,.cls-07-4{stroke-width:0.75px;}.cls-07-10,.cls-07-8,.cls-07-9{stroke-width:1.5px;}.cls-07-10{stroke-miterlimit:10;}""", + """""" + ), + Part( + """.cls-08-2{fill:none;}.cls-08-2,.cls-08-3,.cls-08-5,.cls-08-6,.cls-08-7{stroke:#000;stroke-miterlimit:10;}.cls-08-3,.cls-08-4{fill:#fff;}.cls-08-3,.cls-08-4,.cls-08-8{fill-opacity:0.2;}.cls-08-5{fill:#716558;}.cls-08-6{fill:#9a8479;}.cls-08-7{fill:#c1b49a;}""", + """""" + ), + Part( + """.cls-09-2{fill:none;}.cls-09-2,.cls-09-4{stroke:#000;stroke-miterlimit:10;}.cls-09-3,.cls-09-4{fill:#fff;}.cls-09-3{fill-opacity:0.2;}.cls-09-5{fill:#d0d2d3;}.cls-09-6{fill-opacity:0.4;}""", + """""" + ), +) + +private val bodies: List = listOf( + Part( + """.cls-10-2{fill:#fff;fill-opacity:0.4;}.cls-10-3,.cls-10-5,.cls-10-7{fill:none;}.cls-10-3,.cls-10-4,.cls-10-5,.cls-10-6,.cls-10-7,.cls-10-8{stroke:#000;stroke-miterlimit:10;}.cls-10-3{stroke-width:1.2px;}.cls-10-4,.cls-10-6,.cls-10-9{fill-opacity:0.2;}.cls-10-5{stroke-width:1.25px;}.cls-10-6{stroke-width:0.75px;}.cls-10-8{fill:#6d6e70;}""", + """""" + ), + Part( + """.cls-11-2{fill:#fff;}.cls-11-2,.cls-11-6{fill-opacity:0.2;}.cls-11-3{fill:none;}.cls-11-3,.cls-11-4{stroke:#000;stroke-miterlimit:10;}.cls-11-4{fill:#6d6e70;}.cls-11-5{opacity:0.2;}""", + """""" + ), + Part( + """.cls-12-2{fill:#fff;fill-opacity:0.4;}.cls-12-3,.cls-12-7{fill:none;}.cls-12-3,.cls-12-5,.cls-12-6,.cls-12-7{stroke:#000;}.cls-12-3,.cls-12-5,.cls-12-6{stroke-miterlimit:10;}.cls-12-4,.cls-12-6{fill-opacity:0.2;}.cls-12-5{fill:#6d6e70;}.cls-12-7{stroke-linecap:round;stroke-linejoin:round;}""", + """""" + ), + Part( + """.cls-13-2{fill:none;}.cls-13-2,.cls-13-5{stroke:#000;stroke-miterlimit:10;}.cls-13-3{fill:#fff;fill-opacity:0.4;}.cls-13-4{fill-opacity:0.2;}.cls-13-5{fill:#6d6e70;}""", + """""" + ), + Part( + """.cls-14-2{fill:none;}.cls-14-2,.cls-14-4{stroke:#000;stroke-miterlimit:10;}.cls-14-3,.cls-14-6{fill-opacity:0.2;}.cls-14-4{fill:#6d6e70;}.cls-14-5,.cls-14-6{fill:#fff;}.cls-14-5{fill-opacity:0.4;}""", + """""" + ), + Part( + """.cls-15-2{fill:#fff;}.cls-15-2,.cls-15-4{fill-opacity:0.2;}.cls-15-2,.cls-15-3,.cls-15-5{stroke:#000;stroke-miterlimit:10;}.cls-15-3{fill:none;}.cls-15-5{fill:#6d6e70;}""", + """""" + ), + Part( + """.cls-16-2,.cls-16-5{fill:none;}.cls-16-2,.cls-16-5,.cls-16-6,.cls-16-7,.cls-16-8,.cls-16-9{stroke:#000;}.cls-16-2{stroke-linecap:round;stroke-linejoin:round;}.cls-16-3,.cls-16-6,.cls-16-7{fill-opacity:0.2;}.cls-16-4,.cls-16-6{fill:#fff;}.cls-16-4{fill-opacity:0.4;}.cls-16-5,.cls-16-6,.cls-16-7,.cls-16-8,.cls-16-9{stroke-miterlimit:10;}.cls-16-8{fill:#f9ec31;}.cls-16-9{fill:#6d6e70;}""", + """""" + ), + Part( + """.cls-17-2{fill:#020202;}.cls-17-2,.cls-17-4,.cls-17-6,.cls-17-7{fill-opacity:0.4;}.cls-17-3{fill-opacity:0.2;}.cls-17-4,.cls-17-6{fill:#fff;}.cls-17-4,.cls-17-5,.cls-17-8{stroke:#000;stroke-miterlimit:10;}.cls-17-5{fill:none;}.cls-17-8{fill:#6d6e70;}""", + """""" + ), + Part( + """.cls-fill-2,.cls-18-3,.cls-18-5,.cls-18-6{stroke:#000;stroke-miterlimit:10;}.cls-18-3{fill:none;}.cls-18-4{fill:#fff;}.cls-18-4,.cls-18-5{fill-opacity:0.4;}.cls-18-6{fill:#6d6e70;}.cls-18-7{opacity:0.2;}.cls-18-8{fill-opacity:0.2;}""", + """""" + ), + Part( + """.cls-19-2{fill-opacity:0.6;}.cls-19-11,.cls-19-13,.cls-19-14,.cls-19-2,.cls-19-3,.cls-19-4,.cls-19-6,.cls-19-8{stroke:#000;}.cls-19-11,.cls-19-13,.cls-19-2,.cls-19-4,.cls-19-6,.cls-19-8{stroke-miterlimit:10;}.cls-19-11,.cls-19-14,.cls-19-3,.cls-19-8{fill:none;}.cls-19-14,.cls-19-3{stroke-linecap:round;stroke-linejoin:round;}.cls-19-10,.cls-19-12,.cls-19-4,.cls-19-5{fill:#fff;}.cls-19-12,.cls-19-4{fill-opacity:0.2;}.cls-19-4{stroke-opacity:0;}.cls-19-5{fill-opacity:0.1;}.cls-19-6{fill:#6d6e70;}.cls-19-7{fill:#58595b;}.cls-19-13,.cls-19-9{fill-opacity:0.4;}.cls-19-10{fill-opacity:0.5;}.cls-19-11,.cls-19-14{stroke-width:0.75px;}""", + """""" + ), +) + +private val eyes: List = listOf( + Part( + """.cls-20-2{fill-opacity:0.4;}.cls-20-2,.cls-20-3{stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-20-3{fill:#461917;stroke-width:0.5px;}""", + """""" + ), + Part( + """.cls-21-2{fill-opacity:0.2;}.cls-21-2,.cls-21-3,.cls-21-4,.cls-21-5{stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-21-2,.cls-21-4,.cls-21-5{stroke-width:0.5px;}.cls-21-3,.cls-21-4{fill-opacity:0.4;}.cls-21-5{fill:#461917;}.cls-21-6{fill:#faaf40;}""", + """""" + ), + Part( + """.cls-22-2{opacity:0.4;}.cls-22-3{fill:#461917;}.cls-22-3,.cls-22-4,.cls-22-5{stroke:#000;}.cls-22-3,.cls-22-5{stroke-linecap:round;stroke-linejoin:round;}.cls-22-3,.cls-22-4{stroke-width:0.5px;}.cls-22-4{fill:#ec1c24;stroke-miterlimit:10;}.cls-22-5{fill:none;stroke-width:0.75px;}""", + """""" + ), + Part( + """.cls-23-2,.cls-23-3{fill:#fff;}.cls-23-2{fill-opacity:0.4;}.cls-23-3{fill-opacity:0.2;}.cls-23-3,.cls-23-4,.cls-23-5{stroke:#000;stroke-miterlimit:10;}.cls-23-4{fill:none;}.cls-23-4,.cls-23-5{stroke-width:0.75px;}.cls-23-5{fill:red;}""", + """""" + ), + Part( + """.cls-24-2{fill-opacity:0.4;stroke-miterlimit:10;}.cls-24-2,.cls-24-3,.cls-24-5{stroke:#000;}.cls-24-2,.cls-24-5{stroke-width:0.75px;}.cls-24-3,.cls-24-5{fill:#461917;stroke-linecap:round;stroke-linejoin:round;}.cls-24-3{stroke-width:0.5px;}.cls-24-4{fill:#ec1c24;}""", + """""" + ), + Part( + """.cls-25-2{fill-opacity:0.55;stroke-miterlimit:10;stroke-width:0.75px;}.cls-25-2,.cls-25-3{stroke:#000;}.cls-25-3{fill:#461917;stroke-linecap:round;stroke-linejoin:round;stroke-width:0.5px;}.cls-25-4{fill:#ec1c24;}""", + """""" + ), + Part( + """.cls-26-2{fill-opacity:0.6;}.cls-26-2,.cls-26-3,.cls-26-4,.cls-26-5,.cls-26-6{stroke:#000;}.cls-26-2,.cls-26-3,.cls-26-4,.cls-26-6{stroke-linecap:round;stroke-linejoin:round;}.cls-26-3{fill:#461917;}.cls-26-3,.cls-26-4,.cls-26-5{stroke-width:0.5px;}.cls-26-4,.cls-26-5{fill:#f9ec31;}.cls-26-5{stroke-miterlimit:10;}.cls-26-6{fill:none;}""", + """""" + ), + Part( + """.cls-27-2{fill-opacity:0.4;stroke-miterlimit:10;}.cls-27-2,.cls-27-3,.cls-27-4{stroke:#000;}.cls-27-3,.cls-27-4{fill:#461917;stroke-linecap:round;stroke-linejoin:round;}.cls-27-3{stroke-width:0.5px;}.cls-27-4{stroke-width:0.75px;}""", + """""" + ), + Part( + """.cls-28-2{fill:none;}.cls-28-2,.cls-28-3,.cls-28-4,.cls-28-6{stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-28-2,.cls-28-4,.cls-28-6{stroke-width:0.75px;}.cls-28-3{fill:#461917;stroke-width:0.5px;}.cls-28-4{fill-opacity:0.4;}.cls-28-5{fill:#fff100;}.cls-28-6{fill:#fff;fill-opacity:0.2;}""", + """""" + ), + Part( + """.cls-29-2{fill:#fff;}.cls-29-2,.cls-29-4{fill-opacity:0.4;}.cls-29-3{fill:none;}.cls-29-3,.cls-29-4{stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-29-4{stroke-width:0.75px;}""", + """""" + ), +) + +private val faces: List = listOf( + Part( + """.cls-30-2{fill:#fff;fill-opacity:0.4;}.cls-30-3,.cls-30-4{fill:none;}.cls-30-3,.cls-30-4,.cls-30-6{stroke:#000;}.cls-30-3,.cls-30-6{stroke-linecap:round;stroke-linejoin:round;}.cls-30-4{stroke-miterlimit:10;}.cls-30-5,.cls-30-6{fill-opacity:0.2;}.cls-30-6{stroke-width:0.75px;}""", + """""" + ), + Part( + """.cls-31-2,.cls-31-4{fill-opacity:0.2;}.cls-31-3{fill:none;}.cls-31-3,.cls-31-4{stroke:#000;stroke-miterlimit:10;}.cls-31-4,.cls-31-5{fill:#fff;}.cls-31-4{stroke-width:0.75px;}.cls-31-5{fill-opacity:0.4;}""", + """""" + ), + Part( + """.cls-32-2{fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-32-3{fill-opacity:0.2;}.cls-32-4{fill:#fff;fill-opacity:0.4;}""", + """""" + ), + Part( + """.cls-33-2{fill-opacity:0.2;}.cls-33-3{fill:#fff;fill-opacity:0.4;}.cls-33-4{fill:none;stroke:#000;stroke-miterlimit:10;}""", + """""" + ), + Part( + """.cls-34-2,.cls-34-3{fill:#fff;}.cls-34-2{fill-opacity:0.4;}.cls-34-3,.cls-34-5{fill-opacity:0.2;}.cls-34-3,.cls-34-4{stroke:#000;stroke-miterlimit:10;}.cls-34-4{fill:none;}""", + """""" + ), + Part( + """.cls-35-2{fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-35-3{fill-opacity:0.2;}.cls-35-4{fill:#fff;fill-opacity:0.4;}""", + """""" + ), + Part( + """.cls-36-2,.cls-36-6{fill:#fff;}.cls-36-2{fill-opacity:0.4;}.cls-36-3,.cls-36-4{fill:none;stroke:#000;stroke-miterlimit:10;}.cls-36-3{stroke-width:2px;}.cls-36-5,.cls-36-6{fill-opacity:0.2;}""", + """""" + ), + Part( + """.cls-37-2{fill:#fff;fill-opacity:0.4;}.cls-37-3{fill:none;}.cls-37-3,.cls-37-4{stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-37-4{fill-opacity:0.2;}""", + """""" + ), + Part( + """.cls-38-2{fill:none;stroke:#000;stroke-miterlimit:10;}.cls-38-3,.cls-38-6{fill:#fff;}.cls-38-3,.cls-38-4{fill-opacity:0.2;}.cls-38-5{fill-opacity:0.1;}.cls-38-6{fill-opacity:0.4;}""", + """""" + ), + Part( + """.cls-39-2{fill:#fff;fill-opacity:0.4;}.cls-39-3{fill:none;}.cls-39-3,.cls-39-4{stroke:#000;stroke-miterlimit:10;}.cls-39-4{fill-opacity:0.2;stroke-width:0.75px;}""", + """""" + ), +) + +private val mouths: List = listOf( + Part( + """.cls-40-2{fill-opacity:0.4;stroke:#000;stroke-linecap:round;stroke-linejoin:round;}""", + """""" + ), + Part( + """.cls-41-2,.cls-41-4{fill:none;}.cls-41-2,.cls-41-3,.cls-41-4{stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-41-3{fill-opacity:0.4;}.cls-41-3,.cls-41-4{stroke-width:0.75px;}""", + """""" + ), + Part( + """.cls-42-2{fill-opacity:0.4;}.cls-42-2,.cls-42-5,.cls-42-6{stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-42-3{fill:#ee4036;}.cls-42-4{fill:#f05a28;}.cls-42-5{fill:#faaf40;stroke-width:0.75px;}.cls-42-6{fill:none;}""", + """""" + ), + Part( + """.cls-43-2{fill:#f9ec31;}.cls-43-3{fill:#faaf40;}.cls-43-4,.cls-43-6{fill:none;}.cls-43-4,.cls-43-5,.cls-43-6{stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-43-5{fill-opacity:0.4;}.cls-43-5,.cls-43-6{stroke-width:0.75px;}""", + """""" + ), + Part( + """.cls-44-2{fill:none;}.cls-44-2,.cls-44-5{stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-44-3{opacity:0.4;}.cls-44-4{fill:#461917;}.cls-44-5{fill-opacity:0.4;stroke-width:0.75px;}""", + """""" + ), + Part( + """.cls-45-2{fill-opacity:0.4;stroke-miterlimit:10;}.cls-45-2,.cls-45-3{stroke:#000;}.cls-45-3{fill:#461917;stroke-linecap:round;stroke-linejoin:round;stroke-width:0.5px;}""", + """""" + ), + Part( + """.cls-46-2{fill-opacity:0.4;}.cls-46-3{fill:none;}.cls-46-3,.cls-46-4{stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-46-4{fill:#461917;stroke-width:0.5px;}""", + """""" + ), + Part( + """.cls-47-2{fill-opacity:0.4;}.cls-47-2,.cls-47-3,.cls-47-4,.cls-47-5{stroke:#000;stroke-miterlimit:10;}.cls-47-3{fill:#f6921e;}.cls-47-4{fill:#f9ec31;stroke-width:0.75px;}.cls-47-5{fill:none;}""", + """""" + ), + Part( + """.cls-48-2{opacity:0.4;}.cls-48-3,.cls-48-4,.cls-48-5{fill:none;}.cls-48-3,.cls-48-4,.cls-48-5,.cls-48-6,.cls-48-7,.cls-48-8{stroke:#000;}.cls-48-3,.cls-48-5,.cls-48-7{stroke-miterlimit:10;}.cls-48-4,.cls-48-6,.cls-48-8{stroke-linecap:round;stroke-linejoin:round;}.cls-48-4,.cls-48-5{stroke-width:0.5px;}.cls-48-6{fill:#f6921e;stroke-width:1.2px;}.cls-48-7{fill:#f9ec31;}.cls-48-7,.cls-48-8{stroke-width:0.75px;}.cls-48-8{fill:#f05a27;}""", + """""" + ), + Part( + """.cls-49-2{fill:none;}.cls-49-2,.cls-49-3,.cls-49-5{stroke:#000;}.cls-49-2,.cls-49-3{stroke-linecap:round;stroke-linejoin:round;}.cls-49-3{opacity:0.1;}.cls-49-4{opacity:0.4;}.cls-49-5{fill-opacity:0.4;stroke-miterlimit:10;stroke-width:0.75px;}""", + """""" + ), +) From 6403bd21f8ab9194929aa24697f7b4b48e79f4b6 Mon Sep 17 00:00:00 2001 From: maxmoney21m Date: Tue, 7 Mar 2023 23:46:38 +0800 Subject: [PATCH 2/7] Add async image proxy for robohash images --- .../amethyst/ui/components/AsyncImageProxy.kt | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/AsyncImageProxy.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/AsyncImageProxy.kt index 8e4dbcd33..2a3fb994a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/AsyncImageProxy.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/AsyncImageProxy.kt @@ -1,6 +1,10 @@ package com.vitorpamplona.amethyst.ui.components import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ColorFilter @@ -26,6 +30,48 @@ data class ResizeImage(val url: String?, val size: Dp) { } } +@Composable fun AsyncUserImageProxy( + pubkeyHex: String, + model: ResizeImage, + contentDescription: String?, + modifier: Modifier = Modifier, + alignment: Alignment = Alignment.Center, + contentScale: ContentScale = ContentScale.Fit, + alpha: Float = DefaultAlpha, + colorFilter: ColorFilter? = null, + filterQuality: FilterQuality = DrawScope.DefaultFilterQuality, +) { + var loading by remember { mutableStateOf(false) } + var error by remember { mutableStateOf(false) } + + if (model.url == null || loading || error) { + RoboHashAsyncImage( + message = pubkeyHex, + contentDescription = contentDescription, + modifier = modifier, + alignment = alignment, + contentScale = contentScale, + alpha = alpha, + colorFilter = colorFilter, + filterQuality = filterQuality, + ) + } else { + AsyncImage( + model = model.proxyUrl(), + contentDescription = contentDescription, + modifier = modifier, + onLoading = { loading = true }, + onSuccess = { loading = false; error = false }, + onError = { loading = false; error = true }, + alignment = alignment, + contentScale = contentScale, + alpha = alpha, + colorFilter = colorFilter, + filterQuality = filterQuality + ) + } +} + @Composable fun AsyncImageProxy( model: ResizeImage, From 9f6867b6abed5502cfa75a21fac13bba217fe6c6 Mon Sep 17 00:00:00 2001 From: maxmoney21m Date: Tue, 7 Mar 2023 23:48:39 +0800 Subject: [PATCH 3/7] Switch PFPs to use the robohash proxy --- .../amethyst/ui/components/AsyncImageProxy.kt | 77 +-- .../ui/components/RoboHashAsyncImage.kt | 26 +- .../amethyst/ui/navigation/AppTopBar.kt | 513 +++++++++--------- .../amethyst/ui/navigation/DrawerContent.kt | 13 +- .../ui/note/ChatroomMessageCompose.kt | 11 +- .../amethyst/ui/note/NoteCompose.kt | 17 +- .../amethyst/ui/qrcode/ShowQRDialog.kt | 310 ++++++----- .../ui/screen/loggedIn/ChannelScreen.kt | 10 +- .../ui/screen/loggedIn/ChatroomScreen.kt | 489 +++++++++-------- 9 files changed, 730 insertions(+), 736 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/AsyncImageProxy.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/AsyncImageProxy.kt index 2a3fb994a..456e7e327 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/AsyncImageProxy.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/AsyncImageProxy.kt @@ -1,5 +1,6 @@ package com.vitorpamplona.amethyst.ui.components +import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -31,45 +32,47 @@ data class ResizeImage(val url: String?, val size: Dp) { } @Composable fun AsyncUserImageProxy( - pubkeyHex: String, - model: ResizeImage, - contentDescription: String?, - modifier: Modifier = Modifier, - alignment: Alignment = Alignment.Center, - contentScale: ContentScale = ContentScale.Fit, - alpha: Float = DefaultAlpha, - colorFilter: ColorFilter? = null, - filterQuality: FilterQuality = DrawScope.DefaultFilterQuality, + pubkeyHex: String, + model: ResizeImage, + contentDescription: String?, + modifier: Modifier = Modifier, + alignment: Alignment = Alignment.Center, + contentScale: ContentScale = ContentScale.Fit, + alpha: Float = DefaultAlpha, + colorFilter: ColorFilter? = null, + filterQuality: FilterQuality = DrawScope.DefaultFilterQuality ) { - var loading by remember { mutableStateOf(false) } - var error by remember { mutableStateOf(false) } + var loading by remember { mutableStateOf(false) } + var error by remember { mutableStateOf(false) } - if (model.url == null || loading || error) { - RoboHashAsyncImage( - message = pubkeyHex, - contentDescription = contentDescription, - modifier = modifier, - alignment = alignment, - contentScale = contentScale, - alpha = alpha, - colorFilter = colorFilter, - filterQuality = filterQuality, - ) - } else { - AsyncImage( - model = model.proxyUrl(), - contentDescription = contentDescription, - modifier = modifier, - onLoading = { loading = true }, - onSuccess = { loading = false; error = false }, - onError = { loading = false; error = true }, - alignment = alignment, - contentScale = contentScale, - alpha = alpha, - colorFilter = colorFilter, - filterQuality = filterQuality - ) - } + Box() { + AsyncImage( + model = model.proxyUrl(), + contentDescription = contentDescription, + modifier = modifier, + onLoading = { loading = true }, + onSuccess = { loading = false; error = false }, + onError = { loading = false; error = true }, + alignment = alignment, + contentScale = contentScale, + alpha = alpha, + colorFilter = colorFilter, + filterQuality = filterQuality + ) + + if (model.url == null || loading || error) { + RoboHashAsyncImage( + message = pubkeyHex, + contentDescription = contentDescription, + modifier = modifier, + alignment = alignment, + contentScale = contentScale, + alpha = alpha, + colorFilter = colorFilter, + filterQuality = filterQuality + ) + } + } } @Composable diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RoboHashAsyncImage.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RoboHashAsyncImage.kt index 0b06e3f38..4e2dd9a11 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RoboHashAsyncImage.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RoboHashAsyncImage.kt @@ -37,7 +37,7 @@ private fun bytesToRGB(b1: Byte, b2: Byte, b3: Byte): Color { fun generateRoboHashSvg(msg: String): String { val hash = sha256.digest(msg.toByteArray()) - val hashHex = hash.joinToString(separator = "") {b -> "%02x".format(b)} + val hashHex = hash.joinToString(separator = "") { b -> "%02x".format(b) } val bgColor1 = bytesToRGB(hash[0], hash[1], hash[2]) val bgColor2 = bytesToRGB(hash[3], hash[4], hash[5]) val fgColor = bytesToRGB(hash[6], hash[7], hash[8]) @@ -71,9 +71,11 @@ fun generateRoboHashSvg(msg: String): String { fun roboHashImageRequest(context: Context, message: String): ImageRequest { return ImageRequest .Builder(context) - .data(ByteBuffer.wrap( - generateRoboHashSvg(message).toByteArray() - )) + .data( + ByteBuffer.wrap( + generateRoboHashSvg(message).toByteArray() + ) + ) .build() } @@ -88,7 +90,7 @@ fun RoboHashAsyncImage( contentScale: ContentScale = ContentScale.Fit, alpha: Float = DefaultAlpha, colorFilter: ColorFilter? = null, - filterQuality: FilterQuality = DrawScope.DefaultFilterQuality, + filterQuality: FilterQuality = DrawScope.DefaultFilterQuality ) { AsyncImage( model = roboHashImageRequest(LocalContext.current, message), @@ -100,7 +102,7 @@ fun RoboHashAsyncImage( contentScale = contentScale, alpha = alpha, colorFilter = colorFilter, - filterQuality = filterQuality, + filterQuality = filterQuality ) } @@ -114,7 +116,7 @@ private val backgrounds: List = listOf( """""", """""", """""", - """""", + """""" ) private val accessories: List = listOf( @@ -157,7 +159,7 @@ private val accessories: List = listOf( Part( """.cls-09-2{fill:none;}.cls-09-2,.cls-09-4{stroke:#000;stroke-miterlimit:10;}.cls-09-3,.cls-09-4{fill:#fff;}.cls-09-3{fill-opacity:0.2;}.cls-09-5{fill:#d0d2d3;}.cls-09-6{fill-opacity:0.4;}""", """""" - ), + ) ) private val bodies: List = listOf( @@ -200,7 +202,7 @@ private val bodies: List = listOf( Part( """.cls-19-2{fill-opacity:0.6;}.cls-19-11,.cls-19-13,.cls-19-14,.cls-19-2,.cls-19-3,.cls-19-4,.cls-19-6,.cls-19-8{stroke:#000;}.cls-19-11,.cls-19-13,.cls-19-2,.cls-19-4,.cls-19-6,.cls-19-8{stroke-miterlimit:10;}.cls-19-11,.cls-19-14,.cls-19-3,.cls-19-8{fill:none;}.cls-19-14,.cls-19-3{stroke-linecap:round;stroke-linejoin:round;}.cls-19-10,.cls-19-12,.cls-19-4,.cls-19-5{fill:#fff;}.cls-19-12,.cls-19-4{fill-opacity:0.2;}.cls-19-4{stroke-opacity:0;}.cls-19-5{fill-opacity:0.1;}.cls-19-6{fill:#6d6e70;}.cls-19-7{fill:#58595b;}.cls-19-13,.cls-19-9{fill-opacity:0.4;}.cls-19-10{fill-opacity:0.5;}.cls-19-11,.cls-19-14{stroke-width:0.75px;}""", """""" - ), + ) ) private val eyes: List = listOf( @@ -243,7 +245,7 @@ private val eyes: List = listOf( Part( """.cls-29-2{fill:#fff;}.cls-29-2,.cls-29-4{fill-opacity:0.4;}.cls-29-3{fill:none;}.cls-29-3,.cls-29-4{stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-29-4{stroke-width:0.75px;}""", """""" - ), + ) ) private val faces: List = listOf( @@ -286,7 +288,7 @@ private val faces: List = listOf( Part( """.cls-39-2{fill:#fff;fill-opacity:0.4;}.cls-39-3{fill:none;}.cls-39-3,.cls-39-4{stroke:#000;stroke-miterlimit:10;}.cls-39-4{fill-opacity:0.2;stroke-width:0.75px;}""", """""" - ), + ) ) private val mouths: List = listOf( @@ -329,5 +331,5 @@ private val mouths: List = listOf( Part( """.cls-49-2{fill:none;}.cls-49-2,.cls-49-3,.cls-49-5{stroke:#000;}.cls-49-2,.cls-49-3{stroke-linecap:round;stroke-linejoin:round;}.cls-49-3{opacity:0.1;}.cls-49-4{opacity:0.4;}.cls-49-5{fill-opacity:0.4;stroke-miterlimit:10;stroke-width:0.75px;}""", """""" - ), + ) ) 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 55e2c376b..991c8e8dc 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 @@ -1,257 +1,256 @@ -package com.vitorpamplona.amethyst.ui.navigation - -import android.util.Log -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Divider -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.MaterialTheme -import androidx.compose.material.ScaffoldState -import androidx.compose.material.Text -import androidx.compose.material.TopAppBar -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowBack -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -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.graphics.painter.BitmapPainter -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation.NavHostController -import coil.Coil -import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.RoboHashCache -import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.service.NostrAccountDataSource -import com.vitorpamplona.amethyst.service.NostrChannelDataSource -import com.vitorpamplona.amethyst.service.NostrChatroomDataSource -import com.vitorpamplona.amethyst.service.NostrChatroomListDataSource -import com.vitorpamplona.amethyst.service.NostrGlobalDataSource -import com.vitorpamplona.amethyst.service.NostrHomeDataSource -import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource -import com.vitorpamplona.amethyst.service.NostrSingleChannelDataSource -import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource -import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource -import com.vitorpamplona.amethyst.service.NostrThreadDataSource -import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource -import com.vitorpamplona.amethyst.service.relays.Client -import com.vitorpamplona.amethyst.service.relays.RelayPool -import com.vitorpamplona.amethyst.ui.actions.NewRelayListView -import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy -import com.vitorpamplona.amethyst.ui.components.ResizeImage -import com.vitorpamplona.amethyst.ui.screen.RelayPoolViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import kotlinx.coroutines.launch - -@Composable -fun AppTopBar(navController: NavHostController, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) { - when (currentRoute(navController)) { - // Route.Profile.route -> TopBarWithBackButton(navController) - else -> MainTopBar(scaffoldState, accountViewModel) - } -} - -@Composable -fun MainTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) { - val accountState by accountViewModel.accountLiveData.observeAsState() - val account = accountState?.account ?: return - - val accountUserState by account.userProfile().live().metadata.observeAsState() - val accountUser = accountUserState?.user ?: return - - val relayViewModel: RelayPoolViewModel = viewModel { RelayPoolViewModel() } - val connectedRelaysLiveData by relayViewModel.connectedRelaysLiveData.observeAsState() - val availableRelaysLiveData by relayViewModel.availableRelaysLiveData.observeAsState() - - val coroutineScope = rememberCoroutineScope() - - val context = LocalContext.current - val ctx = LocalContext.current.applicationContext - - var wantsToEditRelays by remember { - mutableStateOf(false) - } - - if (wantsToEditRelays) { - NewRelayListView({ wantsToEditRelays = false }, account) - } - - Column() { - TopAppBar( - elevation = 0.dp, - backgroundColor = Color(0xFFFFFF), - title = { - Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Box(Modifier) { - Column( - modifier = Modifier - .fillMaxWidth() - .fillMaxHeight() - .padding(start = 0.dp, end = 20.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - IconButton( - onClick = { - Client.allSubscriptions().map { - "$it ${ - Client.getSubscriptionFilters(it) - .joinToString { it.filter.toJson() } - }" - }.forEach { - Log.d("STATE DUMP", it) - } - - NostrAccountDataSource.printCounter() - NostrChannelDataSource.printCounter() - NostrChatroomDataSource.printCounter() - NostrChatroomListDataSource.printCounter() - - NostrGlobalDataSource.printCounter() - NostrHomeDataSource.printCounter() - - NostrSingleEventDataSource.printCounter() - NostrSearchEventOrUserDataSource.printCounter() - NostrSingleChannelDataSource.printCounter() - NostrSingleUserDataSource.printCounter() - NostrThreadDataSource.printCounter() - - NostrUserProfileDataSource.printCounter() - - Log.d("STATE DUMP", "Connected Relays: " + RelayPool.connectedRelays()) - - val imageLoader = Coil.imageLoader(context) - Log.d("STATE DUMP", "Image Disk Cache ${(imageLoader.diskCache?.size ?: 0) / (1024 * 1024)}/${(imageLoader.diskCache?.maxSize ?: 0) / (1024 * 1024)} MB") - Log.d("STATE DUMP", "Image Memory Cache ${(imageLoader.memoryCache?.size ?: 0) / (1024 * 1024)}/${(imageLoader.memoryCache?.maxSize ?: 0) / (1024 * 1024)} MB") - - Log.d("STATE DUMP", "Notes: " + LocalCache.notes.filter { it.value.event != null }.size + "/" + LocalCache.notes.size) - Log.d("STATE DUMP", "Users: " + LocalCache.users.filter { it.value.info?.latestMetadata != null }.size + "/" + LocalCache.users.size) - } - ) { - Icon( - painter = painterResource(R.drawable.amethyst), - null, - modifier = Modifier.size(40.dp), - tint = Color.Unspecified - ) - } - } - - Column( - modifier = Modifier - .fillMaxWidth() - .fillMaxHeight(), - horizontalAlignment = Alignment.End - - ) { - Row( - modifier = Modifier.fillMaxHeight(), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - "${connectedRelaysLiveData ?: "--"}/${availableRelaysLiveData ?: "--"}", - color = if (connectedRelaysLiveData == 0) Color.Red else MaterialTheme.colors.onSurface.copy(alpha = 0.32f), - style = MaterialTheme.typography.subtitle1, - modifier = Modifier.clickable( - onClick = { - wantsToEditRelays = true - } - ) - ) - } - } - } - } - }, - navigationIcon = { - IconButton( - onClick = { - coroutineScope.launch { - scaffoldState.drawerState.open() - } - }, - modifier = Modifier - ) { - AsyncImageProxy( - model = ResizeImage(accountUser.profilePicture(), 34.dp), - placeholder = BitmapPainter(RoboHashCache.get(ctx, accountUser.pubkeyHex)), - fallback = BitmapPainter(RoboHashCache.get(ctx, accountUser.pubkeyHex)), - error = BitmapPainter(RoboHashCache.get(ctx, accountUser.pubkeyHex)), - contentDescription = stringResource(id = R.string.profile_image), - modifier = Modifier - .width(34.dp) - .height(34.dp) - .clip(shape = CircleShape) - ) - } - }, - actions = { - IconButton( - onClick = { wantsToEditRelays = true }, - modifier = Modifier - ) { - Icon( - painter = painterResource(R.drawable.ic_trends), - null, - modifier = Modifier.size(24.dp), - tint = Color.Unspecified - ) - } - } - ) - Divider(thickness = 0.25.dp) - } -} - -@Composable -fun TopBarWithBackButton(navController: NavHostController) { - Column() { - TopAppBar( - elevation = 0.dp, - backgroundColor = Color(0xFFFFFF), - title = {}, - navigationIcon = { - IconButton( - onClick = { - navController.popBackStack() - }, - modifier = Modifier - ) { - Icon( - imageVector = Icons.Filled.ArrowBack, - null, - modifier = Modifier.size(28.dp), - tint = MaterialTheme.colors.primary - ) - } - }, - actions = {} - ) - Divider(thickness = 0.25.dp) - } -} +package com.vitorpamplona.amethyst.ui.navigation + +import android.util.Log +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Divider +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.ScaffoldState +import androidx.compose.material.Text +import androidx.compose.material.TopAppBar +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +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.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavHostController +import coil.Coil +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.service.NostrAccountDataSource +import com.vitorpamplona.amethyst.service.NostrChannelDataSource +import com.vitorpamplona.amethyst.service.NostrChatroomDataSource +import com.vitorpamplona.amethyst.service.NostrChatroomListDataSource +import com.vitorpamplona.amethyst.service.NostrGlobalDataSource +import com.vitorpamplona.amethyst.service.NostrHomeDataSource +import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource +import com.vitorpamplona.amethyst.service.NostrSingleChannelDataSource +import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource +import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource +import com.vitorpamplona.amethyst.service.NostrThreadDataSource +import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource +import com.vitorpamplona.amethyst.service.relays.Client +import com.vitorpamplona.amethyst.service.relays.RelayPool +import com.vitorpamplona.amethyst.ui.actions.NewRelayListView +import com.vitorpamplona.amethyst.ui.components.AsyncUserImageProxy +import com.vitorpamplona.amethyst.ui.components.ResizeImage +import com.vitorpamplona.amethyst.ui.screen.RelayPoolViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import kotlinx.coroutines.launch + +@Composable +fun AppTopBar(navController: NavHostController, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) { + when (currentRoute(navController)) { + // Route.Profile.route -> TopBarWithBackButton(navController) + else -> MainTopBar(scaffoldState, accountViewModel) + } +} + +@Composable +fun MainTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) { + val accountState by accountViewModel.accountLiveData.observeAsState() + val account = accountState?.account ?: return + + val accountUserState by account.userProfile().live().metadata.observeAsState() + val accountUser = accountUserState?.user ?: return + + val relayViewModel: RelayPoolViewModel = viewModel { RelayPoolViewModel() } + val connectedRelaysLiveData by relayViewModel.connectedRelaysLiveData.observeAsState() + val availableRelaysLiveData by relayViewModel.availableRelaysLiveData.observeAsState() + + val coroutineScope = rememberCoroutineScope() + + val context = LocalContext.current + val ctx = LocalContext.current.applicationContext + + var wantsToEditRelays by remember { + mutableStateOf(false) + } + + if (wantsToEditRelays) { + NewRelayListView({ wantsToEditRelays = false }, account) + } + + Column() { + TopAppBar( + elevation = 0.dp, + backgroundColor = Color(0xFFFFFF), + title = { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Box(Modifier) { + Column( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + .padding(start = 0.dp, end = 20.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + IconButton( + onClick = { + Client.allSubscriptions().map { + "$it ${ + Client.getSubscriptionFilters(it) + .joinToString { it.filter.toJson() } + }" + }.forEach { + Log.d("STATE DUMP", it) + } + + NostrAccountDataSource.printCounter() + NostrChannelDataSource.printCounter() + NostrChatroomDataSource.printCounter() + NostrChatroomListDataSource.printCounter() + + NostrGlobalDataSource.printCounter() + NostrHomeDataSource.printCounter() + + NostrSingleEventDataSource.printCounter() + NostrSearchEventOrUserDataSource.printCounter() + NostrSingleChannelDataSource.printCounter() + NostrSingleUserDataSource.printCounter() + NostrThreadDataSource.printCounter() + + NostrUserProfileDataSource.printCounter() + + Log.d("STATE DUMP", "Connected Relays: " + RelayPool.connectedRelays()) + + val imageLoader = Coil.imageLoader(context) + Log.d("STATE DUMP", "Image Disk Cache ${(imageLoader.diskCache?.size ?: 0) / (1024 * 1024)}/${(imageLoader.diskCache?.maxSize ?: 0) / (1024 * 1024)} MB") + Log.d("STATE DUMP", "Image Memory Cache ${(imageLoader.memoryCache?.size ?: 0) / (1024 * 1024)}/${(imageLoader.memoryCache?.maxSize ?: 0) / (1024 * 1024)} MB") + + Log.d("STATE DUMP", "Notes: " + LocalCache.notes.filter { it.value.event != null }.size + "/" + LocalCache.notes.size) + Log.d("STATE DUMP", "Users: " + LocalCache.users.filter { it.value.info?.latestMetadata != null }.size + "/" + LocalCache.users.size) + } + ) { + Icon( + painter = painterResource(R.drawable.amethyst), + null, + modifier = Modifier.size(40.dp), + tint = Color.Unspecified + ) + } + } + + Column( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight(), + horizontalAlignment = Alignment.End + + ) { + Row( + modifier = Modifier.fillMaxHeight(), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + "${connectedRelaysLiveData ?: "--"}/${availableRelaysLiveData ?: "--"}", + color = if (connectedRelaysLiveData == 0) Color.Red else MaterialTheme.colors.onSurface.copy(alpha = 0.32f), + style = MaterialTheme.typography.subtitle1, + modifier = Modifier.clickable( + onClick = { + wantsToEditRelays = true + } + ) + ) + } + } + } + } + }, + navigationIcon = { + IconButton( + onClick = { + coroutineScope.launch { + scaffoldState.drawerState.open() + } + }, + modifier = Modifier + ) { + AsyncUserImageProxy( + pubkeyHex = accountUser.pubkeyHex, + model = ResizeImage(accountUser.profilePicture(), 34.dp), +// placeholder = BitmapPainter(RoboHashCache.get(ctx, accountUser.pubkeyHex)), +// fallback = BitmapPainter(RoboHashCache.get(ctx, accountUser.pubkeyHex)), +// error = BitmapPainter(RoboHashCache.get(ctx, accountUser.pubkeyHex)), + contentDescription = stringResource(id = R.string.profile_image), + modifier = Modifier + .width(34.dp) + .height(34.dp) + .clip(shape = CircleShape) + ) + } + }, + actions = { + IconButton( + onClick = { wantsToEditRelays = true }, + modifier = Modifier + ) { + Icon( + painter = painterResource(R.drawable.ic_trends), + null, + modifier = Modifier.size(24.dp), + tint = Color.Unspecified + ) + } + } + ) + Divider(thickness = 0.25.dp) + } +} + +@Composable +fun TopBarWithBackButton(navController: NavHostController) { + Column() { + TopAppBar( + elevation = 0.dp, + backgroundColor = Color(0xFFFFFF), + title = {}, + navigationIcon = { + IconButton( + onClick = { + navController.popBackStack() + }, + modifier = Modifier + ) { + Icon( + imageVector = Icons.Filled.ArrowBack, + null, + modifier = Modifier.size(28.dp), + tint = MaterialTheme.colors.primary + ) + } + }, + actions = {} + ) + Divider(thickness = 0.25.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 09c1aac42..b6ad544ef 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 @@ -35,7 +35,6 @@ 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.graphics.painter.BitmapPainter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource @@ -48,10 +47,9 @@ import androidx.navigation.NavHostController import coil.compose.AsyncImage import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.RoboHashCache import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy +import com.vitorpamplona.amethyst.ui.components.AsyncUserImageProxy import com.vitorpamplona.amethyst.ui.components.ResizeImage import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountBackupDialog import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -137,12 +135,13 @@ fun ProfileContent(baseAccountUser: User, modifier: Modifier = Modifier, scaffol } Column(modifier = modifier) { - AsyncImageProxy( + AsyncUserImageProxy( + pubkeyHex = accountUser.pubkeyHex, model = ResizeImage(accountUser.profilePicture(), 100.dp), contentDescription = stringResource(id = R.string.profile_image), - placeholder = BitmapPainter(RoboHashCache.get(ctx, accountUser.pubkeyHex)), - fallback = BitmapPainter(RoboHashCache.get(ctx, accountUser.pubkeyHex)), - error = BitmapPainter(RoboHashCache.get(ctx, accountUser.pubkeyHex)), +// placeholder = BitmapPainter(RoboHashCache.get(ctx, accountUser.pubkeyHex)), +// fallback = BitmapPainter(RoboHashCache.get(ctx, accountUser.pubkeyHex)), +// error = BitmapPainter(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/ChatroomMessageCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt index 25cae5b8d..b00a1d192 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 @@ -60,7 +60,7 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent -import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy +import com.vitorpamplona.amethyst.ui.components.AsyncUserImageProxy import com.vitorpamplona.amethyst.ui.components.ResizeImage import com.vitorpamplona.amethyst.ui.components.TranslateableRichTextViewer import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -195,11 +195,12 @@ fun ChatroomMessageCompose( horizontalArrangement = alignment, modifier = Modifier.padding(top = 5.dp) ) { - AsyncImageProxy( + AsyncUserImageProxy( + pubkeyHex = author.pubkeyHex, model = ResizeImage(author.profilePicture(), 25.dp), - placeholder = BitmapPainter(RoboHashCache.get(context, author.pubkeyHex)), - fallback = BitmapPainter(RoboHashCache.get(context, author.pubkeyHex)), - error = BitmapPainter(RoboHashCache.get(context, author.pubkeyHex)), +// placeholder = BitmapPainter(RoboHashCache.get(context, author.pubkeyHex)), +// fallback = BitmapPainter(RoboHashCache.get(context, author.pubkeyHex)), +// error = BitmapPainter(RoboHashCache.get(context, author.pubkeyHex)), contentDescription = stringResource(id = R.string.profile_image), modifier = Modifier .width(25.dp) 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 1b836af46..078389a62 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 @@ -53,7 +53,7 @@ import com.vitorpamplona.amethyst.service.model.ReactionEvent import com.vitorpamplona.amethyst.service.model.ReportEvent import com.vitorpamplona.amethyst.service.model.RepostEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent -import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy +import com.vitorpamplona.amethyst.ui.components.AsyncUserImageProxy import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status import com.vitorpamplona.amethyst.ui.components.ResizeImage import com.vitorpamplona.amethyst.ui.components.TranslateableRichTextViewer @@ -217,11 +217,12 @@ fun NoteCompose( .height(30.dp) .align(Alignment.BottomEnd) ) { - AsyncImageProxy( + AsyncUserImageProxy( + pubkeyHex = channel.idHex, model = ResizeImage(channel.profilePicture(), 30.dp), - placeholder = BitmapPainter(RoboHashCache.get(context, channel.idHex)), - fallback = BitmapPainter(RoboHashCache.get(context, channel.idHex)), - error = BitmapPainter(RoboHashCache.get(context, channel.idHex)), +// placeholder = BitmapPainter(RoboHashCache.get(context, channel.idHex)), +// fallback = BitmapPainter(RoboHashCache.get(context, channel.idHex)), +// error = BitmapPainter(RoboHashCache.get(context, channel.idHex)), contentDescription = stringResource(R.string.group_picture), modifier = Modifier .width(30.dp) @@ -723,12 +724,10 @@ fun UserPicture( .width(size) .height(size) ) { - AsyncImageProxy( + AsyncUserImageProxy( + pubkeyHex = user.pubkeyHex, model = ResizeImage(user.profilePicture(), size), contentDescription = stringResource(id = R.string.profile_image), - placeholder = BitmapPainter(RoboHashCache.get(ctx, user.pubkeyHex)), - fallback = BitmapPainter(RoboHashCache.get(ctx, user.pubkeyHex)), - error = BitmapPainter(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 936d3f005..2884b931d 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 @@ -1,157 +1,153 @@ -package com.vitorpamplona.amethyst.ui.navigation - -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Button -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Surface -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -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.graphics.painter.BitmapPainter -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.compose.ui.window.Dialog -import androidx.compose.ui.window.DialogProperties -import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.RoboHashCache -import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.ui.actions.CloseButton -import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy -import com.vitorpamplona.amethyst.ui.components.ResizeImage -import com.vitorpamplona.amethyst.ui.qrcode.QrCodeScanner - -@Composable -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) - ) { - Surface(modifier = Modifier.fillMaxSize()) { - Column( - modifier = Modifier - .background(MaterialTheme.colors.background) - .fillMaxSize() - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(10.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - CloseButton(onCancel = onClose) - } - - Column( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = 10.dp), - verticalArrangement = Arrangement.SpaceBetween - ) { - if (presenting) { - Row( - horizontalArrangement = Arrangement.Center, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 30.dp, vertical = 10.dp) - ) { - } - - Column(modifier = Modifier.fillMaxWidth()) { - Row(horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth()) { - AsyncImageProxy( - model = ResizeImage(user.profilePicture(), 100.dp), - placeholder = BitmapPainter(RoboHashCache.get(ctx, user.pubkeyHex)), - fallback = BitmapPainter(RoboHashCache.get(ctx, user.pubkeyHex)), - error = BitmapPainter(RoboHashCache.get(ctx, user.pubkeyHex)), - contentDescription = stringResource(R.string.profile_image), - modifier = Modifier - .width(100.dp) - .height(100.dp) - .clip(shape = CircleShape) - .border(3.dp, MaterialTheme.colors.background, CircleShape) - .background(MaterialTheme.colors.background) - ) - } - Row(horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth()) { - Text( - user.bestDisplayName() ?: "", - modifier = Modifier.padding(top = 7.dp), - fontWeight = FontWeight.Bold, - fontSize = 18.sp - ) - } - Row(horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth()) { - Text(" @${user.bestUsername()}", color = Color.LightGray) - } - - Row( - horizontalArrangement = Arrangement.Center, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 35.dp, vertical = 10.dp) - ) { - QrCodeDrawer("nostr:${user.pubkeyNpub()}") - } - } - - Row( - horizontalArrangement = Arrangement.Center, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 30.dp, vertical = 10.dp) - ) { - Button( - onClick = { presenting = false }, - shape = RoundedCornerShape(35.dp), - modifier = Modifier - .fillMaxWidth() - .height(50.dp), - colors = ButtonDefaults - .buttonColors( - backgroundColor = MaterialTheme.colors.primary - ) - ) { - Text(text = stringResource(R.string.scan_qr)) - } - } - } else { - QrCodeScanner { - if (it.isNullOrEmpty()) { - presenting = true - } else { - onScan(it) - } - } - } - } - } - } - } -} +package com.vitorpamplona.amethyst.ui.navigation + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +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.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.ui.actions.CloseButton +import com.vitorpamplona.amethyst.ui.components.ResizeImage +import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy +import com.vitorpamplona.amethyst.ui.qrcode.QrCodeScanner + +@Composable +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) + ) { + Surface(modifier = Modifier.fillMaxSize()) { + Column( + modifier = Modifier + .background(MaterialTheme.colors.background) + .fillMaxSize() + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(10.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + CloseButton(onCancel = onClose) + } + + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 10.dp), + verticalArrangement = Arrangement.SpaceBetween + ) { + if (presenting) { + Row( + horizontalArrangement = Arrangement.Center, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 30.dp, vertical = 10.dp) + ) { + } + + Column(modifier = Modifier.fillMaxWidth()) { + Row(horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth()) { + RobohashAsyncImageProxy( + robot = user.pubkeyHex, + model = ResizeImage(user.profilePicture(), 100.dp), + contentDescription = stringResource(R.string.profile_image), + modifier = Modifier + .width(100.dp) + .height(100.dp) + .clip(shape = CircleShape) + .border(3.dp, MaterialTheme.colors.background, CircleShape) + .background(MaterialTheme.colors.background) + ) + } + Row(horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth()) { + Text( + user.bestDisplayName() ?: "", + modifier = Modifier.padding(top = 7.dp), + fontWeight = FontWeight.Bold, + fontSize = 18.sp + ) + } + Row(horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth()) { + Text(" @${user.bestUsername()}", color = Color.LightGray) + } + + Row( + horizontalArrangement = Arrangement.Center, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 35.dp, vertical = 10.dp) + ) { + QrCodeDrawer("nostr:${user.pubkeyNpub()}") + } + } + + Row( + horizontalArrangement = Arrangement.Center, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 30.dp, vertical = 10.dp) + ) { + Button( + onClick = { presenting = false }, + shape = RoundedCornerShape(35.dp), + modifier = Modifier + .fillMaxWidth() + .height(50.dp), + colors = ButtonDefaults + .buttonColors( + backgroundColor = MaterialTheme.colors.primary + ) + ) { + Text(text = stringResource(R.string.scan_qr)) + } + } + } else { + QrCodeScanner { + if (it.isNullOrEmpty()) { + presenting = true + } else { + onScan(it) + } + } + } + } + } + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt index 496666f7e..0dd85989b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt @@ -43,7 +43,6 @@ 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.graphics.painter.BitmapPainter import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLifecycleOwner @@ -61,14 +60,13 @@ import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.RoboHashCache import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.NostrChannelDataSource import com.vitorpamplona.amethyst.ui.actions.NewChannelView import com.vitorpamplona.amethyst.ui.actions.PostButton -import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy +import com.vitorpamplona.amethyst.ui.components.AsyncUserImageProxy import com.vitorpamplona.amethyst.ui.components.ResizeImage import com.vitorpamplona.amethyst.ui.dal.ChannelFeedFilter import com.vitorpamplona.amethyst.ui.navigation.Route @@ -228,11 +226,9 @@ fun ChannelHeader(baseChannel: Channel, account: Account, navController: NavCont Column() { Column(modifier = Modifier.padding(12.dp)) { Row(verticalAlignment = Alignment.CenterVertically) { - AsyncImageProxy( + AsyncUserImageProxy( + pubkeyHex = channel.idHex, model = ResizeImage(channel.profilePicture(), 35.dp), - placeholder = BitmapPainter(RoboHashCache.get(context, channel.idHex)), - fallback = BitmapPainter(RoboHashCache.get(context, channel.idHex)), - error = BitmapPainter(RoboHashCache.get(context, channel.idHex)), contentDescription = context.getString(R.string.profile_image), modifier = Modifier .width(35.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 c4736e760..6d1775651 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 @@ -1,245 +1,244 @@ -package com.vitorpamplona.amethyst.ui.screen.loggedIn - -import androidx.compose.animation.animateContentSize -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.Divider -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.TextField -import androidx.compose.material.TextFieldDefaults -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Cancel -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -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.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.painter.BitmapPainter -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalLifecycleOwner -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.KeyboardCapitalization -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.text.style.TextDirection -import androidx.compose.ui.unit.dp -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleEventObserver -import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation.NavController -import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.RoboHashCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.NostrChatroomDataSource -import com.vitorpamplona.amethyst.ui.actions.PostButton -import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy -import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status -import com.vitorpamplona.amethyst.ui.components.ResizeImage -import com.vitorpamplona.amethyst.ui.dal.ChatroomFeedFilter -import com.vitorpamplona.amethyst.ui.note.ChatroomMessageCompose -import com.vitorpamplona.amethyst.ui.note.UsernameDisplay -import com.vitorpamplona.amethyst.ui.screen.ChatroomFeedView -import com.vitorpamplona.amethyst.ui.screen.NostrChatRoomFeedViewModel - -@Composable -fun ChatroomScreen(userId: String?, accountViewModel: AccountViewModel, navController: NavController) { - val accountState by accountViewModel.accountLiveData.observeAsState() - val account = accountState?.account - - if (account != null && userId != null) { - val newPost = remember { mutableStateOf(TextFieldValue("")) } - val replyTo = remember { mutableStateOf(null) } - - ChatroomFeedFilter.loadMessagesBetween(account, userId) - NostrChatroomDataSource.loadMessagesBetween(account, userId) - - val feedViewModel: NostrChatRoomFeedViewModel = viewModel() - val lifeCycleOwner = LocalLifecycleOwner.current - - LaunchedEffect(userId) { - feedViewModel.refresh() - } - - DisposableEffect(userId) { - val observer = LifecycleEventObserver { source, event -> - if (event == Lifecycle.Event.ON_RESUME) { - println("Private Message Start") - NostrChatroomDataSource.start() - feedViewModel.refresh() - } - if (event == Lifecycle.Event.ON_PAUSE) { - println("Private Message Stop") - NostrChatroomDataSource.stop() - } - } - - lifeCycleOwner.lifecycle.addObserver(observer) - onDispose { - lifeCycleOwner.lifecycle.removeObserver(observer) - } - } - - Column(Modifier.fillMaxHeight()) { - NostrChatroomDataSource.withUser?.let { - ChatroomHeader( - it, - accountViewModel = accountViewModel, - navController = navController - ) - } - - Column( - modifier = Modifier - .fillMaxHeight() - .padding(vertical = 0.dp) - .weight(1f, true) - ) { - ChatroomFeedView(feedViewModel, accountViewModel, navController, "Room/$userId") { - replyTo.value = it - } - } - - Spacer(modifier = Modifier.height(10.dp)) - - Row(Modifier.padding(horizontal = 10.dp).animateContentSize(), verticalAlignment = Alignment.CenterVertically) { - val replyingNote = replyTo.value - if (replyingNote != null) { - Column(Modifier.weight(1f)) { - ChatroomMessageCompose( - baseNote = replyingNote, - null, - innerQuote = true, - accountViewModel = accountViewModel, - navController = navController, - onWantsToReply = { - replyTo.value = it - } - ) - } - - Column(Modifier.padding(end = 10.dp)) { - IconButton( - modifier = Modifier.size(30.dp), - onClick = { replyTo.value = null } - ) { - Icon( - imageVector = Icons.Default.Cancel, - null, - modifier = Modifier.padding(end = 5.dp).size(30.dp), - tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) - ) - } - } - } - } - - // LAST ROW - Row( - modifier = Modifier.padding(start = 10.dp, end = 10.dp, bottom = 10.dp, top = 5.dp) - .fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - TextField( - value = newPost.value, - onValueChange = { newPost.value = it }, - keyboardOptions = KeyboardOptions.Default.copy( - capitalization = KeyboardCapitalization.Sentences - ), - modifier = Modifier.weight(1f, true), - shape = RoundedCornerShape(25.dp), - placeholder = { - Text( - text = stringResource(id = R.string.reply_here), - color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) - ) - }, - textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content), - trailingIcon = { - PostButton( - onPost = { - account.sendPrivateMeesage(newPost.value.text, userId, replyTo.value) - newPost.value = TextFieldValue("") - replyTo.value = null - feedViewModel.refresh() // Don't wait a full second before updating - }, - newPost.value.text.isNotBlank(), - modifier = Modifier.padding(end = 10.dp) - ) - }, - colors = TextFieldDefaults.textFieldColors( - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent - ) - ) - } - } - } -} - -@Composable -fun ChatroomHeader(baseUser: User, accountViewModel: AccountViewModel, navController: NavController) { - val ctx = LocalContext.current.applicationContext - - Column( - modifier = Modifier.clickable( - onClick = { navController.navigate("User/${baseUser.pubkeyHex}") } - ) - ) { - Column(modifier = Modifier.padding(12.dp)) { - Row(verticalAlignment = Alignment.CenterVertically) { - val authorState by baseUser.live().metadata.observeAsState() - val author = authorState?.user!! - - AsyncImageProxy( - model = ResizeImage(author.profilePicture(), 35.dp), - placeholder = BitmapPainter(RoboHashCache.get(ctx, author.pubkeyHex)), - fallback = BitmapPainter(RoboHashCache.get(ctx, author.pubkeyHex)), - error = BitmapPainter(RoboHashCache.get(ctx, author.pubkeyHex)), - contentDescription = stringResource(id = R.string.profile_image), - modifier = Modifier - .width(35.dp) - .height(35.dp) - .clip(shape = CircleShape) - ) - - Column(modifier = Modifier.padding(start = 10.dp)) { - Row(verticalAlignment = Alignment.CenterVertically) { - UsernameDisplay(baseUser) - } - - Row(verticalAlignment = Alignment.CenterVertically) { - ObserveDisplayNip05Status(baseUser) - } - } - } - } - - Divider( - modifier = Modifier.padding(start = 12.dp, end = 12.dp), - thickness = 0.25.dp - ) - } -} +package com.vitorpamplona.amethyst.ui.screen.loggedIn + +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.Divider +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.TextField +import androidx.compose.material.TextFieldDefaults +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Cancel +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +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.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextDirection +import androidx.compose.ui.unit.dp +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.service.NostrChatroomDataSource +import com.vitorpamplona.amethyst.ui.actions.PostButton +import com.vitorpamplona.amethyst.ui.components.AsyncUserImageProxy +import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status +import com.vitorpamplona.amethyst.ui.components.ResizeImage +import com.vitorpamplona.amethyst.ui.dal.ChatroomFeedFilter +import com.vitorpamplona.amethyst.ui.note.ChatroomMessageCompose +import com.vitorpamplona.amethyst.ui.note.UsernameDisplay +import com.vitorpamplona.amethyst.ui.screen.ChatroomFeedView +import com.vitorpamplona.amethyst.ui.screen.NostrChatRoomFeedViewModel + +@Composable +fun ChatroomScreen(userId: String?, accountViewModel: AccountViewModel, navController: NavController) { + val accountState by accountViewModel.accountLiveData.observeAsState() + val account = accountState?.account + + if (account != null && userId != null) { + val newPost = remember { mutableStateOf(TextFieldValue("")) } + val replyTo = remember { mutableStateOf(null) } + + ChatroomFeedFilter.loadMessagesBetween(account, userId) + NostrChatroomDataSource.loadMessagesBetween(account, userId) + + val feedViewModel: NostrChatRoomFeedViewModel = viewModel() + val lifeCycleOwner = LocalLifecycleOwner.current + + LaunchedEffect(userId) { + feedViewModel.refresh() + } + + DisposableEffect(userId) { + val observer = LifecycleEventObserver { source, event -> + if (event == Lifecycle.Event.ON_RESUME) { + println("Private Message Start") + NostrChatroomDataSource.start() + feedViewModel.refresh() + } + if (event == Lifecycle.Event.ON_PAUSE) { + println("Private Message Stop") + NostrChatroomDataSource.stop() + } + } + + lifeCycleOwner.lifecycle.addObserver(observer) + onDispose { + lifeCycleOwner.lifecycle.removeObserver(observer) + } + } + + Column(Modifier.fillMaxHeight()) { + NostrChatroomDataSource.withUser?.let { + ChatroomHeader( + it, + accountViewModel = accountViewModel, + navController = navController + ) + } + + Column( + modifier = Modifier + .fillMaxHeight() + .padding(vertical = 0.dp) + .weight(1f, true) + ) { + ChatroomFeedView(feedViewModel, accountViewModel, navController, "Room/$userId") { + replyTo.value = it + } + } + + Spacer(modifier = Modifier.height(10.dp)) + + Row(Modifier.padding(horizontal = 10.dp).animateContentSize(), verticalAlignment = Alignment.CenterVertically) { + val replyingNote = replyTo.value + if (replyingNote != null) { + Column(Modifier.weight(1f)) { + ChatroomMessageCompose( + baseNote = replyingNote, + null, + innerQuote = true, + accountViewModel = accountViewModel, + navController = navController, + onWantsToReply = { + replyTo.value = it + } + ) + } + + Column(Modifier.padding(end = 10.dp)) { + IconButton( + modifier = Modifier.size(30.dp), + onClick = { replyTo.value = null } + ) { + Icon( + imageVector = Icons.Default.Cancel, + null, + modifier = Modifier.padding(end = 5.dp).size(30.dp), + tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) + ) + } + } + } + } + + // LAST ROW + Row( + modifier = Modifier.padding(start = 10.dp, end = 10.dp, bottom = 10.dp, top = 5.dp) + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + TextField( + value = newPost.value, + onValueChange = { newPost.value = it }, + keyboardOptions = KeyboardOptions.Default.copy( + capitalization = KeyboardCapitalization.Sentences + ), + modifier = Modifier.weight(1f, true), + shape = RoundedCornerShape(25.dp), + placeholder = { + Text( + text = stringResource(id = R.string.reply_here), + color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) + ) + }, + textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content), + trailingIcon = { + PostButton( + onPost = { + account.sendPrivateMeesage(newPost.value.text, userId, replyTo.value) + newPost.value = TextFieldValue("") + replyTo.value = null + feedViewModel.refresh() // Don't wait a full second before updating + }, + newPost.value.text.isNotBlank(), + modifier = Modifier.padding(end = 10.dp) + ) + }, + colors = TextFieldDefaults.textFieldColors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent + ) + ) + } + } + } +} + +@Composable +fun ChatroomHeader(baseUser: User, accountViewModel: AccountViewModel, navController: NavController) { + val ctx = LocalContext.current.applicationContext + + Column( + modifier = Modifier.clickable( + onClick = { navController.navigate("User/${baseUser.pubkeyHex}") } + ) + ) { + Column(modifier = Modifier.padding(12.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + val authorState by baseUser.live().metadata.observeAsState() + val author = authorState?.user!! + + AsyncUserImageProxy( + pubkeyHex = author.pubkeyHex, + model = ResizeImage(author.profilePicture(), 35.dp), +// placeholder = BitmapPainter(RoboHashCache.get(ctx, author.pubkeyHex)), +// fallback = BitmapPainter(RoboHashCache.get(ctx, author.pubkeyHex)), +// error = BitmapPainter(RoboHashCache.get(ctx, author.pubkeyHex)), + contentDescription = stringResource(id = R.string.profile_image), + modifier = Modifier + .width(35.dp) + .height(35.dp) + .clip(shape = CircleShape) + ) + + Column(modifier = Modifier.padding(start = 10.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + UsernameDisplay(baseUser) + } + + Row(verticalAlignment = Alignment.CenterVertically) { + ObserveDisplayNip05Status(baseUser) + } + } + } + } + + Divider( + modifier = Modifier.padding(start = 12.dp, end = 12.dp), + thickness = 0.25.dp + ) + } +} From 0eb21a6650f93f182482d3aae12ce30be3bf65c8 Mon Sep 17 00:00:00 2001 From: maxmoney21m Date: Sat, 11 Mar 2023 13:52:17 +0800 Subject: [PATCH 4/7] Replace remaining avatars with robot fallback images --- .../amethyst/ui/components/AsyncImageProxy.kt | 49 -- .../{RoboHashAsyncImage.kt => Robohash.kt} | 634 +++++++++--------- .../ui/components/RobohashAsyncImage.kt | 128 ++++ .../amethyst/ui/navigation/AppTopBar.kt | 9 +- .../amethyst/ui/navigation/DrawerContent.kt | 9 +- .../amethyst/ui/note/ChatroomCompose.kt | 20 +- .../ui/note/ChatroomMessageCompose.kt | 58 +- .../amethyst/ui/note/NoteCompose.kt | 27 +- .../ui/screen/loggedIn/ChannelScreen.kt | 6 +- .../ui/screen/loggedIn/ChatroomScreen.kt | 9 +- .../ui/screen/loggedIn/ProfileScreen.kt | 22 +- .../ui/screen/loggedIn/SearchScreen.kt | 4 +- 12 files changed, 498 insertions(+), 477 deletions(-) rename app/src/main/java/com/vitorpamplona/amethyst/ui/components/{RoboHashAsyncImage.kt => Robohash.kt} (98%) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/components/RobohashAsyncImage.kt diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/AsyncImageProxy.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/AsyncImageProxy.kt index 456e7e327..8e4dbcd33 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/AsyncImageProxy.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/AsyncImageProxy.kt @@ -1,11 +1,6 @@ package com.vitorpamplona.amethyst.ui.components -import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ColorFilter @@ -31,50 +26,6 @@ data class ResizeImage(val url: String?, val size: Dp) { } } -@Composable fun AsyncUserImageProxy( - pubkeyHex: String, - model: ResizeImage, - contentDescription: String?, - modifier: Modifier = Modifier, - alignment: Alignment = Alignment.Center, - contentScale: ContentScale = ContentScale.Fit, - alpha: Float = DefaultAlpha, - colorFilter: ColorFilter? = null, - filterQuality: FilterQuality = DrawScope.DefaultFilterQuality -) { - var loading by remember { mutableStateOf(false) } - var error by remember { mutableStateOf(false) } - - Box() { - AsyncImage( - model = model.proxyUrl(), - contentDescription = contentDescription, - modifier = modifier, - onLoading = { loading = true }, - onSuccess = { loading = false; error = false }, - onError = { loading = false; error = true }, - alignment = alignment, - contentScale = contentScale, - alpha = alpha, - colorFilter = colorFilter, - filterQuality = filterQuality - ) - - if (model.url == null || loading || error) { - RoboHashAsyncImage( - message = pubkeyHex, - contentDescription = contentDescription, - modifier = modifier, - alignment = alignment, - contentScale = contentScale, - alpha = alpha, - colorFilter = colorFilter, - filterQuality = filterQuality - ) - } - } -} - @Composable fun AsyncImageProxy( model: ResizeImage, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RoboHashAsyncImage.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/Robohash.kt similarity index 98% rename from app/src/main/java/com/vitorpamplona/amethyst/ui/components/RoboHashAsyncImage.kt rename to app/src/main/java/com/vitorpamplona/amethyst/ui/components/Robohash.kt index 4e2dd9a11..e3f9a2f97 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RoboHashAsyncImage.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/Robohash.kt @@ -1,335 +1,299 @@ -package com.vitorpamplona.amethyst.ui.components - -import android.content.Context -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.graphics.DefaultAlpha -import androidx.compose.ui.graphics.FilterQuality -import androidx.compose.ui.graphics.drawscope.DrawScope -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext -import coil.compose.AsyncImage -import coil.compose.AsyncImagePainter -import coil.request.ImageRequest -import java.nio.ByteBuffer -import java.security.MessageDigest - -private fun toHex(color: Color): String { - val argb = color.toArgb() - val rgb = argb and 0x00FFFFFF // Mask out the alpha channel - return String.format("#%06X", rgb) -} - -private val sha256: MessageDigest = MessageDigest.getInstance("SHA-256") - -private fun byteMod(byte: Byte, modulo: Int): Int { - val ub = byte.toUByte().toInt() - return ub % modulo -} - -private fun bytesToRGB(b1: Byte, b2: Byte, b3: Byte): Color { - return Color(b1.toUByte().toInt(), b2.toUByte().toInt(), b3.toUByte().toInt()) -} - -fun generateRoboHashSvg(msg: String): String { - val hash = sha256.digest(msg.toByteArray()) - val hashHex = hash.joinToString(separator = "") { b -> "%02x".format(b) } - val bgColor1 = bytesToRGB(hash[0], hash[1], hash[2]) - val bgColor2 = bytesToRGB(hash[3], hash[4], hash[5]) - val fgColor = bytesToRGB(hash[6], hash[7], hash[8]) - val bgIndex = byteMod(hash[9], 8) - val bodyIndex = byteMod(hash[10], 10) - val faceIndex = byteMod(hash[11], 10) - val eyesIndex = byteMod(hash[12], 10) - val mouthIndex = byteMod(hash[13], 10) - val accIndex = byteMod(hash[14], 10) - val background = backgrounds[bgIndex] - val body = bodies[bodyIndex] - val face = faces[faceIndex] - val eye = eyes[eyesIndex] - val mouth = mouths[mouthIndex] - val accessory = accessories[accIndex] - - return """ - - - - - RoboHash $hashHex - ${background}${body.paths}${face.paths}${eye.paths}${mouth.paths}${accessory.paths} - - """.trimIndent() -} - -fun roboHashImageRequest(context: Context, message: String): ImageRequest { - return ImageRequest - .Builder(context) - .data( - ByteBuffer.wrap( - generateRoboHashSvg(message).toByteArray() - ) - ) - .build() -} - -@Composable -fun RoboHashAsyncImage( - message: String, - modifier: Modifier = Modifier, - contentDescription: String? = null, - transform: (AsyncImagePainter.State) -> AsyncImagePainter.State = AsyncImagePainter.DefaultTransform, - onState: ((AsyncImagePainter.State) -> Unit)? = null, - alignment: Alignment = Alignment.Center, - contentScale: ContentScale = ContentScale.Fit, - alpha: Float = DefaultAlpha, - colorFilter: ColorFilter? = null, - filterQuality: FilterQuality = DrawScope.DefaultFilterQuality -) { - AsyncImage( - model = roboHashImageRequest(LocalContext.current, message), - contentDescription = contentDescription, - modifier = modifier, - transform = transform, - onState = onState, - alignment = alignment, - contentScale = contentScale, - alpha = alpha, - colorFilter = colorFilter, - filterQuality = filterQuality - ) -} - -private data class Part(val style: String, val paths: String) - -private val backgrounds: List = listOf( - """""", - """""", - """""", - """""", - """""", - """""", - """""", - """""" -) - -private val accessories: List = listOf( - Part( - """.cls-00-2{fill:none;}.cls-00-2,.cls-00-3,.cls-00-4{stroke:#000;}.cls-00-2,.cls-00-3{stroke-linecap:round;stroke-linejoin:round;}.cls-00-3{fill-opacity:0.4;stroke-width:0.75px;}.cls-00-4{fill-opacity:0.2;stroke-miterlimit:10;stroke-width:0.5px;}""", - """""" - ), - Part( - """.cls-01-2{fill:#fff;fill-opacity:0.2;}.cls-01-3,.cls-01-4{fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-01-4{stroke-width:0.75px;}""", - """""" - ), - Part( - """.cls-02-2{fill:#be1e2d;}.cls-02-11,.cls-02-2,.cls-02-5,.cls-02-6,.cls-02-7,.cls-02-9{stroke:#000;}.cls-02-11,.cls-02-2,.cls-02-5,.cls-02-9{stroke-miterlimit:10;}.cls-02-3{fill:#561317;}.cls-02-4{fill:#ed293b;}.cls-02-11,.cls-02-5,.cls-02-7{fill:none;}.cls-02-5,.cls-02-6{stroke-width:0.75px;}.cls-02-6{fill:#fff;}.cls-02-6,.cls-02-8{fill-opacity:0.2;}.cls-02-6,.cls-02-7{stroke-linecap:round;stroke-linejoin:round;}.cls-02-9{fill:#e6e7e8;}.cls-02-10{fill:#d0d2d3;}""", - """""" - ), - Part( - """.cls-03-2,.cls-03-3,.cls-03-8{fill:#fff;}.cls-03-2,.cls-03-3,.cls-03-7{fill-opacity:0.2;}.cls-03-2,.cls-03-4,.cls-03-5{stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-03-2{stroke-width:0.75px;}.cls-03-4{fill:none;}.cls-03-5{fill:#ec1c24;}.cls-03-6{fill-opacity:0.1;}.cls-03-8{fill-opacity:0.4;}""", - """""" - ), - Part( - """.cls-04-2,.cls-04-3{fill:none;stroke-linecap:round;stroke-linejoin:round;}.cls-04-2,.cls-04-3,.cls-04-5{stroke:#000;}.cls-04-3,.cls-04-5{stroke-width:0.75px;}.cls-04-4,.cls-04-6{fill:#fff;}.cls-04-4{fill-opacity:0.4;}.cls-04-5{fill:#ec1c24;stroke-miterlimit:10;}.cls-04-6,.cls-04-7{fill-opacity:0.2;}""", - """""" - ), - Part( - """.cls-05-2{fill:#fff;fill-opacity:0.4;}.cls-05-2,.cls-05-3{stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-05-3{fill:none;stroke-width:0.75px;}""", - """""" - ), - Part( - """.cls-06-2,.cls-06-4{fill-opacity:0.2;}.cls-06-3,.cls-06-6{fill:none;}.cls-06-3,.cls-06-5,.cls-06-6{stroke:#000;}.cls-06-3,.cls-06-5{stroke-miterlimit:10;}.cls-06-4{fill:#fff;}.cls-06-5{fill:#ec1c24;stroke-width:0.75px;}.cls-06-6{stroke-linecap:round;stroke-linejoin:round;}""", - """""" - ), - Part( - """.cls-07-10,.cls-07-2,.cls-07-4,.cls-07-8,.cls-07-9{fill:none;}.cls-07-10,.cls-07-2,.cls-07-3,.cls-07-4,.cls-07-5,.cls-07-6,.cls-07-8,.cls-07-9{stroke:#000;}.cls-07-2,.cls-07-3,.cls-07-4,.cls-07-5,.cls-07-6,.cls-07-8{stroke-linecap:round;}.cls-07-2,.cls-07-3,.cls-07-4,.cls-07-5,.cls-07-6,.cls-07-8,.cls-07-9{stroke-linejoin:round;}.cls-07-3,.cls-07-5{fill:#fff;}.cls-07-3,.cls-07-5,.cls-07-6,.cls-07-7{fill-opacity:0.2;}.cls-07-3,.cls-07-4{stroke-width:0.75px;}.cls-07-10,.cls-07-8,.cls-07-9{stroke-width:1.5px;}.cls-07-10{stroke-miterlimit:10;}""", - """""" - ), - Part( - """.cls-08-2{fill:none;}.cls-08-2,.cls-08-3,.cls-08-5,.cls-08-6,.cls-08-7{stroke:#000;stroke-miterlimit:10;}.cls-08-3,.cls-08-4{fill:#fff;}.cls-08-3,.cls-08-4,.cls-08-8{fill-opacity:0.2;}.cls-08-5{fill:#716558;}.cls-08-6{fill:#9a8479;}.cls-08-7{fill:#c1b49a;}""", - """""" - ), - Part( - """.cls-09-2{fill:none;}.cls-09-2,.cls-09-4{stroke:#000;stroke-miterlimit:10;}.cls-09-3,.cls-09-4{fill:#fff;}.cls-09-3{fill-opacity:0.2;}.cls-09-5{fill:#d0d2d3;}.cls-09-6{fill-opacity:0.4;}""", - """""" - ) -) - -private val bodies: List = listOf( - Part( - """.cls-10-2{fill:#fff;fill-opacity:0.4;}.cls-10-3,.cls-10-5,.cls-10-7{fill:none;}.cls-10-3,.cls-10-4,.cls-10-5,.cls-10-6,.cls-10-7,.cls-10-8{stroke:#000;stroke-miterlimit:10;}.cls-10-3{stroke-width:1.2px;}.cls-10-4,.cls-10-6,.cls-10-9{fill-opacity:0.2;}.cls-10-5{stroke-width:1.25px;}.cls-10-6{stroke-width:0.75px;}.cls-10-8{fill:#6d6e70;}""", - """""" - ), - Part( - """.cls-11-2{fill:#fff;}.cls-11-2,.cls-11-6{fill-opacity:0.2;}.cls-11-3{fill:none;}.cls-11-3,.cls-11-4{stroke:#000;stroke-miterlimit:10;}.cls-11-4{fill:#6d6e70;}.cls-11-5{opacity:0.2;}""", - """""" - ), - Part( - """.cls-12-2{fill:#fff;fill-opacity:0.4;}.cls-12-3,.cls-12-7{fill:none;}.cls-12-3,.cls-12-5,.cls-12-6,.cls-12-7{stroke:#000;}.cls-12-3,.cls-12-5,.cls-12-6{stroke-miterlimit:10;}.cls-12-4,.cls-12-6{fill-opacity:0.2;}.cls-12-5{fill:#6d6e70;}.cls-12-7{stroke-linecap:round;stroke-linejoin:round;}""", - """""" - ), - Part( - """.cls-13-2{fill:none;}.cls-13-2,.cls-13-5{stroke:#000;stroke-miterlimit:10;}.cls-13-3{fill:#fff;fill-opacity:0.4;}.cls-13-4{fill-opacity:0.2;}.cls-13-5{fill:#6d6e70;}""", - """""" - ), - Part( - """.cls-14-2{fill:none;}.cls-14-2,.cls-14-4{stroke:#000;stroke-miterlimit:10;}.cls-14-3,.cls-14-6{fill-opacity:0.2;}.cls-14-4{fill:#6d6e70;}.cls-14-5,.cls-14-6{fill:#fff;}.cls-14-5{fill-opacity:0.4;}""", - """""" - ), - Part( - """.cls-15-2{fill:#fff;}.cls-15-2,.cls-15-4{fill-opacity:0.2;}.cls-15-2,.cls-15-3,.cls-15-5{stroke:#000;stroke-miterlimit:10;}.cls-15-3{fill:none;}.cls-15-5{fill:#6d6e70;}""", - """""" - ), - Part( - """.cls-16-2,.cls-16-5{fill:none;}.cls-16-2,.cls-16-5,.cls-16-6,.cls-16-7,.cls-16-8,.cls-16-9{stroke:#000;}.cls-16-2{stroke-linecap:round;stroke-linejoin:round;}.cls-16-3,.cls-16-6,.cls-16-7{fill-opacity:0.2;}.cls-16-4,.cls-16-6{fill:#fff;}.cls-16-4{fill-opacity:0.4;}.cls-16-5,.cls-16-6,.cls-16-7,.cls-16-8,.cls-16-9{stroke-miterlimit:10;}.cls-16-8{fill:#f9ec31;}.cls-16-9{fill:#6d6e70;}""", - """""" - ), - Part( - """.cls-17-2{fill:#020202;}.cls-17-2,.cls-17-4,.cls-17-6,.cls-17-7{fill-opacity:0.4;}.cls-17-3{fill-opacity:0.2;}.cls-17-4,.cls-17-6{fill:#fff;}.cls-17-4,.cls-17-5,.cls-17-8{stroke:#000;stroke-miterlimit:10;}.cls-17-5{fill:none;}.cls-17-8{fill:#6d6e70;}""", - """""" - ), - Part( - """.cls-fill-2,.cls-18-3,.cls-18-5,.cls-18-6{stroke:#000;stroke-miterlimit:10;}.cls-18-3{fill:none;}.cls-18-4{fill:#fff;}.cls-18-4,.cls-18-5{fill-opacity:0.4;}.cls-18-6{fill:#6d6e70;}.cls-18-7{opacity:0.2;}.cls-18-8{fill-opacity:0.2;}""", - """""" - ), - Part( - """.cls-19-2{fill-opacity:0.6;}.cls-19-11,.cls-19-13,.cls-19-14,.cls-19-2,.cls-19-3,.cls-19-4,.cls-19-6,.cls-19-8{stroke:#000;}.cls-19-11,.cls-19-13,.cls-19-2,.cls-19-4,.cls-19-6,.cls-19-8{stroke-miterlimit:10;}.cls-19-11,.cls-19-14,.cls-19-3,.cls-19-8{fill:none;}.cls-19-14,.cls-19-3{stroke-linecap:round;stroke-linejoin:round;}.cls-19-10,.cls-19-12,.cls-19-4,.cls-19-5{fill:#fff;}.cls-19-12,.cls-19-4{fill-opacity:0.2;}.cls-19-4{stroke-opacity:0;}.cls-19-5{fill-opacity:0.1;}.cls-19-6{fill:#6d6e70;}.cls-19-7{fill:#58595b;}.cls-19-13,.cls-19-9{fill-opacity:0.4;}.cls-19-10{fill-opacity:0.5;}.cls-19-11,.cls-19-14{stroke-width:0.75px;}""", - """""" - ) -) - -private val eyes: List = listOf( - Part( - """.cls-20-2{fill-opacity:0.4;}.cls-20-2,.cls-20-3{stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-20-3{fill:#461917;stroke-width:0.5px;}""", - """""" - ), - Part( - """.cls-21-2{fill-opacity:0.2;}.cls-21-2,.cls-21-3,.cls-21-4,.cls-21-5{stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-21-2,.cls-21-4,.cls-21-5{stroke-width:0.5px;}.cls-21-3,.cls-21-4{fill-opacity:0.4;}.cls-21-5{fill:#461917;}.cls-21-6{fill:#faaf40;}""", - """""" - ), - Part( - """.cls-22-2{opacity:0.4;}.cls-22-3{fill:#461917;}.cls-22-3,.cls-22-4,.cls-22-5{stroke:#000;}.cls-22-3,.cls-22-5{stroke-linecap:round;stroke-linejoin:round;}.cls-22-3,.cls-22-4{stroke-width:0.5px;}.cls-22-4{fill:#ec1c24;stroke-miterlimit:10;}.cls-22-5{fill:none;stroke-width:0.75px;}""", - """""" - ), - Part( - """.cls-23-2,.cls-23-3{fill:#fff;}.cls-23-2{fill-opacity:0.4;}.cls-23-3{fill-opacity:0.2;}.cls-23-3,.cls-23-4,.cls-23-5{stroke:#000;stroke-miterlimit:10;}.cls-23-4{fill:none;}.cls-23-4,.cls-23-5{stroke-width:0.75px;}.cls-23-5{fill:red;}""", - """""" - ), - Part( - """.cls-24-2{fill-opacity:0.4;stroke-miterlimit:10;}.cls-24-2,.cls-24-3,.cls-24-5{stroke:#000;}.cls-24-2,.cls-24-5{stroke-width:0.75px;}.cls-24-3,.cls-24-5{fill:#461917;stroke-linecap:round;stroke-linejoin:round;}.cls-24-3{stroke-width:0.5px;}.cls-24-4{fill:#ec1c24;}""", - """""" - ), - Part( - """.cls-25-2{fill-opacity:0.55;stroke-miterlimit:10;stroke-width:0.75px;}.cls-25-2,.cls-25-3{stroke:#000;}.cls-25-3{fill:#461917;stroke-linecap:round;stroke-linejoin:round;stroke-width:0.5px;}.cls-25-4{fill:#ec1c24;}""", - """""" - ), - Part( - """.cls-26-2{fill-opacity:0.6;}.cls-26-2,.cls-26-3,.cls-26-4,.cls-26-5,.cls-26-6{stroke:#000;}.cls-26-2,.cls-26-3,.cls-26-4,.cls-26-6{stroke-linecap:round;stroke-linejoin:round;}.cls-26-3{fill:#461917;}.cls-26-3,.cls-26-4,.cls-26-5{stroke-width:0.5px;}.cls-26-4,.cls-26-5{fill:#f9ec31;}.cls-26-5{stroke-miterlimit:10;}.cls-26-6{fill:none;}""", - """""" - ), - Part( - """.cls-27-2{fill-opacity:0.4;stroke-miterlimit:10;}.cls-27-2,.cls-27-3,.cls-27-4{stroke:#000;}.cls-27-3,.cls-27-4{fill:#461917;stroke-linecap:round;stroke-linejoin:round;}.cls-27-3{stroke-width:0.5px;}.cls-27-4{stroke-width:0.75px;}""", - """""" - ), - Part( - """.cls-28-2{fill:none;}.cls-28-2,.cls-28-3,.cls-28-4,.cls-28-6{stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-28-2,.cls-28-4,.cls-28-6{stroke-width:0.75px;}.cls-28-3{fill:#461917;stroke-width:0.5px;}.cls-28-4{fill-opacity:0.4;}.cls-28-5{fill:#fff100;}.cls-28-6{fill:#fff;fill-opacity:0.2;}""", - """""" - ), - Part( - """.cls-29-2{fill:#fff;}.cls-29-2,.cls-29-4{fill-opacity:0.4;}.cls-29-3{fill:none;}.cls-29-3,.cls-29-4{stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-29-4{stroke-width:0.75px;}""", - """""" - ) -) - -private val faces: List = listOf( - Part( - """.cls-30-2{fill:#fff;fill-opacity:0.4;}.cls-30-3,.cls-30-4{fill:none;}.cls-30-3,.cls-30-4,.cls-30-6{stroke:#000;}.cls-30-3,.cls-30-6{stroke-linecap:round;stroke-linejoin:round;}.cls-30-4{stroke-miterlimit:10;}.cls-30-5,.cls-30-6{fill-opacity:0.2;}.cls-30-6{stroke-width:0.75px;}""", - """""" - ), - Part( - """.cls-31-2,.cls-31-4{fill-opacity:0.2;}.cls-31-3{fill:none;}.cls-31-3,.cls-31-4{stroke:#000;stroke-miterlimit:10;}.cls-31-4,.cls-31-5{fill:#fff;}.cls-31-4{stroke-width:0.75px;}.cls-31-5{fill-opacity:0.4;}""", - """""" - ), - Part( - """.cls-32-2{fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-32-3{fill-opacity:0.2;}.cls-32-4{fill:#fff;fill-opacity:0.4;}""", - """""" - ), - Part( - """.cls-33-2{fill-opacity:0.2;}.cls-33-3{fill:#fff;fill-opacity:0.4;}.cls-33-4{fill:none;stroke:#000;stroke-miterlimit:10;}""", - """""" - ), - Part( - """.cls-34-2,.cls-34-3{fill:#fff;}.cls-34-2{fill-opacity:0.4;}.cls-34-3,.cls-34-5{fill-opacity:0.2;}.cls-34-3,.cls-34-4{stroke:#000;stroke-miterlimit:10;}.cls-34-4{fill:none;}""", - """""" - ), - Part( - """.cls-35-2{fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-35-3{fill-opacity:0.2;}.cls-35-4{fill:#fff;fill-opacity:0.4;}""", - """""" - ), - Part( - """.cls-36-2,.cls-36-6{fill:#fff;}.cls-36-2{fill-opacity:0.4;}.cls-36-3,.cls-36-4{fill:none;stroke:#000;stroke-miterlimit:10;}.cls-36-3{stroke-width:2px;}.cls-36-5,.cls-36-6{fill-opacity:0.2;}""", - """""" - ), - Part( - """.cls-37-2{fill:#fff;fill-opacity:0.4;}.cls-37-3{fill:none;}.cls-37-3,.cls-37-4{stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-37-4{fill-opacity:0.2;}""", - """""" - ), - Part( - """.cls-38-2{fill:none;stroke:#000;stroke-miterlimit:10;}.cls-38-3,.cls-38-6{fill:#fff;}.cls-38-3,.cls-38-4{fill-opacity:0.2;}.cls-38-5{fill-opacity:0.1;}.cls-38-6{fill-opacity:0.4;}""", - """""" - ), - Part( - """.cls-39-2{fill:#fff;fill-opacity:0.4;}.cls-39-3{fill:none;}.cls-39-3,.cls-39-4{stroke:#000;stroke-miterlimit:10;}.cls-39-4{fill-opacity:0.2;stroke-width:0.75px;}""", - """""" - ) -) - -private val mouths: List = listOf( - Part( - """.cls-40-2{fill-opacity:0.4;stroke:#000;stroke-linecap:round;stroke-linejoin:round;}""", - """""" - ), - Part( - """.cls-41-2,.cls-41-4{fill:none;}.cls-41-2,.cls-41-3,.cls-41-4{stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-41-3{fill-opacity:0.4;}.cls-41-3,.cls-41-4{stroke-width:0.75px;}""", - """""" - ), - Part( - """.cls-42-2{fill-opacity:0.4;}.cls-42-2,.cls-42-5,.cls-42-6{stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-42-3{fill:#ee4036;}.cls-42-4{fill:#f05a28;}.cls-42-5{fill:#faaf40;stroke-width:0.75px;}.cls-42-6{fill:none;}""", - """""" - ), - Part( - """.cls-43-2{fill:#f9ec31;}.cls-43-3{fill:#faaf40;}.cls-43-4,.cls-43-6{fill:none;}.cls-43-4,.cls-43-5,.cls-43-6{stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-43-5{fill-opacity:0.4;}.cls-43-5,.cls-43-6{stroke-width:0.75px;}""", - """""" - ), - Part( - """.cls-44-2{fill:none;}.cls-44-2,.cls-44-5{stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-44-3{opacity:0.4;}.cls-44-4{fill:#461917;}.cls-44-5{fill-opacity:0.4;stroke-width:0.75px;}""", - """""" - ), - Part( - """.cls-45-2{fill-opacity:0.4;stroke-miterlimit:10;}.cls-45-2,.cls-45-3{stroke:#000;}.cls-45-3{fill:#461917;stroke-linecap:round;stroke-linejoin:round;stroke-width:0.5px;}""", - """""" - ), - Part( - """.cls-46-2{fill-opacity:0.4;}.cls-46-3{fill:none;}.cls-46-3,.cls-46-4{stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-46-4{fill:#461917;stroke-width:0.5px;}""", - """""" - ), - Part( - """.cls-47-2{fill-opacity:0.4;}.cls-47-2,.cls-47-3,.cls-47-4,.cls-47-5{stroke:#000;stroke-miterlimit:10;}.cls-47-3{fill:#f6921e;}.cls-47-4{fill:#f9ec31;stroke-width:0.75px;}.cls-47-5{fill:none;}""", - """""" - ), - Part( - """.cls-48-2{opacity:0.4;}.cls-48-3,.cls-48-4,.cls-48-5{fill:none;}.cls-48-3,.cls-48-4,.cls-48-5,.cls-48-6,.cls-48-7,.cls-48-8{stroke:#000;}.cls-48-3,.cls-48-5,.cls-48-7{stroke-miterlimit:10;}.cls-48-4,.cls-48-6,.cls-48-8{stroke-linecap:round;stroke-linejoin:round;}.cls-48-4,.cls-48-5{stroke-width:0.5px;}.cls-48-6{fill:#f6921e;stroke-width:1.2px;}.cls-48-7{fill:#f9ec31;}.cls-48-7,.cls-48-8{stroke-width:0.75px;}.cls-48-8{fill:#f05a27;}""", - """""" - ), - Part( - """.cls-49-2{fill:none;}.cls-49-2,.cls-49-3,.cls-49-5{stroke:#000;}.cls-49-2,.cls-49-3{stroke-linecap:round;stroke-linejoin:round;}.cls-49-3{opacity:0.1;}.cls-49-4{opacity:0.4;}.cls-49-5{fill-opacity:0.4;stroke-miterlimit:10;stroke-width:0.75px;}""", - """""" - ) -) +package com.vitorpamplona.amethyst.ui.components + +import android.content.Context +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import coil.request.ImageRequest +import java.nio.ByteBuffer +import java.security.MessageDigest + +private fun toHex(color: Color): String { + val argb = color.toArgb() + val rgb = argb and 0x00FFFFFF // Mask out the alpha channel + return String.format("#%06X", rgb) +} + +private val sha256: MessageDigest = MessageDigest.getInstance("SHA-256") + +private fun byteMod(byte: Byte, modulo: Int): Int { + val ub = byte.toUByte().toInt() + return ub % modulo +} + +private fun bytesToRGB(b1: Byte, b2: Byte, b3: Byte): Color { + return Color(b1.toUByte().toInt(), b2.toUByte().toInt(), b3.toUByte().toInt()) +} + +private fun svgString(msg: String): String { + val hash = sha256.digest(msg.toByteArray()) + val hashHex = hash.joinToString(separator = "") { b -> "%02x".format(b) } + val bgColor1 = bytesToRGB(hash[0], hash[1], hash[2]) + val bgColor2 = bytesToRGB(hash[3], hash[4], hash[5]) + val fgColor = bytesToRGB(hash[6], hash[7], hash[8]) + val bgIndex = byteMod(hash[9], 8) + val bodyIndex = byteMod(hash[10], 10) + val faceIndex = byteMod(hash[11], 10) + val eyesIndex = byteMod(hash[12], 10) + val mouthIndex = byteMod(hash[13], 10) + val accIndex = byteMod(hash[14], 10) + val background = backgrounds[bgIndex] + val body = bodies[bodyIndex] + val face = faces[faceIndex] + val eye = eyes[eyesIndex] + val mouth = mouths[mouthIndex] + val accessory = accessories[accIndex] + + return """ + + + + + RoboHash $hashHex + ${background}${body.paths}${face.paths}${eye.paths}${mouth.paths}${accessory.paths} + + """.trimIndent() +} + +object Robohash { + fun imageRequest(context: Context, message: String): ImageRequest { + return ImageRequest + .Builder(context) + .data( + ByteBuffer.wrap( + svgString(message).toByteArray() + ) + ) + .build() + } +} + +private data class Part(val style: String, val paths: String) + +private val backgrounds: List = listOf( + """""", + """""", + """""", + """""", + """""", + """""", + """""", + """""" +) + +private val accessories: List = listOf( + Part( + """.cls-00-2{fill:none;}.cls-00-2,.cls-00-3,.cls-00-4{stroke:#000;}.cls-00-2,.cls-00-3{stroke-linecap:round;stroke-linejoin:round;}.cls-00-3{fill-opacity:0.4;stroke-width:0.75px;}.cls-00-4{fill-opacity:0.2;stroke-miterlimit:10;stroke-width:0.5px;}""", + """""" + ), + Part( + """.cls-01-2{fill:#fff;fill-opacity:0.2;}.cls-01-3,.cls-01-4{fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-01-4{stroke-width:0.75px;}""", + """""" + ), + Part( + """.cls-02-2{fill:#be1e2d;}.cls-02-11,.cls-02-2,.cls-02-5,.cls-02-6,.cls-02-7,.cls-02-9{stroke:#000;}.cls-02-11,.cls-02-2,.cls-02-5,.cls-02-9{stroke-miterlimit:10;}.cls-02-3{fill:#561317;}.cls-02-4{fill:#ed293b;}.cls-02-11,.cls-02-5,.cls-02-7{fill:none;}.cls-02-5,.cls-02-6{stroke-width:0.75px;}.cls-02-6{fill:#fff;}.cls-02-6,.cls-02-8{fill-opacity:0.2;}.cls-02-6,.cls-02-7{stroke-linecap:round;stroke-linejoin:round;}.cls-02-9{fill:#e6e7e8;}.cls-02-10{fill:#d0d2d3;}""", + """""" + ), + Part( + """.cls-03-2,.cls-03-3,.cls-03-8{fill:#fff;}.cls-03-2,.cls-03-3,.cls-03-7{fill-opacity:0.2;}.cls-03-2,.cls-03-4,.cls-03-5{stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-03-2{stroke-width:0.75px;}.cls-03-4{fill:none;}.cls-03-5{fill:#ec1c24;}.cls-03-6{fill-opacity:0.1;}.cls-03-8{fill-opacity:0.4;}""", + """""" + ), + Part( + """.cls-04-2,.cls-04-3{fill:none;stroke-linecap:round;stroke-linejoin:round;}.cls-04-2,.cls-04-3,.cls-04-5{stroke:#000;}.cls-04-3,.cls-04-5{stroke-width:0.75px;}.cls-04-4,.cls-04-6{fill:#fff;}.cls-04-4{fill-opacity:0.4;}.cls-04-5{fill:#ec1c24;stroke-miterlimit:10;}.cls-04-6,.cls-04-7{fill-opacity:0.2;}""", + """""" + ), + Part( + """.cls-05-2{fill:#fff;fill-opacity:0.4;}.cls-05-2,.cls-05-3{stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-05-3{fill:none;stroke-width:0.75px;}""", + """""" + ), + Part( + """.cls-06-2,.cls-06-4{fill-opacity:0.2;}.cls-06-3,.cls-06-6{fill:none;}.cls-06-3,.cls-06-5,.cls-06-6{stroke:#000;}.cls-06-3,.cls-06-5{stroke-miterlimit:10;}.cls-06-4{fill:#fff;}.cls-06-5{fill:#ec1c24;stroke-width:0.75px;}.cls-06-6{stroke-linecap:round;stroke-linejoin:round;}""", + """""" + ), + Part( + """.cls-07-10,.cls-07-2,.cls-07-4,.cls-07-8,.cls-07-9{fill:none;}.cls-07-10,.cls-07-2,.cls-07-3,.cls-07-4,.cls-07-5,.cls-07-6,.cls-07-8,.cls-07-9{stroke:#000;}.cls-07-2,.cls-07-3,.cls-07-4,.cls-07-5,.cls-07-6,.cls-07-8{stroke-linecap:round;}.cls-07-2,.cls-07-3,.cls-07-4,.cls-07-5,.cls-07-6,.cls-07-8,.cls-07-9{stroke-linejoin:round;}.cls-07-3,.cls-07-5{fill:#fff;}.cls-07-3,.cls-07-5,.cls-07-6,.cls-07-7{fill-opacity:0.2;}.cls-07-3,.cls-07-4{stroke-width:0.75px;}.cls-07-10,.cls-07-8,.cls-07-9{stroke-width:1.5px;}.cls-07-10{stroke-miterlimit:10;}""", + """""" + ), + Part( + """.cls-08-2{fill:none;}.cls-08-2,.cls-08-3,.cls-08-5,.cls-08-6,.cls-08-7{stroke:#000;stroke-miterlimit:10;}.cls-08-3,.cls-08-4{fill:#fff;}.cls-08-3,.cls-08-4,.cls-08-8{fill-opacity:0.2;}.cls-08-5{fill:#716558;}.cls-08-6{fill:#9a8479;}.cls-08-7{fill:#c1b49a;}""", + """""" + ), + Part( + """.cls-09-2{fill:none;}.cls-09-2,.cls-09-4{stroke:#000;stroke-miterlimit:10;}.cls-09-3,.cls-09-4{fill:#fff;}.cls-09-3{fill-opacity:0.2;}.cls-09-5{fill:#d0d2d3;}.cls-09-6{fill-opacity:0.4;}""", + """""" + ) +) + +private val bodies: List = listOf( + Part( + """.cls-10-2{fill:#fff;fill-opacity:0.4;}.cls-10-3,.cls-10-5,.cls-10-7{fill:none;}.cls-10-3,.cls-10-4,.cls-10-5,.cls-10-6,.cls-10-7,.cls-10-8{stroke:#000;stroke-miterlimit:10;}.cls-10-3{stroke-width:1.2px;}.cls-10-4,.cls-10-6,.cls-10-9{fill-opacity:0.2;}.cls-10-5{stroke-width:1.25px;}.cls-10-6{stroke-width:0.75px;}.cls-10-8{fill:#6d6e70;}""", + """""" + ), + Part( + """.cls-11-2{fill:#fff;}.cls-11-2,.cls-11-6{fill-opacity:0.2;}.cls-11-3{fill:none;}.cls-11-3,.cls-11-4{stroke:#000;stroke-miterlimit:10;}.cls-11-4{fill:#6d6e70;}.cls-11-5{opacity:0.2;}""", + """""" + ), + Part( + """.cls-12-2{fill:#fff;fill-opacity:0.4;}.cls-12-3,.cls-12-7{fill:none;}.cls-12-3,.cls-12-5,.cls-12-6,.cls-12-7{stroke:#000;}.cls-12-3,.cls-12-5,.cls-12-6{stroke-miterlimit:10;}.cls-12-4,.cls-12-6{fill-opacity:0.2;}.cls-12-5{fill:#6d6e70;}.cls-12-7{stroke-linecap:round;stroke-linejoin:round;}""", + """""" + ), + Part( + """.cls-13-2{fill:none;}.cls-13-2,.cls-13-5{stroke:#000;stroke-miterlimit:10;}.cls-13-3{fill:#fff;fill-opacity:0.4;}.cls-13-4{fill-opacity:0.2;}.cls-13-5{fill:#6d6e70;}""", + """""" + ), + Part( + """.cls-14-2{fill:none;}.cls-14-2,.cls-14-4{stroke:#000;stroke-miterlimit:10;}.cls-14-3,.cls-14-6{fill-opacity:0.2;}.cls-14-4{fill:#6d6e70;}.cls-14-5,.cls-14-6{fill:#fff;}.cls-14-5{fill-opacity:0.4;}""", + """""" + ), + Part( + """.cls-15-2{fill:#fff;}.cls-15-2,.cls-15-4{fill-opacity:0.2;}.cls-15-2,.cls-15-3,.cls-15-5{stroke:#000;stroke-miterlimit:10;}.cls-15-3{fill:none;}.cls-15-5{fill:#6d6e70;}""", + """""" + ), + Part( + """.cls-16-2,.cls-16-5{fill:none;}.cls-16-2,.cls-16-5,.cls-16-6,.cls-16-7,.cls-16-8,.cls-16-9{stroke:#000;}.cls-16-2{stroke-linecap:round;stroke-linejoin:round;}.cls-16-3,.cls-16-6,.cls-16-7{fill-opacity:0.2;}.cls-16-4,.cls-16-6{fill:#fff;}.cls-16-4{fill-opacity:0.4;}.cls-16-5,.cls-16-6,.cls-16-7,.cls-16-8,.cls-16-9{stroke-miterlimit:10;}.cls-16-8{fill:#f9ec31;}.cls-16-9{fill:#6d6e70;}""", + """""" + ), + Part( + """.cls-17-2{fill:#020202;}.cls-17-2,.cls-17-4,.cls-17-6,.cls-17-7{fill-opacity:0.4;}.cls-17-3{fill-opacity:0.2;}.cls-17-4,.cls-17-6{fill:#fff;}.cls-17-4,.cls-17-5,.cls-17-8{stroke:#000;stroke-miterlimit:10;}.cls-17-5{fill:none;}.cls-17-8{fill:#6d6e70;}""", + """""" + ), + Part( + """.cls-fill-2,.cls-18-3,.cls-18-5,.cls-18-6{stroke:#000;stroke-miterlimit:10;}.cls-18-3{fill:none;}.cls-18-4{fill:#fff;}.cls-18-4,.cls-18-5{fill-opacity:0.4;}.cls-18-6{fill:#6d6e70;}.cls-18-7{opacity:0.2;}.cls-18-8{fill-opacity:0.2;}""", + """""" + ), + Part( + """.cls-19-2{fill-opacity:0.6;}.cls-19-11,.cls-19-13,.cls-19-14,.cls-19-2,.cls-19-3,.cls-19-4,.cls-19-6,.cls-19-8{stroke:#000;}.cls-19-11,.cls-19-13,.cls-19-2,.cls-19-4,.cls-19-6,.cls-19-8{stroke-miterlimit:10;}.cls-19-11,.cls-19-14,.cls-19-3,.cls-19-8{fill:none;}.cls-19-14,.cls-19-3{stroke-linecap:round;stroke-linejoin:round;}.cls-19-10,.cls-19-12,.cls-19-4,.cls-19-5{fill:#fff;}.cls-19-12,.cls-19-4{fill-opacity:0.2;}.cls-19-4{stroke-opacity:0;}.cls-19-5{fill-opacity:0.1;}.cls-19-6{fill:#6d6e70;}.cls-19-7{fill:#58595b;}.cls-19-13,.cls-19-9{fill-opacity:0.4;}.cls-19-10{fill-opacity:0.5;}.cls-19-11,.cls-19-14{stroke-width:0.75px;}""", + """""" + ) +) + +private val eyes: List = listOf( + Part( + """.cls-20-2{fill-opacity:0.4;}.cls-20-2,.cls-20-3{stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-20-3{fill:#461917;stroke-width:0.5px;}""", + """""" + ), + Part( + """.cls-21-2{fill-opacity:0.2;}.cls-21-2,.cls-21-3,.cls-21-4,.cls-21-5{stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-21-2,.cls-21-4,.cls-21-5{stroke-width:0.5px;}.cls-21-3,.cls-21-4{fill-opacity:0.4;}.cls-21-5{fill:#461917;}.cls-21-6{fill:#faaf40;}""", + """""" + ), + Part( + """.cls-22-2{opacity:0.4;}.cls-22-3{fill:#461917;}.cls-22-3,.cls-22-4,.cls-22-5{stroke:#000;}.cls-22-3,.cls-22-5{stroke-linecap:round;stroke-linejoin:round;}.cls-22-3,.cls-22-4{stroke-width:0.5px;}.cls-22-4{fill:#ec1c24;stroke-miterlimit:10;}.cls-22-5{fill:none;stroke-width:0.75px;}""", + """""" + ), + Part( + """.cls-23-2,.cls-23-3{fill:#fff;}.cls-23-2{fill-opacity:0.4;}.cls-23-3{fill-opacity:0.2;}.cls-23-3,.cls-23-4,.cls-23-5{stroke:#000;stroke-miterlimit:10;}.cls-23-4{fill:none;}.cls-23-4,.cls-23-5{stroke-width:0.75px;}.cls-23-5{fill:red;}""", + """""" + ), + Part( + """.cls-24-2{fill-opacity:0.4;stroke-miterlimit:10;}.cls-24-2,.cls-24-3,.cls-24-5{stroke:#000;}.cls-24-2,.cls-24-5{stroke-width:0.75px;}.cls-24-3,.cls-24-5{fill:#461917;stroke-linecap:round;stroke-linejoin:round;}.cls-24-3{stroke-width:0.5px;}.cls-24-4{fill:#ec1c24;}""", + """""" + ), + Part( + """.cls-25-2{fill-opacity:0.55;stroke-miterlimit:10;stroke-width:0.75px;}.cls-25-2,.cls-25-3{stroke:#000;}.cls-25-3{fill:#461917;stroke-linecap:round;stroke-linejoin:round;stroke-width:0.5px;}.cls-25-4{fill:#ec1c24;}""", + """""" + ), + Part( + """.cls-26-2{fill-opacity:0.6;}.cls-26-2,.cls-26-3,.cls-26-4,.cls-26-5,.cls-26-6{stroke:#000;}.cls-26-2,.cls-26-3,.cls-26-4,.cls-26-6{stroke-linecap:round;stroke-linejoin:round;}.cls-26-3{fill:#461917;}.cls-26-3,.cls-26-4,.cls-26-5{stroke-width:0.5px;}.cls-26-4,.cls-26-5{fill:#f9ec31;}.cls-26-5{stroke-miterlimit:10;}.cls-26-6{fill:none;}""", + """""" + ), + Part( + """.cls-27-2{fill-opacity:0.4;stroke-miterlimit:10;}.cls-27-2,.cls-27-3,.cls-27-4{stroke:#000;}.cls-27-3,.cls-27-4{fill:#461917;stroke-linecap:round;stroke-linejoin:round;}.cls-27-3{stroke-width:0.5px;}.cls-27-4{stroke-width:0.75px;}""", + """""" + ), + Part( + """.cls-28-2{fill:none;}.cls-28-2,.cls-28-3,.cls-28-4,.cls-28-6{stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-28-2,.cls-28-4,.cls-28-6{stroke-width:0.75px;}.cls-28-3{fill:#461917;stroke-width:0.5px;}.cls-28-4{fill-opacity:0.4;}.cls-28-5{fill:#fff100;}.cls-28-6{fill:#fff;fill-opacity:0.2;}""", + """""" + ), + Part( + """.cls-29-2{fill:#fff;}.cls-29-2,.cls-29-4{fill-opacity:0.4;}.cls-29-3{fill:none;}.cls-29-3,.cls-29-4{stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-29-4{stroke-width:0.75px;}""", + """""" + ) +) + +private val faces: List = listOf( + Part( + """.cls-30-2{fill:#fff;fill-opacity:0.4;}.cls-30-3,.cls-30-4{fill:none;}.cls-30-3,.cls-30-4,.cls-30-6{stroke:#000;}.cls-30-3,.cls-30-6{stroke-linecap:round;stroke-linejoin:round;}.cls-30-4{stroke-miterlimit:10;}.cls-30-5,.cls-30-6{fill-opacity:0.2;}.cls-30-6{stroke-width:0.75px;}""", + """""" + ), + Part( + """.cls-31-2,.cls-31-4{fill-opacity:0.2;}.cls-31-3{fill:none;}.cls-31-3,.cls-31-4{stroke:#000;stroke-miterlimit:10;}.cls-31-4,.cls-31-5{fill:#fff;}.cls-31-4{stroke-width:0.75px;}.cls-31-5{fill-opacity:0.4;}""", + """""" + ), + Part( + """.cls-32-2{fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-32-3{fill-opacity:0.2;}.cls-32-4{fill:#fff;fill-opacity:0.4;}""", + """""" + ), + Part( + """.cls-33-2{fill-opacity:0.2;}.cls-33-3{fill:#fff;fill-opacity:0.4;}.cls-33-4{fill:none;stroke:#000;stroke-miterlimit:10;}""", + """""" + ), + Part( + """.cls-34-2,.cls-34-3{fill:#fff;}.cls-34-2{fill-opacity:0.4;}.cls-34-3,.cls-34-5{fill-opacity:0.2;}.cls-34-3,.cls-34-4{stroke:#000;stroke-miterlimit:10;}.cls-34-4{fill:none;}""", + """""" + ), + Part( + """.cls-35-2{fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-35-3{fill-opacity:0.2;}.cls-35-4{fill:#fff;fill-opacity:0.4;}""", + """""" + ), + Part( + """.cls-36-2,.cls-36-6{fill:#fff;}.cls-36-2{fill-opacity:0.4;}.cls-36-3,.cls-36-4{fill:none;stroke:#000;stroke-miterlimit:10;}.cls-36-3{stroke-width:2px;}.cls-36-5,.cls-36-6{fill-opacity:0.2;}""", + """""" + ), + Part( + """.cls-37-2{fill:#fff;fill-opacity:0.4;}.cls-37-3{fill:none;}.cls-37-3,.cls-37-4{stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-37-4{fill-opacity:0.2;}""", + """""" + ), + Part( + """.cls-38-2{fill:none;stroke:#000;stroke-miterlimit:10;}.cls-38-3,.cls-38-6{fill:#fff;}.cls-38-3,.cls-38-4{fill-opacity:0.2;}.cls-38-5{fill-opacity:0.1;}.cls-38-6{fill-opacity:0.4;}""", + """""" + ), + Part( + """.cls-39-2{fill:#fff;fill-opacity:0.4;}.cls-39-3{fill:none;}.cls-39-3,.cls-39-4{stroke:#000;stroke-miterlimit:10;}.cls-39-4{fill-opacity:0.2;stroke-width:0.75px;}""", + """""" + ) +) + +private val mouths: List = listOf( + Part( + """.cls-40-2{fill-opacity:0.4;stroke:#000;stroke-linecap:round;stroke-linejoin:round;}""", + """""" + ), + Part( + """.cls-41-2,.cls-41-4{fill:none;}.cls-41-2,.cls-41-3,.cls-41-4{stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-41-3{fill-opacity:0.4;}.cls-41-3,.cls-41-4{stroke-width:0.75px;}""", + """""" + ), + Part( + """.cls-42-2{fill-opacity:0.4;}.cls-42-2,.cls-42-5,.cls-42-6{stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-42-3{fill:#ee4036;}.cls-42-4{fill:#f05a28;}.cls-42-5{fill:#faaf40;stroke-width:0.75px;}.cls-42-6{fill:none;}""", + """""" + ), + Part( + """.cls-43-2{fill:#f9ec31;}.cls-43-3{fill:#faaf40;}.cls-43-4,.cls-43-6{fill:none;}.cls-43-4,.cls-43-5,.cls-43-6{stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-43-5{fill-opacity:0.4;}.cls-43-5,.cls-43-6{stroke-width:0.75px;}""", + """""" + ), + Part( + """.cls-44-2{fill:none;}.cls-44-2,.cls-44-5{stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-44-3{opacity:0.4;}.cls-44-4{fill:#461917;}.cls-44-5{fill-opacity:0.4;stroke-width:0.75px;}""", + """""" + ), + Part( + """.cls-45-2{fill-opacity:0.4;stroke-miterlimit:10;}.cls-45-2,.cls-45-3{stroke:#000;}.cls-45-3{fill:#461917;stroke-linecap:round;stroke-linejoin:round;stroke-width:0.5px;}""", + """""" + ), + Part( + """.cls-46-2{fill-opacity:0.4;}.cls-46-3{fill:none;}.cls-46-3,.cls-46-4{stroke:#000;stroke-linecap:round;stroke-linejoin:round;}.cls-46-4{fill:#461917;stroke-width:0.5px;}""", + """""" + ), + Part( + """.cls-47-2{fill-opacity:0.4;}.cls-47-2,.cls-47-3,.cls-47-4,.cls-47-5{stroke:#000;stroke-miterlimit:10;}.cls-47-3{fill:#f6921e;}.cls-47-4{fill:#f9ec31;stroke-width:0.75px;}.cls-47-5{fill:none;}""", + """""" + ), + Part( + """.cls-48-2{opacity:0.4;}.cls-48-3,.cls-48-4,.cls-48-5{fill:none;}.cls-48-3,.cls-48-4,.cls-48-5,.cls-48-6,.cls-48-7,.cls-48-8{stroke:#000;}.cls-48-3,.cls-48-5,.cls-48-7{stroke-miterlimit:10;}.cls-48-4,.cls-48-6,.cls-48-8{stroke-linecap:round;stroke-linejoin:round;}.cls-48-4,.cls-48-5{stroke-width:0.5px;}.cls-48-6{fill:#f6921e;stroke-width:1.2px;}.cls-48-7{fill:#f9ec31;}.cls-48-7,.cls-48-8{stroke-width:0.75px;}.cls-48-8{fill:#f05a27;}""", + """""" + ), + Part( + """.cls-49-2{fill:none;}.cls-49-2,.cls-49-3,.cls-49-5{stroke:#000;}.cls-49-2,.cls-49-3{stroke-linecap:round;stroke-linejoin:round;}.cls-49-3{opacity:0.1;}.cls-49-4{opacity:0.4;}.cls-49-5{fill-opacity:0.4;stroke-miterlimit:10;stroke-width:0.75px;}""", + """""" + ) +) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RobohashAsyncImage.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RobohashAsyncImage.kt new file mode 100644 index 000000000..7523dff6e --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RobohashAsyncImage.kt @@ -0,0 +1,128 @@ +package com.vitorpamplona.amethyst.ui.components + +import androidx.compose.foundation.layout.Box +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.DefaultAlpha +import androidx.compose.ui.graphics.FilterQuality +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import coil.compose.AsyncImage +import coil.compose.AsyncImagePainter + +@Composable +fun RobohashAsyncImage( + robot: String, + modifier: Modifier = Modifier, + contentDescription: String? = null, + transform: (AsyncImagePainter.State) -> AsyncImagePainter.State = AsyncImagePainter.DefaultTransform, + onState: ((AsyncImagePainter.State) -> Unit)? = null, + alignment: Alignment = Alignment.Center, + contentScale: ContentScale = ContentScale.Fit, + alpha: Float = DefaultAlpha, + colorFilter: ColorFilter? = null, + filterQuality: FilterQuality = DrawScope.DefaultFilterQuality +) { + AsyncImage( + model = Robohash.imageRequest(LocalContext.current, robot), + contentDescription = contentDescription, + modifier = modifier, + transform = transform, + onState = onState, + alignment = alignment, + contentScale = contentScale, + alpha = alpha, + colorFilter = colorFilter, + filterQuality = filterQuality + ) +} + +@Composable +fun RobohashFallbackAsyncImage( + robot: String = "aaaa", + model: Any?, + contentDescription: String?, + modifier: Modifier = Modifier, + alignment: Alignment = Alignment.Center, + contentScale: ContentScale = ContentScale.Fit, + alpha: Float = DefaultAlpha, + colorFilter: ColorFilter? = null, + filterQuality: FilterQuality = DrawScope.DefaultFilterQuality +) { + var loading by remember { mutableStateOf(false) } + var error by remember { mutableStateOf(false) } + + Box { + AsyncImage( + model = model, + contentDescription = contentDescription, + modifier = modifier, + onLoading = { loading = true }, + onSuccess = { loading = false; error = false }, + onError = { error = true }, + alignment = alignment, + contentScale = contentScale, + alpha = alpha, + colorFilter = colorFilter, + filterQuality = filterQuality + ) + + if (loading || error) { + RobohashAsyncImage( + robot = robot, + contentDescription = contentDescription, + modifier = modifier, + alignment = alignment, + contentScale = contentScale, + alpha = alpha, + colorFilter = colorFilter, + filterQuality = filterQuality + ) + } + } +} + +@Composable +fun RobohashAsyncImageProxy( + robot: String, + model: ResizeImage, + contentDescription: String?, + modifier: Modifier = Modifier, + alignment: Alignment = Alignment.Center, + contentScale: ContentScale = ContentScale.Fit, + alpha: Float = DefaultAlpha, + colorFilter: ColorFilter? = null, + filterQuality: FilterQuality = DrawScope.DefaultFilterQuality +) { + if (model.url == null) { + RobohashAsyncImage( + robot = robot, + contentDescription = contentDescription, + modifier = modifier, + alignment = alignment, + contentScale = contentScale, + alpha = alpha, + colorFilter = colorFilter, + filterQuality = filterQuality + ) + } else { + RobohashFallbackAsyncImage( + robot = robot, + model = model.proxyUrl(), + contentDescription = contentDescription, + modifier = modifier, + alignment = alignment, + contentScale = contentScale, + alpha = alpha, + colorFilter = colorFilter, + filterQuality = filterQuality + ) + } +} 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 991c8e8dc..eed7bfd60 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 @@ -57,8 +57,8 @@ import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource import com.vitorpamplona.amethyst.service.relays.Client import com.vitorpamplona.amethyst.service.relays.RelayPool import com.vitorpamplona.amethyst.ui.actions.NewRelayListView -import com.vitorpamplona.amethyst.ui.components.AsyncUserImageProxy import com.vitorpamplona.amethyst.ui.components.ResizeImage +import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy import com.vitorpamplona.amethyst.ui.screen.RelayPoolViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import kotlinx.coroutines.launch @@ -195,12 +195,9 @@ fun MainTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) }, modifier = Modifier ) { - AsyncUserImageProxy( - pubkeyHex = accountUser.pubkeyHex, + RobohashAsyncImageProxy( + robot = accountUser.pubkeyHex, model = ResizeImage(accountUser.profilePicture(), 34.dp), -// placeholder = BitmapPainter(RoboHashCache.get(ctx, accountUser.pubkeyHex)), -// fallback = BitmapPainter(RoboHashCache.get(ctx, accountUser.pubkeyHex)), -// error = BitmapPainter(RoboHashCache.get(ctx, accountUser.pubkeyHex)), contentDescription = stringResource(id = R.string.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 b6ad544ef..aa337915d 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 @@ -49,8 +49,8 @@ import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.ui.components.AsyncUserImageProxy import com.vitorpamplona.amethyst.ui.components.ResizeImage +import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountBackupDialog import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import kotlinx.coroutines.launch @@ -135,13 +135,10 @@ fun ProfileContent(baseAccountUser: User, modifier: Modifier = Modifier, scaffol } Column(modifier = modifier) { - AsyncUserImageProxy( - pubkeyHex = accountUser.pubkeyHex, + RobohashAsyncImageProxy( + robot = accountUser.pubkeyHex, model = ResizeImage(accountUser.profilePicture(), 100.dp), contentDescription = stringResource(id = R.string.profile_image), -// placeholder = BitmapPainter(RoboHashCache.get(ctx, accountUser.pubkeyHex)), -// fallback = BitmapPainter(RoboHashCache.get(ctx, accountUser.pubkeyHex)), -// error = BitmapPainter(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 c0dd8c182..023d9cedd 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 @@ -27,8 +27,6 @@ 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.graphics.painter.BitmapPainter -import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.SpanStyle @@ -43,12 +41,11 @@ import androidx.compose.ui.unit.sp import androidx.navigation.NavController import com.vitorpamplona.amethyst.NotificationCache import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.RoboHashCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent -import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy import com.vitorpamplona.amethyst.ui.components.ResizeImage +import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -98,13 +95,8 @@ fun ChatroomCompose( } ChannelName( + channelIdHex = channel.idHex, channelPicture = channel.profilePicture(), - channelPicturePlaceholder = BitmapPainter( - RoboHashCache.get( - context, - channel.idHex - ) - ), channelTitle = { Text( text = buildAnnotatedString { @@ -183,8 +175,8 @@ fun ChatroomCompose( @Composable fun ChannelName( + channelIdHex: String, channelPicture: String?, - channelPicturePlaceholder: Painter?, channelTitle: @Composable (Modifier) -> Unit, channelLastTime: Long?, channelLastContent: String?, @@ -193,11 +185,9 @@ fun ChannelName( ) { ChannelName( channelPicture = { - AsyncImageProxy( + RobohashAsyncImageProxy( + robot = channelIdHex, model = ResizeImage(channelPicture, 55.dp), - placeholder = channelPicturePlaceholder, - fallback = channelPicturePlaceholder, - error = channelPicturePlaceholder, contentDescription = stringResource(R.string.channel_image), modifier = Modifier .width(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 b00a1d192..0c7a730bb 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 @@ -40,7 +40,6 @@ import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorMatrix import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.compositeOver -import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity @@ -51,17 +50,16 @@ import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavController -import coil.compose.AsyncImage 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.Note import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent -import com.vitorpamplona.amethyst.ui.components.AsyncUserImageProxy import com.vitorpamplona.amethyst.ui.components.ResizeImage +import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy +import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.components.TranslateableRichTextViewer import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import kotlinx.coroutines.Dispatchers @@ -149,12 +147,14 @@ fun ChatroomMessageCompose( val modif = if (innerQuote) { Modifier.padding(top = 10.dp, end = 5.dp) } else { - Modifier.fillMaxWidth(1f).padding( - start = 12.dp, - end = 12.dp, - top = 5.dp, - bottom = 5.dp - ) + Modifier + .fillMaxWidth(1f) + .padding( + start = 12.dp, + end = 12.dp, + top = 5.dp, + bottom = 5.dp + ) } Row( @@ -182,9 +182,11 @@ fun ChatroomMessageCompose( var bubbleSize by remember { mutableStateOf(IntSize.Zero) } Column( - modifier = Modifier.padding(start = 10.dp, end = 5.dp, bottom = 5.dp).onSizeChanged { - bubbleSize = it - } + modifier = Modifier + .padding(start = 10.dp, end = 5.dp, bottom = 5.dp) + .onSizeChanged { + bubbleSize = it + } ) { val authorState by note.author!!.live().metadata.observeAsState() val author = authorState?.user!! @@ -195,12 +197,9 @@ fun ChatroomMessageCompose( horizontalArrangement = alignment, modifier = Modifier.padding(top = 5.dp) ) { - AsyncUserImageProxy( - pubkeyHex = author.pubkeyHex, + RobohashAsyncImageProxy( + robot = author.pubkeyHex, model = ResizeImage(author.profilePicture(), 25.dp), -// placeholder = BitmapPainter(RoboHashCache.get(context, author.pubkeyHex)), -// fallback = BitmapPainter(RoboHashCache.get(context, author.pubkeyHex)), -// error = BitmapPainter(RoboHashCache.get(context, author.pubkeyHex)), contentDescription = stringResource(id = R.string.profile_image), modifier = Modifier .width(25.dp) @@ -308,11 +307,16 @@ fun ChatroomMessageCompose( Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier.padding(top = 5.dp).then( - with(LocalDensity.current) { - Modifier.widthIn(bubbleSize.width.toDp(), availableBubbleSize.width.toDp()) - } - ) + modifier = Modifier + .padding(top = 5.dp) + .then( + with(LocalDensity.current) { + Modifier.widthIn( + bubbleSize.width.toDp(), + availableBubbleSize.width.toDp() + ) + } + ) ) { Row() { Text( @@ -366,18 +370,16 @@ private fun RelayBadges(baseNote: Note) { .size(15.dp) .padding(1.dp) ) { - AsyncImage( + RobohashFallbackAsyncImage( + robot = "https://$url/favicon.ico", model = "https://$url/favicon.ico", - placeholder = BitmapPainter(RoboHashCache.get(ctx, url)), - fallback = BitmapPainter(RoboHashCache.get(ctx, url)), - error = BitmapPainter(RoboHashCache.get(ctx, url)), contentDescription = stringResource(id = R.string.relay_icon), colorFilter = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(0f) }), modifier = Modifier .fillMaxSize(1f) .clip(shape = CircleShape) .background(MaterialTheme.colors.background) - .clickable(onClick = { uri.openUri("https://" + url) }) + .clickable(onClick = { uri.openUri("https://$url") }) ) } } 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 078389a62..24dced5f6 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 @@ -19,7 +19,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorMatrix import androidx.compose.ui.graphics.compositeOver -import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalContext @@ -38,7 +37,6 @@ import coil.compose.AsyncImage 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.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User @@ -53,9 +51,11 @@ import com.vitorpamplona.amethyst.service.model.ReactionEvent import com.vitorpamplona.amethyst.service.model.ReportEvent import com.vitorpamplona.amethyst.service.model.RepostEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent -import com.vitorpamplona.amethyst.ui.components.AsyncUserImageProxy import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status import com.vitorpamplona.amethyst.ui.components.ResizeImage +import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage +import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy +import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.components.TranslateableRichTextViewer import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader @@ -217,12 +217,9 @@ fun NoteCompose( .height(30.dp) .align(Alignment.BottomEnd) ) { - AsyncUserImageProxy( - pubkeyHex = channel.idHex, + RobohashAsyncImageProxy( + robot = channel.idHex, model = ResizeImage(channel.profilePicture(), 30.dp), -// placeholder = BitmapPainter(RoboHashCache.get(context, channel.idHex)), -// fallback = BitmapPainter(RoboHashCache.get(context, channel.idHex)), -// error = BitmapPainter(RoboHashCache.get(context, channel.idHex)), contentDescription = stringResource(R.string.group_picture), modifier = Modifier .width(30.dp) @@ -603,11 +600,9 @@ private fun RelayBadges(baseNote: Note) { .size(15.dp) .padding(1.dp) ) { - AsyncImage( + RobohashFallbackAsyncImage( + robot = "https://$url/favicon.ico", model = "https://$url/favicon.ico", - placeholder = BitmapPainter(RoboHashCache.get(ctx, url)), - fallback = BitmapPainter(RoboHashCache.get(ctx, url)), - error = BitmapPainter(RoboHashCache.get(ctx, url)), contentDescription = stringResource(R.string.relay_icon), colorFilter = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(0f) }), modifier = Modifier @@ -677,8 +672,8 @@ fun NoteAuthorPicture( .height(size) ) { if (author == null) { - Image( - painter = BitmapPainter(RoboHashCache.get(ctx, "ohnothisauthorisnotfound")), + RobohashAsyncImage( + robot = "authornotfound", contentDescription = stringResource(R.string.unknown_author), modifier = pictureModifier .fillMaxSize(1f) @@ -724,8 +719,8 @@ fun UserPicture( .width(size) .height(size) ) { - AsyncUserImageProxy( - pubkeyHex = user.pubkeyHex, + RobohashAsyncImageProxy( + robot = user.pubkeyHex, model = ResizeImage(user.profilePicture(), size), contentDescription = stringResource(id = R.string.profile_image), modifier = pictureModifier diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt index 0dd85989b..5d54cbdc5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt @@ -66,8 +66,8 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.NostrChannelDataSource import com.vitorpamplona.amethyst.ui.actions.NewChannelView import com.vitorpamplona.amethyst.ui.actions.PostButton -import com.vitorpamplona.amethyst.ui.components.AsyncUserImageProxy import com.vitorpamplona.amethyst.ui.components.ResizeImage +import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy import com.vitorpamplona.amethyst.ui.dal.ChannelFeedFilter import com.vitorpamplona.amethyst.ui.navigation.Route import com.vitorpamplona.amethyst.ui.note.ChatroomMessageCompose @@ -226,8 +226,8 @@ fun ChannelHeader(baseChannel: Channel, account: Account, navController: NavCont Column() { Column(modifier = Modifier.padding(12.dp)) { Row(verticalAlignment = Alignment.CenterVertically) { - AsyncUserImageProxy( - pubkeyHex = channel.idHex, + RobohashAsyncImageProxy( + robot = channel.idHex, model = ResizeImage(channel.profilePicture(), 35.dp), contentDescription = context.getString(R.string.profile_image), modifier = Modifier 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 6d1775651..e566bb055 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 @@ -52,9 +52,9 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.NostrChatroomDataSource import com.vitorpamplona.amethyst.ui.actions.PostButton -import com.vitorpamplona.amethyst.ui.components.AsyncUserImageProxy import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status import com.vitorpamplona.amethyst.ui.components.ResizeImage +import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy import com.vitorpamplona.amethyst.ui.dal.ChatroomFeedFilter import com.vitorpamplona.amethyst.ui.note.ChatroomMessageCompose import com.vitorpamplona.amethyst.ui.note.UsernameDisplay @@ -211,12 +211,9 @@ fun ChatroomHeader(baseUser: User, accountViewModel: AccountViewModel, navContro val authorState by baseUser.live().metadata.observeAsState() val author = authorState?.user!! - AsyncUserImageProxy( - pubkeyHex = author.pubkeyHex, + RobohashAsyncImageProxy( + robot = author.pubkeyHex, model = ResizeImage(author.profilePicture(), 35.dp), -// placeholder = BitmapPainter(RoboHashCache.get(ctx, author.pubkeyHex)), -// fallback = BitmapPainter(RoboHashCache.get(ctx, author.pubkeyHex)), -// error = BitmapPainter(RoboHashCache.get(ctx, author.pubkeyHex)), contentDescription = stringResource(id = R.string.profile_image), modifier = Modifier .width(35.dp) 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 198b6c5ca..cc7cf9e55 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 @@ -21,7 +21,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.nestedscroll.nestedScroll @@ -51,7 +50,6 @@ 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.RoboHashCache import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note @@ -65,6 +63,8 @@ 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.components.ResizeImage +import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage +import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.components.ZoomableImageDialog import com.vitorpamplona.amethyst.ui.dal.UserProfileConversationsFeedFilter import com.vitorpamplona.amethyst.ui.dal.UserProfileFollowersFeedFilter @@ -432,13 +432,17 @@ private fun DrawAdditionalInfo(baseUser: User, account: Account, navController: ) IconButton( - modifier = Modifier.size(30.dp).padding(start = 5.dp), + modifier = Modifier + .size(30.dp) + .padding(start = 5.dp), onClick = { clipboardManager.setText(AnnotatedString(user.pubkeyNpub())); } ) { Icon( imageVector = Icons.Default.ContentCopy, null, - modifier = Modifier.padding(end = 5.dp).size(15.dp), + modifier = Modifier + .padding(end = 5.dp) + .size(15.dp), tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) ) } @@ -580,20 +584,18 @@ fun BadgeThumb( .height(size) ) { if (image == null) { - Image( - painter = BitmapPainter(RoboHashCache.get(ctx, "ohnothisauthorisnotfound")), + RobohashAsyncImage( + robot = "authornotfound", contentDescription = stringResource(R.string.unknown_author), modifier = pictureModifier .fillMaxSize(1f) .background(MaterialTheme.colors.background) ) } else { - AsyncImage( + RobohashFallbackAsyncImage( + robot = note.idHex, model = image, contentDescription = stringResource(id = R.string.profile_image), - placeholder = BitmapPainter(RoboHashCache.get(ctx, note.idHex)), - fallback = BitmapPainter(RoboHashCache.get(ctx, note.idHex)), - error = BitmapPainter(RoboHashCache.get(ctx, note.idHex)), modifier = pictureModifier .fillMaxSize(1f) .clip(shape = CircleShape) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt index f55a5f3a8..1707582bc 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt @@ -36,7 +36,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.painterResource @@ -49,7 +48,6 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.navigation.NavController import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.RoboHashCache import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.LocalCache @@ -245,8 +243,8 @@ private fun SearchBar(accountViewModel: AccountViewModel, navController: NavCont itemsIndexed(searchResultsChannels.value, key = { _, item -> "c" + item.idHex }) { index, item -> ChannelName( + channelIdHex = item.idHex, channelPicture = item.profilePicture(), - channelPicturePlaceholder = BitmapPainter(RoboHashCache.get(ctx, item.idHex)), channelTitle = { Text( "${item.info.name}", From 887d963c5b3e28757b4c1950ee3f65ef0304ca2f Mon Sep 17 00:00:00 2001 From: maxmoney21m Date: Sat, 11 Mar 2023 13:52:44 +0800 Subject: [PATCH 5/7] Remove original robohash dependency --- app/build.gradle | 3 - .../vitorpamplona/amethyst/RoboHashCache.kt | 167 ------------------ 2 files changed, 170 deletions(-) delete mode 100644 app/src/main/java/com/vitorpamplona/amethyst/RoboHashCache.kt diff --git a/app/build.gradle b/app/build.gradle index ade9ce843..09cf8b8e4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -97,9 +97,6 @@ dependencies { implementation 'com.fasterxml.jackson.module:jackson-module-kotlin:2.14.2' implementation 'com.squareup.retrofit2:converter-gson:2.9.0' - // 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' diff --git a/app/src/main/java/com/vitorpamplona/amethyst/RoboHashCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/RoboHashCache.kt deleted file mode 100644 index a4d95abd4..000000000 --- a/app/src/main/java/com/vitorpamplona/amethyst/RoboHashCache.kt +++ /dev/null @@ -1,167 +0,0 @@ -package com.vitorpamplona.amethyst - -import android.content.Context -import android.graphics.Bitmap -import android.graphics.Canvas -import android.graphics.Paint -import android.util.LruCache -import androidx.compose.ui.graphics.ImageBitmap -import androidx.compose.ui.graphics.asImageBitmap -import name.neuhalfen.projects.android.robohash.buckets.VariableSizeHashing -import name.neuhalfen.projects.android.robohash.handle.Handle -import name.neuhalfen.projects.android.robohash.handle.HandleFactory -import name.neuhalfen.projects.android.robohash.paths.Configuration -import name.neuhalfen.projects.android.robohash.repository.ImageRepository -import java.util.UUID - -object RoboHashCache { - - lateinit var robots: MyRoboHash - - lateinit var defaultAvatar: ImageBitmap - - @Synchronized - fun get(context: Context, hash: String): ImageBitmap { - if (!this::robots.isInitialized) { - robots = MyRoboHash(context) - - defaultAvatar = robots.imageForHandle(robots.calculateHandleFromUUID(UUID.nameUUIDFromBytes("aaaa".toByteArray()))).asImageBitmap() - } - - return defaultAvatar - } -} - -/** - * Recreates RoboHash to use a custom configuration - */ -class MyRoboHash(context: Context) { - private val configuration: Configuration = ModifiedSet1Configuration() - private val repository: ImageRepository - private val hashing = VariableSizeHashing(configuration.bucketSizes) - - // Optional - private var memoryCache: LruCache? = null - - init { - repository = ImageRepository(context.assets) - } - - fun useCache(memoryCache: LruCache?) { - this.memoryCache = memoryCache - } - - fun calculateHandleFromUUID(uuid: UUID?): Handle { - val data = hashing.createBuckets(uuid) - return handleFactory.calculateHandle(data) - } - - fun imageForHandle(handle: Handle): Bitmap { - if (null != memoryCache) { - val cached = memoryCache!![handle.toString()] - if (null != cached) return cached - } - val bucketValues = handle.bucketValues() - val paths = configuration.convertToFacetParts(bucketValues) - val sampleSize = 1 - val buffer = repository.createBuffer(configuration.width(), configuration.height()) - val target = buffer.copy(Bitmap.Config.ARGB_8888, true) - val merged = Canvas(target) - val paint = Paint(0) - - // The first image is not added as copy form the buffer - for (i in paths.indices) { - merged.drawBitmap(repository.getInto(buffer, paths[i], sampleSize), 0f, 0f, paint) - } - repository.returnBuffer(buffer) - if (null != memoryCache) { - memoryCache!!.put(handle.toString(), target) - } - return target - } - - companion object { - private val handleFactory = HandleFactory() - } -} - -/** - * Custom configuration to avoid the use of String.format in the GeneratePath - * This uses the default location and ends up encoding number in the local language - */ -class ModifiedSet1Configuration : Configuration { - override fun convertToFacetParts(bucketValues: ByteArray): Array { - require(bucketValues.size == BUCKET_COUNT) - val color = INT_TO_COLOR[bucketValues[BUCKET_COLOR].toInt()] - val paths = mutableListOf() - - // e.g. - // blue face #2 - // blue nose #7 - // blue - val firstFacetBucket = BUCKET_COLOR + 1 - for (facet in 0 until FACET_COUNT) { - val bucketValue = bucketValues[firstFacetBucket + facet].toInt() - paths.add(generatePath(FACET_PATH_TEMPLATES[facet], color, bucketValue)) - } - return paths.toTypedArray() - } - - private fun generatePath(facetPathTemplate: String, color: String, bucketValue: Int): String { - // TODO: Make more efficient - return facetPathTemplate.replace("#ROOT#", ROOT).replace("#COLOR#".toRegex(), color) - .replace("#ITEM#".toRegex(), (bucketValue + 1).toString().padStart(2, '0')) - } - - override fun getBucketSizes(): ByteArray { - return BUCKET_SIZES - } - - override fun width(): Int { - return 300 - } - - override fun height(): Int { - return 300 - } - - companion object { - private const val ROOT = "sets/set1" - private const val BUCKET_COLOR = 0 - private const val COLOR_COUNT = 10 - private const val BODY_COUNT = 10 - private const val FACE_COUNT = 10 - private const val MOUTH_COUNT = 10 - private const val EYES_COUNT = 10 - private const val ACCESSORY_COUNT = 10 - private const val BUCKET_COUNT = 6 - private const val FACET_COUNT = 5 - private val BUCKET_SIZES = byteArrayOf( - COLOR_COUNT.toByte(), - BODY_COUNT.toByte(), - FACE_COUNT.toByte(), - MOUTH_COUNT.toByte(), - EYES_COUNT.toByte(), - ACCESSORY_COUNT.toByte() - ) - private val INT_TO_COLOR = arrayOf( - "blue", - "brown", - "green", - "grey", - "orange", - "pink", - "purple", - "red", - "white", - "yellow" - ) - private val FACET_PATH_TEMPLATES = arrayOf( - "#ROOT#/#COLOR#/01Body/#COLOR#_body-#ITEM#.png", - "#ROOT#/#COLOR#/02Face/#COLOR#_face-#ITEM#.png", - "#ROOT#/#COLOR#/Mouth/#COLOR#_mouth-#ITEM#.png", - "#ROOT#/#COLOR#/Eyes/#COLOR#_eyes-#ITEM#.png", - "#ROOT#/#COLOR#/Accessory/#COLOR#_accessory-#ITEM#.png" - ) - } -} From 5c518501ab282e4f72a1f2bb40d6f242a0082d54 Mon Sep 17 00:00:00 2001 From: maxmoney21m Date: Mon, 13 Mar 2023 11:24:11 +0800 Subject: [PATCH 6/7] Edit background shape, use painter placeholder/fallback Use logo-shaped background Use painter as placeholder and fallback --- .../amethyst/ui/components/Robohash.kt | 37 +++++-------- .../ui/components/RobohashAsyncImage.kt | 55 ++++++------------- 2 files changed, 31 insertions(+), 61 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/Robohash.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/Robohash.kt index e3f9a2f97..28def6986 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/Robohash.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/Robohash.kt @@ -15,9 +15,9 @@ private fun toHex(color: Color): String { private val sha256: MessageDigest = MessageDigest.getInstance("SHA-256") -private fun byteMod(byte: Byte, modulo: Int): Int { +private fun byteMod10(byte: Byte): Int { val ub = byte.toUByte().toInt() - return ub % modulo + return ub % 10 } private fun bytesToRGB(b1: Byte, b2: Byte, b3: Byte): Color { @@ -27,16 +27,13 @@ private fun bytesToRGB(b1: Byte, b2: Byte, b3: Byte): Color { private fun svgString(msg: String): String { val hash = sha256.digest(msg.toByteArray()) val hashHex = hash.joinToString(separator = "") { b -> "%02x".format(b) } - val bgColor1 = bytesToRGB(hash[0], hash[1], hash[2]) - val bgColor2 = bytesToRGB(hash[3], hash[4], hash[5]) - val fgColor = bytesToRGB(hash[6], hash[7], hash[8]) - val bgIndex = byteMod(hash[9], 8) - val bodyIndex = byteMod(hash[10], 10) - val faceIndex = byteMod(hash[11], 10) - val eyesIndex = byteMod(hash[12], 10) - val mouthIndex = byteMod(hash[13], 10) - val accIndex = byteMod(hash[14], 10) - val background = backgrounds[bgIndex] + val bgColor = bytesToRGB(hash[0], hash[1], hash[2]) + val fgColor = bytesToRGB(hash[3], hash[4], hash[5]) + val bodyIndex = byteMod10(hash[6]) + val faceIndex = byteMod10(hash[7]) + val eyesIndex = byteMod10(hash[8]) + val mouthIndex = byteMod10(hash[9]) + val accIndex = byteMod10(hash[10]) val body = bodies[bodyIndex] val face = faces[faceIndex] val eye = eyes[eyesIndex] @@ -47,11 +44,11 @@ private fun svgString(msg: String): String { - RoboHash $hashHex + Robohash $hashHex ${background}${body.paths}${face.paths}${eye.paths}${mouth.paths}${accessory.paths} """.trimIndent() @@ -66,22 +63,14 @@ object Robohash { svgString(message).toByteArray() ) ) + .crossfade(100) .build() } } private data class Part(val style: String, val paths: String) -private val backgrounds: List = listOf( - """""", - """""", - """""", - """""", - """""", - """""", - """""", - """""" -) +private const val background = """""" private val accessories: List = listOf( Part( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RobohashAsyncImage.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RobohashAsyncImage.kt index 7523dff6e..73b500ccf 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RobohashAsyncImage.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RobohashAsyncImage.kt @@ -1,11 +1,6 @@ package com.vitorpamplona.amethyst.ui.components -import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ColorFilter @@ -16,6 +11,7 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import coil.compose.AsyncImage import coil.compose.AsyncImagePainter +import coil.compose.rememberAsyncImagePainter @Composable fun RobohashAsyncImage( @@ -46,8 +42,8 @@ fun RobohashAsyncImage( @Composable fun RobohashFallbackAsyncImage( - robot: String = "aaaa", - model: Any?, + robot: String, + model: String?, contentDescription: String?, modifier: Modifier = Modifier, alignment: Alignment = Alignment.Center, @@ -56,37 +52,22 @@ fun RobohashFallbackAsyncImage( colorFilter: ColorFilter? = null, filterQuality: FilterQuality = DrawScope.DefaultFilterQuality ) { - var loading by remember { mutableStateOf(false) } - var error by remember { mutableStateOf(false) } + val context = LocalContext.current + val painter = rememberAsyncImagePainter(model = Robohash.imageRequest(context, robot)) - Box { - AsyncImage( - model = model, - contentDescription = contentDescription, - modifier = modifier, - onLoading = { loading = true }, - onSuccess = { loading = false; error = false }, - onError = { error = true }, - alignment = alignment, - contentScale = contentScale, - alpha = alpha, - colorFilter = colorFilter, - filterQuality = filterQuality - ) - - if (loading || error) { - RobohashAsyncImage( - robot = robot, - contentDescription = contentDescription, - modifier = modifier, - alignment = alignment, - contentScale = contentScale, - alpha = alpha, - colorFilter = colorFilter, - filterQuality = filterQuality - ) - } - } + AsyncImage( + model = model, + contentDescription = contentDescription, + modifier = modifier, + placeholder = painter, + fallback = painter, + error = painter, + alignment = alignment, + contentScale = contentScale, + alpha = alpha, + colorFilter = colorFilter, + filterQuality = filterQuality + ) } @Composable From 0f42c2707ed5ab5d50d74b79729fa50a47f78854 Mon Sep 17 00:00:00 2001 From: maxmoney21m Date: Mon, 13 Mar 2023 23:17:38 +0800 Subject: [PATCH 7/7] Rebase on main / fix merge conflicts --- .../ui/navigation/AccountSwitchBottomSheet.kt | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AccountSwitchBottomSheet.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AccountSwitchBottomSheet.kt index cbcc742ca..eeb90bdcc 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AccountSwitchBottomSheet.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AccountSwitchBottomSheet.kt @@ -38,7 +38,6 @@ 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.graphics.painter.BitmapPainter import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight @@ -47,13 +46,14 @@ import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.RoboHashCache -import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy import com.vitorpamplona.amethyst.ui.components.ResizeImage +import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy import com.vitorpamplona.amethyst.ui.note.toShortenHex import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedOff.LoginPage +import nostr.postr.bechToBytes +import nostr.postr.toHex @Composable fun AccountSwitchBottomSheet( @@ -108,18 +108,15 @@ fun AccountSwitchBottomSheet( .width(55.dp) .padding(0.dp) ) { - AsyncImageProxy( + RobohashAsyncImageProxy( + robot = acc.npub.bechToBytes("npub").toHex(), model = ResizeImage(acc.profilePicture, 55.dp), - placeholder = BitmapPainter(RoboHashCache.get(context, acc.npub)), - fallback = BitmapPainter(RoboHashCache.get(context, acc.npub)), - error = BitmapPainter(RoboHashCache.get(context, acc.npub)), contentDescription = stringResource(R.string.profile_image), modifier = Modifier .width(55.dp) .height(55.dp) .clip(shape = CircleShape) ) - Box( modifier = Modifier .size(20.dp)