From ae6cf15768270e081ee9c27df29d1a6b4f1aae43 Mon Sep 17 00:00:00 2001 From: maxmoney21m Date: Fri, 10 Mar 2023 22:06:15 +0800 Subject: [PATCH 01/45] Replace mlkit with zxing QR scanner --- app/build.gradle | 12 +- app/src/main/AndroidManifest.xml | 6 + .../amethyst/ui/qrcode/QrCodeDrawer.kt | 5 +- .../amethyst/ui/qrcode/QrCodeScanner.kt | 213 +++++------------- .../amethyst/ui/qrcode/ShowQRDialog.kt | 57 ++--- 5 files changed, 86 insertions(+), 207 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 878ecefcd..7467cdb16 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -133,29 +133,19 @@ dependencies { // For QR generation implementation 'com.google.zxing:core:3.5.1' - implementation "androidx.camera:camera-camera2:1.2.1" - implementation 'androidx.camera:camera-lifecycle:1.2.1' - implementation 'androidx.camera:camera-view:1.2.1' + implementation 'com.journeyapps:zxing-android-embedded:4.3.0' // Markdown implementation "com.halilibo.compose-richtext:richtext-ui:0.16.0" implementation "com.halilibo.compose-richtext:richtext-ui-material:0.16.0" implementation "com.halilibo.compose-richtext:richtext-commonmark:0.16.0" - // For QR Scanning - implementation 'com.google.mlkit:vision-common:17.3.0' - - // Local Barcode Scanning model - // The idea is to make it work for degoogled phones - implementation 'com.google.mlkit:barcode-scanning:17.0.3' - // Local model for language identification implementation 'com.google.mlkit:language-id:17.0.4' // Google services model the translate text implementation 'com.google.mlkit:translate:17.0.1' - // Automatic memory leak detection debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 641da01bb..ae4d3376c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -22,6 +22,7 @@ android:theme="@style/Theme.Amethyst" android:largeHeap="true" android:usesCleartextTraffic="true" + android:hardwareAccelerated="true" tools:targetApi="33"> + + \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/QrCodeDrawer.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/QrCodeDrawer.kt index ac9669c6f..e01e94b19 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/QrCodeDrawer.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/QrCodeDrawer.kt @@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier @@ -34,13 +33,13 @@ fun QrCodeDrawer(contents: String, modifier: Modifier = Modifier) { createQrCode(contents = contents) } - val foregroundColor = MaterialTheme.colors.onSurface + val foregroundColor = Color.Black Box( modifier = modifier .defaultMinSize(48.dp, 48.dp) .aspectRatio(1f) - .background(MaterialTheme.colors.background) + .background(Color.White) ) { Canvas(modifier = Modifier.fillMaxSize()) { // Calculate the height and width of each column/row diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/QrCodeScanner.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/QrCodeScanner.kt index 1d3aa37d4..086ca2ef5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/QrCodeScanner.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/QrCodeScanner.kt @@ -1,156 +1,57 @@ -package com.vitorpamplona.amethyst.ui.qrcode -import android.Manifest -import android.content.pm.PackageManager -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.camera.core.CameraSelector -import androidx.camera.core.ImageAnalysis -import androidx.camera.core.ImageProxy -import androidx.camera.core.Preview -import androidx.camera.lifecycle.ProcessCameraProvider -import androidx.camera.view.PreviewView -import androidx.compose.foundation.layout.Column -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalLifecycleOwner -import androidx.compose.ui.viewinterop.AndroidView -import androidx.core.content.ContextCompat -import androidx.lifecycle.LifecycleOwner -import com.google.mlkit.vision.barcode.BarcodeScannerOptions -import com.google.mlkit.vision.barcode.BarcodeScanning -import com.google.mlkit.vision.barcode.common.Barcode -import com.google.mlkit.vision.common.InputImage -import com.vitorpamplona.amethyst.service.nip19.Nip19 -import java.util.concurrent.ExecutorService -import java.util.concurrent.Executors - -@Composable -fun QrCodeScanner(onScan: (String) -> Unit) { - val context = LocalContext.current - val lifecycleOwner = LocalLifecycleOwner.current - val cameraProviderFuture = remember { ProcessCameraProvider.getInstance(context) } - - val cameraExecutor = Executors.newSingleThreadExecutor() - - var hasCameraPermission by remember { - mutableStateOf( - ContextCompat.checkSelfPermission( - context, - Manifest.permission.CAMERA - ) == PackageManager.PERMISSION_GRANTED - ) - } - - val launcher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.RequestPermission(), - onResult = { granted -> - hasCameraPermission = granted - } - ) - - val analyzer = QRCodeAnalyzer { result -> - result?.let { - try { - val nip19 = Nip19.uriToRoute(it) - val startingPage = when (nip19?.type) { - Nip19.Type.USER -> "User/${nip19.hex}" - Nip19.Type.NOTE -> "Note/${nip19.hex}" - else -> null - } - - if (startingPage != null) { - onScan(startingPage) - } - } catch (e: Throwable) { - // QR can be anythign. do not throw errors. - } - } - } - - DisposableEffect(key1 = true) { - launcher.launch(Manifest.permission.CAMERA) - onDispose() { - cameraProviderFuture.get().unbindAll() - cameraExecutor.shutdown() - } - } - - Column() { - if (hasCameraPermission) { - AndroidView( - factory = { context -> - val previewView = PreviewView(context) - - cameraProviderFuture.addListener({ - val cameraProvider = cameraProviderFuture.get() - bindPreview(analyzer, previewView, cameraExecutor, cameraProvider, lifecycleOwner) - }, ContextCompat.getMainExecutor(context)) - - return@AndroidView previewView - }, - modifier = Modifier.weight(1f) - ) - } - } -} - -fun bindPreview( - analyzer: ImageAnalysis.Analyzer, - previewView: PreviewView, - cameraExecutor: ExecutorService, - cameraProvider: ProcessCameraProvider, - lifecycleOwner: LifecycleOwner -) { - val preview = Preview.Builder().build() - - val selector = CameraSelector.Builder() - .requireLensFacing(CameraSelector.LENS_FACING_BACK) - .build() - - preview.setSurfaceProvider(previewView.surfaceProvider) - - val imageAnalysis = ImageAnalysis.Builder() - .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) - .build() - - imageAnalysis.setAnalyzer( - cameraExecutor, - analyzer - ) - - cameraProvider.bindToLifecycle( - lifecycleOwner, - selector, - imageAnalysis, - preview - ) -} - -class QRCodeAnalyzer( - private val onQrCodeScanned: (result: String?) -> Unit -) : ImageAnalysis.Analyzer { - - private val scanningOptions = BarcodeScannerOptions.Builder().setBarcodeFormats(Barcode.FORMAT_QR_CODE).build() - - fun scanBarcodes(inputImage: InputImage) { - BarcodeScanning.getClient(scanningOptions).process(inputImage) - .addOnSuccessListener { barcodes -> - if (barcodes.isNotEmpty()) { - onQrCodeScanned(barcodes[0].displayValue) - } - } - .addOnFailureListener { - it.printStackTrace() - } - } - - @androidx.annotation.OptIn(androidx.camera.core.ExperimentalGetImage::class) - override fun analyze(imageProxy: ImageProxy) { - imageProxy.image?.let { image -> - val inputImage = InputImage.fromMediaImage(image, imageProxy.imageInfo.rotationDegrees) - scanBarcodes(inputImage) - } - imageProxy.close() - } -} +package com.vitorpamplona.amethyst.ui.qrcode + +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.res.stringResource +import com.journeyapps.barcodescanner.ScanContract +import com.journeyapps.barcodescanner.ScanOptions +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.service.nip19.Nip19 + +@Composable +fun QrCodeScanner(onScan: (String?) -> Unit) { + val lifecycleOwner = LocalLifecycleOwner.current + + val parseQrResult = { it: String -> + try { + val nip19 = Nip19.uriToRoute(it) + val startingPage = when (nip19?.type) { + Nip19.Type.USER -> "User/${nip19.hex}" + Nip19.Type.NOTE -> "Note/${nip19.hex}" + else -> null + } + + if (startingPage != null) { + onScan(startingPage) + } else { + onScan(null) + } + } catch (e: Throwable) { + // QR can be anything, do not throw errors. + onScan(null) + } + } + + val qrLauncher = + rememberLauncherForActivityResult(ScanContract()) { + if (it.contents != null) { + parseQrResult(it.contents) + } else { + onScan(null) + } + } + + val scanOptions = ScanOptions().apply { + setDesiredBarcodeFormats(ScanOptions.QR_CODE) + setPrompt(stringResource(id = R.string.point_to_the_qr_code)) + setBeepEnabled(false) + setOrientationLocked(false) + } + + DisposableEffect(lifecycleOwner) { + qrLauncher.launch(scanOptions) + onDispose { } + } +} 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 eb61f2ede..936d3f005 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 @@ -59,7 +59,9 @@ fun ShowQRDialog(user: User, onScan: (String) -> Unit, onClose: () -> Unit) { .fillMaxSize() ) { Row( - modifier = Modifier.fillMaxWidth().padding(10.dp), + modifier = Modifier + .fillMaxWidth() + .padding(10.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { @@ -67,13 +69,17 @@ fun ShowQRDialog(user: User, onScan: (String) -> Unit, onClose: () -> Unit) { } Column( - modifier = Modifier.fillMaxSize().padding(horizontal = 10.dp), + 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) + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 30.dp, vertical = 10.dp) ) { } @@ -107,7 +113,9 @@ fun ShowQRDialog(user: User, onScan: (String) -> Unit, onClose: () -> Unit) { Row( horizontalArrangement = Arrangement.Center, - modifier = Modifier.fillMaxWidth().padding(horizontal = 35.dp, vertical = 10.dp) + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 35.dp, vertical = 10.dp) ) { QrCodeDrawer("nostr:${user.pubkeyNpub()}") } @@ -115,7 +123,9 @@ fun ShowQRDialog(user: User, onScan: (String) -> Unit, onClose: () -> Unit) { Row( horizontalArrangement = Arrangement.Center, - modifier = Modifier.fillMaxWidth().padding(horizontal = 30.dp, vertical = 10.dp) + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 30.dp, vertical = 10.dp) ) { Button( onClick = { presenting = false }, @@ -132,38 +142,11 @@ fun ShowQRDialog(user: User, onScan: (String) -> Unit, onClose: () -> Unit) { } } } else { - Row(horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth()) { - Text( - stringResource(R.string.point_to_the_qr_code), - modifier = Modifier.padding(top = 7.dp), - fontWeight = FontWeight.Bold, - fontSize = 25.sp - ) - } - - Row( - horizontalArrangement = Arrangement.Center, - modifier = Modifier.fillMaxWidth().padding(30.dp) - ) { - QrCodeScanner(onScan) - } - - Row( - horizontalArrangement = Arrangement.Center, - modifier = Modifier.fillMaxWidth().padding(horizontal = 30.dp, vertical = 10.dp) - ) { - Button( - onClick = { presenting = true }, - shape = RoundedCornerShape(35.dp), - modifier = Modifier - .fillMaxWidth() - .height(50.dp), - colors = ButtonDefaults - .buttonColors( - backgroundColor = MaterialTheme.colors.primary - ) - ) { - Text(text = stringResource(R.string.show_qr)) + QrCodeScanner { + if (it.isNullOrEmpty()) { + presenting = true + } else { + onScan(it) } } } From d4cee941ef457a411365472d05bd8141fae815d8 Mon Sep 17 00:00:00 2001 From: maxmoney21m Date: Sat, 11 Mar 2023 01:22:23 +0800 Subject: [PATCH 02/45] Fix inverted QR scan --- .../com/vitorpamplona/amethyst/ui/qrcode/QrCodeDrawer.kt | 5 +++-- .../com/vitorpamplona/amethyst/ui/qrcode/QrCodeScanner.kt | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/QrCodeDrawer.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/QrCodeDrawer.kt index e01e94b19..ac9669c6f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/QrCodeDrawer.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/QrCodeDrawer.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier @@ -33,13 +34,13 @@ fun QrCodeDrawer(contents: String, modifier: Modifier = Modifier) { createQrCode(contents = contents) } - val foregroundColor = Color.Black + val foregroundColor = MaterialTheme.colors.onSurface Box( modifier = modifier .defaultMinSize(48.dp, 48.dp) .aspectRatio(1f) - .background(Color.White) + .background(MaterialTheme.colors.background) ) { Canvas(modifier = Modifier.fillMaxSize()) { // Calculate the height and width of each column/row diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/QrCodeScanner.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/QrCodeScanner.kt index 086ca2ef5..5be23dc40 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/QrCodeScanner.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/QrCodeScanner.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.stringResource +import com.google.zxing.client.android.Intents import com.journeyapps.barcodescanner.ScanContract import com.journeyapps.barcodescanner.ScanOptions import com.vitorpamplona.amethyst.R @@ -48,6 +49,7 @@ fun QrCodeScanner(onScan: (String?) -> Unit) { setPrompt(stringResource(id = R.string.point_to_the_qr_code)) setBeepEnabled(false) setOrientationLocked(false) + addExtra(Intents.Scan.SCAN_TYPE, Intents.Scan.MIXED_SCAN) } DisposableEffect(lifecycleOwner) { From a8dc03c4770d983188c386b42eeddd114f60f554 Mon Sep 17 00:00:00 2001 From: heyhoe <38322494+h3y6e@users.noreply.github.com> Date: Sat, 11 Mar 2023 03:46:07 +0900 Subject: [PATCH 03/45] Correct identity claim for mastodon --- .../com/vitorpamplona/amethyst/service/model/MetadataEvent.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/MetadataEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/MetadataEvent.kt index b3da7c803..1b57e095e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/MetadataEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/MetadataEvent.kt @@ -128,7 +128,7 @@ class MastodonIdentity( if (proofUrl.isBlank()) return null val path = proofUrl.removePrefix("https://").split("?")[0].split("/") - return MastodonIdentity(path[0], path[1]) + return MastodonIdentity("${path[0]}/${path[1]}", path[2]) } catch (e: Exception) { null } From fe9798fdfcbc59e33b8f2647e80b5c49bd440191 Mon Sep 17 00:00:00 2001 From: maxmoney21m Date: Sat, 11 Mar 2023 10:50:37 +0800 Subject: [PATCH 04/45] Fix string --- app/src/main/res/values/strings.xml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index baff7e1ad..1a79917c0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -178,7 +178,13 @@ Mark all New as read Mark all as read Backup Keys - " ## Key Backup and Safety Tips Your account is secured by a secret key. The key is long random string starting with **nsec1**. Anyone who has access to your secret key can publish content using your identity. - Do **not** put your secret key in any website or software you do not trust. - Amethyst developers will **never** ask you for your secret key. - **Do** keep a secure backup of your secret key for account recovery. We recommend using a password manager. " + + ## Key Backup and Safety Tips + \n\nYour account is secured by a secret key. The key is long random string starting with **nsec1**. Anyone who has access to your secret key can publish content using your identity. + \n\n- Do **not** put your secret key in any website or software you do not trust. + \n- Amethyst developers will **never** ask you for your secret key. + \n- **Do** keep a secure backup of your secret key for account recovery. We recommend using a password manager. + Secret key (nsec) copied to clipboard Copy my secret key Authentication failed @@ -202,17 +208,14 @@ Follow Request Deletion Amethyst will request that your note be deleted from the relays you are currently connected to. There is no guarantee that your note will be permanently deleted from those relays, or from other relays where it may be stored. - Github Gist w/ Proof Telegram Mastodon Post ID w/ Proof Twitter Status w/ Proof - https://gist.github.com/<user>/<gist> https://t.me/<proof post> https://<server>/<user>/<proof post> https://twitter.com/<user>/status/<proof post> - "<Unable to decrypt private message>\n\nYou were cited in a private/encrypted conversation between %1$s and %2$s." Delete Don\'t show again From 6109131ef5640006358c797cb4aa9a7aa6570d25 Mon Sep 17 00:00:00 2001 From: maxmoney21m Date: Sat, 11 Mar 2023 11:21:16 +0800 Subject: [PATCH 05/45] Add quick action menu in chats and threads --- .../amethyst/ui/note/ChatroomMessageCompose.kt | 2 +- .../amethyst/ui/screen/ThreadFeedView.kt | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) 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 a4a619c1c..25cae5b8d 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 @@ -339,7 +339,7 @@ fun ChatroomMessageCompose( } } - NoteDropDownMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel) + NoteQuickActionMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt index fc40ed0ad..aeffef514 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt @@ -2,7 +2,9 @@ package com.vitorpamplona.amethyst.ui.screen import androidx.compose.animation.Crossfade import androidx.compose.animation.core.tween +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row @@ -56,6 +58,7 @@ import com.vitorpamplona.amethyst.ui.note.HiddenNote import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.note.NoteDropDownMenu +import com.vitorpamplona.amethyst.ui.note.NoteQuickActionMenu import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay import com.vitorpamplona.amethyst.ui.note.ReactionsRow import com.vitorpamplona.amethyst.ui.note.timeAgo @@ -187,6 +190,7 @@ fun Modifier.drawReplyLevel(level: Int, color: Color, selected: Color): Modifier } .padding(start = (2 + (level * 3)).dp) +@OptIn(ExperimentalFoundationApi::class) @Composable fun NoteMaster( baseNote: Note, @@ -211,6 +215,8 @@ fun NoteMaster( val noteEvent = note?.event + var popupExpanded by remember { mutableStateOf(false) } + if (noteEvent == null) { BlankNote() } else if (!account.isAcceptable(noteForReports) && !showHiddenNote) { @@ -314,7 +320,14 @@ fun NoteMaster( } } - Row(modifier = Modifier.padding(horizontal = 12.dp)) { + Row( + modifier = Modifier + .padding(horizontal = 12.dp) + .combinedClickable( + onClick = { }, + onLongClick = { popupExpanded = true } + ) + ) { Column() { val eventContent = note.event?.content() @@ -343,5 +356,7 @@ fun NoteMaster( } } } + + NoteQuickActionMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel) } } From 31e724662d43e415ac34fc26382d7b65ecee5f67 Mon Sep 17 00:00:00 2001 From: Akiomi Kamakura Date: Sat, 11 Mar 2023 13:00:35 +0900 Subject: [PATCH 06/45] Add string resources to strings.xml --- .../com/vitorpamplona/amethyst/ui/actions/NewRelayListView.kt | 2 +- .../main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt | 2 +- app/src/main/res/values/strings.xml | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListView.kt index 86502b820..c96ba3b56 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListView.kt @@ -186,7 +186,7 @@ fun ServerConfigHeader() { Spacer(modifier = Modifier.size(5.dp)) Text( - text = "Spam", + text = stringResource(R.string.spam), maxLines = 1, fontSize = 14.sp, modifier = Modifier.weight(1f), 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 271120cf7..1b836af46 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 @@ -829,7 +829,7 @@ fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit, if (note.author == accountViewModel.accountLiveData.value?.account?.userProfile()) { Divider() DropdownMenuItem(onClick = { accountViewModel.delete(note); onDismiss() }) { - Text("Request Deletion") + Text(stringResource(R.string.request_deletion)) } } if (note.author != accountViewModel.accountLiveData.value?.account?.userProfile()) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index baff7e1ad..453d2fcb5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -23,6 +23,7 @@ Copy Author ID Copy Note ID Broadcast + Request Deletion Report Spam / Scam Report Impersonation From bee361cea7de715ee0d4eb7293a2f851dfb97db1 Mon Sep 17 00:00:00 2001 From: Bardesss <30936735+Bardesss@users.noreply.github.com> Date: Sat, 11 Mar 2023 13:26:59 +0000 Subject: [PATCH 07/45] Update strings.xml Fixed threads --- app/src/main/res/values-nl/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index d61578c79..fdb575b91 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -104,7 +104,7 @@ Bekend Nieuw verzoek Geblokeerde gebruikers - Nieuwe draadjes + Nieuwe notities Gesprekken Notities Reacties From d166f22c58588c454f75c5cabbaf74c90f21fa97 Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Sat, 11 Mar 2023 20:34:44 +0000 Subject: [PATCH 08/45] Extract checkIfValidHex() --- .../com/vitorpamplona/amethyst/model/LocalCache.kt | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 1c06fec3e..221862b37 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -58,7 +58,7 @@ object LocalCache { fun checkGetOrCreateUser(key: String): User? { return try { - val checkHex = Hex.decode(key).toNpub() // Checks if this is a valid Hex + checkIfValidHex(key) getOrCreateUser(key) } catch (e: IllegalArgumentException) { Log.e("LocalCache", "Invalid Key to create user: $key", e) @@ -80,7 +80,7 @@ object LocalCache { return checkGetOrCreateAddressableNote(key) } return try { - val checkHex = Hex.decode(key).toNote() // Checks if this is a valid Hex + checkIfValidHex(key) getOrCreateNote(key) } catch (e: IllegalArgumentException) { Log.e("LocalCache", "Invalid Key to create note: $key", e) @@ -99,7 +99,7 @@ object LocalCache { fun checkGetOrCreateChannel(key: String): Channel? { return try { - val checkHex = Hex.decode(key).toNote() // Checks if this is a valid Hex + checkIfValidHex(key) getOrCreateChannel(key) } catch (e: IllegalArgumentException) { Log.e("LocalCache", "Invalid Key to create channel: $key", e) @@ -107,6 +107,10 @@ object LocalCache { } } + private fun checkIfValidHex(key: String) { + Hex.decode(key).toNpub() + } + @Synchronized fun getOrCreateChannel(key: String): Channel { return channels[key] ?: run { From 30f4ff9d6fa2cae9a4fe76ff5740df254842bc8f Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Sat, 11 Mar 2023 20:43:46 +0000 Subject: [PATCH 09/45] Remove indentation level LocalCache.consume(ChannelCreateEvent) --- .../amethyst/model/LocalCache.kt | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 221862b37..d6fdfcdb9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -589,21 +589,19 @@ object LocalCache { fun consume(event: ChannelCreateEvent) { // Log.d("MT", "New Event ${event.content} ${event.id.toHex()}") - // new event val oldChannel = getOrCreateChannel(event.id) val author = getOrCreateUser(event.pubKey) - if (event.createdAt > oldChannel.updatedMetadataAt) { - if (oldChannel.creator == null || oldChannel.creator == author) { - oldChannel.updateChannelInfo(author, event.channelInfo(), event.createdAt) + if (event.createdAt <= oldChannel.updatedMetadataAt) { + return // older data, does nothing + } + if (oldChannel.creator == null || oldChannel.creator == author) { + oldChannel.updateChannelInfo(author, event.channelInfo(), event.createdAt) - val note = getOrCreateNote(event.id) - oldChannel.addNote(note) - note.loadEvent(event, author, emptyList(), emptyList()) + val note = getOrCreateNote(event.id) + oldChannel.addNote(note) + note.loadEvent(event, author, emptyList(), emptyList()) - refreshObservers() - } - } else { - // older data, does nothing + refreshObservers() } } @@ -799,7 +797,7 @@ object LocalCache { } fun pruneOldAndHiddenMessages(account: Account) { - channels.forEach { + channels.forEach { it -> val toBeRemoved = it.value.pruneOldAndHiddenMessages(account) toBeRemoved.forEach { @@ -815,7 +813,7 @@ object LocalCache { } // Counts the replies - it.replyTo?.forEach { replyingNote -> + it.replyTo?.forEach { _ -> it.removeReply(it) } } From 332ddc15f7ef201d69259211695d45bde3d553f2 Mon Sep 17 00:00:00 2001 From: kappaseijin Date: Sun, 12 Mar 2023 14:43:33 +0900 Subject: [PATCH 10/45] Fixed a variable name for NIP-05 --- .../java/com/vitorpamplona/amethyst/service/Nip05Verifier.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip05Verifier.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip05Verifier.kt index 28bb298f4..27555bcac 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip05Verifier.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip05Verifier.kt @@ -25,10 +25,10 @@ class Nip05Verifier { return null } - fun fetchNip05Json(lnaddress: String, onSuccess: (String) -> Unit, onError: (String) -> Unit) { + fun fetchNip05Json(nip05address: String, onSuccess: (String) -> Unit, onError: (String) -> Unit) { val scope = CoroutineScope(Job() + Dispatchers.IO) scope.launch { - fetchNip05JsonSuspend(lnaddress, onSuccess, onError) + fetchNip05JsonSuspend(nip05address, onSuccess, onError) } } From b7f8241a086daf0318e4d05ff60123e72be81f68 Mon Sep 17 00:00:00 2001 From: maxmoney21m Date: Sat, 11 Mar 2023 23:13:04 +0800 Subject: [PATCH 11/45] Add account switch bottom sheet and update LocalPrefs --- .../amethyst/LocalPreferences.kt | 164 ++++++++++++------ .../ui/navigation/AccountSwitchBottomSheet.kt | 122 +++++++++++++ .../amethyst/ui/navigation/DrawerContent.kt | 23 ++- .../amethyst/ui/navigation/Routes.kt | 115 ++++++++---- .../ui/screen/loggedIn/ChannelScreen.kt | 7 +- .../amethyst/ui/screen/loggedIn/MainScreen.kt | 56 ++++-- app/src/main/res/drawable/manage_accounts.xml | 7 + 7 files changed, 384 insertions(+), 110 deletions(-) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AccountSwitchBottomSheet.kt create mode 100644 app/src/main/res/drawable/manage_accounts.xml diff --git a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index 83e0f331a..bbe4e24d2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -11,21 +11,44 @@ import com.vitorpamplona.amethyst.service.model.Event import com.vitorpamplona.amethyst.service.model.Event.Companion.getRefinedEvent import nostr.postr.Persona import nostr.postr.toHex +import nostr.postr.toNpub import java.util.Locale +data class AccountInfo(val npub: String, val current: Boolean, val displayName: String?, val profilePicture: String?) + class LocalPreferences(context: Context) { + + private fun prefKeysForAccount(npub: String) = object { + val NOSTR_PRIVKEY = "$npub/nostr_privkey" + val NOSTR_PUBKEY = "$npub/nostr_pubkey" + val DISPLAY_NAME = "$npub/display_name" + val PROFILE_PICTURE_URL = "$npub/profile_picture" + val FOLLOWING_CHANNELS = "$npub/following_channels" + val HIDDEN_USERS = "$npub/hidden_users" + val RELAYS = "$npub/relays" + val DONT_TRANSLATE_FROM = "$npub/dontTranslateFrom" + val LANGUAGE_PREFS = "$npub/languagePreferences" + val TRANSLATE_TO = "$npub/translateTo" + val ZAP_AMOUNTS = "$npub/zapAmounts" + val LATEST_CONTACT_LIST = "$npub/latestContactList" + val HIDE_DELETE_REQUEST_INFO = "$npub/hideDeleteRequestInfo" +// val LAST_READ: (String) -> String = { route -> "$npub/last_read_route_$route" } + } + private object PrefKeys { - const val NOSTR_PRIVKEY = "nostr_privkey" - const val NOSTR_PUBKEY = "nostr_pubkey" - const val FOLLOWING_CHANNELS = "following_channels" - const val HIDDEN_USERS = "hidden_users" - const val RELAYS = "relays" - const val DONT_TRANSLATE_FROM = "dontTranslateFrom" - const val LANGUAGE_PREFS = "languagePreferences" - const val TRANSLATE_TO = "translateTo" - const val ZAP_AMOUNTS = "zapAmounts" - const val LATEST_CONTACT_LIST = "latestContactList" - const val HIDE_DELETE_REQUEST_INFO = "hideDeleteRequestInfo" + const val CURRENT_ACCOUNT = "currentlyLoggedInAccount" + +// val NOSTR_PRIVKEY = "nostr_privkey" +// val NOSTR_PUBKEY = "nostr_pubkey" +// val FOLLOWING_CHANNELS = "following_channels" +// val HIDDEN_USERS = "hidden_users" +// val RELAYS = "relays" +// val DONT_TRANSLATE_FROM = "dontTranslateFrom" +// val LANGUAGE_PREFS = "languagePreferences" +// val TRANSLATE_TO = "translateTo" +// val ZAP_AMOUNTS = "zapAmounts" +// val LATEST_CONTACT_LIST = "latestContactList" +// val HIDE_DELETE_REQUEST_INFO = "hideDeleteRequestInfo" val LAST_READ: (String) -> String = { route -> "last_read_route_$route" } } @@ -34,47 +57,88 @@ class LocalPreferences(context: Context) { fun clearEncryptedStorage() { encryptedPreferences.edit().apply { - encryptedPreferences.all.keys.forEach { remove(it) } + encryptedPreferences.all.keys.forEach { + remove(it) + } +// encryptedPreferences.all.keys.filter { +// it.startsWith(npub) +// }.forEach { +// remove(it) +// } }.apply() } + fun findAllLocalAccounts(): List { + encryptedPreferences.apply { + val currentAccount = getString(PrefKeys.CURRENT_ACCOUNT, null) + return encryptedPreferences.all.keys.filter { + it.endsWith("nostr_pubkey") + }.map { + val npub = it.substringBefore("/") + val myPrefs = prefKeysForAccount(npub) + AccountInfo( + npub, + npub == currentAccount, + getString(myPrefs.DISPLAY_NAME, null), + getString(myPrefs.PROFILE_PICTURE_URL, null) + ) + } + } + } + fun saveToEncryptedStorage(account: Account) { + val npub = account.loggedIn.pubKey.toNpub() + val myPrefs = prefKeysForAccount(npub) + encryptedPreferences.edit().apply { - account.loggedIn.privKey?.let { putString(PrefKeys.NOSTR_PRIVKEY, it.toHex()) } - account.loggedIn.pubKey.let { putString(PrefKeys.NOSTR_PUBKEY, it.toHex()) } - account.followingChannels.let { putStringSet(PrefKeys.FOLLOWING_CHANNELS, it) } - account.hiddenUsers.let { putStringSet(PrefKeys.HIDDEN_USERS, it) } - account.localRelays.let { putString(PrefKeys.RELAYS, gson.toJson(it)) } - account.dontTranslateFrom.let { putStringSet(PrefKeys.DONT_TRANSLATE_FROM, it) } - account.languagePreferences.let { putString(PrefKeys.LANGUAGE_PREFS, gson.toJson(it)) } - account.translateTo.let { putString(PrefKeys.TRANSLATE_TO, it) } - account.zapAmountChoices.let { putString(PrefKeys.ZAP_AMOUNTS, gson.toJson(it)) } - account.backupContactList.let { putString(PrefKeys.LATEST_CONTACT_LIST, Event.gson.toJson(it)) } - putBoolean(PrefKeys.HIDE_DELETE_REQUEST_INFO, account.hideDeleteRequestInfo) + putString(PrefKeys.CURRENT_ACCOUNT, npub) + account.loggedIn.privKey?.let { putString(myPrefs.NOSTR_PRIVKEY, it.toHex()) } + account.loggedIn.pubKey.let { putString(myPrefs.NOSTR_PUBKEY, it.toHex()) } + putStringSet(myPrefs.FOLLOWING_CHANNELS, account.followingChannels) + putStringSet(myPrefs.HIDDEN_USERS, account.hiddenUsers) + putString(myPrefs.RELAYS, gson.toJson(account.localRelays)) + putStringSet(myPrefs.DONT_TRANSLATE_FROM, account.dontTranslateFrom) + putString(myPrefs.LANGUAGE_PREFS, gson.toJson(account.languagePreferences)) + putString(myPrefs.TRANSLATE_TO, account.translateTo) + putString(myPrefs.ZAP_AMOUNTS, gson.toJson(account.zapAmountChoices)) + putString(myPrefs.LATEST_CONTACT_LIST, Event.gson.toJson(account.backupContactList)) + putBoolean(myPrefs.HIDE_DELETE_REQUEST_INFO, account.hideDeleteRequestInfo) + }.apply() + } + + fun saveCurrentAccountMetadata(account: Account) { + val myPrefs = prefKeysForAccount(account.loggedIn.pubKey.toNpub()) + + encryptedPreferences.edit().apply { + putString(myPrefs.DISPLAY_NAME, account.userProfile().toBestDisplayName()) + putString(myPrefs.PROFILE_PICTURE_URL, account.userProfile().profilePicture()) }.apply() } fun loadFromEncryptedStorage(): Account? { encryptedPreferences.apply { - val privKey = getString(PrefKeys.NOSTR_PRIVKEY, null) - val pubKey = getString(PrefKeys.NOSTR_PUBKEY, null) - val followingChannels = getStringSet(PrefKeys.FOLLOWING_CHANNELS, null) ?: setOf() - val hiddenUsers = getStringSet(PrefKeys.HIDDEN_USERS, emptySet()) ?: setOf() + val npub = getString(PrefKeys.CURRENT_ACCOUNT, null) ?: return null + val myPrefs = prefKeysForAccount(npub) + + val pubKey = getString(myPrefs.NOSTR_PUBKEY, null) ?: return null + val privKey = getString(myPrefs.NOSTR_PRIVKEY, null) + val followingChannels = getStringSet(myPrefs.FOLLOWING_CHANNELS, null) ?: setOf() + val hiddenUsers = getStringSet(myPrefs.HIDDEN_USERS, emptySet()) ?: setOf() val localRelays = gson.fromJson( - getString(PrefKeys.RELAYS, "[]"), + getString(myPrefs.RELAYS, "[]"), object : TypeToken>() {}.type ) ?: setOf() - val dontTranslateFrom = getStringSet(PrefKeys.DONT_TRANSLATE_FROM, null) ?: setOf() - val translateTo = getString(PrefKeys.TRANSLATE_TO, null) ?: Locale.getDefault().language + val dontTranslateFrom = getStringSet(myPrefs.DONT_TRANSLATE_FROM, null) ?: setOf() + val translateTo = getString(myPrefs.TRANSLATE_TO, null) ?: Locale.getDefault().language val zapAmountChoices = gson.fromJson( - getString(PrefKeys.ZAP_AMOUNTS, "[]"), + getString(myPrefs.ZAP_AMOUNTS, "[]"), object : TypeToken>() {}.type ) ?: listOf(500L, 1000L, 5000L) val latestContactList = try { - getString(PrefKeys.LATEST_CONTACT_LIST, null)?.let { + getString(myPrefs.LATEST_CONTACT_LIST, null)?.let { Event.gson.fromJson(it, Event::class.java).getRefinedEvent(true) as ContactListEvent } } catch (e: Throwable) { @@ -83,32 +147,28 @@ class LocalPreferences(context: Context) { } val languagePreferences = try { - getString(PrefKeys.LANGUAGE_PREFS, null)?.let { + getString(myPrefs.LANGUAGE_PREFS, null)?.let { gson.fromJson(it, object : TypeToken>() {}.type) as Map - } ?: mapOf() + } ?: mapOf() } catch (e: Throwable) { e.printStackTrace() - mapOf() + mapOf() } - val hideDeleteRequestInfo = getBoolean(PrefKeys.HIDE_DELETE_REQUEST_INFO, false) + val hideDeleteRequestInfo = getBoolean(myPrefs.HIDE_DELETE_REQUEST_INFO, false) - if (pubKey != null) { - return Account( - Persona(privKey = privKey?.toByteArray(), pubKey = pubKey.toByteArray()), - followingChannels, - hiddenUsers, - localRelays, - dontTranslateFrom, - languagePreferences, - translateTo, - zapAmountChoices, - hideDeleteRequestInfo, - latestContactList - ) - } else { - return null - } + return Account( + Persona(privKey = privKey?.toByteArray(), pubKey = pubKey.toByteArray()), + followingChannels, + hiddenUsers, + localRelays, + dontTranslateFrom, + languagePreferences, + translateTo, + zapAmountChoices, + hideDeleteRequestInfo, + latestContactList + ) } } 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 new file mode 100644 index 000000000..f206ac2d0 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AccountSwitchBottomSheet.kt @@ -0,0 +1,122 @@ +package com.vitorpamplona.amethyst.ui.navigation + +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.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.material.ExperimentalMaterialApi +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.ModalBottomSheetState +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Logout +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +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 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.note.toShortenHex +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun AccountSwitchBottomSheet( + accountViewModel: AccountViewModel, + sheetState: ModalBottomSheetState +) { + val coroutineScope = rememberCoroutineScope() + val context = LocalContext.current + val localPrefs = LocalPreferences(context) + val accounts = localPrefs.findAllLocalAccounts() + + val accountState by accountViewModel.accountLiveData.observeAsState() + val account = accountState?.account ?: return + + val accountUserState by account.userProfile().live().metadata.observeAsState() + val accountUser = accountUserState?.user ?: return + + LaunchedEffect(key1 = accountUser) { + localPrefs.saveCurrentAccountMetadata(account) + } + + Column { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text("Select Account", fontWeight = FontWeight.Bold) + } + accounts.forEach { acc -> + val current = accountUser.pubkeyNpub() == acc.npub + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(32.dp, 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + AsyncImageProxy( + model = ResizeImage(acc.profilePicture, 64.dp), + placeholder = BitmapPainter(RoboHashCache.get(context, acc.npub)), + fallback = BitmapPainter(RoboHashCache.get(context, acc.npub)), + error = BitmapPainter(RoboHashCache.get(context, acc.npub)), + contentDescription = stringResource(id = R.string.profile_image), + modifier = Modifier + .width(64.dp) + .height(64.dp) + .clip(shape = CircleShape) + ) + Spacer(modifier = Modifier.width(16.dp)) + Column { + acc.displayName?.let { + Text(it) + } + Text(acc.npub.toShortenHex()) + } + Spacer(modifier = Modifier.width(8.dp)) + if (current) { + Text("✓") + } + Spacer(modifier = Modifier.weight(1f)) + IconButton(onClick = { /*TODO*/ }) { + Icon(imageVector = Icons.Default.Logout, "Logout") + } + } + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + TextButton(onClick = { coroutineScope.launch { sheetState.hide() } }) { + Text("Add New Account") + } + } + } +} 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 9ac1cb2c4..8198b7058 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 @@ -16,9 +16,11 @@ 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.ExperimentalMaterialApi import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.MaterialTheme +import androidx.compose.material.ModalBottomSheetState import androidx.compose.material.ScaffoldState import androidx.compose.material.Surface import androidx.compose.material.Text @@ -56,10 +58,12 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountBackupDialog import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import kotlinx.coroutines.launch +@OptIn(ExperimentalMaterialApi::class) @Composable fun DrawerContent( navController: NavHostController, scaffoldState: ScaffoldState, + sheetState: ModalBottomSheetState, accountViewModel: AccountViewModel, accountStateViewModel: AccountStateViewModel ) { @@ -88,6 +92,7 @@ fun DrawerContent( account.userProfile(), navController, scaffoldState, + sheetState, modifier = Modifier .fillMaxWidth() .weight(1F), @@ -214,15 +219,18 @@ fun ProfileContent(baseAccountUser: User, modifier: Modifier = Modifier, scaffol } } +@OptIn(ExperimentalMaterialApi::class) @Composable fun ListContent( accountUser: User?, navController: NavHostController, scaffoldState: ScaffoldState, + sheetState: ModalBottomSheetState, modifier: Modifier, accountViewModel: AccountStateViewModel, account: Account ) { + val coroutineScope = rememberCoroutineScope() var backupDialogOpen by remember { mutableStateOf(false) } Column(modifier = modifier.fillMaxHeight()) { @@ -260,11 +268,18 @@ fun ListContent( Spacer(modifier = Modifier.weight(1f)) IconRow( - stringResource(R.string.log_out), - R.drawable.ic_logout, - MaterialTheme.colors.onBackground, - onClick = { accountViewModel.logOff() } + title = "Accounts", + icon = R.drawable.manage_accounts, + tint = MaterialTheme.colors.onBackground, + onClick = { coroutineScope.launch { sheetState.show() } } ) + +// IconRow( +// title = stringResource(R.string.log_out), +// icon = R.drawable.ic_logout, +// tint = MaterialTheme.colors.onBackground, +// onClick = { accountViewModel.logOff() } +// ) } if (backupDialogOpen) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt index 66bed57fa..8ca43e4e1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt @@ -36,62 +36,95 @@ sealed class Route( val buildScreen: (AccountViewModel, AccountStateViewModel, NavController) -> @Composable (NavBackStackEntry) -> Unit ) { object Home : Route( - "Home", - R.drawable.ic_home, - hasNewItems = { acc, cache, ctx -> homeHasNewItems(acc, cache, ctx) }, - buildScreen = { acc, accSt, nav -> { _ -> HomeScreen(acc, nav) } } + route = "Home", + icon = R.drawable.ic_home, + hasNewItems = { accountViewModel, cache, context -> + homeHasNewItems(accountViewModel, cache, context) + }, + buildScreen = { accountViewModel, _, navController -> + { HomeScreen(accountViewModel, navController) } + } ) + object Search : Route( - "Search", - R.drawable.ic_globe, - buildScreen = { acc, accSt, nav -> { _ -> SearchScreen(acc, nav) } } + route = "Search", + icon = R.drawable.ic_globe, + buildScreen = { accountViewModel, _, navController -> + { SearchScreen(accountViewModel, navController) } + } ) + object Notification : Route( - "Notification", - R.drawable.ic_notifications, - hasNewItems = { acc, cache, ctx -> notificationHasNewItems(acc, cache, ctx) }, - buildScreen = { acc, accSt, nav -> { _ -> NotificationScreen(acc, nav) } } + route = "Notification", + icon = R.drawable.ic_notifications, + hasNewItems = { accountViewModel, cache, context -> + notificationHasNewItems(accountViewModel, cache, context) + }, + buildScreen = { accountViewModel, _, navController -> + { NotificationScreen(accountViewModel, navController) } + } ) object Message : Route( - "Message", - R.drawable.ic_dm, - hasNewItems = { acc, cache, ctx -> messagesHasNewItems(acc, cache, ctx) }, - buildScreen = { acc, accSt, nav -> { _ -> ChatroomListScreen(acc, nav) } } + route = "Message", + icon = R.drawable.ic_dm, + hasNewItems = { accountViewModel, cache, context -> + messagesHasNewItems(accountViewModel, cache, context) + }, + buildScreen = { accountViewModel, _, navController -> + { ChatroomListScreen(accountViewModel, navController) } + } ) object Filters : Route( - "Filters", - R.drawable.ic_security, - buildScreen = { acc, accSt, nav -> { _ -> FiltersScreen(acc, nav) } } + route = "Filters", + icon = R.drawable.ic_security, + buildScreen = { accountViewModel, _, navController -> + { FiltersScreen(accountViewModel, navController) } + } ) object Profile : Route( - "User/{id}", - R.drawable.ic_profile, + route = "User/{id}", + icon = R.drawable.ic_profile, arguments = listOf(navArgument("id") { type = NavType.StringType }), - buildScreen = { acc, accSt, nav -> { ProfileScreen(it.arguments?.getString("id"), acc, nav) } } + buildScreen = { accountViewModel, _, navController -> + { ProfileScreen(it.arguments?.getString("id"), accountViewModel, navController) } + } ) object Note : Route( - "Note/{id}", - R.drawable.ic_moments, + route = "Note/{id}", + icon = R.drawable.ic_moments, arguments = listOf(navArgument("id") { type = NavType.StringType }), - buildScreen = { acc, accSt, nav -> { ThreadScreen(it.arguments?.getString("id"), acc, nav) } } + buildScreen = { accountViewModel, _, navController -> + { ThreadScreen(it.arguments?.getString("id"), accountViewModel, navController) } + } ) object Room : Route( - "Room/{id}", - R.drawable.ic_moments, + route = "Room/{id}", + icon = R.drawable.ic_moments, arguments = listOf(navArgument("id") { type = NavType.StringType }), - buildScreen = { acc, accSt, nav -> { ChatroomScreen(it.arguments?.getString("id"), acc, nav) } } + buildScreen = { accountViewModel, _, navController -> + { ChatroomScreen(it.arguments?.getString("id"), accountViewModel, navController) } + } ) object Channel : Route( - "Channel/{id}", - R.drawable.ic_moments, + route = "Channel/{id}", + icon = R.drawable.ic_moments, arguments = listOf(navArgument("id") { type = NavType.StringType }), - buildScreen = { acc, accSt, nav -> { ChannelScreen(it.arguments?.getString("id"), acc, accSt, nav) } } + buildScreen = { accountViewModel, accountStateViewModel, navController -> + { + ChannelScreen( + it.arguments?.getString("id"), + accountViewModel, + accountStateViewModel, + navController + ) + } + } ) } @@ -124,18 +157,32 @@ private fun homeHasNewItems(account: Account, cache: NotificationCache, context: HomeNewThreadFeedFilter.account = account - return (HomeNewThreadFeedFilter.feed().firstOrNull { it.createdAt() != null }?.createdAt() ?: 0) > lastTime + return ( + HomeNewThreadFeedFilter.feed().firstOrNull { it.createdAt() != null }?.createdAt() + ?: 0 + ) > lastTime } -private fun notificationHasNewItems(account: Account, cache: NotificationCache, context: Context): Boolean { +private fun notificationHasNewItems( + account: Account, + cache: NotificationCache, + context: Context +): Boolean { val lastTime = cache.load("Notification", context) NotificationFeedFilter.account = account - return (NotificationFeedFilter.feed().firstOrNull { it.createdAt() != null }?.createdAt() ?: 0) > lastTime + return ( + NotificationFeedFilter.feed().firstOrNull { it.createdAt() != null }?.createdAt() + ?: 0 + ) > lastTime } -private fun messagesHasNewItems(account: Account, cache: NotificationCache, context: Context): Boolean { +private fun messagesHasNewItems( + account: Account, + cache: NotificationCache, + context: Context +): Boolean { ChatroomListKnownFeedFilter.account = account val note = ChatroomListKnownFeedFilter.feed().firstOrNull { 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 71fa2ee6c..496666f7e 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 @@ -78,7 +78,12 @@ import com.vitorpamplona.amethyst.ui.screen.ChatroomFeedView import com.vitorpamplona.amethyst.ui.screen.NostrChannelFeedViewModel @Composable -fun ChannelScreen(channelId: String?, accountViewModel: AccountViewModel, accountStateViewModel: AccountStateViewModel, navController: NavController) { +fun ChannelScreen( + channelId: String?, + accountViewModel: AccountViewModel, + accountStateViewModel: AccountStateViewModel, + navController: NavController +) { val accountState by accountViewModel.accountLiveData.observeAsState() val account = accountState?.account diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt index fa698b275..e296fec90 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt @@ -7,9 +7,13 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.material.DrawerValue +import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.MaterialTheme +import androidx.compose.material.ModalBottomSheetLayout +import androidx.compose.material.ModalBottomSheetValue import androidx.compose.material.Scaffold import androidx.compose.material.rememberDrawerState +import androidx.compose.material.rememberModalBottomSheetState import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -19,6 +23,7 @@ import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController import com.vitorpamplona.amethyst.buttons.NewChannelButton import com.vitorpamplona.amethyst.buttons.NewNoteButton +import com.vitorpamplona.amethyst.ui.navigation.AccountSwitchBottomSheet import com.vitorpamplona.amethyst.ui.navigation.AppBottomBar import com.vitorpamplona.amethyst.ui.navigation.AppNavigation import com.vitorpamplona.amethyst.ui.navigation.AppTopBar @@ -28,31 +33,44 @@ import com.vitorpamplona.amethyst.ui.navigation.currentRoute import com.vitorpamplona.amethyst.ui.screen.AccountState import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel +@OptIn(ExperimentalMaterialApi::class) @Composable fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: AccountStateViewModel, startingPage: String? = null) { val navController = rememberNavController() val scaffoldState = rememberScaffoldState(rememberDrawerState(DrawerValue.Closed)) + val sheetState = rememberModalBottomSheetState( + initialValue = ModalBottomSheetValue.Hidden, + confirmValueChange = { it != ModalBottomSheetValue.HalfExpanded }, + skipHalfExpanded = true + ) - Scaffold( - modifier = Modifier - .background(MaterialTheme.colors.primaryVariant) - .statusBarsPadding(), - bottomBar = { - AppBottomBar(navController, accountViewModel) - }, - topBar = { - AppTopBar(navController, scaffoldState, accountViewModel) - }, - drawerContent = { - DrawerContent(navController, scaffoldState, accountViewModel, accountStateViewModel) - }, - floatingActionButton = { - FloatingButton(navController, accountStateViewModel) - }, - scaffoldState = scaffoldState + ModalBottomSheetLayout( + sheetState = sheetState, + sheetContent = { + AccountSwitchBottomSheet(accountViewModel = accountViewModel, sheetState = sheetState) + } ) { - Column(modifier = Modifier.padding(bottom = it.calculateBottomPadding())) { - AppNavigation(navController, accountViewModel, accountStateViewModel, startingPage) + Scaffold( + modifier = Modifier + .background(MaterialTheme.colors.primaryVariant) + .statusBarsPadding(), + bottomBar = { + AppBottomBar(navController, accountViewModel) + }, + topBar = { + AppTopBar(navController, scaffoldState, accountViewModel) + }, + drawerContent = { + DrawerContent(navController, scaffoldState, sheetState, accountViewModel, accountStateViewModel) + }, + floatingActionButton = { + FloatingButton(navController, accountStateViewModel) + }, + scaffoldState = scaffoldState + ) { + Column(modifier = Modifier.padding(bottom = it.calculateBottomPadding())) { + AppNavigation(navController, accountViewModel, accountStateViewModel, startingPage) + } } } } diff --git a/app/src/main/res/drawable/manage_accounts.xml b/app/src/main/res/drawable/manage_accounts.xml new file mode 100644 index 000000000..734c70559 --- /dev/null +++ b/app/src/main/res/drawable/manage_accounts.xml @@ -0,0 +1,7 @@ + + + + + From 3a2403b3445cbf1006261b8a6bb6c369109ccb4d Mon Sep 17 00:00:00 2001 From: maxmoney21m Date: Sun, 12 Mar 2023 02:18:43 +0800 Subject: [PATCH 12/45] Use multiple preference files for different accounts --- .idea/deploymentTargetDropDown.xml | 17 ++ app/src/main/AndroidManifest.xml | 1 + .../com/vitorpamplona/amethyst/Amethyst.kt | 15 ++ .../amethyst/EncryptedStorage.kt | 8 +- .../amethyst/LocalPreferences.kt | 197 ++++++++++-------- .../amethyst/NotificationCache.kt | 4 +- .../vitorpamplona/amethyst/ui/MainActivity.kt | 3 +- .../ui/navigation/AccountSwitchBottomSheet.kt | 132 +++++++++++- .../ui/screen/AccountStateViewModel.kt | 15 +- .../amethyst/ui/screen/loggedIn/MainScreen.kt | 2 +- 10 files changed, 278 insertions(+), 116 deletions(-) create mode 100644 .idea/deploymentTargetDropDown.xml create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml new file mode 100644 index 000000000..0c0237e9c --- /dev/null +++ b/.idea/deploymentTargetDropDown.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 641da01bb..5a3417398 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -13,6 +13,7 @@ String = { route -> "last_read_route_$route" } +} - private fun prefKeysForAccount(npub: String) = object { - val NOSTR_PRIVKEY = "$npub/nostr_privkey" - val NOSTR_PUBKEY = "$npub/nostr_pubkey" - val DISPLAY_NAME = "$npub/display_name" - val PROFILE_PICTURE_URL = "$npub/profile_picture" - val FOLLOWING_CHANNELS = "$npub/following_channels" - val HIDDEN_USERS = "$npub/hidden_users" - val RELAYS = "$npub/relays" - val DONT_TRANSLATE_FROM = "$npub/dontTranslateFrom" - val LANGUAGE_PREFS = "$npub/languagePreferences" - val TRANSLATE_TO = "$npub/translateTo" - val ZAP_AMOUNTS = "$npub/zapAmounts" - val LATEST_CONTACT_LIST = "$npub/latestContactList" - val HIDE_DELETE_REQUEST_INFO = "$npub/hideDeleteRequestInfo" -// val LAST_READ: (String) -> String = { route -> "$npub/last_read_route_$route" } +private val gson = GsonBuilder().create() + +object LocalPreferences { + private var currentAccount: String? + get() = encryptedPreferences().getString(PrefKeys.CURRENT_ACCOUNT, null) + set(npub) { + val prefs = encryptedPreferences() + prefs.edit().apply { + putString(PrefKeys.CURRENT_ACCOUNT, npub) + }.apply() + } + + private val savedAccounts: Set + get() = encryptedPreferences().getStringSet(PrefKeys.SAVED_ACCOUNTS, null) ?: setOf() + + private fun addAccount(npub: String) { + val accounts = savedAccounts.toMutableSet() + accounts.add(npub) + val prefs = encryptedPreferences() + prefs.edit().apply { + putStringSet(PrefKeys.SAVED_ACCOUNTS, accounts) + }.apply() } - private object PrefKeys { - const val CURRENT_ACCOUNT = "currentlyLoggedInAccount" - -// val NOSTR_PRIVKEY = "nostr_privkey" -// val NOSTR_PUBKEY = "nostr_pubkey" -// val FOLLOWING_CHANNELS = "following_channels" -// val HIDDEN_USERS = "hidden_users" -// val RELAYS = "relays" -// val DONT_TRANSLATE_FROM = "dontTranslateFrom" -// val LANGUAGE_PREFS = "languagePreferences" -// val TRANSLATE_TO = "translateTo" -// val ZAP_AMOUNTS = "zapAmounts" -// val LATEST_CONTACT_LIST = "latestContactList" -// val HIDE_DELETE_REQUEST_INFO = "hideDeleteRequestInfo" - val LAST_READ: (String) -> String = { route -> "last_read_route_$route" } + private fun removeAccount(npub: String) { + val accounts = savedAccounts.toMutableSet() + accounts.remove(npub) + val prefs = encryptedPreferences() + prefs.edit().apply { + putStringSet(PrefKeys.SAVED_ACCOUNTS, accounts) + }.apply() } - private val encryptedPreferences = EncryptedStorage.preferences(context) - private val gson = GsonBuilder().create() + private fun encryptedPreferences(npub: String? = null): SharedPreferences { + return if (DEBUG_PLAINTEXT_PREFERENCES) { + val preferenceFile = if (npub == null) "testing_only" else "testing_only_$npub" + Amethyst.instance.getSharedPreferences(preferenceFile, Context.MODE_PRIVATE) + } else { + return EncryptedStorage.preferences(npub) + } + } - fun clearEncryptedStorage() { - encryptedPreferences.edit().apply { - encryptedPreferences.all.keys.forEach { + fun clearEncryptedStorage(npub: String? = null) { + val encPrefs = encryptedPreferences(npub) + encPrefs.edit().apply { + encPrefs.all.keys.forEach { remove(it) } // encryptedPreferences.all.keys.filter { @@ -69,76 +94,64 @@ class LocalPreferences(context: Context) { } fun findAllLocalAccounts(): List { - encryptedPreferences.apply { - val currentAccount = getString(PrefKeys.CURRENT_ACCOUNT, null) - return encryptedPreferences.all.keys.filter { - it.endsWith("nostr_pubkey") - }.map { - val npub = it.substringBefore("/") - val myPrefs = prefKeysForAccount(npub) - AccountInfo( - npub, - npub == currentAccount, - getString(myPrefs.DISPLAY_NAME, null), - getString(myPrefs.PROFILE_PICTURE_URL, null) - ) - } + return savedAccounts.map { npub -> + val prefs = encryptedPreferences(npub) + + AccountInfo( + npub = npub, + current = npub == currentAccount, + displayName = prefs.getString(PrefKeys.DISPLAY_NAME, null), + profilePicture = prefs.getString(PrefKeys.PROFILE_PICTURE_URL, null) + ) } } - fun saveToEncryptedStorage(account: Account) { - val npub = account.loggedIn.pubKey.toNpub() - val myPrefs = prefKeysForAccount(npub) - - encryptedPreferences.edit().apply { - putString(PrefKeys.CURRENT_ACCOUNT, npub) - account.loggedIn.privKey?.let { putString(myPrefs.NOSTR_PRIVKEY, it.toHex()) } - account.loggedIn.pubKey.let { putString(myPrefs.NOSTR_PUBKEY, it.toHex()) } - putStringSet(myPrefs.FOLLOWING_CHANNELS, account.followingChannels) - putStringSet(myPrefs.HIDDEN_USERS, account.hiddenUsers) - putString(myPrefs.RELAYS, gson.toJson(account.localRelays)) - putStringSet(myPrefs.DONT_TRANSLATE_FROM, account.dontTranslateFrom) - putString(myPrefs.LANGUAGE_PREFS, gson.toJson(account.languagePreferences)) - putString(myPrefs.TRANSLATE_TO, account.translateTo) - putString(myPrefs.ZAP_AMOUNTS, gson.toJson(account.zapAmountChoices)) - putString(myPrefs.LATEST_CONTACT_LIST, Event.gson.toJson(account.backupContactList)) - putBoolean(myPrefs.HIDE_DELETE_REQUEST_INFO, account.hideDeleteRequestInfo) - }.apply() + fun setCurrentAccount(account: Account) { + val npub = account.userProfile().pubkeyNpub() + currentAccount = npub + addAccount(npub) } - fun saveCurrentAccountMetadata(account: Account) { - val myPrefs = prefKeysForAccount(account.loggedIn.pubKey.toNpub()) - - encryptedPreferences.edit().apply { - putString(myPrefs.DISPLAY_NAME, account.userProfile().toBestDisplayName()) - putString(myPrefs.PROFILE_PICTURE_URL, account.userProfile().profilePicture()) + fun saveToEncryptedStorage(account: Account) { + val prefs = encryptedPreferences(account.userProfile().pubkeyNpub()) + prefs.edit().apply { + account.loggedIn.privKey?.let { putString(PrefKeys.NOSTR_PRIVKEY, it.toHex()) } + account.loggedIn.pubKey.let { putString(PrefKeys.NOSTR_PUBKEY, it.toHex()) } + putStringSet(PrefKeys.FOLLOWING_CHANNELS, account.followingChannels) + putStringSet(PrefKeys.HIDDEN_USERS, account.hiddenUsers) + putString(PrefKeys.RELAYS, gson.toJson(account.localRelays)) + putStringSet(PrefKeys.DONT_TRANSLATE_FROM, account.dontTranslateFrom) + putString(PrefKeys.LANGUAGE_PREFS, gson.toJson(account.languagePreferences)) + putString(PrefKeys.TRANSLATE_TO, account.translateTo) + putString(PrefKeys.ZAP_AMOUNTS, gson.toJson(account.zapAmountChoices)) + putString(PrefKeys.LATEST_CONTACT_LIST, Event.gson.toJson(account.backupContactList)) + putBoolean(PrefKeys.HIDE_DELETE_REQUEST_INFO, account.hideDeleteRequestInfo) + putString(PrefKeys.DISPLAY_NAME, account.userProfile().toBestDisplayName()) + putString(PrefKeys.PROFILE_PICTURE_URL, account.userProfile().profilePicture()) }.apply() } fun loadFromEncryptedStorage(): Account? { - encryptedPreferences.apply { - val npub = getString(PrefKeys.CURRENT_ACCOUNT, null) ?: return null - val myPrefs = prefKeysForAccount(npub) - - val pubKey = getString(myPrefs.NOSTR_PUBKEY, null) ?: return null - val privKey = getString(myPrefs.NOSTR_PRIVKEY, null) - val followingChannels = getStringSet(myPrefs.FOLLOWING_CHANNELS, null) ?: setOf() - val hiddenUsers = getStringSet(myPrefs.HIDDEN_USERS, emptySet()) ?: setOf() + encryptedPreferences(currentAccount).apply { + val pubKey = getString(PrefKeys.NOSTR_PUBKEY, null) ?: return null + val privKey = getString(PrefKeys.NOSTR_PRIVKEY, null) + val followingChannels = getStringSet(PrefKeys.FOLLOWING_CHANNELS, null) ?: setOf() + val hiddenUsers = getStringSet(PrefKeys.HIDDEN_USERS, emptySet()) ?: setOf() val localRelays = gson.fromJson( - getString(myPrefs.RELAYS, "[]"), + getString(PrefKeys.RELAYS, "[]"), object : TypeToken>() {}.type ) ?: setOf() - val dontTranslateFrom = getStringSet(myPrefs.DONT_TRANSLATE_FROM, null) ?: setOf() - val translateTo = getString(myPrefs.TRANSLATE_TO, null) ?: Locale.getDefault().language + val dontTranslateFrom = getStringSet(PrefKeys.DONT_TRANSLATE_FROM, null) ?: setOf() + val translateTo = getString(PrefKeys.TRANSLATE_TO, null) ?: Locale.getDefault().language val zapAmountChoices = gson.fromJson( - getString(myPrefs.ZAP_AMOUNTS, "[]"), + getString(PrefKeys.ZAP_AMOUNTS, "[]"), object : TypeToken>() {}.type ) ?: listOf(500L, 1000L, 5000L) val latestContactList = try { - getString(myPrefs.LATEST_CONTACT_LIST, null)?.let { + getString(PrefKeys.LATEST_CONTACT_LIST, null)?.let { Event.gson.fromJson(it, Event::class.java).getRefinedEvent(true) as ContactListEvent } } catch (e: Throwable) { @@ -147,7 +160,7 @@ class LocalPreferences(context: Context) { } val languagePreferences = try { - getString(myPrefs.LANGUAGE_PREFS, null)?.let { + getString(PrefKeys.LANGUAGE_PREFS, null)?.let { gson.fromJson(it, object : TypeToken>() {}.type) as Map } ?: mapOf() } catch (e: Throwable) { @@ -155,7 +168,7 @@ class LocalPreferences(context: Context) { mapOf() } - val hideDeleteRequestInfo = getBoolean(myPrefs.HIDE_DELETE_REQUEST_INFO, false) + val hideDeleteRequestInfo = getBoolean(PrefKeys.HIDE_DELETE_REQUEST_INFO, false) return Account( Persona(privKey = privKey?.toByteArray(), pubKey = pubKey.toByteArray()), @@ -173,13 +186,13 @@ class LocalPreferences(context: Context) { } fun saveLastRead(route: String, timestampInSecs: Long) { - encryptedPreferences.edit().apply { + encryptedPreferences(currentAccount).edit().apply { putLong(PrefKeys.LAST_READ(route), timestampInSecs) }.apply() } fun loadLastRead(route: String): Long { - encryptedPreferences.run { + encryptedPreferences(currentAccount).run { return getLong(PrefKeys.LAST_READ(route), 0) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/NotificationCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/NotificationCache.kt index 0173fe3bd..891303243 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/NotificationCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/NotificationCache.kt @@ -21,7 +21,7 @@ object NotificationCache { val scope = CoroutineScope(Job() + Dispatchers.IO) scope.launch { - LocalPreferences(context).saveLastRead(route, timestampInSecs) + LocalPreferences.saveLastRead(route, timestampInSecs) live.invalidateData() } } @@ -30,7 +30,7 @@ object NotificationCache { fun load(route: String, context: Context): Long { var lastTime = lastReadByRoute[route] if (lastTime == null) { - lastTime = LocalPreferences(context).loadLastRead(route) + lastTime = LocalPreferences.loadLastRead(route) lastReadByRoute[route] = lastTime } return lastTime diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt index cdb3aab35..a11394367 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt @@ -14,7 +14,6 @@ import coil.ImageLoader import coil.decode.GifDecoder import coil.decode.ImageDecoderDecoder import coil.decode.SvgDecoder -import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.ServiceManager import com.vitorpamplona.amethyst.service.nip19.Nip19 import com.vitorpamplona.amethyst.service.relays.Client @@ -54,7 +53,7 @@ class MainActivity : FragmentActivity() { // A surface container using the 'background' color from the theme Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) { val accountStateViewModel: AccountStateViewModel = viewModel { - AccountStateViewModel(LocalPreferences(applicationContext)) + AccountStateViewModel() } AccountScreen(accountStateViewModel, startingPage) 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 f206ac2d0..87685baee 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 @@ -1,54 +1,81 @@ package com.vitorpamplona.amethyst.ui.navigation +import androidx.compose.foundation.background 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.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.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.Icon import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme import androidx.compose.material.ModalBottomSheetState +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.material.TextButton import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Logout +import androidx.compose.material.icons.outlined.Visibility +import androidx.compose.material.icons.outlined.VisibilityOff import androidx.compose.runtime.Composable -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.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.autofill.AutofillNode +import androidx.compose.ui.autofill.AutofillType import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.compose.ui.layout.boundsInWindow +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalAutofill +import androidx.compose.ui.platform.LocalAutofillTree import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp +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.note.toShortenHex +import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import kotlinx.coroutines.launch -@OptIn(ExperimentalMaterialApi::class) +@OptIn(ExperimentalMaterialApi::class, ExperimentalComposeUiApi::class) @Composable fun AccountSwitchBottomSheet( accountViewModel: AccountViewModel, + accountStateViewModel: AccountStateViewModel, sheetState: ModalBottomSheetState ) { val coroutineScope = rememberCoroutineScope() val context = LocalContext.current - val localPrefs = LocalPreferences(context) - val accounts = localPrefs.findAllLocalAccounts() + val accounts = LocalPreferences.findAllLocalAccounts() val accountState by accountViewModel.accountLiveData.observeAsState() val account = accountState?.account ?: return @@ -56,9 +83,7 @@ fun AccountSwitchBottomSheet( val accountUserState by account.userProfile().live().metadata.observeAsState() val accountUser = accountUserState?.user ?: return - LaunchedEffect(key1 = accountUser) { - localPrefs.saveCurrentAccountMetadata(account) - } + var popupExpanded by remember { mutableStateOf(false) } Column { Row( @@ -114,9 +139,98 @@ fun AccountSwitchBottomSheet( horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically ) { - TextButton(onClick = { coroutineScope.launch { sheetState.hide() } }) { + TextButton(onClick = { popupExpanded = true }) { Text("Add New Account") } } } + + if (popupExpanded) { + Dialog( + onDismissRequest = { popupExpanded = false }, + properties = DialogProperties(usePlatformDefaultWidth = false) + ) { + Surface(modifier = Modifier.fillMaxSize()) { + Column( + modifier = Modifier + .fillMaxHeight() + .background(MaterialTheme.colors.surface), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + val key = remember { mutableStateOf(TextFieldValue("")) } + var errorMessage by remember { mutableStateOf("") } + var showPassword by remember { + mutableStateOf(false) + } + val autofillNode = AutofillNode( + autofillTypes = listOf(AutofillType.Password), + onFill = { key.value = TextFieldValue(it) } + ) + val autofill = LocalAutofill.current + LocalAutofillTree.current += autofillNode + + OutlinedTextField( + modifier = Modifier + .onGloballyPositioned { coordinates -> + autofillNode.boundingBox = coordinates.boundsInWindow() + } + .onFocusChanged { focusState -> + autofill?.run { + if (focusState.isFocused) { + requestAutofillForNode(autofillNode) + } else { + cancelAutofillForNode(autofillNode) + } + } + }, + value = key.value, + onValueChange = { key.value = it }, + keyboardOptions = KeyboardOptions( + autoCorrect = false, + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Go + ), + placeholder = { + Text( + text = stringResource(R.string.nsec_npub_hex_private_key), + color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) + ) + }, + trailingIcon = { + IconButton(onClick = { showPassword = !showPassword }) { + Icon( + imageVector = if (showPassword) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility, + contentDescription = if (showPassword) { + stringResource(R.string.show_password) + } else { + stringResource( + R.string.hide_password + ) + } + ) + } + }, + visualTransformation = if (showPassword) VisualTransformation.None else PasswordVisualTransformation(), + keyboardActions = KeyboardActions( + onGo = { + try { + accountStateViewModel.login(key.value.text) + } catch (e: Exception) { + errorMessage = context.getString(R.string.invalid_key) + } + } + ) + ) + if (errorMessage.isNotBlank()) { + Text( + text = errorMessage, + color = MaterialTheme.colors.error, + style = MaterialTheme.typography.caption + ) + } + } + } + } + } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt index e57a4ae1b..3321b42df 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt @@ -17,7 +17,7 @@ import nostr.postr.Persona import nostr.postr.bechToBytes import java.util.regex.Pattern -class AccountStateViewModel(private val localPreferences: LocalPreferences) : ViewModel() { +class AccountStateViewModel() : ViewModel() { private val _accountContent = MutableStateFlow(AccountState.LoggedOff) val accountContent = _accountContent.asStateFlow() @@ -26,7 +26,7 @@ class AccountStateViewModel(private val localPreferences: LocalPreferences) : Vi // Keeps it in the the UI thread to void blinking the login page. // viewModelScope.launch(Dispatchers.IO) { - localPreferences.loadFromEncryptedStorage()?.let { + LocalPreferences.loadFromEncryptedStorage()?.let { login(it) } // } @@ -47,18 +47,19 @@ class AccountStateViewModel(private val localPreferences: LocalPreferences) : Vi Account(Persona(Hex.decode(key))) } - localPreferences.saveToEncryptedStorage(account) - + LocalPreferences.saveToEncryptedStorage(account) login(account) } fun newKey() { val account = Account(Persona()) - localPreferences.saveToEncryptedStorage(account) + LocalPreferences.saveToEncryptedStorage(account) login(account) } fun login(account: Account) { + LocalPreferences.setCurrentAccount(account) + if (account.loggedIn.privKey != null) { _accountContent.update { AccountState.LoggedIn(account) } } else { @@ -77,7 +78,7 @@ class AccountStateViewModel(private val localPreferences: LocalPreferences) : Vi private val saveListener: (com.vitorpamplona.amethyst.model.AccountState) -> Unit = { GlobalScope.launch(Dispatchers.IO) { - localPreferences.saveToEncryptedStorage(it.account) + LocalPreferences.saveToEncryptedStorage(it.account) } } @@ -100,6 +101,6 @@ class AccountStateViewModel(private val localPreferences: LocalPreferences) : Vi _accountContent.update { AccountState.LoggedOff } - localPreferences.clearEncryptedStorage() + LocalPreferences.clearEncryptedStorage() } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt index e296fec90..a1cb6f101 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt @@ -47,7 +47,7 @@ fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: Accoun ModalBottomSheetLayout( sheetState = sheetState, sheetContent = { - AccountSwitchBottomSheet(accountViewModel = accountViewModel, sheetState = sheetState) + AccountSwitchBottomSheet(accountViewModel = accountViewModel, accountStateViewModel = accountStateViewModel, sheetState = sheetState) } ) { Scaffold( From 9561261bf4c3ea6c320ee8585c4c4a2efe6cc948 Mon Sep 17 00:00:00 2001 From: maxmoney21m Date: Sun, 12 Mar 2023 02:19:56 +0800 Subject: [PATCH 13/45] Add note --- app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index 9705b5829..5a9b72878 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -14,6 +14,7 @@ import nostr.postr.Persona import nostr.postr.toHex import java.util.Locale +// MUST BE SET TO FALSE FOR PRODUCTION!!!!! const val DEBUG_PLAINTEXT_PREFERENCES = true data class AccountInfo(val npub: String, val current: Boolean, val displayName: String?, val profilePicture: String?) From b40bde10a0714ee354f13d602ee2304591baca11 Mon Sep 17 00:00:00 2001 From: maxmoney21m Date: Sun, 12 Mar 2023 02:42:50 +0800 Subject: [PATCH 14/45] Add account, switch account, and logout flows --- .../amethyst/LocalPreferences.kt | 28 +++-- .../ui/navigation/AccountSwitchBottomSheet.kt | 106 ++++++++++++++---- .../ui/screen/AccountStateViewModel.kt | 17 ++- 3 files changed, 116 insertions(+), 35 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index 5a9b72878..78840dae1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -80,18 +80,27 @@ object LocalPreferences { } } - fun clearEncryptedStorage(npub: String? = null) { - val encPrefs = encryptedPreferences(npub) - encPrefs.edit().apply { - encPrefs.all.keys.forEach { - remove(it) - } + fun clearEncryptedStorage(npub: String) { + val userPrefs = encryptedPreferences(npub) + userPrefs.edit().clear().apply() + removeAccount(npub) + + if (savedAccounts.isEmpty()) { + val appPrefs = encryptedPreferences() + appPrefs.edit().clear().apply() + } else if (currentAccount == npub) { + currentAccount = savedAccounts.elementAt(0) + } +// encPrefs.edit().apply { +// encPrefs.all.keys.forEach { +// remove(it) +// } // encryptedPreferences.all.keys.filter { // it.startsWith(npub) // }.forEach { // remove(it) // } - }.apply() +// }.apply() } fun findAllLocalAccounts(): List { @@ -132,6 +141,11 @@ object LocalPreferences { }.apply() } + fun login(account: Account) { + setCurrentAccount(account) + saveToEncryptedStorage(account) + } + fun loadFromEncryptedStorage(): Account? { encryptedPreferences(currentAccount).apply { val pubKey = getString(PrefKeys.NOSTR_PUBKEY, null) ?: return null 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 87685baee..affca2daf 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 @@ -1,6 +1,7 @@ package com.vitorpamplona.amethyst.ui.navigation import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -12,8 +13,12 @@ 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.foundation.text.ClickableText import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.Icon import androidx.compose.material.IconButton @@ -48,13 +53,18 @@ import androidx.compose.ui.platform.LocalAutofill import androidx.compose.ui.platform.LocalAutofillTree import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration 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.LocalPreferences @@ -104,30 +114,39 @@ fun AccountSwitchBottomSheet( .padding(32.dp, 16.dp), verticalAlignment = Alignment.CenterVertically ) { - AsyncImageProxy( - model = ResizeImage(acc.profilePicture, 64.dp), - placeholder = BitmapPainter(RoboHashCache.get(context, acc.npub)), - fallback = BitmapPainter(RoboHashCache.get(context, acc.npub)), - error = BitmapPainter(RoboHashCache.get(context, acc.npub)), - contentDescription = stringResource(id = R.string.profile_image), - modifier = Modifier - .width(64.dp) - .height(64.dp) - .clip(shape = CircleShape) - ) - Spacer(modifier = Modifier.width(16.dp)) - Column { - acc.displayName?.let { - Text(it) + Row( + modifier = Modifier.clickable { + accountStateViewModel.login(acc.npub) } - Text(acc.npub.toShortenHex()) + ) { + AsyncImageProxy( + model = ResizeImage(acc.profilePicture, 64.dp), + placeholder = BitmapPainter(RoboHashCache.get(context, acc.npub)), + fallback = BitmapPainter(RoboHashCache.get(context, acc.npub)), + error = BitmapPainter(RoboHashCache.get(context, acc.npub)), + contentDescription = stringResource(id = R.string.profile_image), + modifier = Modifier + .width(64.dp) + .height(64.dp) + .clip(shape = CircleShape) + ) + Spacer(modifier = Modifier.width(16.dp)) + Column { + acc.displayName?.let { + Text(it) + } + Text(acc.npub.toShortenHex()) + } + Spacer(modifier = Modifier.width(8.dp)) + if (current) { + Text("✓") + } + Spacer(modifier = Modifier.weight(1f)) } - Spacer(modifier = Modifier.width(8.dp)) - if (current) { - Text("✓") - } - Spacer(modifier = Modifier.weight(1f)) - IconButton(onClick = { /*TODO*/ }) { + + IconButton( + onClick = { accountStateViewModel.logOff(acc.npub) } + ) { Icon(imageVector = Icons.Default.Logout, "Logout") } } @@ -229,6 +248,49 @@ fun AccountSwitchBottomSheet( style = MaterialTheme.typography.caption ) } + + Spacer(modifier = Modifier.height(16.dp)) + + Button( + onClick = { + if (key.value.text.isBlank()) { + errorMessage = context.getString(R.string.key_is_required) + } + try { + accountStateViewModel.login(key.value.text) + } catch (e: Exception) { + errorMessage = context.getString(R.string.invalid_key) + } + }, + shape = RoundedCornerShape(35.dp), + modifier = Modifier + .fillMaxWidth() + .height(50.dp), + colors = ButtonDefaults + .buttonColors( + backgroundColor = MaterialTheme.colors.primary + ) + ) { + Text(text = stringResource(R.string.login)) + } + + Spacer(modifier = Modifier.height(16.dp)) + + ClickableText( + text = AnnotatedString(stringResource(R.string.generate_a_new_key)), + modifier = Modifier + .padding(20.dp) + .fillMaxWidth(), + onClick = { + accountStateViewModel.newKey() + }, + style = TextStyle( + fontSize = 14.sp, + textDecoration = TextDecoration.Underline, + color = MaterialTheme.colors.primary, + textAlign = TextAlign.Center + ) + ) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt index 3321b42df..707a877aa 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt @@ -26,10 +26,14 @@ class AccountStateViewModel() : ViewModel() { // Keeps it in the the UI thread to void blinking the login page. // viewModelScope.launch(Dispatchers.IO) { + tryLoginExistingAccount() + // } + } + + private fun tryLoginExistingAccount() { LocalPreferences.loadFromEncryptedStorage()?.let { login(it) } - // } } fun login(key: String) { @@ -47,18 +51,18 @@ class AccountStateViewModel() : ViewModel() { Account(Persona(Hex.decode(key))) } - LocalPreferences.saveToEncryptedStorage(account) + LocalPreferences.login(account) login(account) } fun newKey() { val account = Account(Persona()) - LocalPreferences.saveToEncryptedStorage(account) + LocalPreferences.login(account) login(account) } fun login(account: Account) { - LocalPreferences.setCurrentAccount(account) + LocalPreferences.login(account) if (account.loggedIn.privKey != null) { _accountContent.update { AccountState.LoggedIn(account) } @@ -82,7 +86,7 @@ class AccountStateViewModel() : ViewModel() { } } - fun logOff() { + fun logOff(npub: String) { val state = accountContent.value when (state) { @@ -101,6 +105,7 @@ class AccountStateViewModel() : ViewModel() { _accountContent.update { AccountState.LoggedOff } - LocalPreferences.clearEncryptedStorage() + LocalPreferences.clearEncryptedStorage(npub) + tryLoginExistingAccount() } } From e472a494a1bd0d5fc0eadf9e10904c8e6c92803b Mon Sep 17 00:00:00 2001 From: maxmoney21m Date: Sun, 12 Mar 2023 11:59:16 +0800 Subject: [PATCH 15/45] Re-use existing LoginPage component in account switcher --- .../ui/navigation/AccountSwitchBottomSheet.kt | 181 +++--------------- .../amethyst/ui/screen/AccountScreen.kt | 3 +- .../amethyst/ui/screen/loggedIn/MainScreen.kt | 2 +- .../ui/screen/loggedOff/LoginScreen.kt | 86 +++++---- 4 files changed, 75 insertions(+), 197 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 affca2daf..d090c46b5 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 @@ -1,70 +1,42 @@ package com.vitorpamplona.amethyst.ui.navigation -import androidx.compose.foundation.background 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.Spacer -import androidx.compose.foundation.layout.fillMaxHeight 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.foundation.text.ClickableText -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.Button -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.MaterialTheme -import androidx.compose.material.ModalBottomSheetState -import androidx.compose.material.OutlinedTextField import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.material.TextButton +import androidx.compose.material.TopAppBar import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.Logout -import androidx.compose.material.icons.outlined.Visibility -import androidx.compose.material.icons.outlined.VisibilityOff 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.ExperimentalComposeUiApi import androidx.compose.ui.Modifier -import androidx.compose.ui.autofill.AutofillNode -import androidx.compose.ui.autofill.AutofillType import androidx.compose.ui.draw.clip -import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.BitmapPainter -import androidx.compose.ui.layout.boundsInWindow -import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.platform.LocalAutofill -import androidx.compose.ui.platform.LocalAutofillTree import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.input.PasswordVisualTransformation -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.text.input.VisualTransformation -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextDecoration 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.LocalPreferences @@ -75,15 +47,13 @@ import com.vitorpamplona.amethyst.ui.components.ResizeImage 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 -@OptIn(ExperimentalMaterialApi::class, ExperimentalComposeUiApi::class) @Composable fun AccountSwitchBottomSheet( accountViewModel: AccountViewModel, - accountStateViewModel: AccountStateViewModel, - sheetState: ModalBottomSheetState + accountStateViewModel: AccountStateViewModel ) { - val coroutineScope = rememberCoroutineScope() val context = LocalContext.current val accounts = LocalPreferences.findAllLocalAccounts() @@ -117,7 +87,8 @@ fun AccountSwitchBottomSheet( Row( modifier = Modifier.clickable { accountStateViewModel.login(acc.npub) - } + }, + verticalAlignment = Alignment.CenterVertically ) { AsyncImageProxy( model = ResizeImage(acc.profilePicture, 64.dp), @@ -141,13 +112,18 @@ fun AccountSwitchBottomSheet( if (current) { Text("✓") } - Spacer(modifier = Modifier.weight(1f)) } + Spacer(modifier = Modifier.weight(1f)) + IconButton( onClick = { accountStateViewModel.logOff(acc.npub) } ) { - Icon(imageVector = Icons.Default.Logout, "Logout") + Icon( + imageVector = Icons.Default.Logout, + contentDescription = "Logout", + tint = MaterialTheme.colors.onSurface + ) } } } @@ -170,126 +146,21 @@ fun AccountSwitchBottomSheet( properties = DialogProperties(usePlatformDefaultWidth = false) ) { Surface(modifier = Modifier.fillMaxSize()) { - Column( - modifier = Modifier - .fillMaxHeight() - .background(MaterialTheme.colors.surface), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - val key = remember { mutableStateOf(TextFieldValue("")) } - var errorMessage by remember { mutableStateOf("") } - var showPassword by remember { - mutableStateOf(false) - } - val autofillNode = AutofillNode( - autofillTypes = listOf(AutofillType.Password), - onFill = { key.value = TextFieldValue(it) } - ) - val autofill = LocalAutofill.current - LocalAutofillTree.current += autofillNode - - OutlinedTextField( - modifier = Modifier - .onGloballyPositioned { coordinates -> - autofillNode.boundingBox = coordinates.boundsInWindow() - } - .onFocusChanged { focusState -> - autofill?.run { - if (focusState.isFocused) { - requestAutofillForNode(autofillNode) - } else { - cancelAutofillForNode(autofillNode) - } - } - }, - value = key.value, - onValueChange = { key.value = it }, - keyboardOptions = KeyboardOptions( - autoCorrect = false, - keyboardType = KeyboardType.Password, - imeAction = ImeAction.Go - ), - placeholder = { - Text( - text = stringResource(R.string.nsec_npub_hex_private_key), - color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) - ) - }, - trailingIcon = { - IconButton(onClick = { showPassword = !showPassword }) { + Box { + LoginPage(accountStateViewModel, isFirstLogin = false) + TopAppBar( + title = { Text(text = "Add New Account") }, + navigationIcon = { + IconButton(onClick = { popupExpanded = false }) { Icon( - imageVector = if (showPassword) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility, - contentDescription = if (showPassword) { - stringResource(R.string.show_password) - } else { - stringResource( - R.string.hide_password - ) - } + imageVector = Icons.Default.ArrowBack, + contentDescription = "Back", + tint = MaterialTheme.colors.onSurface ) } }, - visualTransformation = if (showPassword) VisualTransformation.None else PasswordVisualTransformation(), - keyboardActions = KeyboardActions( - onGo = { - try { - accountStateViewModel.login(key.value.text) - } catch (e: Exception) { - errorMessage = context.getString(R.string.invalid_key) - } - } - ) - ) - if (errorMessage.isNotBlank()) { - Text( - text = errorMessage, - color = MaterialTheme.colors.error, - style = MaterialTheme.typography.caption - ) - } - - Spacer(modifier = Modifier.height(16.dp)) - - Button( - onClick = { - if (key.value.text.isBlank()) { - errorMessage = context.getString(R.string.key_is_required) - } - try { - accountStateViewModel.login(key.value.text) - } catch (e: Exception) { - errorMessage = context.getString(R.string.invalid_key) - } - }, - shape = RoundedCornerShape(35.dp), - modifier = Modifier - .fillMaxWidth() - .height(50.dp), - colors = ButtonDefaults - .buttonColors( - backgroundColor = MaterialTheme.colors.primary - ) - ) { - Text(text = stringResource(R.string.login)) - } - - Spacer(modifier = Modifier.height(16.dp)) - - ClickableText( - text = AnnotatedString(stringResource(R.string.generate_a_new_key)), - modifier = Modifier - .padding(20.dp) - .fillMaxWidth(), - onClick = { - accountStateViewModel.newKey() - }, - style = TextStyle( - fontSize = 14.sp, - textDecoration = TextDecoration.Underline, - color = MaterialTheme.colors.primary, - textAlign = TextAlign.Center - ) + backgroundColor = Color.Transparent, + elevation = 0.dp ) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt index d82d6a353..f96914f78 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt @@ -8,6 +8,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.MainScreen +import com.vitorpamplona.amethyst.ui.screen.loggedOff.LoginPage @Composable fun AccountScreen(accountStateViewModel: AccountStateViewModel, startingPage: String?) { @@ -17,7 +18,7 @@ fun AccountScreen(accountStateViewModel: AccountStateViewModel, startingPage: St Crossfade(targetState = accountState, animationSpec = tween(durationMillis = 100)) { state -> when (state) { is AccountState.LoggedOff -> { - LoginPage(accountStateViewModel) + LoginPage(accountStateViewModel, isFirstLogin = true) } is AccountState.LoggedIn -> { MainScreen(AccountViewModel(state.account), accountStateViewModel, startingPage) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt index a1cb6f101..90fac6a13 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt @@ -47,7 +47,7 @@ fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: Accoun ModalBottomSheetLayout( sheetState = sheetState, sheetContent = { - AccountSwitchBottomSheet(accountViewModel = accountViewModel, accountStateViewModel = accountStateViewModel, sheetState = sheetState) + AccountSwitchBottomSheet(accountViewModel = accountViewModel, accountStateViewModel = accountStateViewModel) } ) { Scaffold( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt index ffa3c8ce4..d78673f7f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt @@ -1,4 +1,4 @@ -package com.vitorpamplona.amethyst.ui.screen +package com.vitorpamplona.amethyst.ui.screen.loggedOff import androidx.compose.foundation.Image import androidx.compose.foundation.layout.* @@ -36,14 +36,18 @@ import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel import java.util.* @OptIn(ExperimentalComposeUiApi::class) @Composable -fun LoginPage(accountViewModel: AccountStateViewModel) { +fun LoginPage( + accountViewModel: AccountStateViewModel, + isFirstLogin: Boolean +) { val key = remember { mutableStateOf(TextFieldValue("")) } var errorMessage by remember { mutableStateOf("") } - val acceptedTerms = remember { mutableStateOf(false) } + val acceptedTerms = remember { mutableStateOf(!isFirstLogin) } var termsAcceptanceIsRequired by remember { mutableStateOf("") } val uri = LocalUriHandler.current val context = LocalContext.current @@ -147,48 +151,50 @@ fun LoginPage(accountViewModel: AccountStateViewModel) { Spacer(modifier = Modifier.height(20.dp)) - Row(verticalAlignment = Alignment.CenterVertically) { - Checkbox( - checked = acceptedTerms.value, - onCheckedChange = { acceptedTerms.value = it } - ) + if (isFirstLogin) { + Row(verticalAlignment = Alignment.CenterVertically) { + Checkbox( + checked = acceptedTerms.value, + onCheckedChange = { acceptedTerms.value = it } + ) - val regularText = - SpanStyle(color = MaterialTheme.colors.onBackground) + val regularText = + SpanStyle(color = MaterialTheme.colors.onBackground) - val clickableTextStyle = - SpanStyle(color = MaterialTheme.colors.primary) + val clickableTextStyle = + SpanStyle(color = MaterialTheme.colors.primary) - val annotatedTermsString = buildAnnotatedString { - withStyle(regularText) { - append(stringResource(R.string.i_accept_the)) - } - - withStyle(clickableTextStyle) { - pushStringAnnotation("openTerms", "") - append(stringResource(R.string.terms_of_use)) - } - } - - ClickableText( - text = annotatedTermsString - ) { spanOffset -> - annotatedTermsString.getStringAnnotations(spanOffset, spanOffset) - .firstOrNull() - ?.also { span -> - if (span.tag == "openTerms") { - runCatching { uri.openUri("https://github.com/vitorpamplona/amethyst/blob/main/PRIVACY.md") } - } + val annotatedTermsString = buildAnnotatedString { + withStyle(regularText) { + append(stringResource(R.string.i_accept_the)) } - } - } - if (termsAcceptanceIsRequired.isNotBlank()) { - Text( - text = termsAcceptanceIsRequired, - color = MaterialTheme.colors.error, - style = MaterialTheme.typography.caption - ) + withStyle(clickableTextStyle) { + pushStringAnnotation("openTerms", "") + append(stringResource(R.string.terms_of_use)) + } + } + + ClickableText( + text = annotatedTermsString + ) { spanOffset -> + annotatedTermsString.getStringAnnotations(spanOffset, spanOffset) + .firstOrNull() + ?.also { span -> + if (span.tag == "openTerms") { + runCatching { uri.openUri("https://github.com/vitorpamplona/amethyst/blob/main/PRIVACY.md") } + } + } + } + } + + if (termsAcceptanceIsRequired.isNotBlank()) { + Text( + text = termsAcceptanceIsRequired, + color = MaterialTheme.colors.error, + style = MaterialTheme.typography.caption + ) + } } Spacer(modifier = Modifier.height(20.dp)) From 992796a7bf05dd24a8761dfa4b094091d3341c16 Mon Sep 17 00:00:00 2001 From: maxmoney21m Date: Sun, 12 Mar 2023 12:00:01 +0800 Subject: [PATCH 16/45] Delete user-specific preference xml file on logout --- .../amethyst/LocalPreferences.kt | 42 +++++++++++++------ 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index 78840dae1..c9698182d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -1,5 +1,6 @@ package com.vitorpamplona.amethyst +import android.annotation.SuppressLint import android.content.Context import android.content.SharedPreferences import com.google.gson.GsonBuilder @@ -12,9 +13,12 @@ import com.vitorpamplona.amethyst.service.model.Event import com.vitorpamplona.amethyst.service.model.Event.Companion.getRefinedEvent import nostr.postr.Persona import nostr.postr.toHex +import java.io.File import java.util.Locale -// MUST BE SET TO FALSE FOR PRODUCTION!!!!! +// Release mode (!BuildConfig.DEBUG) always uses encrypted preferences +// To use plaintext SharedPreferences for debugging, set this to true +// It will only apply in Debug builds const val DEBUG_PLAINTEXT_PREFERENCES = true data class AccountInfo(val npub: String, val current: Boolean, val displayName: String?, val profilePicture: String?) @@ -62,6 +66,9 @@ object LocalPreferences { }.apply() } + /** + * Removes the account from the app level shared preferences + */ private fun removeAccount(npub: String) { val accounts = savedAccounts.toMutableSet() accounts.remove(npub) @@ -71,8 +78,21 @@ object LocalPreferences { }.apply() } + /** + * Deletes the npub-specific shared preference file + */ + private fun deleteUserPreferenceFile(npub: String) { + val context = Amethyst.instance + val prefsDir = File("${context.filesDir.parent}/shared_prefs/") + prefsDir.list()?.forEach { + if (it.contains(npub)) { + File(prefsDir, it).delete() + } + } + } + private fun encryptedPreferences(npub: String? = null): SharedPreferences { - return if (DEBUG_PLAINTEXT_PREFERENCES) { + return if (BuildConfig.DEBUG && DEBUG_PLAINTEXT_PREFERENCES) { val preferenceFile = if (npub == null) "testing_only" else "testing_only_$npub" Amethyst.instance.getSharedPreferences(preferenceFile, Context.MODE_PRIVATE) } else { @@ -80,10 +100,16 @@ object LocalPreferences { } } + /** + * Clears the preferences for a given npub, deletes the preferences xml file, + * and switches the user to the first account in the list if it exists + */ + @SuppressLint("ApplySharedPref") fun clearEncryptedStorage(npub: String) { val userPrefs = encryptedPreferences(npub) - userPrefs.edit().clear().apply() + userPrefs.edit().clear().commit() removeAccount(npub) + deleteUserPreferenceFile(npub) if (savedAccounts.isEmpty()) { val appPrefs = encryptedPreferences() @@ -91,16 +117,6 @@ object LocalPreferences { } else if (currentAccount == npub) { currentAccount = savedAccounts.elementAt(0) } -// encPrefs.edit().apply { -// encPrefs.all.keys.forEach { -// remove(it) -// } -// encryptedPreferences.all.keys.filter { -// it.startsWith(npub) -// }.forEach { -// remove(it) -// } -// }.apply() } fun findAllLocalAccounts(): List { From 269197a59d744075ee2ab4baeee55a19550da95b Mon Sep 17 00:00:00 2001 From: maxmoney21m Date: Sun, 12 Mar 2023 13:24:51 +0800 Subject: [PATCH 17/45] Migration from old to new preferences --- .../amethyst/EncryptedStorage.kt | 6 +- .../amethyst/LocalPreferences.kt | 99 +++++++++++++++---- .../vitorpamplona/amethyst/ui/MainActivity.kt | 3 + .../ui/navigation/AccountSwitchBottomSheet.kt | 2 +- .../ui/screen/AccountStateViewModel.kt | 8 +- 5 files changed, 92 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/EncryptedStorage.kt b/app/src/main/java/com/vitorpamplona/amethyst/EncryptedStorage.kt index 002c8a744..bfcea8192 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/EncryptedStorage.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/EncryptedStorage.kt @@ -6,13 +6,17 @@ import androidx.security.crypto.MasterKey object EncryptedStorage { private const val PREFERENCES_NAME = "secret_keeper" + fun prefsFileName(npub: String? = null): String { + return if (npub == null) PREFERENCES_NAME else "${PREFERENCES_NAME}_$npub" + } + fun preferences(npub: String? = null): EncryptedSharedPreferences { val context = Amethyst.instance val masterKey: MasterKey = MasterKey.Builder(context, MasterKey.DEFAULT_MASTER_KEY_ALIAS) .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) .build() - val preferencesName = if (npub == null) PREFERENCES_NAME else "${PREFERENCES_NAME}_$npub" + val preferencesName = prefsFileName(npub) return EncryptedSharedPreferences.create( context, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index c9698182d..89ffd99ac 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -11,17 +11,25 @@ import com.vitorpamplona.amethyst.model.toByteArray import com.vitorpamplona.amethyst.service.model.ContactListEvent import com.vitorpamplona.amethyst.service.model.Event import com.vitorpamplona.amethyst.service.model.Event.Companion.getRefinedEvent +import fr.acinq.secp256k1.Hex import nostr.postr.Persona import nostr.postr.toHex +import nostr.postr.toNpub import java.io.File import java.util.Locale // Release mode (!BuildConfig.DEBUG) always uses encrypted preferences // To use plaintext SharedPreferences for debugging, set this to true // It will only apply in Debug builds -const val DEBUG_PLAINTEXT_PREFERENCES = true +private const val DEBUG_PLAINTEXT_PREFERENCES = false +private const val OLD_PREFS_FILENAME = "secret_keeper" -data class AccountInfo(val npub: String, val current: Boolean, val displayName: String?, val profilePicture: String?) +data class AccountInfo( + val npub: String, + val current: Boolean, + val displayName: String?, + val profilePicture: String? +) private object PrefKeys { const val CURRENT_ACCOUNT = "currently_logged_in_account" @@ -57,6 +65,9 @@ object LocalPreferences { private val savedAccounts: Set get() = encryptedPreferences().getStringSet(PrefKeys.SAVED_ACCOUNTS, null) ?: setOf() + private val prefsDirPath: String + get() = "${Amethyst.instance.filesDir.parent}/shared_prefs/" + private fun addAccount(npub: String) { val accounts = savedAccounts.toMutableSet() accounts.add(npub) @@ -66,6 +77,12 @@ object LocalPreferences { }.apply() } + private fun setCurrentAccount(account: Account) { + val npub = account.userProfile().pubkeyNpub() + currentAccount = npub + addAccount(npub) + } + /** * Removes the account from the app level shared preferences */ @@ -82,8 +99,7 @@ object LocalPreferences { * Deletes the npub-specific shared preference file */ private fun deleteUserPreferenceFile(npub: String) { - val context = Amethyst.instance - val prefsDir = File("${context.filesDir.parent}/shared_prefs/") + val prefsDir = File(prefsDirPath) prefsDir.list()?.forEach { if (it.contains(npub)) { File(prefsDir, it).delete() @@ -93,7 +109,7 @@ object LocalPreferences { private fun encryptedPreferences(npub: String? = null): SharedPreferences { return if (BuildConfig.DEBUG && DEBUG_PLAINTEXT_PREFERENCES) { - val preferenceFile = if (npub == null) "testing_only" else "testing_only_$npub" + val preferenceFile = if (npub == null) "debug_prefs" else "debug_prefs_$npub" Amethyst.instance.getSharedPreferences(preferenceFile, Context.MODE_PRIVATE) } else { return EncryptedStorage.preferences(npub) @@ -103,9 +119,13 @@ object LocalPreferences { /** * Clears the preferences for a given npub, deletes the preferences xml file, * and switches the user to the first account in the list if it exists + * + * We need to use `commit()` to write changes to disk and release the file + * lock so that it can be deleted. If we use `apply()` there is a race + * condition and the file will probably not be deleted */ @SuppressLint("ApplySharedPref") - fun clearEncryptedStorage(npub: String) { + fun updatePrefsForLogout(npub: String) { val userPrefs = encryptedPreferences(npub) userPrefs.edit().clear().commit() removeAccount(npub) @@ -119,7 +139,12 @@ object LocalPreferences { } } - fun findAllLocalAccounts(): List { + fun updatePrefsForLogin(account: Account) { + setCurrentAccount(account) + saveToEncryptedStorage(account) + } + + fun allSavedAccounts(): List { return savedAccounts.map { npub -> val prefs = encryptedPreferences(npub) @@ -132,12 +157,6 @@ object LocalPreferences { } } - fun setCurrentAccount(account: Account) { - val npub = account.userProfile().pubkeyNpub() - currentAccount = npub - addAccount(npub) - } - fun saveToEncryptedStorage(account: Account) { val prefs = encryptedPreferences(account.userProfile().pubkeyNpub()) prefs.edit().apply { @@ -157,11 +176,6 @@ object LocalPreferences { }.apply() } - fun login(account: Account) { - setCurrentAccount(account) - saveToEncryptedStorage(account) - } - fun loadFromEncryptedStorage(): Account? { encryptedPreferences(currentAccount).apply { val pubKey = getString(PrefKeys.NOSTR_PUBKEY, null) ?: return null @@ -183,7 +197,8 @@ object LocalPreferences { val latestContactList = try { getString(PrefKeys.LATEST_CONTACT_LIST, null)?.let { - Event.gson.fromJson(it, Event::class.java).getRefinedEvent(true) as ContactListEvent + Event.gson.fromJson(it, Event::class.java) + .getRefinedEvent(true) as ContactListEvent } } catch (e: Throwable) { e.printStackTrace() @@ -192,7 +207,10 @@ object LocalPreferences { val languagePreferences = try { getString(PrefKeys.LANGUAGE_PREFS, null)?.let { - gson.fromJson(it, object : TypeToken>() {}.type) as Map + gson.fromJson( + it, + object : TypeToken>() {}.type + ) as Map } ?: mapOf() } catch (e: Throwable) { e.printStackTrace() @@ -227,4 +245,45 @@ object LocalPreferences { return getLong(PrefKeys.LAST_READ(route), 0) } } + + fun migrateSingleUserPrefs() { + if (currentAccount != null) return + + val pubkey = encryptedPreferences().getString(PrefKeys.NOSTR_PUBKEY, null) ?: return + val npub = Hex.decode(pubkey).toNpub() + + val stringPrefs = listOf( + PrefKeys.NOSTR_PRIVKEY, + PrefKeys.NOSTR_PUBKEY, + PrefKeys.RELAYS, + PrefKeys.LANGUAGE_PREFS, + PrefKeys.TRANSLATE_TO, + PrefKeys.ZAP_AMOUNTS, + PrefKeys.LATEST_CONTACT_LIST + ) + + val stringSetPrefs = listOf( + PrefKeys.FOLLOWING_CHANNELS, + PrefKeys.HIDDEN_USERS, + PrefKeys.DONT_TRANSLATE_FROM + ) + + encryptedPreferences().apply { + val appPrefs = this + encryptedPreferences(npub).edit().apply { + val userPrefs = this + + stringPrefs.forEach { userPrefs.putString(it, appPrefs.getString(it, null)) } + stringSetPrefs.forEach { userPrefs.putStringSet(it, appPrefs.getStringSet(it, null)) } + userPrefs.putBoolean( + PrefKeys.HIDE_DELETE_REQUEST_INFO, + appPrefs.getBoolean(PrefKeys.HIDE_DELETE_REQUEST_INFO, false) + ) + }.apply() + } + + encryptedPreferences().edit().clear().apply() + addAccount(npub) + currentAccount = npub + } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt index a11394367..0cd9b18dd 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt @@ -14,6 +14,7 @@ import coil.ImageLoader import coil.decode.GifDecoder import coil.decode.ImageDecoderDecoder import coil.decode.SvgDecoder +import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.ServiceManager import com.vitorpamplona.amethyst.service.nip19.Nip19 import com.vitorpamplona.amethyst.service.relays.Client @@ -48,6 +49,8 @@ class MainActivity : FragmentActivity() { .build() } + LocalPreferences.migrateSingleUserPrefs() + setContent { AmethystTheme { // A surface container using the 'background' color from the theme 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 d090c46b5..fb751c5d6 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 @@ -55,7 +55,7 @@ fun AccountSwitchBottomSheet( accountStateViewModel: AccountStateViewModel ) { val context = LocalContext.current - val accounts = LocalPreferences.findAllLocalAccounts() + val accounts = LocalPreferences.allSavedAccounts() val accountState by accountViewModel.accountLiveData.observeAsState() val account = accountState?.account ?: return diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt index 707a877aa..eff4f583d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt @@ -51,18 +51,18 @@ class AccountStateViewModel() : ViewModel() { Account(Persona(Hex.decode(key))) } - LocalPreferences.login(account) + LocalPreferences.updatePrefsForLogin(account) login(account) } fun newKey() { val account = Account(Persona()) - LocalPreferences.login(account) + LocalPreferences.updatePrefsForLogin(account) login(account) } fun login(account: Account) { - LocalPreferences.login(account) + LocalPreferences.updatePrefsForLogin(account) if (account.loggedIn.privKey != null) { _accountContent.update { AccountState.LoggedIn(account) } @@ -105,7 +105,7 @@ class AccountStateViewModel() : ViewModel() { _accountContent.update { AccountState.LoggedOff } - LocalPreferences.clearEncryptedStorage(npub) + LocalPreferences.updatePrefsForLogout(npub) tryLoginExistingAccount() } } From f34d79106dfdc25c393a41c49ddecacd1467ed70 Mon Sep 17 00:00:00 2001 From: maxmoney21m Date: Sun, 12 Mar 2023 14:27:34 +0800 Subject: [PATCH 18/45] Fix account switching --- .../com/vitorpamplona/amethyst/LocalPreferences.kt | 4 ++++ .../ui/navigation/AccountSwitchBottomSheet.kt | 2 +- .../amethyst/ui/screen/AccountStateViewModel.kt | 11 ++++++++++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index 89ffd99ac..5f5354b12 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -83,6 +83,10 @@ object LocalPreferences { addAccount(npub) } + fun switchToAccount(npub: String) { + currentAccount = npub + } + /** * Removes the account from the app level shared preferences */ 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 fb751c5d6..0f414299e 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 @@ -86,7 +86,7 @@ fun AccountSwitchBottomSheet( ) { Row( modifier = Modifier.clickable { - accountStateViewModel.login(acc.npub) + accountStateViewModel.switchUser(acc.npub) }, verticalAlignment = Alignment.CenterVertically ) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt index eff4f583d..10cc60808 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt @@ -55,6 +55,12 @@ class AccountStateViewModel() : ViewModel() { login(account) } + fun switchUser(npub: String) { + prepareLogoutOrSwitch() + LocalPreferences.switchToAccount(npub) + tryLoginExistingAccount() + } + fun newKey() { val account = Account(Persona()) LocalPreferences.updatePrefsForLogin(account) @@ -86,7 +92,7 @@ class AccountStateViewModel() : ViewModel() { } } - fun logOff(npub: String) { + private fun prepareLogoutOrSwitch() { val state = accountContent.value when (state) { @@ -104,7 +110,10 @@ class AccountStateViewModel() : ViewModel() { } _accountContent.update { AccountState.LoggedOff } + } + fun logOff(npub: String) { + prepareLogoutOrSwitch() LocalPreferences.updatePrefsForLogout(npub) tryLoginExistingAccount() } From b1cb7d599fd5b92adc0bd81f74f67fefb13dd029 Mon Sep 17 00:00:00 2001 From: maxmoney21m Date: Sun, 12 Mar 2023 20:34:50 +0800 Subject: [PATCH 19/45] Make account switcher scrollable --- .idea/deploymentTargetDropDown.xml | 17 ----------------- .../ui/navigation/AccountSwitchBottomSheet.kt | 5 ++++- 2 files changed, 4 insertions(+), 18 deletions(-) delete mode 100644 .idea/deploymentTargetDropDown.xml diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml deleted file mode 100644 index 0c0237e9c..000000000 --- a/.idea/deploymentTargetDropDown.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file 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 0f414299e..0541d2351 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 @@ -11,7 +11,9 @@ 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.rememberScrollState import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.verticalScroll import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.MaterialTheme @@ -64,8 +66,9 @@ fun AccountSwitchBottomSheet( val accountUser = accountUserState?.user ?: return var popupExpanded by remember { mutableStateOf(false) } + val scrollState = rememberScrollState() - Column { + Column(modifier = Modifier.verticalScroll(scrollState)) { Row( modifier = Modifier .fillMaxWidth() From 32b50418de02b27c436f1468be50b5a16c45f5f5 Mon Sep 17 00:00:00 2001 From: maxmoney21m Date: Sun, 12 Mar 2023 21:20:00 +0800 Subject: [PATCH 20/45] Indicate whether account has pubkey or privkey --- .../amethyst/LocalPreferences.kt | 3 + .../ui/navigation/AccountSwitchBottomSheet.kt | 80 +++++++++++++------ 2 files changed, 60 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index 5f5354b12..4b41607b4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -26,6 +26,7 @@ private const val OLD_PREFS_FILENAME = "secret_keeper" data class AccountInfo( val npub: String, + val hasPrivKey: Boolean, val current: Boolean, val displayName: String?, val profilePicture: String? @@ -151,9 +152,11 @@ object LocalPreferences { fun allSavedAccounts(): List { return savedAccounts.map { npub -> val prefs = encryptedPreferences(npub) + val hasPrivKey = prefs.getString(PrefKeys.NOSTR_PRIVKEY, null) != null AccountInfo( npub = npub, + hasPrivKey = hasPrivKey, current = npub == currentAccount, displayName = prefs.getString(PrefKeys.DISPLAY_NAME, null), profilePicture = prefs.getString(PrefKeys.PROFILE_PICTURE_URL, null) 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 0541d2351..d0d8a0010 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 @@ -10,6 +10,7 @@ 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.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape @@ -23,7 +24,10 @@ import androidx.compose.material.TextButton import androidx.compose.material.TopAppBar import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.filled.Key import androidx.compose.material.icons.filled.Logout +import androidx.compose.material.icons.filled.RadioButtonChecked +import androidx.compose.material.icons.filled.Visibility import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState @@ -84,41 +88,71 @@ fun AccountSwitchBottomSheet( Row( modifier = Modifier .fillMaxWidth() - .padding(32.dp, 16.dp), + .padding(16.dp, 16.dp), verticalAlignment = Alignment.CenterVertically ) { Row( - modifier = Modifier.clickable { + modifier = Modifier.weight(1f).clickable { accountStateViewModel.switchUser(acc.npub) }, verticalAlignment = Alignment.CenterVertically ) { - AsyncImageProxy( - model = ResizeImage(acc.profilePicture, 64.dp), - placeholder = BitmapPainter(RoboHashCache.get(context, acc.npub)), - fallback = BitmapPainter(RoboHashCache.get(context, acc.npub)), - error = BitmapPainter(RoboHashCache.get(context, acc.npub)), - contentDescription = stringResource(id = R.string.profile_image), - modifier = Modifier - .width(64.dp) - .height(64.dp) - .clip(shape = CircleShape) - ) - Spacer(modifier = Modifier.width(16.dp)) - Column { - acc.displayName?.let { - Text(it) + Box( + modifier = Modifier.width(55.dp).padding(0.dp) + ) { + AsyncImageProxy( + 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(id = R.string.profile_image), + modifier = Modifier + .width(55.dp) + .height(55.dp) + .clip(shape = CircleShape) + ) + + Box( + modifier = Modifier.size(20.dp).align(Alignment.TopEnd) + ) { + if (acc.hasPrivKey) { + Icon( + imageVector = Icons.Default.Key, + contentDescription = "Has private key", + modifier = Modifier.size(20.dp), + tint = MaterialTheme.colors.primary + ) + } else { + Icon( + imageVector = Icons.Default.Visibility, + contentDescription = "Read only, no private key", + modifier = Modifier.size(20.dp), + tint = MaterialTheme.colors.primary + ) + } } - Text(acc.npub.toShortenHex()) } - Spacer(modifier = Modifier.width(8.dp)) - if (current) { - Text("✓") + Spacer(modifier = Modifier.width(16.dp)) + Column(modifier = Modifier.weight(1f)) { + val npubShortHex = acc.npub.toShortenHex() + + if (acc.displayName != null && acc.displayName != npubShortHex) { + Text(acc.displayName) + } + + Text(npubShortHex) + } + Column(modifier = Modifier.width(32.dp)) { + if (current) { + Icon( + imageVector = Icons.Default.RadioButtonChecked, + contentDescription = "Active account", + tint = MaterialTheme.colors.secondary + ) + } } } - Spacer(modifier = Modifier.weight(1f)) - IconButton( onClick = { accountStateViewModel.logOff(acc.npub) } ) { From cc36dcffe0e503a898a4a258971336052c4a3f3a Mon Sep 17 00:00:00 2001 From: maxmoney21m Date: Sun, 12 Mar 2023 21:57:32 +0800 Subject: [PATCH 21/45] Extract strings --- .../amethyst/LocalPreferences.kt | 4 +-- .../ui/navigation/AccountSwitchBottomSheet.kt | 34 +++++++++++-------- .../amethyst/ui/navigation/DrawerContent.kt | 17 ++-------- .../amethyst/ui/screen/loggedIn/MainScreen.kt | 2 +- app/src/main/res/values/strings.xml | 8 +++++ 5 files changed, 34 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index 4b41607b4..31548102e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -22,7 +22,7 @@ import java.util.Locale // To use plaintext SharedPreferences for debugging, set this to true // It will only apply in Debug builds private const val DEBUG_PLAINTEXT_PREFERENCES = false -private const val OLD_PREFS_FILENAME = "secret_keeper" +private const val DEBUG_PREFERENCES_NAME = "debug_prefs" data class AccountInfo( val npub: String, @@ -114,7 +114,7 @@ object LocalPreferences { private fun encryptedPreferences(npub: String? = null): SharedPreferences { return if (BuildConfig.DEBUG && DEBUG_PLAINTEXT_PREFERENCES) { - val preferenceFile = if (npub == null) "debug_prefs" else "debug_prefs_$npub" + val preferenceFile = if (npub == null) DEBUG_PREFERENCES_NAME else "${DEBUG_PREFERENCES_NAME}_$npub" Amethyst.instance.getSharedPreferences(preferenceFile, Context.MODE_PRIVATE) } else { return EncryptedStorage.preferences(npub) 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 d0d8a0010..6b793447d 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 @@ -80,7 +80,7 @@ fun AccountSwitchBottomSheet( horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically ) { - Text("Select Account", fontWeight = FontWeight.Bold) + Text(stringResource(R.string.account_switch_select_account), fontWeight = FontWeight.Bold) } accounts.forEach { acc -> val current = accountUser.pubkeyNpub() == acc.npub @@ -92,20 +92,24 @@ fun AccountSwitchBottomSheet( verticalAlignment = Alignment.CenterVertically ) { Row( - modifier = Modifier.weight(1f).clickable { - accountStateViewModel.switchUser(acc.npub) - }, + modifier = Modifier + .weight(1f) + .clickable { + accountStateViewModel.switchUser(acc.npub) + }, verticalAlignment = Alignment.CenterVertically ) { Box( - modifier = Modifier.width(55.dp).padding(0.dp) + modifier = Modifier + .width(55.dp) + .padding(0.dp) ) { AsyncImageProxy( 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(id = R.string.profile_image), + contentDescription = stringResource(R.string.profile_image), modifier = Modifier .width(55.dp) .height(55.dp) @@ -113,19 +117,21 @@ fun AccountSwitchBottomSheet( ) Box( - modifier = Modifier.size(20.dp).align(Alignment.TopEnd) + modifier = Modifier + .size(20.dp) + .align(Alignment.TopEnd) ) { if (acc.hasPrivKey) { Icon( imageVector = Icons.Default.Key, - contentDescription = "Has private key", + contentDescription = stringResource(R.string.account_switch_has_private_key), modifier = Modifier.size(20.dp), tint = MaterialTheme.colors.primary ) } else { Icon( imageVector = Icons.Default.Visibility, - contentDescription = "Read only, no private key", + contentDescription = stringResource(R.string.account_switch_pubkey_only), modifier = Modifier.size(20.dp), tint = MaterialTheme.colors.primary ) @@ -146,7 +152,7 @@ fun AccountSwitchBottomSheet( if (current) { Icon( imageVector = Icons.Default.RadioButtonChecked, - contentDescription = "Active account", + contentDescription = stringResource(R.string.account_switch_active_account), tint = MaterialTheme.colors.secondary ) } @@ -158,7 +164,7 @@ fun AccountSwitchBottomSheet( ) { Icon( imageVector = Icons.Default.Logout, - contentDescription = "Logout", + contentDescription = stringResource(R.string.log_out), tint = MaterialTheme.colors.onSurface ) } @@ -172,7 +178,7 @@ fun AccountSwitchBottomSheet( verticalAlignment = Alignment.CenterVertically ) { TextButton(onClick = { popupExpanded = true }) { - Text("Add New Account") + Text(stringResource(R.string.account_switch_add_account_btn)) } } } @@ -186,12 +192,12 @@ fun AccountSwitchBottomSheet( Box { LoginPage(accountStateViewModel, isFirstLogin = false) TopAppBar( - title = { Text(text = "Add New Account") }, + title = { Text(text = stringResource(R.string.account_switch_add_account_dialog_title)) }, navigationIcon = { IconButton(onClick = { popupExpanded = false }) { Icon( imageVector = Icons.Default.ArrowBack, - contentDescription = "Back", + contentDescription = stringResource(R.string.back), tint = MaterialTheme.colors.onSurface ) } 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 8198b7058..09c1aac42 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 @@ -53,7 +53,6 @@ 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.ResizeImage -import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountBackupDialog import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import kotlinx.coroutines.launch @@ -64,8 +63,7 @@ fun DrawerContent( navController: NavHostController, scaffoldState: ScaffoldState, sheetState: ModalBottomSheetState, - accountViewModel: AccountViewModel, - accountStateViewModel: AccountStateViewModel + accountViewModel: AccountViewModel ) { val accountState by accountViewModel.accountLiveData.observeAsState() val account = accountState?.account ?: return @@ -95,8 +93,7 @@ fun DrawerContent( sheetState, modifier = Modifier .fillMaxWidth() - .weight(1F), - accountStateViewModel, + .weight(1f), account ) @@ -227,7 +224,6 @@ fun ListContent( scaffoldState: ScaffoldState, sheetState: ModalBottomSheetState, modifier: Modifier, - accountViewModel: AccountStateViewModel, account: Account ) { val coroutineScope = rememberCoroutineScope() @@ -268,18 +264,11 @@ fun ListContent( Spacer(modifier = Modifier.weight(1f)) IconRow( - title = "Accounts", + title = stringResource(R.string.drawer_accounts), icon = R.drawable.manage_accounts, tint = MaterialTheme.colors.onBackground, onClick = { coroutineScope.launch { sheetState.show() } } ) - -// IconRow( -// title = stringResource(R.string.log_out), -// icon = R.drawable.ic_logout, -// tint = MaterialTheme.colors.onBackground, -// onClick = { accountViewModel.logOff() } -// ) } if (backupDialogOpen) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt index 90fac6a13..ff5f893e7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt @@ -61,7 +61,7 @@ fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: Accoun AppTopBar(navController, scaffoldState, accountViewModel) }, drawerContent = { - DrawerContent(navController, scaffoldState, sheetState, accountViewModel, accountStateViewModel) + DrawerContent(navController, scaffoldState, sheetState, accountViewModel) }, floatingActionButton = { FloatingButton(navController, accountStateViewModel) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a10ecee71..9874b9513 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -220,5 +220,13 @@ "<Unable to decrypt private message>\n\nYou were cited in a private/encrypted conversation between %1$s and %2$s." Delete Don\'t show again + Add New Account + Accounts + Select Account + Add New Account + Active account + Has private key + Read only, no private key + Back From 20431a87eaa4b73bee35afee6f1cf6bea634d77b Mon Sep 17 00:00:00 2001 From: maxmoney21m Date: Sun, 12 Mar 2023 22:26:38 +0800 Subject: [PATCH 22/45] Use List instead of Set to keep account order --- .../amethyst/LocalPreferences.kt | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index 31548102e..0d60e8885 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -54,6 +54,8 @@ private object PrefKeys { private val gson = GsonBuilder().create() object LocalPreferences { + private const val comma = "," + private var currentAccount: String? get() = encryptedPreferences().getString(PrefKeys.CURRENT_ACCOUNT, null) set(npub) { @@ -63,18 +65,21 @@ object LocalPreferences { }.apply() } - private val savedAccounts: Set - get() = encryptedPreferences().getStringSet(PrefKeys.SAVED_ACCOUNTS, null) ?: setOf() + private val savedAccounts: List + get() = encryptedPreferences() + .getString(PrefKeys.SAVED_ACCOUNTS, null)?.split(comma) ?: listOf() private val prefsDirPath: String get() = "${Amethyst.instance.filesDir.parent}/shared_prefs/" private fun addAccount(npub: String) { - val accounts = savedAccounts.toMutableSet() - accounts.add(npub) + val accounts = savedAccounts.toMutableList() + if (npub !in accounts) { + accounts.add(npub) + } val prefs = encryptedPreferences() prefs.edit().apply { - putStringSet(PrefKeys.SAVED_ACCOUNTS, accounts) + putString(PrefKeys.SAVED_ACCOUNTS, accounts.joinToString(comma)) }.apply() } @@ -92,12 +97,13 @@ object LocalPreferences { * Removes the account from the app level shared preferences */ private fun removeAccount(npub: String) { - val accounts = savedAccounts.toMutableSet() - accounts.remove(npub) - val prefs = encryptedPreferences() - prefs.edit().apply { - putStringSet(PrefKeys.SAVED_ACCOUNTS, accounts) - }.apply() + val accounts = savedAccounts.toMutableList() + if (accounts.remove(npub)) { + val prefs = encryptedPreferences() + prefs.edit().apply { + putString(PrefKeys.SAVED_ACCOUNTS, accounts.joinToString(comma)) + }.apply() + } } /** From be19d0a9cf3cc74a6ba057ba199fa0c6a95d4223 Mon Sep 17 00:00:00 2001 From: maxmoney21m Date: Fri, 10 Mar 2023 10:55:59 +0800 Subject: [PATCH 23/45] Add persistent lazy list state composable --- .../amethyst/ui/screen/LazyListState.kt | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LazyListState.kt diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LazyListState.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LazyListState.kt new file mode 100644 index 000000000..e6eb0c39e --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LazyListState.kt @@ -0,0 +1,34 @@ +package com.vitorpamplona.amethyst.ui.screen + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.saveable.rememberSaveable + +private val savedScrollStates = mutableMapOf() +private data class ScrollState(val index: Int, val scrollOffset: Int) + +@Composable +fun rememberForeverLazyListState( + key: String, + initialFirstVisibleItemIndex: Int = 0, + initialFirstVisibleItemScrollOffset: Int = 0 +): LazyListState { + val scrollState = rememberSaveable(saver = LazyListState.Saver) { + val savedValue = savedScrollStates[key] + val savedIndex = savedValue?.index ?: initialFirstVisibleItemIndex + val savedOffset = savedValue?.scrollOffset ?: initialFirstVisibleItemScrollOffset + LazyListState( + savedIndex, + savedOffset + ) + } + DisposableEffect(Unit) { + onDispose { + val lastIndex = scrollState.firstVisibleItemIndex + val lastOffset = scrollState.firstVisibleItemScrollOffset + savedScrollStates[key] = ScrollState(lastIndex, lastOffset) + } + } + return scrollState +} From 5da70a0f9554453aed0e3abd9d84a196700c9edf Mon Sep 17 00:00:00 2001 From: maxmoney21m Date: Fri, 10 Mar 2023 11:00:18 +0800 Subject: [PATCH 24/45] Persist lazy list state on Home, Global and Chatrooms --- .../amethyst/ui/screen/ChatroomFeedView.kt | 5 ++--- .../vitorpamplona/amethyst/ui/screen/FeedView.kt | 15 +++++++++++---- .../amethyst/ui/screen/loggedIn/HomeScreen.kt | 4 ++-- .../amethyst/ui/screen/loggedIn/SearchScreen.kt | 2 +- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomFeedView.kt index 6763b4d7c..805f9abc5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomFeedView.kt @@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -21,12 +20,12 @@ import com.vitorpamplona.amethyst.ui.note.ChatroomMessageCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @Composable -fun ChatroomFeedView(viewModel: FeedViewModel, accountViewModel: AccountViewModel, navController: NavController, routeForLastRead: String?, onWantsToReply: (Note) -> Unit) { +fun ChatroomFeedView(viewModel: FeedViewModel, accountViewModel: AccountViewModel, navController: NavController, routeForLastRead: String, onWantsToReply: (Note) -> Unit) { val feedState by viewModel.feedContent.collectAsState() var isRefreshing by remember { mutableStateOf(false) } - val listState = rememberLazyListState() + val listState = rememberForeverLazyListState(routeForLastRead) LaunchedEffect(isRefreshing) { if (isRefreshing) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt index be08602fb..ea412e861 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt @@ -36,7 +36,8 @@ fun FeedView( viewModel: FeedViewModel, accountViewModel: AccountViewModel, navController: NavController, - routeForLastRead: String? + routeForLastRead: String?, + scrollStateKey: String? = null ) { val feedState by viewModel.feedContent.collectAsState() @@ -74,7 +75,8 @@ fun FeedView( state, routeForLastRead, accountViewModel, - navController + navController, + scrollStateKey ) } is FeedState.Loading -> { @@ -91,9 +93,14 @@ private fun FeedLoaded( state: FeedState.Loaded, routeForLastRead: String?, accountViewModel: AccountViewModel, - navController: NavController + navController: NavController, + scrollStateKey: String? ) { - val listState = rememberLazyListState() + val listState = if (scrollStateKey != null) { + rememberForeverLazyListState(scrollStateKey) + } else { + rememberLazyListState() + } LazyColumn( contentPadding = PaddingValues( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt index fb0809f79..b5a8e35aa 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt @@ -106,8 +106,8 @@ fun HomeScreen(accountViewModel: AccountViewModel, navController: NavController) } HorizontalPager(count = 2, state = pagerState) { when (pagerState.currentPage) { - 0 -> FeedView(feedViewModel, accountViewModel, navController, Route.Home.route + "Follows") - 1 -> FeedView(feedViewModelReplies, accountViewModel, navController, Route.Home.route + "FollowsReplies") + 0 -> FeedView(feedViewModel, accountViewModel, navController, Route.Home.route + "Follows", Route.Home.route + "Follows") + 1 -> FeedView(feedViewModelReplies, accountViewModel, navController, Route.Home.route + "FollowsReplies", Route.Home.route + "FollowsReplies") } } } 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 444e46271..b9000e89b 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 @@ -114,7 +114,7 @@ fun SearchScreen(accountViewModel: AccountViewModel, navController: NavControlle modifier = Modifier.padding(vertical = 0.dp) ) { SearchBar(accountViewModel, navController) - FeedView(feedViewModel, accountViewModel, navController, null) + FeedView(feedViewModel, accountViewModel, navController, null, "Global") } } } From 7630e07dc9b065ae9d384564a431528b0ddef54d Mon Sep 17 00:00:00 2001 From: maxmoney21m Date: Fri, 10 Mar 2023 11:09:03 +0800 Subject: [PATCH 25/45] Replace deprecated SwipeRefresh with PullRefresh --- .../amethyst/ui/screen/CardFeedView.kt | 37 +++++++------- .../amethyst/ui/screen/ChatroomFeedView.kt | 3 +- .../ui/screen/ChatroomListFeedView.kt | 49 ++++++++----------- .../amethyst/ui/screen/FeedView.kt | 47 +++++++++--------- .../amethyst/ui/screen/LnZapFeedView.kt | 37 +++++++------- .../amethyst/ui/screen/RelayFeedView.kt | 32 ++++++------ .../amethyst/ui/screen/ThreadFeedView.kt | 34 ++++++------- .../amethyst/ui/screen/UserFeedView.kt | 37 +++++++------- 8 files changed, 126 insertions(+), 150 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt index df4fa0ebf..9b77d4156 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt @@ -2,22 +2,26 @@ package com.vitorpamplona.amethyst.ui.screen import androidx.compose.animation.Crossfade import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState 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.unit.dp import androidx.navigation.NavController -import com.google.accompanist.swiperefresh.SwipeRefresh -import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import com.vitorpamplona.amethyst.ui.note.BadgeCompose import com.vitorpamplona.amethyst.ui.note.BoostSetCompose import com.vitorpamplona.amethyst.ui.note.LikeSetCompose @@ -27,40 +31,31 @@ import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.note.ZapSetCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +@OptIn(ExperimentalMaterialApi::class) @Composable fun CardFeedView(viewModel: CardFeedViewModel, accountViewModel: AccountViewModel, navController: NavController, routeForLastRead: String) { val feedState by viewModel.feedContent.collectAsState() - var isRefreshing by remember { mutableStateOf(false) } - val swipeRefreshState = rememberSwipeRefreshState(isRefreshing) + var refreshing by remember { mutableStateOf(false) } + val refresh = { refreshing = true; viewModel.refresh(); refreshing = false } + val pullRefreshState = rememberPullRefreshState(refreshing, onRefresh = refresh) - LaunchedEffect(isRefreshing) { - if (isRefreshing) { - viewModel.refresh() - isRefreshing = false - } - } - - SwipeRefresh( - state = swipeRefreshState, - onRefresh = { - isRefreshing = true - } - ) { + Box(Modifier.pullRefresh(pullRefreshState)) { Column() { Crossfade(targetState = feedState, animationSpec = tween(durationMillis = 100)) { state -> when (state) { is CardFeedState.Empty -> { FeedEmpty { - isRefreshing = true + refreshing = true } } is CardFeedState.FeedError -> { FeedError(state.errorMessage) { - isRefreshing = true + refreshing = true } } is CardFeedState.Loaded -> { + refreshing = false FeedLoaded( state, accountViewModel, @@ -74,6 +69,8 @@ fun CardFeedView(viewModel: CardFeedViewModel, accountViewModel: AccountViewMode } } } + + PullRefreshIndicator(refreshing, pullRefreshState, Modifier.align(Alignment.TopCenter)) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomFeedView.kt index 805f9abc5..8ba6eb059 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomFeedView.kt @@ -62,8 +62,7 @@ fun ChatroomFeedView(viewModel: FeedViewModel, accountViewModel: AccountViewMode reverseLayout = true, state = listState ) { - var previousDate: String = "" - itemsIndexed(state.feed.value, key = { index, item -> item.idHex }) { index, item -> + itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { _, item -> ChatroomMessageCompose(item, routeForLastRead, accountViewModel = accountViewModel, navController = navController, onWantsToReply = onWantsToReply) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomListFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomListFeedView.kt index 1bc51f58d..743bd4f5c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomListFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomListFeedView.kt @@ -2,11 +2,16 @@ package com.vitorpamplona.amethyst.ui.screen import androidx.compose.animation.Crossfade import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState @@ -14,18 +19,18 @@ 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.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController -import com.google.accompanist.swiperefresh.SwipeRefresh -import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import com.vitorpamplona.amethyst.NotificationCache import com.vitorpamplona.amethyst.ui.note.ChatroomCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +@OptIn(ExperimentalMaterialApi::class) @Composable fun ChatroomListFeedView( viewModel: FeedViewModel, @@ -35,24 +40,11 @@ fun ChatroomListFeedView( ) { val feedState by viewModel.feedContent.collectAsStateWithLifecycle() - var isRefreshing by remember { mutableStateOf(false) } - val swipeRefreshState = rememberSwipeRefreshState(isRefreshing) + var refreshing by remember { mutableStateOf(false) } + val refresh = { refreshing = true; viewModel.refresh(); refreshing = false } + val pullRefreshState = rememberPullRefreshState(refreshing, onRefresh = refresh) - val coroutineScope = rememberCoroutineScope() - - LaunchedEffect(isRefreshing) { - if (isRefreshing) { - viewModel.refresh() - isRefreshing = false - } - } - - SwipeRefresh( - state = swipeRefreshState, - onRefresh = { - isRefreshing = true - } - ) { + Box(Modifier.pullRefresh(pullRefreshState)) { Column() { Crossfade( targetState = feedState, @@ -61,17 +53,18 @@ fun ChatroomListFeedView( when (state) { is FeedState.Empty -> { FeedEmpty { - isRefreshing = true + refreshing = true } } is FeedState.FeedError -> { FeedError(state.errorMessage) { - isRefreshing = true + refreshing = true } } is FeedState.Loaded -> { + refreshing = false FeedLoaded(state, accountViewModel, navController, markAsRead) } @@ -81,6 +74,8 @@ fun ChatroomListFeedView( } } } + + PullRefreshIndicator(refreshing, pullRefreshState, Modifier.align(Alignment.TopCenter)) } } @@ -103,11 +98,9 @@ private fun FeedLoaded( if (markAsRead.value) { for (note in state.feed.value) { note.event?.let { - var route = "" val channel = note.channel() - - if (channel != null) { - route = "Channel/${channel.idHex}" + val route = if (channel != null) { + "Channel/${channel.idHex}" } else { val replyAuthorBase = note.mentions?.first() var userToComposeOn = note.author!! @@ -116,7 +109,7 @@ private fun FeedLoaded( userToComposeOn = replyAuthorBase } } - route = "Room/${userToComposeOn.pubkeyHex}" + "Room/${userToComposeOn.pubkeyHex}" } notificationCache.cache.markAsRead(route, it.createdAt(), context) @@ -136,7 +129,7 @@ private fun FeedLoaded( itemsIndexed( state.feed.value, key = { index, item -> if (index == 0) index else item.idHex } - ) { index, item -> + ) { _, item -> ChatroomCompose( item, accountViewModel = accountViewModel, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt index ea412e861..aed87aaee 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt @@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.ui.screen import androidx.compose.animation.Crossfade import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxHeight @@ -11,10 +12,13 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.Button +import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.OutlinedButton import androidx.compose.material.Text +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -25,12 +29,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.navigation.NavController -import com.google.accompanist.swiperefresh.SwipeRefresh -import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +@OptIn(ExperimentalMaterialApi::class) @Composable fun FeedView( viewModel: FeedViewModel, @@ -41,36 +44,31 @@ fun FeedView( ) { val feedState by viewModel.feedContent.collectAsState() - var isRefreshing by remember { mutableStateOf(false) } - val swipeRefreshState = rememberSwipeRefreshState(isRefreshing) + var refreshing by remember { mutableStateOf(false) } + val refresh = { refreshing = true; viewModel.refresh(); refreshing = false } + val pullRefreshState = rememberPullRefreshState(refreshing, onRefresh = refresh) - LaunchedEffect(isRefreshing) { - if (isRefreshing) { - viewModel.refresh() - isRefreshing = false - } - } - - SwipeRefresh( - state = swipeRefreshState, - onRefresh = { - isRefreshing = true - } - ) { - Column() { - Crossfade(targetState = feedState, animationSpec = tween(durationMillis = 100)) { state -> + Box(Modifier.pullRefresh(pullRefreshState)) { + Column { + Crossfade( + targetState = feedState, + animationSpec = tween(durationMillis = 100) + ) { state -> when (state) { is FeedState.Empty -> { FeedEmpty { - isRefreshing = true + refreshing = true } } + is FeedState.FeedError -> { FeedError(state.errorMessage) { - isRefreshing = true + refreshing = true } } + is FeedState.Loaded -> { + refreshing = false FeedLoaded( state, routeForLastRead, @@ -79,12 +77,15 @@ fun FeedView( scrollStateKey ) } + is FeedState.Loading -> { LoadingFeed() } } } } + + PullRefreshIndicator(refreshing, pullRefreshState, Modifier.align(Alignment.TopCenter)) } } @@ -109,7 +110,7 @@ private fun FeedLoaded( ), state = listState ) { - itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { index, item -> + itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { _, item -> NoteCompose( item, isBoostedNote = false, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LnZapFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LnZapFeedView.kt index 4570bef66..d4d8767bd 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LnZapFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LnZapFeedView.kt @@ -2,59 +2,54 @@ package com.vitorpamplona.amethyst.ui.screen import androidx.compose.animation.Crossfade import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState 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.unit.dp import androidx.navigation.NavController -import com.google.accompanist.swiperefresh.SwipeRefresh -import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import com.vitorpamplona.amethyst.ui.note.ZapNoteCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +@OptIn(ExperimentalMaterialApi::class) @Composable fun LnZapFeedView(viewModel: LnZapFeedViewModel, accountViewModel: AccountViewModel, navController: NavController) { val feedState by viewModel.feedContent.collectAsState() - var isRefreshing by remember { mutableStateOf(false) } - val swipeRefreshState = rememberSwipeRefreshState(isRefreshing) + var refreshing by remember { mutableStateOf(false) } + val refresh = { refreshing = true; viewModel.refresh(); refreshing = false } + val pullRefreshState = rememberPullRefreshState(refreshing, onRefresh = refresh) - LaunchedEffect(isRefreshing) { - if (isRefreshing) { - viewModel.refresh() - isRefreshing = false - } - } - - SwipeRefresh( - state = swipeRefreshState, - onRefresh = { - isRefreshing = true - } - ) { + Box(Modifier.pullRefresh(pullRefreshState)) { Column() { Crossfade(targetState = feedState, animationSpec = tween(durationMillis = 100)) { state -> when (state) { is LnZapFeedState.Empty -> { FeedEmpty { - isRefreshing = true + refreshing = true } } is LnZapFeedState.FeedError -> { FeedError(state.errorMessage) { - isRefreshing = true + refreshing = true } } is LnZapFeedState.Loaded -> { + refreshing = false LnZapFeedLoaded(state, accountViewModel, navController) } is LnZapFeedState.Loading -> { @@ -63,6 +58,8 @@ fun LnZapFeedView(viewModel: LnZapFeedViewModel, accountViewModel: AccountViewMo } } } + + PullRefreshIndicator(refreshing, pullRefreshState, Modifier.align(Alignment.TopCenter)) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/RelayFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/RelayFeedView.kt index 2461b982f..2b27ef43c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/RelayFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/RelayFeedView.kt @@ -1,24 +1,28 @@ package com.vitorpamplona.amethyst.ui.screen +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState 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.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation.NavController -import com.google.accompanist.swiperefresh.SwipeRefresh -import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import com.vitorpamplona.amethyst.model.RelayInfo import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.UserState @@ -101,6 +105,7 @@ class RelayFeedViewModel : ViewModel() { } } +@OptIn(ExperimentalMaterialApi::class) @Composable fun RelayFeedView(viewModel: RelayFeedViewModel, accountViewModel: AccountViewModel, navController: NavController) { val accountState by accountViewModel.accountLiveData.observeAsState() @@ -108,9 +113,6 @@ fun RelayFeedView(viewModel: RelayFeedViewModel, accountViewModel: AccountViewMo val feedState by viewModel.feedContent.collectAsState() - var isRefreshing by remember { mutableStateOf(false) } - val swipeRefreshState = rememberSwipeRefreshState(isRefreshing) - var wantsToAddRelay by remember { mutableStateOf("") } @@ -119,19 +121,11 @@ fun RelayFeedView(viewModel: RelayFeedViewModel, accountViewModel: AccountViewMo NewRelayListView({ wantsToAddRelay = "" }, account, wantsToAddRelay) } - LaunchedEffect(isRefreshing) { - if (isRefreshing) { - viewModel.refresh() - isRefreshing = false - } - } + var refreshing by remember { mutableStateOf(false) } + val refresh = { refreshing = true; viewModel.refresh(); refreshing = false } + val pullRefreshState = rememberPullRefreshState(refreshing, onRefresh = refresh) - SwipeRefresh( - state = swipeRefreshState, - onRefresh = { - isRefreshing = true - } - ) { + Box(Modifier.pullRefresh(pullRefreshState)) { Column() { val listState = rememberLazyListState() @@ -153,5 +147,7 @@ fun RelayFeedView(viewModel: RelayFeedViewModel, accountViewModel: AccountViewMo } } } + + PullRefreshIndicator(refreshing, pullRefreshState, Modifier.align(Alignment.TopCenter)) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt index aeffef514..d9a5a667b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt @@ -5,6 +5,7 @@ import androidx.compose.animation.core.tween import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row @@ -16,12 +17,16 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.Divider +import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -44,8 +49,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavController import coil.compose.AsyncImage -import com.google.accompanist.swiperefresh.SwipeRefresh -import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.model.BadgeDefinitionEvent @@ -65,42 +68,33 @@ import com.vitorpamplona.amethyst.ui.note.timeAgo import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import kotlinx.coroutines.delay +@OptIn(ExperimentalMaterialApi::class) @Composable fun ThreadFeedView(noteId: String, viewModel: FeedViewModel, accountViewModel: AccountViewModel, navController: NavController) { val feedState by viewModel.feedContent.collectAsState() - var isRefreshing by remember { mutableStateOf(false) } - val swipeRefreshState = rememberSwipeRefreshState(isRefreshing) - val listState = rememberLazyListState() - LaunchedEffect(isRefreshing) { - if (isRefreshing) { - viewModel.refresh() - isRefreshing = false - } - } + var refreshing by remember { mutableStateOf(false) } + val refresh = { refreshing = true; viewModel.refresh(); refreshing = false } + val pullRefreshState = rememberPullRefreshState(refreshing, onRefresh = refresh) - SwipeRefresh( - state = swipeRefreshState, - onRefresh = { - isRefreshing = true - } - ) { + Box(Modifier.pullRefresh(pullRefreshState)) { Column() { Crossfade(targetState = feedState, animationSpec = tween(durationMillis = 100)) { state -> when (state) { is FeedState.Empty -> { FeedEmpty { - isRefreshing = true + refreshing = true } } is FeedState.FeedError -> { FeedError(state.errorMessage) { - isRefreshing = true + refreshing = true } } is FeedState.Loaded -> { + refreshing = false LaunchedEffect(noteId) { // waits to load the thread to scroll to item. delay(100) @@ -163,6 +157,8 @@ fun ThreadFeedView(noteId: String, viewModel: FeedViewModel, accountViewModel: A } } } + + PullRefreshIndicator(refreshing, pullRefreshState, Modifier.align(Alignment.TopCenter)) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedView.kt index ed9b04b7c..3452677c8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedView.kt @@ -2,59 +2,54 @@ package com.vitorpamplona.amethyst.ui.screen import androidx.compose.animation.Crossfade import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState 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.unit.dp import androidx.navigation.NavController -import com.google.accompanist.swiperefresh.SwipeRefresh -import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import com.vitorpamplona.amethyst.ui.note.UserCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +@OptIn(ExperimentalMaterialApi::class) @Composable fun UserFeedView(viewModel: UserFeedViewModel, accountViewModel: AccountViewModel, navController: NavController) { val feedState by viewModel.feedContent.collectAsState() - var isRefreshing by remember { mutableStateOf(false) } - val swipeRefreshState = rememberSwipeRefreshState(isRefreshing) + var refreshing by remember { mutableStateOf(false) } + val refresh = { refreshing = true; viewModel.refresh(); refreshing = false } + val pullRefreshState = rememberPullRefreshState(refreshing, onRefresh = refresh) - LaunchedEffect(isRefreshing) { - if (isRefreshing) { - viewModel.refresh() - isRefreshing = false - } - } - - SwipeRefresh( - state = swipeRefreshState, - onRefresh = { - isRefreshing = true - } - ) { + Box(Modifier.pullRefresh(pullRefreshState)) { Column() { Crossfade(targetState = feedState, animationSpec = tween(durationMillis = 100)) { state -> when (state) { is UserFeedState.Empty -> { FeedEmpty { - isRefreshing = true + refreshing = true } } is UserFeedState.FeedError -> { FeedError(state.errorMessage) { - isRefreshing = true + refreshing = true } } is UserFeedState.Loaded -> { + refreshing = false FeedLoaded(state, accountViewModel, navController) } is UserFeedState.Loading -> { @@ -63,6 +58,8 @@ fun UserFeedView(viewModel: UserFeedViewModel, accountViewModel: AccountViewMode } } } + + PullRefreshIndicator(refreshing, pullRefreshState, Modifier.align(Alignment.TopCenter)) } } From 4fa584ca1a3551b9d3b552b20189ca085b2eef96 Mon Sep 17 00:00:00 2001 From: maxmoney21m Date: Fri, 10 Mar 2023 11:14:32 +0800 Subject: [PATCH 26/45] Remove swiperefresh dependency --- app/build.gradle | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 0477fed22..b395bd09d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -80,9 +80,6 @@ dependencies { // Biometrics implementation "androidx.biometric:biometric-ktx:1.2.0-alpha05" - // Swipe Refresh - implementation 'com.google.accompanist:accompanist-swiperefresh:0.29.1-alpha' - // Bitcoin secp256k1 bindings to Android implementation 'fr.acinq.secp256k1:secp256k1-kmp-jni-android:0.7.1' From 90147ce557a8948330ea9e33ace33f9bf10aaaf2 Mon Sep 17 00:00:00 2001 From: maxmoney21m Date: Sat, 11 Mar 2023 00:59:18 +0800 Subject: [PATCH 27/45] Allow tapping nav icon to refresh & scroll to top --- .../amethyst/ui/navigation/AppBottomBar.kt | 15 ++++++------ .../amethyst/ui/navigation/Routes.kt | 23 +++++++++++++++---- .../amethyst/ui/screen/FeedView.kt | 18 +++++++++++---- .../amethyst/ui/screen/LazyListState.kt | 7 ++++++ .../amethyst/ui/screen/loggedIn/HomeScreen.kt | 7 +++--- .../amethyst/ui/screen/loggedIn/MainScreen.kt | 4 ++-- .../ui/screen/loggedIn/SearchScreen.kt | 5 ++-- 7 files changed, 56 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt index f59437670..52c54034c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt @@ -83,7 +83,7 @@ fun keyboardAsState(): State { @Composable fun AppBottomBar(navController: NavHostController, accountViewModel: AccountViewModel) { - val currentRoute = currentRoute(navController) + val currentRoute = currentRoute(navController)?.substringBefore("?") val coroutineScope = rememberCoroutineScope() val isKeyboardOpen by keyboardAsState() @@ -101,10 +101,10 @@ fun AppBottomBar(navController: NavHostController, accountViewModel: AccountView bottomNavigationItems.forEach { item -> BottomNavigationItem( icon = { NotifiableIcon(item, currentRoute, accountViewModel) }, - selected = currentRoute == item.route, + selected = currentRoute == item.base, onClick = { coroutineScope.launch { - if (currentRoute != item.route) { + if (currentRoute != item.base) { navController.navigate(item.route) { navController.graph.startDestinationRoute?.let { start -> popUpTo(start) @@ -114,8 +114,7 @@ fun AppBottomBar(navController: NavHostController, accountViewModel: AccountView restoreState = true } } else { - // TODO: Make it scrool to the top - navController.navigate(item.route) { + navController.navigate("${item.base}?forceRefresh=${true}") { navController.graph.startDestinationRoute?.let { start -> popUpTo(start) { inclusive = item.route == Route.Home.route } restoreState = true @@ -136,12 +135,12 @@ fun AppBottomBar(navController: NavHostController, accountViewModel: AccountView @Composable private fun NotifiableIcon(item: Route, currentRoute: String?, accountViewModel: AccountViewModel) { - Box(Modifier.size(if ("Home" == item.route) 25.dp else 23.dp)) { + Box(Modifier.size(if ("Home" == item.base) 25.dp else 23.dp)) { Icon( painter = painterResource(id = item.icon), null, - modifier = Modifier.size(if ("Home" == item.route) 24.dp else 20.dp), - tint = if (currentRoute == item.route) MaterialTheme.colors.primary else Color.Unspecified + modifier = Modifier.size(if ("Home" == item.base) 24.dp else 20.dp), + tint = if (currentRoute == item.base) MaterialTheme.colors.primary else Color.Unspecified ) val accountState by accountViewModel.accountLiveData.observeAsState() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt index 66bed57fa..69b7ecba8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt @@ -35,17 +35,32 @@ sealed class Route( val arguments: List = emptyList(), val buildScreen: (AccountViewModel, AccountStateViewModel, NavController) -> @Composable (NavBackStackEntry) -> Unit ) { + val base: String + get() = route.substringBefore("?") + object Home : Route( - "Home", + "Home?forceRefresh={forceRefresh}", R.drawable.ic_home, + arguments = listOf(navArgument("forceRefresh") { type = NavType.BoolType; defaultValue = false }), hasNewItems = { acc, cache, ctx -> homeHasNewItems(acc, cache, ctx) }, - buildScreen = { acc, accSt, nav -> { _ -> HomeScreen(acc, nav) } } + buildScreen = { acc, accSt, nav -> + { backStackEntry -> + HomeScreen(acc, nav, backStackEntry.arguments?.getBoolean("forceRefresh", false)) + } + } ) + object Search : Route( - "Search", + "Search?forceRefresh={forceRefresh}", R.drawable.ic_globe, - buildScreen = { acc, accSt, nav -> { _ -> SearchScreen(acc, nav) } } + arguments = listOf(navArgument("forceRefresh") { type = NavType.BoolType; defaultValue = false }), + buildScreen = { acc, accSt, nav -> + { backStackEntry -> + SearchScreen(acc, nav, backStackEntry.arguments?.getBoolean("forceRefresh", false)) + } + } ) + object Notification : Route( "Notification", R.drawable.ic_notifications, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt index aed87aaee..bca089f1e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt @@ -19,6 +19,7 @@ import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -40,11 +41,12 @@ fun FeedView( accountViewModel: AccountViewModel, navController: NavController, routeForLastRead: String?, - scrollStateKey: String? = null + scrollStateKey: String? = null, + forceRefresh: Boolean? = false ) { val feedState by viewModel.feedContent.collectAsState() - var refreshing by remember { mutableStateOf(false) } + var refreshing by remember { mutableStateOf(forceRefresh!!) } val refresh = { refreshing = true; viewModel.refresh(); refreshing = false } val pullRefreshState = rememberPullRefreshState(refreshing, onRefresh = refresh) @@ -74,7 +76,8 @@ fun FeedView( routeForLastRead, accountViewModel, navController, - scrollStateKey + scrollStateKey, + forceRefresh!! ) } @@ -95,7 +98,8 @@ private fun FeedLoaded( routeForLastRead: String?, accountViewModel: AccountViewModel, navController: NavController, - scrollStateKey: String? + scrollStateKey: String?, + forceRefresh: Boolean = false ) { val listState = if (scrollStateKey != null) { rememberForeverLazyListState(scrollStateKey) @@ -103,6 +107,12 @@ private fun FeedLoaded( rememberLazyListState() } + if (forceRefresh) { + LaunchedEffect(Unit) { + listState.animateScrollToItem(0) + } + } + LazyColumn( contentPadding = PaddingValues( top = 10.dp, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LazyListState.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LazyListState.kt index e6eb0c39e..0a4a2d5cf 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LazyListState.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LazyListState.kt @@ -4,10 +4,17 @@ import androidx.compose.foundation.lazy.LazyListState import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.saveable.rememberSaveable +import com.vitorpamplona.amethyst.ui.navigation.Route private val savedScrollStates = mutableMapOf() private data class ScrollState(val index: Int, val scrollOffset: Int) +object ScrollStateKeys { + const val GLOBAL_SCREEN = "Global" + val HOME_FOLLOWS = Route.Home.base + "Follows" + val HOME_REPLIES = Route.Home.base + "FollowsReplies" +} + @Composable fun rememberForeverLazyListState( key: String, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt index b5a8e35aa..0e80382ec 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt @@ -34,11 +34,12 @@ import com.vitorpamplona.amethyst.ui.navigation.Route import com.vitorpamplona.amethyst.ui.screen.FeedView import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrHomeRepliesFeedViewModel +import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys import kotlinx.coroutines.launch @OptIn(ExperimentalPagerApi::class) @Composable -fun HomeScreen(accountViewModel: AccountViewModel, navController: NavController) { +fun HomeScreen(accountViewModel: AccountViewModel, navController: NavController, forceRefresh: Boolean? = false) { val accountState by accountViewModel.accountLiveData.observeAsState() val account = accountState?.account ?: return @@ -106,8 +107,8 @@ fun HomeScreen(accountViewModel: AccountViewModel, navController: NavController) } HorizontalPager(count = 2, state = pagerState) { when (pagerState.currentPage) { - 0 -> FeedView(feedViewModel, accountViewModel, navController, Route.Home.route + "Follows", Route.Home.route + "Follows") - 1 -> FeedView(feedViewModelReplies, accountViewModel, navController, Route.Home.route + "FollowsReplies", Route.Home.route + "FollowsReplies") + 0 -> FeedView(feedViewModel, accountViewModel, navController, Route.Home.base + "Follows", ScrollStateKeys.HOME_FOLLOWS, forceRefresh) + 1 -> FeedView(feedViewModelReplies, accountViewModel, navController, Route.Home.base + "FollowsReplies", ScrollStateKeys.HOME_REPLIES) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt index fa698b275..62345d678 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt @@ -61,7 +61,7 @@ fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: Accoun fun FloatingButton(navController: NavHostController, accountViewModel: AccountStateViewModel) { val accountState by accountViewModel.accountContent.collectAsState() - if (currentRoute(navController) == Route.Home.route) { + if (currentRoute(navController)?.substringBefore("?") == Route.Home.base) { Crossfade(targetState = accountState, animationSpec = tween(durationMillis = 100)) { state -> when (state) { is AccountState.LoggedInViewOnly -> { @@ -77,7 +77,7 @@ fun FloatingButton(navController: NavHostController, accountViewModel: AccountSt } } - if (currentRoute(navController) == Route.Message.route) { + if (currentRoute(navController) == Route.Message.base) { Crossfade(targetState = accountState, animationSpec = tween(durationMillis = 100)) { state -> when (state) { is AccountState.LoggedInViewOnly -> { 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 b9000e89b..f74e4f553 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 @@ -66,6 +66,7 @@ import com.vitorpamplona.amethyst.ui.note.UserPicture import com.vitorpamplona.amethyst.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.ui.screen.FeedView import com.vitorpamplona.amethyst.ui.screen.NostrGlobalFeedViewModel +import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.collectLatest @@ -78,7 +79,7 @@ import kotlinx.coroutines.withContext import kotlinx.coroutines.channels.Channel as CoroutineChannel @Composable -fun SearchScreen(accountViewModel: AccountViewModel, navController: NavController) { +fun SearchScreen(accountViewModel: AccountViewModel, navController: NavController, forceRefresh: Boolean? = false) { val accountState by accountViewModel.accountLiveData.observeAsState() val account = accountState?.account ?: return @@ -114,7 +115,7 @@ fun SearchScreen(accountViewModel: AccountViewModel, navController: NavControlle modifier = Modifier.padding(vertical = 0.dp) ) { SearchBar(accountViewModel, navController) - FeedView(feedViewModel, accountViewModel, navController, null, "Global") + FeedView(feedViewModel, accountViewModel, navController, null, ScrollStateKeys.GLOBAL_SCREEN, forceRefresh) } } } From 2fea7dac6272b6963867d6ec0f105d2e6832d380 Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Sun, 12 Mar 2023 18:42:17 +0100 Subject: [PATCH 28/45] Refactor LocalCache.isValidHexNpub() --- .../amethyst/model/LocalCache.kt | 68 ++++++------------- 1 file changed, 19 insertions(+), 49 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index d6fdfcdb9..7d1375df9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -5,37 +5,10 @@ import androidx.lifecycle.LiveData import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.google.gson.reflect.TypeToken -import com.vitorpamplona.amethyst.service.model.ATag -import com.vitorpamplona.amethyst.service.model.BadgeAwardEvent -import com.vitorpamplona.amethyst.service.model.BadgeDefinitionEvent -import com.vitorpamplona.amethyst.service.model.BadgeProfilesEvent -import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent -import com.vitorpamplona.amethyst.service.model.ChannelHideMessageEvent -import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent -import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent -import com.vitorpamplona.amethyst.service.model.ChannelMuteUserEvent -import com.vitorpamplona.amethyst.service.model.ContactListEvent -import com.vitorpamplona.amethyst.service.model.DeletionEvent -import com.vitorpamplona.amethyst.service.model.Event -import com.vitorpamplona.amethyst.service.model.LnZapEvent -import com.vitorpamplona.amethyst.service.model.LnZapRequestEvent -import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent -import com.vitorpamplona.amethyst.service.model.MetadataEvent -import com.vitorpamplona.amethyst.service.model.PrivateDmEvent -import com.vitorpamplona.amethyst.service.model.ReactionEvent -import com.vitorpamplona.amethyst.service.model.RecommendRelayEvent -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.service.model.* import com.vitorpamplona.amethyst.service.relays.Relay import fr.acinq.secp256k1.Hex -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.NonCancellable -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +import kotlinx.coroutines.* import nostr.postr.toNpub import java.io.ByteArrayInputStream import java.time.Instant @@ -57,13 +30,10 @@ object LocalCache { val addressables = ConcurrentHashMap() fun checkGetOrCreateUser(key: String): User? { - return try { - checkIfValidHex(key) - getOrCreateUser(key) - } catch (e: IllegalArgumentException) { - Log.e("LocalCache", "Invalid Key to create user: $key", e) - null + if (isValidHexNpub(key)) { + return getOrCreateUser(key) } + return null } @Synchronized @@ -79,13 +49,10 @@ object LocalCache { if (ATag.isATag(key)) { return checkGetOrCreateAddressableNote(key) } - return try { - checkIfValidHex(key) - getOrCreateNote(key) - } catch (e: IllegalArgumentException) { - Log.e("LocalCache", "Invalid Key to create note: $key", e) - null + if (isValidHexNpub(key)) { + return getOrCreateNote(key) } + return null } @Synchronized @@ -98,17 +65,20 @@ object LocalCache { } fun checkGetOrCreateChannel(key: String): Channel? { - return try { - checkIfValidHex(key) - getOrCreateChannel(key) - } catch (e: IllegalArgumentException) { - Log.e("LocalCache", "Invalid Key to create channel: $key", e) - null + if (isValidHexNpub(key)) { + return getOrCreateChannel(key) } + return null } - private fun checkIfValidHex(key: String) { - Hex.decode(key).toNpub() + private fun isValidHexNpub(key: String): Boolean { + return try { + Hex.decode(key).toNpub() + true + } catch (e: IllegalArgumentException) { + Log.e("LocalCache", "Invalid Key to create user: $key", e) + false + } } @Synchronized From 260ed293091b1653aee03bf45b73be76c41dfbd9 Mon Sep 17 00:00:00 2001 From: matata <127629040+matata2140@users.noreply.github.com> Date: Mon, 13 Mar 2023 00:12:00 +0100 Subject: [PATCH 29/45] Update strings.xml I wasn't sure about translating 'channel' so I left them as is, but: If 'channel' refers to 'dm group-chat' then go on and replace all 'kanaal' with 'groep' --- app/src/main/res/values-nl/strings.xml | 66 +++++++++++++------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index fdb575b91..bdd90f7bb 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -1,17 +1,17 @@ Amethyst Amethyst debug - Richt naar de QR-Code - QR tonen - Profielafbeelding + Richt camera op de QR-Code + Toon QR + Profielfoto Scan QR - Laat evengoed zien - Bericht was gemarkeerd als ongepast door + Laat toch zien + Bericht gemarkeerd als ongepast door Bericht niet gevonden - Kanaalafbeelding + Groepsafbeelding Verwezen event niet gevonden - Kon het bericht niet ontcijferen - Groepafbeelding + Note versleuteld met encryptie + Kanaal-afbeelding Expliciete inhoud Spam Imitatie @@ -23,7 +23,7 @@ Kopieer auteur ID Kopieer note ID Verzenden - + Meld spam / scam Meld imitatie Rapporteer expliciete inhoud @@ -34,20 +34,20 @@ Geen Zap bedrag. Houdt ingedrukt om te veranderen Login met een privésleutel om Zaps te versturen Zaps - Bekijk telling + Aantal keer bekeken Boost boosted Quote Nieuw bedrag in sats Toevoegen - "reageren op " + "reageert op " " en " "in kanaal " Profielbanner " Volgend" " Volgers" Profiel - Beveiligingsfilters + Veiligheidsfilter Uitloggen Meer Lightning invoice @@ -57,8 +57,8 @@ Hartelijk bedankt! Bedrag in sats Verstuur sats - "Nooit vertalen van " - "Foutieve parsing preview voor %1$s : %2$s" + "Nooit vertalen vanuit " + "Error parsing preview voor %1$s : %2$s" "Voorbeeld kaartafbeelding voor %1$s" Nieuw kanaal Kanaalnaam @@ -71,33 +71,33 @@ Opslaan Maken Annuleren - Het uploaden van de afbeelding is mislukt - Relay addres + Uploaden afbeelding mislukt + Relay adres Berichten Errors - Beginfeed + Startpagina Privéberichten Publieke chats Globale feed Zoeken - AVoeg een relay toe + Voeg relay toe Naam Mijn naam Gebruikersnaam Mijn gebruikersnaam Over mij - Avatar URL + Profielfoto URL Banner URL Website URL LN Address LN URL (verouderd) - Afbeelding opgeslagen in de galerij + Afbeelding opgeslagen in galerij De afbeelding is niet opgeslagen Afbeelding uploaden Uploaden… - De gebruiker heeft geen Lightning Adress ingesteld om sats te ontvangen + Gebruiker heeft geen Lightning Adress ingesteld om sats te ontvangen "hier reageren.. " - Kopieert de Notitie ID naar klembord om te delen + Kopieert Notitie ID naar klembord om te delen Kopieer kanaal ID (Notitie) naar klembord Past de kanaal-metadata aan Lid worden @@ -111,18 +111,18 @@ "Volgend" "Rapporten" Meer opties - " Relays" + "Relays" Website Lightning Address - Kopieert het NSec ID (uw wachtwoord) naar het klembord voor back-up. + Kopieert het NSEC ID (uw wachtwoord) naar klembord voor back-up. Privésleutel kopiëren naar het klembord - Kopieert de publieke sleutel naar het klembord om te delen - Kopieer publieke sleutel (NPub) naar het klembord + Kopieert de publieke sleutel naar klembord om te delen + Kopieer publieke sleutel (NPUB) naar klembord Stuur een privébericht - Bewerkt de metagegevens van de gebruiker + Bewerkt de metadata van de gebruiker Volgen Deblokkeren - Kopieer gebruikers ID + Kopieer gebruiker ID Deblokkeer gebruiker "npub, hex, gebruikersnaam " Wissen @@ -137,12 +137,12 @@ Sleutel is vereist Inloggen Genereer een nieuwe sleutel - Feed laden + Feed laden… "Foutmelding bij het laden reacties: " Opnieuw proberen Feed is leeg. - Verversen - gemaakt + verversen + gecreëerd met beschrijving van en afbeelding heeft chatnaam veranderd naar @@ -154,7 +154,7 @@ "Kanaalinformatie veranderd naar" Publieke chat berichten ontvangen - Verwijderen + verwijderen sats Auto vertaald van @@ -177,7 +177,7 @@ Alle bekende als gelezen markeren Alle nieuwe als gelezen markeren Alles als gelezen markeren - Backup sleutels + Back-up sleutels ## Sleutel back-up en veiligheidstips \n\nUw account is beveiligd met een privésleutel. De sleutel is een lange, willekeurige reeks die begint met **nsec1**. Iedereen die toegang heeft tot uw privésleutel kan inhoud publiceren met uw identiteit. From e4cec20d1a2b8a388d36f5979a88e4dc3756d715 Mon Sep 17 00:00:00 2001 From: maxmoney21m Date: Mon, 13 Mar 2023 13:07:01 +0800 Subject: [PATCH 30/45] Make forceRefresh non-null --- .../java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt | 4 ++-- .../java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt | 6 +++--- .../vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt | 2 +- .../amethyst/ui/screen/loggedIn/SearchScreen.kt | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt index e94e29581..d84b0b1cc 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt @@ -45,7 +45,7 @@ sealed class Route( hasNewItems = { accountViewModel, cache, context -> homeHasNewItems(accountViewModel, cache, context) }, buildScreen = { accountViewModel, _, navController -> { backStackEntry -> - HomeScreen(accountViewModel, navController, backStackEntry.arguments?.getBoolean("forceRefresh", false)) + HomeScreen(accountViewModel, navController, backStackEntry.arguments?.getBoolean("forceRefresh", false) ?: false) } } ) @@ -56,7 +56,7 @@ sealed class Route( arguments = listOf(navArgument("forceRefresh") { type = NavType.BoolType; defaultValue = false }), buildScreen = { acc, _, navController -> { backStackEntry -> - SearchScreen(acc, navController, backStackEntry.arguments?.getBoolean("forceRefresh", false)) + SearchScreen(acc, navController, backStackEntry.arguments?.getBoolean("forceRefresh", false) ?: false) } } ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt index bca089f1e..af98fe35c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt @@ -42,11 +42,11 @@ fun FeedView( navController: NavController, routeForLastRead: String?, scrollStateKey: String? = null, - forceRefresh: Boolean? = false + forceRefresh: Boolean = false ) { val feedState by viewModel.feedContent.collectAsState() - var refreshing by remember { mutableStateOf(forceRefresh!!) } + var refreshing by remember { mutableStateOf(forceRefresh) } val refresh = { refreshing = true; viewModel.refresh(); refreshing = false } val pullRefreshState = rememberPullRefreshState(refreshing, onRefresh = refresh) @@ -77,7 +77,7 @@ fun FeedView( accountViewModel, navController, scrollStateKey, - forceRefresh!! + forceRefresh ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt index 0e80382ec..203684542 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt @@ -39,7 +39,7 @@ import kotlinx.coroutines.launch @OptIn(ExperimentalPagerApi::class) @Composable -fun HomeScreen(accountViewModel: AccountViewModel, navController: NavController, forceRefresh: Boolean? = false) { +fun HomeScreen(accountViewModel: AccountViewModel, navController: NavController, forceRefresh: Boolean = false) { val accountState by accountViewModel.accountLiveData.observeAsState() val account = accountState?.account ?: return 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 f74e4f553..c4dbfaab3 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 @@ -79,7 +79,7 @@ import kotlinx.coroutines.withContext import kotlinx.coroutines.channels.Channel as CoroutineChannel @Composable -fun SearchScreen(accountViewModel: AccountViewModel, navController: NavController, forceRefresh: Boolean? = false) { +fun SearchScreen(accountViewModel: AccountViewModel, navController: NavController, forceRefresh: Boolean = false) { val accountState by accountViewModel.accountLiveData.observeAsState() val account = accountState?.account ?: return From 85d722f96d8ed1183ad45a9ff22677118da84753 Mon Sep 17 00:00:00 2001 From: maxmoney21m Date: Mon, 13 Mar 2023 19:03:15 +0800 Subject: [PATCH 31/45] Hoist globalfeed state out of Screen component --- .../amethyst/ui/navigation/AppBottomBar.kt | 31 ++++++++++--------- .../amethyst/ui/navigation/AppNavigation.kt | 27 +++++++++++++++- .../amethyst/ui/navigation/Routes.kt | 24 +++++--------- .../amethyst/ui/screen/FeedView.kt | 12 +++---- .../amethyst/ui/screen/loggedIn/HomeScreen.kt | 4 +-- .../ui/screen/loggedIn/SearchScreen.kt | 18 +++++------ 6 files changed, 67 insertions(+), 49 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt index 52c54034c..5d68ddba0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt @@ -83,9 +83,9 @@ fun keyboardAsState(): State { @Composable fun AppBottomBar(navController: NavHostController, accountViewModel: AccountViewModel) { - val currentRoute = currentRoute(navController)?.substringBefore("?") + val currentRoute = currentRoute(navController) + val currentRouteBase = currentRoute?.substringBefore("?") val coroutineScope = rememberCoroutineScope() - val isKeyboardOpen by keyboardAsState() if (isKeyboardOpen == Keyboard.Closed) { @@ -99,13 +99,15 @@ fun AppBottomBar(navController: NavHostController, accountViewModel: AccountView backgroundColor = MaterialTheme.colors.background ) { bottomNavigationItems.forEach { item -> + val selected = currentRouteBase == item.base + BottomNavigationItem( - icon = { NotifiableIcon(item, currentRoute, accountViewModel) }, - selected = currentRoute == item.base, + icon = { NotifiableIcon(item, selected, accountViewModel) }, + selected = selected, onClick = { coroutineScope.launch { - if (currentRoute != item.base) { - navController.navigate(item.route) { + if (currentRouteBase != item.base) { + navController.navigate(item.base) { navController.graph.startDestinationRoute?.let { start -> popUpTo(start) restoreState = true @@ -114,7 +116,8 @@ fun AppBottomBar(navController: NavHostController, accountViewModel: AccountView restoreState = true } } else { - navController.navigate("${item.base}?forceRefresh=${true}") { + val route = currentRoute.replace("{scrollToTop}", "true") + navController.navigate(route) { navController.graph.startDestinationRoute?.let { start -> popUpTo(start) { inclusive = item.route == Route.Home.route } restoreState = true @@ -134,13 +137,13 @@ fun AppBottomBar(navController: NavHostController, accountViewModel: AccountView } @Composable -private fun NotifiableIcon(item: Route, currentRoute: String?, accountViewModel: AccountViewModel) { - Box(Modifier.size(if ("Home" == item.base) 25.dp else 23.dp)) { +private fun NotifiableIcon(route: Route, selected: Boolean, accountViewModel: AccountViewModel) { + Box(Modifier.size(if ("Home" == route.base) 25.dp else 23.dp)) { Icon( - painter = painterResource(id = item.icon), + painter = painterResource(id = route.icon), null, - modifier = Modifier.size(if ("Home" == item.base) 24.dp else 20.dp), - tint = if (currentRoute == item.base) MaterialTheme.colors.primary else Color.Unspecified + modifier = Modifier.size(if ("Home" == route.base) 24.dp else 20.dp), + tint = if (selected) MaterialTheme.colors.primary else Color.Unspecified ) val accountState by accountViewModel.accountLiveData.observeAsState() @@ -159,13 +162,13 @@ private fun NotifiableIcon(item: Route, currentRoute: String?, accountViewModel: LaunchedEffect(key1 = notif) { withContext(Dispatchers.IO) { - hasNewItems = item.hasNewItems(account, notif.cache, context) + hasNewItems = route.hasNewItems(account, notif.cache, context) } } LaunchedEffect(key1 = db) { withContext(Dispatchers.IO) { - hasNewItems = item.hasNewItems(account, notif.cache, context) + hasNewItems = route.hasNewItems(account, notif.cache, context) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index 76e7164ed..27d2b4b16 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -1,11 +1,17 @@ package com.vitorpamplona.amethyst.ui.navigation import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable +import com.vitorpamplona.amethyst.ui.dal.GlobalFeedFilter import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel +import com.vitorpamplona.amethyst.ui.screen.NostrGlobalFeedViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.SearchScreen @Composable fun AppNavigation( @@ -14,9 +20,28 @@ fun AppNavigation( accountStateViewModel: AccountStateViewModel, nextPage: String? = null ) { + val accountState by accountViewModel.accountLiveData.observeAsState() + val account = accountState?.account ?: return + + GlobalFeedFilter.account = account + val globalFeedViewModel: NostrGlobalFeedViewModel = viewModel() + NavHost(navController, startDestination = Route.Home.route) { + Route.Search.let { route -> + composable(route.route, route.arguments, content = { + SearchScreen( + accountViewModel = accountViewModel, + feedViewModel = globalFeedViewModel, + navController = navController, + scrollToTop = it.arguments?.getBoolean("scrollToTop") ?: false + ) + }) + } + Routes.forEach { - composable(it.route, it.arguments, content = it.buildScreen(accountViewModel, accountStateViewModel, navController)) + it.buildScreen?.let { fn -> + composable(it.route, it.arguments, content = fn(accountViewModel, accountStateViewModel, navController)) + } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt index d84b0b1cc..ec7b30a00 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt @@ -25,7 +25,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.FiltersScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.HomeScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.NotificationScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.ProfileScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.SearchScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.ThreadScreen sealed class Route( @@ -33,32 +32,25 @@ sealed class Route( val icon: Int, val hasNewItems: (Account, NotificationCache, Context) -> Boolean = { _, _, _ -> false }, val arguments: List = emptyList(), - val buildScreen: (AccountViewModel, AccountStateViewModel, NavController) -> @Composable (NavBackStackEntry) -> Unit + val buildScreen: ((AccountViewModel, AccountStateViewModel, NavController) -> @Composable (NavBackStackEntry) -> Unit)? = null ) { val base: String get() = route.substringBefore("?") object Home : Route( - "Home?forceRefresh={forceRefresh}", + "Home?scrollToTop={scrollToTop}", R.drawable.ic_home, - arguments = listOf(navArgument("forceRefresh") { type = NavType.BoolType; defaultValue = false }), + arguments = listOf(navArgument("scrollToTop") { type = NavType.BoolType; defaultValue = false }), hasNewItems = { accountViewModel, cache, context -> homeHasNewItems(accountViewModel, cache, context) }, buildScreen = { accountViewModel, _, navController -> - { backStackEntry -> - HomeScreen(accountViewModel, navController, backStackEntry.arguments?.getBoolean("forceRefresh", false) ?: false) - } + { HomeScreen(accountViewModel, navController, it.arguments?.getBoolean("scrollToTop", false) ?: false) } } ) object Search : Route( - "Search?forceRefresh={forceRefresh}", - R.drawable.ic_globe, - arguments = listOf(navArgument("forceRefresh") { type = NavType.BoolType; defaultValue = false }), - buildScreen = { acc, _, navController -> - { backStackEntry -> - SearchScreen(acc, navController, backStackEntry.arguments?.getBoolean("forceRefresh", false) ?: false) - } - } + route = "Search?scrollToTop={scrollToTop}", + icon = R.drawable.ic_globe, + arguments = listOf(navArgument("scrollToTop") { type = NavType.BoolType; defaultValue = false }) ) object Notification : Route( @@ -154,7 +146,7 @@ val Routes = listOf( // * Functions below only exist because we have not broken the datasource classes into backend and frontend. // ** @Composable -public fun currentRoute(navController: NavHostController): String? { +fun currentRoute(navController: NavHostController): String? { val navBackStackEntry by navController.currentBackStackEntryAsState() return navBackStackEntry?.destination?.route } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt index af98fe35c..10453c6df 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt @@ -42,11 +42,11 @@ fun FeedView( navController: NavController, routeForLastRead: String?, scrollStateKey: String? = null, - forceRefresh: Boolean = false + scrollToTop: Boolean = false ) { val feedState by viewModel.feedContent.collectAsState() - var refreshing by remember { mutableStateOf(forceRefresh) } + var refreshing by remember { mutableStateOf(false) } val refresh = { refreshing = true; viewModel.refresh(); refreshing = false } val pullRefreshState = rememberPullRefreshState(refreshing, onRefresh = refresh) @@ -77,7 +77,7 @@ fun FeedView( accountViewModel, navController, scrollStateKey, - forceRefresh + scrollToTop ) } @@ -99,7 +99,7 @@ private fun FeedLoaded( accountViewModel: AccountViewModel, navController: NavController, scrollStateKey: String?, - forceRefresh: Boolean = false + scrollToTop: Boolean = false ) { val listState = if (scrollStateKey != null) { rememberForeverLazyListState(scrollStateKey) @@ -107,9 +107,9 @@ private fun FeedLoaded( rememberLazyListState() } - if (forceRefresh) { + if (scrollToTop) { LaunchedEffect(Unit) { - listState.animateScrollToItem(0) + listState.scrollToItem(index = 0) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt index 203684542..cfed81664 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt @@ -39,7 +39,7 @@ import kotlinx.coroutines.launch @OptIn(ExperimentalPagerApi::class) @Composable -fun HomeScreen(accountViewModel: AccountViewModel, navController: NavController, forceRefresh: Boolean = false) { +fun HomeScreen(accountViewModel: AccountViewModel, navController: NavController, scrollToTop: Boolean = false) { val accountState by accountViewModel.accountLiveData.observeAsState() val account = accountState?.account ?: return @@ -107,7 +107,7 @@ fun HomeScreen(accountViewModel: AccountViewModel, navController: NavController, } HorizontalPager(count = 2, state = pagerState) { when (pagerState.currentPage) { - 0 -> FeedView(feedViewModel, accountViewModel, navController, Route.Home.base + "Follows", ScrollStateKeys.HOME_FOLLOWS, forceRefresh) + 0 -> FeedView(feedViewModel, accountViewModel, navController, Route.Home.base + "Follows", ScrollStateKeys.HOME_FOLLOWS, scrollToTop) 1 -> FeedView(feedViewModelReplies, accountViewModel, navController, Route.Home.base + "FollowsReplies", ScrollStateKeys.HOME_REPLIES) } } 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 c4dbfaab3..f55a5f3a8 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 @@ -47,7 +47,6 @@ import androidx.compose.ui.text.style.TextOverflow 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 @@ -58,14 +57,13 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.NostrGlobalDataSource import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource -import com.vitorpamplona.amethyst.ui.dal.GlobalFeedFilter import com.vitorpamplona.amethyst.ui.note.ChannelName import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.note.UserCompose import com.vitorpamplona.amethyst.ui.note.UserPicture import com.vitorpamplona.amethyst.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.ui.screen.FeedView -import com.vitorpamplona.amethyst.ui.screen.NostrGlobalFeedViewModel +import com.vitorpamplona.amethyst.ui.screen.FeedViewModel import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview @@ -79,12 +77,12 @@ import kotlinx.coroutines.withContext import kotlinx.coroutines.channels.Channel as CoroutineChannel @Composable -fun SearchScreen(accountViewModel: AccountViewModel, navController: NavController, forceRefresh: Boolean = false) { - val accountState by accountViewModel.accountLiveData.observeAsState() - val account = accountState?.account ?: return - - GlobalFeedFilter.account = account - val feedViewModel: NostrGlobalFeedViewModel = viewModel() +fun SearchScreen( + accountViewModel: AccountViewModel, + feedViewModel: FeedViewModel, + navController: NavController, + scrollToTop: Boolean = false +) { val lifeCycleOwner = LocalLifecycleOwner.current LaunchedEffect(accountViewModel) { @@ -115,7 +113,7 @@ fun SearchScreen(accountViewModel: AccountViewModel, navController: NavControlle modifier = Modifier.padding(vertical = 0.dp) ) { SearchBar(accountViewModel, navController) - FeedView(feedViewModel, accountViewModel, navController, null, ScrollStateKeys.GLOBAL_SCREEN, forceRefresh) + FeedView(feedViewModel, accountViewModel, navController, null, ScrollStateKeys.GLOBAL_SCREEN, scrollToTop) } } } From 70434eb24440ffe98158b6d06d5b6961c4ffb729 Mon Sep 17 00:00:00 2001 From: maxmoney21m Date: Mon, 13 Mar 2023 20:17:35 +0800 Subject: [PATCH 32/45] Remove Route buildScreen member --- .../amethyst/ui/navigation/AppNavigation.kt | 65 +++++++++++++++- .../amethyst/ui/navigation/Routes.kt | 77 +++---------------- 2 files changed, 70 insertions(+), 72 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index 27d2b4b16..86935b29b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -11,7 +11,15 @@ import com.vitorpamplona.amethyst.ui.dal.GlobalFeedFilter import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel import com.vitorpamplona.amethyst.ui.screen.NostrGlobalFeedViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelScreen +import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChatroomListScreen +import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChatroomScreen +import com.vitorpamplona.amethyst.ui.screen.loggedIn.FiltersScreen +import com.vitorpamplona.amethyst.ui.screen.loggedIn.HomeScreen +import com.vitorpamplona.amethyst.ui.screen.loggedIn.NotificationScreen +import com.vitorpamplona.amethyst.ui.screen.loggedIn.ProfileScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.SearchScreen +import com.vitorpamplona.amethyst.ui.screen.loggedIn.ThreadScreen @Composable fun AppNavigation( @@ -38,10 +46,59 @@ fun AppNavigation( }) } - Routes.forEach { - it.buildScreen?.let { fn -> - composable(it.route, it.arguments, content = fn(accountViewModel, accountStateViewModel, navController)) - } + Route.Home.let { route -> + composable(route.route, route.arguments, content = { + HomeScreen( + accountViewModel = accountViewModel, + navController = navController, + scrollToTop = it.arguments?.getBoolean("scrollToTop") ?: false + ) + }) + } + + composable(Route.Message.route, content = { ChatroomListScreen(accountViewModel, navController) }) + composable(Route.Notification.route, content = { NotificationScreen(accountViewModel, navController) }) + composable(Route.Filters.route, content = { FiltersScreen(accountViewModel, navController) }) + + Route.Profile.let { route -> + composable(route.route, route.arguments, content = { + ProfileScreen( + userId = it.arguments?.getString("id"), + accountViewModel = accountViewModel, + navController = navController + ) + }) + } + + Route.Note.let { route -> + composable(route.route, route.arguments, content = { + ThreadScreen( + noteId = it.arguments?.getString("id"), + accountViewModel = accountViewModel, + navController = navController + ) + }) + } + + Route.Room.let { route -> + composable(route.route, route.arguments, content = { + ChatroomScreen( + userId = it.arguments?.getString("id"), + accountViewModel = accountViewModel, + navController = navController + ) + }) + } + + Route.Channel.let { route -> + composable(route.route, route.arguments, content = { + ChannelScreen( + channelId = it.arguments?.getString("id"), + accountViewModel = accountViewModel, + accountStateViewModel = accountStateViewModel, + navController = navController + ) + }) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt index ec7b30a00..3ee743e87 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt @@ -4,8 +4,6 @@ import android.content.Context import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.navigation.NamedNavArgument -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavController import androidx.navigation.NavHostController import androidx.navigation.NavType import androidx.navigation.compose.currentBackStackEntryAsState @@ -16,35 +14,21 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.ui.dal.ChatroomListKnownFeedFilter import com.vitorpamplona.amethyst.ui.dal.HomeNewThreadFeedFilter import com.vitorpamplona.amethyst.ui.dal.NotificationFeedFilter -import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChatroomListScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChatroomScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.FiltersScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.HomeScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.NotificationScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.ProfileScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.ThreadScreen sealed class Route( val route: String, val icon: Int, val hasNewItems: (Account, NotificationCache, Context) -> Boolean = { _, _, _ -> false }, - val arguments: List = emptyList(), - val buildScreen: ((AccountViewModel, AccountStateViewModel, NavController) -> @Composable (NavBackStackEntry) -> Unit)? = null + val arguments: List = emptyList() ) { val base: String get() = route.substringBefore("?") object Home : Route( - "Home?scrollToTop={scrollToTop}", - R.drawable.ic_home, + route = "Home?scrollToTop={scrollToTop}", + icon = R.drawable.ic_home, arguments = listOf(navArgument("scrollToTop") { type = NavType.BoolType; defaultValue = false }), - hasNewItems = { accountViewModel, cache, context -> homeHasNewItems(accountViewModel, cache, context) }, - buildScreen = { accountViewModel, _, navController -> - { HomeScreen(accountViewModel, navController, it.arguments?.getBoolean("scrollToTop", false) ?: false) } - } + hasNewItems = { accountViewModel, cache, context -> homeHasNewItems(accountViewModel, cache, context) } ) object Search : Route( @@ -58,9 +42,6 @@ sealed class Route( icon = R.drawable.ic_notifications, hasNewItems = { accountViewModel, cache, context -> notificationHasNewItems(accountViewModel, cache, context) - }, - buildScreen = { accountViewModel, _, navController -> - { NotificationScreen(accountViewModel, navController) } } ) @@ -69,79 +50,39 @@ sealed class Route( icon = R.drawable.ic_dm, hasNewItems = { accountViewModel, cache, context -> messagesHasNewItems(accountViewModel, cache, context) - }, - buildScreen = { accountViewModel, _, navController -> - { ChatroomListScreen(accountViewModel, navController) } } ) object Filters : Route( route = "Filters", - icon = R.drawable.ic_security, - buildScreen = { accountViewModel, _, navController -> - { FiltersScreen(accountViewModel, navController) } - } + icon = R.drawable.ic_security ) object Profile : Route( route = "User/{id}", icon = R.drawable.ic_profile, - arguments = listOf(navArgument("id") { type = NavType.StringType }), - buildScreen = { accountViewModel, _, navController -> - { ProfileScreen(it.arguments?.getString("id"), accountViewModel, navController) } - } + arguments = listOf(navArgument("id") { type = NavType.StringType }) ) object Note : Route( route = "Note/{id}", icon = R.drawable.ic_moments, - arguments = listOf(navArgument("id") { type = NavType.StringType }), - buildScreen = { accountViewModel, _, navController -> - { ThreadScreen(it.arguments?.getString("id"), accountViewModel, navController) } - } + arguments = listOf(navArgument("id") { type = NavType.StringType }) ) object Room : Route( route = "Room/{id}", icon = R.drawable.ic_moments, - arguments = listOf(navArgument("id") { type = NavType.StringType }), - buildScreen = { accountViewModel, _, navController -> - { ChatroomScreen(it.arguments?.getString("id"), accountViewModel, navController) } - } + arguments = listOf(navArgument("id") { type = NavType.StringType }) ) object Channel : Route( route = "Channel/{id}", icon = R.drawable.ic_moments, - arguments = listOf(navArgument("id") { type = NavType.StringType }), - buildScreen = { accountViewModel, accountStateViewModel, navController -> - { - ChannelScreen( - it.arguments?.getString("id"), - accountViewModel, - accountStateViewModel, - navController - ) - } - } + arguments = listOf(navArgument("id") { type = NavType.StringType }) ) } -val Routes = listOf( - // bottom - Route.Home, - Route.Message, - Route.Search, - Route.Notification, - - // drawer - Route.Profile, - Route.Note, - Route.Room, - Route.Channel, - Route.Filters -) - // ** // * Functions below only exist because we have not broken the datasource classes into backend and frontend. // ** From d8c2e623d1872e7bf54951fb84cc9ddea8ed3a1b Mon Sep 17 00:00:00 2001 From: maxmoney21m Date: Mon, 13 Mar 2023 20:28:30 +0800 Subject: [PATCH 33/45] Hoist HomeScreen state --- .../amethyst/ui/navigation/AppNavigation.kt | 16 ++++++++ .../amethyst/ui/screen/loggedIn/HomeScreen.kt | 40 ++++++++----------- 2 files changed, 32 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index 86935b29b..95f475004 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -7,9 +7,15 @@ import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable +import com.google.accompanist.pager.ExperimentalPagerApi +import com.google.accompanist.pager.rememberPagerState import com.vitorpamplona.amethyst.ui.dal.GlobalFeedFilter +import com.vitorpamplona.amethyst.ui.dal.HomeConversationsFeedFilter +import com.vitorpamplona.amethyst.ui.dal.HomeNewThreadFeedFilter import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel import com.vitorpamplona.amethyst.ui.screen.NostrGlobalFeedViewModel +import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedViewModel +import com.vitorpamplona.amethyst.ui.screen.NostrHomeRepliesFeedViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChatroomListScreen @@ -21,6 +27,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.ProfileScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.SearchScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.ThreadScreen +@OptIn(ExperimentalPagerApi::class) @Composable fun AppNavigation( navController: NavHostController, @@ -32,7 +39,13 @@ fun AppNavigation( val account = accountState?.account ?: return GlobalFeedFilter.account = account + HomeNewThreadFeedFilter.account = account + HomeConversationsFeedFilter.account = account + val globalFeedViewModel: NostrGlobalFeedViewModel = viewModel() + val homeFeedViewModel: NostrHomeFeedViewModel = viewModel() + val homeRepliesFeedViewModel: NostrHomeRepliesFeedViewModel = viewModel() + val homePagerState = rememberPagerState() NavHost(navController, startDestination = Route.Home.route) { Route.Search.let { route -> @@ -51,6 +64,9 @@ fun AppNavigation( HomeScreen( accountViewModel = accountViewModel, navController = navController, + homeFeedViewModel = homeFeedViewModel, + repliesFeedViewModel = homeRepliesFeedViewModel, + pagerState = homePagerState, scrollToTop = it.arguments?.getBoolean("scrollToTop") ?: false ) }) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt index cfed81664..ddf6af5ac 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt @@ -11,8 +11,6 @@ import androidx.compose.material.Text 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.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLifecycleOwner @@ -20,16 +18,13 @@ import androidx.compose.ui.res.stringResource 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.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.HorizontalPager +import com.google.accompanist.pager.PagerState import com.google.accompanist.pager.pagerTabIndicatorOffset -import com.google.accompanist.pager.rememberPagerState import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.service.NostrHomeDataSource -import com.vitorpamplona.amethyst.ui.dal.HomeConversationsFeedFilter -import com.vitorpamplona.amethyst.ui.dal.HomeNewThreadFeedFilter import com.vitorpamplona.amethyst.ui.navigation.Route import com.vitorpamplona.amethyst.ui.screen.FeedView import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedViewModel @@ -39,33 +34,30 @@ import kotlinx.coroutines.launch @OptIn(ExperimentalPagerApi::class) @Composable -fun HomeScreen(accountViewModel: AccountViewModel, navController: NavController, scrollToTop: Boolean = false) { - val accountState by accountViewModel.accountLiveData.observeAsState() - val account = accountState?.account ?: return - - HomeNewThreadFeedFilter.account = account - HomeConversationsFeedFilter.account = account - - val feedViewModel: NostrHomeFeedViewModel = viewModel() - val feedViewModelReplies: NostrHomeRepliesFeedViewModel = viewModel() - - val pagerState = rememberPagerState() +fun HomeScreen( + accountViewModel: AccountViewModel, + navController: NavController, + homeFeedViewModel: NostrHomeFeedViewModel, + repliesFeedViewModel: NostrHomeRepliesFeedViewModel, + pagerState: PagerState, + scrollToTop: Boolean = false +) { val coroutineScope = rememberCoroutineScope() LaunchedEffect(accountViewModel) { NostrHomeDataSource.resetFilters() - feedViewModel.refresh() - feedViewModelReplies.refresh() + homeFeedViewModel.refresh() + repliesFeedViewModel.refresh() } val lifeCycleOwner = LocalLifecycleOwner.current DisposableEffect(accountViewModel) { - val observer = LifecycleEventObserver { source, event -> + val observer = LifecycleEventObserver { _, event -> if (event == Lifecycle.Event.ON_RESUME) { NostrHomeDataSource.resetFilters() - feedViewModel.refresh() - feedViewModelReplies.refresh() + homeFeedViewModel.refresh() + repliesFeedViewModel.refresh() } } @@ -107,8 +99,8 @@ fun HomeScreen(accountViewModel: AccountViewModel, navController: NavController, } HorizontalPager(count = 2, state = pagerState) { when (pagerState.currentPage) { - 0 -> FeedView(feedViewModel, accountViewModel, navController, Route.Home.base + "Follows", ScrollStateKeys.HOME_FOLLOWS, scrollToTop) - 1 -> FeedView(feedViewModelReplies, accountViewModel, navController, Route.Home.base + "FollowsReplies", ScrollStateKeys.HOME_REPLIES) + 0 -> FeedView(homeFeedViewModel, accountViewModel, navController, Route.Home.base + "Follows", ScrollStateKeys.HOME_FOLLOWS, scrollToTop) + 1 -> FeedView(repliesFeedViewModel, accountViewModel, navController, Route.Home.base + "FollowsReplies", ScrollStateKeys.HOME_REPLIES, scrollToTop) } } } From a367bf5982a07becd882672c097473f192a106e1 Mon Sep 17 00:00:00 2001 From: maxmoney21m Date: Mon, 13 Mar 2023 21:41:02 +0800 Subject: [PATCH 34/45] Update gitignore --- .gitignore | 60 +++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 41 insertions(+), 19 deletions(-) diff --git a/.gitignore b/.gitignore index f928a5380..cf483fc1e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,3 @@ -*.iml -.gradle /local.properties /.idea/caches /.idea/libraries @@ -11,9 +9,7 @@ .DS_Store /build /captures -.externalNativeBuild .cxx -local.properties # Built application files @@ -36,6 +32,7 @@ out/ # release/ # Gradle files +.gradle .gradle/ build/ @@ -48,25 +45,50 @@ proguard/ # Log Files *.log -# Android Studio Navigation editor temp files -.navigation/ - -# Android Studio captures folder +# Android Studio +/*/build/ +/*/local.properties +/*/out +/*/*/build +/*/*/production captures/ +.navigation/ +*.ipr +*~ +*.swp # IntelliJ *.iml -.idea/workspace.xml -.idea/tasks.xml -.idea/gradle.xml -.idea/assetWizardSettings.xml -.idea/dictionaries -.idea/libraries -# Android Studio 3 in .gitignore file. -.idea/caches -.idea/modules.xml -# Comment next line if keeping position of elements in Navigation Editor is relevant for you -.idea/navEditor.xml +*.iws +/out/ +deploymentTargetDropdown.xml +render.experimental.xml + +# User-specific configurations +.idea/**/caches/ +.idea/**/libraries/ +.idea/**/shelf/ +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/.name +.idea/**/compiler.xml +.idea/**/copyright/profiles_settings.xml +.idea/**/encodings.xml +.idea/**/misc.xml +.idea/**/modules.xml +.idea/**/scopes/scope_settings.xml +.idea/**/dictionaries +.idea/**/vcs.xml +.idea/**/jsLibraryMappings.xml +.idea/**/datasources.xml +.idea/**/dataSources.ids +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/assetWizardSettings.xml +.idea/**/gradle.xml +.idea/**/jarRepositories.xml +.idea/**/navEditor.xml # Keystore files # Uncomment the following lines if you do not want to check your keystore files in. From 02d74c611ad1d88253da7b612754c20cff553d53 Mon Sep 17 00:00:00 2001 From: maxmoney21m Date: Mon, 13 Mar 2023 21:42:09 +0800 Subject: [PATCH 35/45] Remove .idea files --- .idea/compiler.xml | 6 ------ .idea/misc.xml | 10 ---------- 2 files changed, 16 deletions(-) delete mode 100644 .idea/compiler.xml delete mode 100644 .idea/misc.xml diff --git a/.idea/compiler.xml b/.idea/compiler.xml deleted file mode 100644 index fb7f4a8a4..000000000 --- a/.idea/compiler.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index bdd92780c..000000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - \ No newline at end of file From cd5e0187e853f53f6580543fecc979a6154d74cf Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 13 Mar 2023 10:20:48 -0400 Subject: [PATCH 36/45] Refining the selection size when clicking in an account. --- .../ui/navigation/AccountSwitchBottomSheet.kt | 105 +++++++++--------- 1 file changed, 55 insertions(+), 50 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 6b793447d..cbcc742ca 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 @@ -86,9 +86,7 @@ fun AccountSwitchBottomSheet( val current = accountUser.pubkeyNpub() == acc.npub Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp, 16.dp), + modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically ) { Row( @@ -99,62 +97,69 @@ fun AccountSwitchBottomSheet( }, verticalAlignment = Alignment.CenterVertically ) { - Box( + Row( modifier = Modifier - .width(55.dp) - .padding(0.dp) + .padding(16.dp, 16.dp) + .weight(1f), + verticalAlignment = Alignment.CenterVertically ) { - AsyncImageProxy( - 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) - .align(Alignment.TopEnd) + .width(55.dp) + .padding(0.dp) ) { - if (acc.hasPrivKey) { - Icon( - imageVector = Icons.Default.Key, - contentDescription = stringResource(R.string.account_switch_has_private_key), - modifier = Modifier.size(20.dp), - tint = MaterialTheme.colors.primary - ) - } else { - Icon( - imageVector = Icons.Default.Visibility, - contentDescription = stringResource(R.string.account_switch_pubkey_only), - modifier = Modifier.size(20.dp), - tint = MaterialTheme.colors.primary - ) + AsyncImageProxy( + 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) + .align(Alignment.TopEnd) + ) { + if (acc.hasPrivKey) { + Icon( + imageVector = Icons.Default.Key, + contentDescription = stringResource(R.string.account_switch_has_private_key), + modifier = Modifier.size(20.dp), + tint = MaterialTheme.colors.primary + ) + } else { + Icon( + imageVector = Icons.Default.Visibility, + contentDescription = stringResource(R.string.account_switch_pubkey_only), + modifier = Modifier.size(20.dp), + tint = MaterialTheme.colors.primary + ) + } } } - } - Spacer(modifier = Modifier.width(16.dp)) - Column(modifier = Modifier.weight(1f)) { - val npubShortHex = acc.npub.toShortenHex() + Spacer(modifier = Modifier.width(16.dp)) + Column(modifier = Modifier.weight(1f)) { + val npubShortHex = acc.npub.toShortenHex() - if (acc.displayName != null && acc.displayName != npubShortHex) { - Text(acc.displayName) + if (acc.displayName != null && acc.displayName != npubShortHex) { + Text(acc.displayName) + } + + Text(npubShortHex) } - - Text(npubShortHex) - } - Column(modifier = Modifier.width(32.dp)) { - if (current) { - Icon( - imageVector = Icons.Default.RadioButtonChecked, - contentDescription = stringResource(R.string.account_switch_active_account), - tint = MaterialTheme.colors.secondary - ) + Column(modifier = Modifier.width(32.dp)) { + if (current) { + Icon( + imageVector = Icons.Default.RadioButtonChecked, + contentDescription = stringResource(R.string.account_switch_active_account), + tint = MaterialTheme.colors.secondary + ) + } } } } From 53d692ec7f488670aafebc58fe6d04be14070c0b Mon Sep 17 00:00:00 2001 From: maxmoney21m Date: Mon, 13 Mar 2023 23:02:53 +0800 Subject: [PATCH 37/45] Add user-agent header, fixes #259 --- .../vitorpamplona/amethyst/service/Nip05Verifier.kt | 5 ++++- .../amethyst/service/lnurl/LightningAddressResolver.kt | 10 ++++++++-- .../com/vitorpamplona/amethyst/service/relays/Relay.kt | 5 ++++- .../vitorpamplona/amethyst/ui/actions/ImageSaver.kt | 1 + .../vitorpamplona/amethyst/ui/actions/ImageUploader.kt | 3 ++- 5 files changed, 19 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip05Verifier.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip05Verifier.kt index 27555bcac..81edc34ae 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip05Verifier.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip05Verifier.kt @@ -42,7 +42,10 @@ class Nip05Verifier { withContext(Dispatchers.IO) { try { - val request: Request = Request.Builder().url(url).build() + val request = Request.Builder() + .header("User-Agent", "Amethyst") + .url(url) + .build() client.newCall(request).enqueue(object : Callback { override fun onResponse(call: Call, response: Response) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt index 1a2371805..7beaf1777 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt @@ -51,7 +51,10 @@ class LightningAddressResolver { } withContext(Dispatchers.IO) { - val request: Request = Request.Builder().url(url).build() + val request: Request = Request.Builder() + .header("User-Agent", "Amethyst") + .url(url) + .build() client.newCall(request).enqueue(object : Callback { override fun onResponse(call: Call, response: Response) { @@ -91,7 +94,10 @@ class LightningAddressResolver { url += "&nostr=$encodedNostrRequest" } - val request: Request = Request.Builder().url(url).build() + val request: Request = Request.Builder() + .header("User-Agent", "Amethyst") + .url(url) + .build() client.newCall(request).enqueue(object : Callback { override fun onResponse(call: Call, response: Response) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt index 3b132324f..309badbf0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt @@ -55,7 +55,10 @@ class Relay( if (socket != null) return try { - val request = Request.Builder().url(url.trim()).build() + val request = Request.Builder() + .header("User-Agent", "Amethyst") + .url(url.trim()) + .build() val listener = object : WebSocketListener() { override fun onOpen(webSocket: WebSocket, response: Response) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageSaver.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageSaver.kt index 386ab20bf..7f3666703 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageSaver.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageSaver.kt @@ -31,6 +31,7 @@ object ImageSaver { val client = OkHttpClient.Builder().build() val request = Request.Builder() + .header("User-Agent", "Amethyst") .get() .url(url) .build() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageUploader.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageUploader.kt index e2fcf31b1..8e23f66a6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageUploader.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageUploader.kt @@ -43,8 +43,9 @@ object ImageUploader { .build() val request: Request = Request.Builder() - .url("https://api.imgur.com/3/image") .header("Authorization", "Client-ID e6aea87296f3f96") + .header("User-Agent", "Amethyst") + .url("https://api.imgur.com/3/image") .post(requestBody) .build() From b3f2a032959697517e2a4241bae2fa6ac2412b1c Mon Sep 17 00:00:00 2001 From: maxmoney21m Date: Tue, 7 Mar 2023 23:45:40 +0800 Subject: [PATCH 38/45] 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 39/45] 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 40/45] 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 41/45] 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 42/45] 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 43/45] 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 44/45] 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) From d19592aef367121915f5c618a1f6bdc00abf1595 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 13 Mar 2023 13:49:06 -0400 Subject: [PATCH 45/45] Activates .mov video previews --- .../com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt index 59e223fe9..ed19bcc92 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt @@ -42,7 +42,7 @@ import java.net.URL import java.util.regex.Pattern val imageExtension = Pattern.compile("(.*/)*.+\\.(png|jpg|gif|bmp|jpeg|webp|svg)$") -val videoExtension = Pattern.compile("(.*/)*.+\\.(mp4|avi|wmv|mpg|amv|webm)$") +val videoExtension = Pattern.compile("(.*/)*.+\\.(mp4|avi|wmv|mpg|amv|webm|mov)$") val noProtocolUrlValidator = Pattern.compile("^[-a-zA-Z0-9@:%._\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b(?:[-a-zA-Z0-9()@:%_\\+.~#?&//=]*)$") val tagIndex = Pattern.compile(".*\\#\\[([0-9]+)\\].*")