From b75c3e30313cfd70b9802c6c4791820ce0d79738 Mon Sep 17 00:00:00 2001
From: Vitor Pamplona <vitor@vitorpamplona.com>
Date: Tue, 19 Mar 2024 14:17:15 -0400
Subject: [PATCH] Refactoring caching systems for the Compose layer.

---
 .../amethyst/ui/components/RichTextViewer.kt  | 24 +++----
 .../ui/screen/loggedIn/AccountViewModel.kt    | 63 ++++++++++---------
 .../amethyst/commons/compose/CachedState.kt   | 62 ++++++++++++++++++
 3 files changed, 103 insertions(+), 46 deletions(-)
 create mode 100644 commons/src/main/java/com/vitorpamplona/amethyst/commons/compose/CachedState.kt

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 ee51500eb..c4d70ba67 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
@@ -69,6 +69,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
 import com.halilibo.richtext.markdown.Markdown
 import com.halilibo.richtext.markdown.MarkdownParseOptions
 import com.halilibo.richtext.ui.material3.Material3RichText
+import com.vitorpamplona.amethyst.commons.compose.produceCachedState
 import com.vitorpamplona.amethyst.commons.richtext.BechSegment
 import com.vitorpamplona.amethyst.commons.richtext.CashuSegment
 import com.vitorpamplona.amethyst.commons.richtext.EmailSegment
@@ -98,7 +99,6 @@ import com.vitorpamplona.amethyst.ui.note.NoteCompose
 import com.vitorpamplona.amethyst.ui.note.toShortenHex
 import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel
 import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
-import com.vitorpamplona.amethyst.ui.screen.loggedIn.LoadedBechLink
 import com.vitorpamplona.amethyst.ui.theme.Font17SP
 import com.vitorpamplona.amethyst.ui.theme.HalfVertPadding
 import com.vitorpamplona.amethyst.ui.theme.MarkdownTextStyle
@@ -562,15 +562,15 @@ fun ObserveNIP19(
     accountViewModel: AccountViewModel,
     onRefresh: () -> Unit,
 ) {
-    when (val parsed = entity) {
-        is Nip19Bech32.NPub -> ObserveNIP19User(parsed.hex, accountViewModel, onRefresh)
-        is Nip19Bech32.NProfile -> ObserveNIP19User(parsed.hex, accountViewModel, onRefresh)
+    when (entity) {
+        is Nip19Bech32.NPub -> ObserveNIP19User(entity.hex, accountViewModel, onRefresh)
+        is Nip19Bech32.NProfile -> ObserveNIP19User(entity.hex, accountViewModel, onRefresh)
 
-        is Nip19Bech32.Note -> ObserveNIP19Event(parsed.hex, accountViewModel, onRefresh)
-        is Nip19Bech32.NEvent -> ObserveNIP19Event(parsed.hex, accountViewModel, onRefresh)
-        is Nip19Bech32.NEmbed -> ObserveNIP19Event(parsed.event.id, accountViewModel, onRefresh)
+        is Nip19Bech32.Note -> ObserveNIP19Event(entity.hex, accountViewModel, onRefresh)
+        is Nip19Bech32.NEvent -> ObserveNIP19Event(entity.hex, accountViewModel, onRefresh)
+        is Nip19Bech32.NEmbed -> ObserveNIP19Event(entity.event.id, accountViewModel, onRefresh)
 
-        is Nip19Bech32.NAddress -> ObserveNIP19Event(parsed.atag, accountViewModel, onRefresh)
+        is Nip19Bech32.NAddress -> ObserveNIP19Event(entity.atag, accountViewModel, onRefresh)
 
         is Nip19Bech32.NSec -> {}
         is Nip19Bech32.NRelay -> {}
@@ -652,13 +652,7 @@ fun BechLink(
     accountViewModel: AccountViewModel,
     nav: (String) -> Unit,
 ) {
-    var loadedLink by remember { mutableStateOf<LoadedBechLink?>(null) }
-
-    if (loadedLink == null) {
-        LaunchedEffect(key1 = word) {
-            accountViewModel.parseNIP19(word) { loadedLink = it }
-        }
-    }
+    val loadedLink by produceCachedState(cache = accountViewModel.bechLinkCache, key = word)
 
     val baseNote = loadedLink?.baseNote
 
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt
index e52c4d40c..394f662c4 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt
@@ -37,6 +37,7 @@ import coil.imageLoader
 import coil.request.ImageRequest
 import com.vitorpamplona.amethyst.R
 import com.vitorpamplona.amethyst.ServiceManager
+import com.vitorpamplona.amethyst.commons.compose.GenericBaseCache
 import com.vitorpamplona.amethyst.model.Account
 import com.vitorpamplona.amethyst.model.AccountState
 import com.vitorpamplona.amethyst.model.AddressableNote
@@ -1022,37 +1023,6 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
         }
     }
 
-    suspend fun parseNIP19(
-        str: String,
-        onNote: (LoadedBechLink) -> Unit,
-    ) {
-        withContext(Dispatchers.IO) {
-            Nip19Bech32.uriToRoute(str)?.let {
-                var returningNote: Note? = null
-
-                when (val parsed = it.entity) {
-                    is Nip19Bech32.NSec -> {}
-                    is Nip19Bech32.NPub -> {}
-                    is Nip19Bech32.NProfile -> {}
-                    is Nip19Bech32.Note -> LocalCache.checkGetOrCreateNote(parsed.hex)?.let { note -> returningNote = note }
-                    is Nip19Bech32.NEvent -> LocalCache.checkGetOrCreateNote(parsed.hex)?.let { note -> returningNote = note }
-                    is Nip19Bech32.NEmbed -> {
-                        loadNEmbedIfNeeded(parsed.event)
-
-                        LocalCache.checkGetOrCreateNote(parsed.event.id)?.let { note ->
-                            returningNote = note
-                        }
-                    }
-                    is Nip19Bech32.NRelay -> {}
-                    is Nip19Bech32.NAddress -> LocalCache.checkGetOrCreateNote(parsed.atag)?.let { note -> returningNote = note }
-                    else -> {}
-                }
-
-                onNote(LoadedBechLink(returningNote, it))
-            }
-        }
-    }
-
     fun checkIsOnline(
         media: String?,
         onDone: (Boolean) -> Unit,
@@ -1321,6 +1291,37 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
             }
         }
     }
+
+    val bechLinkCache = CachedLoadedBechLink(this)
+
+    class CachedLoadedBechLink(val accountViewModel: AccountViewModel) : GenericBaseCache<String, LoadedBechLink>(20) {
+        override suspend fun compute(key: String): LoadedBechLink? {
+            return Nip19Bech32.uriToRoute(key)?.let {
+                var returningNote: Note? = null
+
+                when (val parsed = it.entity) {
+                    is Nip19Bech32.NSec -> {}
+                    is Nip19Bech32.NPub -> {}
+                    is Nip19Bech32.NProfile -> {}
+                    is Nip19Bech32.Note -> withContext(Dispatchers.IO) { LocalCache.checkGetOrCreateNote(parsed.hex)?.let { note -> returningNote = note } }
+                    is Nip19Bech32.NEvent -> withContext(Dispatchers.IO) { LocalCache.checkGetOrCreateNote(parsed.hex)?.let { note -> returningNote = note } }
+                    is Nip19Bech32.NEmbed ->
+                        withContext(Dispatchers.IO) {
+                            accountViewModel.loadNEmbedIfNeeded(parsed.event)
+
+                            LocalCache.checkGetOrCreateNote(parsed.event.id)?.let { note ->
+                                returningNote = note
+                            }
+                        }
+                    is Nip19Bech32.NRelay -> {}
+                    is Nip19Bech32.NAddress -> withContext(Dispatchers.IO) { LocalCache.checkGetOrCreateNote(parsed.atag)?.let { note -> returningNote = note } }
+                    else -> {}
+                }
+
+                LoadedBechLink(returningNote, it)
+            }
+        }
+    }
 }
 
 class HasNotificationDot(bottomNavigationItems: ImmutableList<Route>) {
diff --git a/commons/src/main/java/com/vitorpamplona/amethyst/commons/compose/CachedState.kt b/commons/src/main/java/com/vitorpamplona/amethyst/commons/compose/CachedState.kt
new file mode 100644
index 000000000..3e2f9eea7
--- /dev/null
+++ b/commons/src/main/java/com/vitorpamplona/amethyst/commons/compose/CachedState.kt
@@ -0,0 +1,62 @@
+/**
+ * Copyright (c) 2024 Vitor Pamplona
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
+ * Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+ * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+ * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package com.vitorpamplona.amethyst.commons.compose
+
+import android.util.LruCache
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.produceState
+
+@Composable
+fun <K, V> produceCachedState(
+    cache: CachedState<K, V>,
+    key: K,
+): State<V?> {
+    return produceState(initialValue = cache.cached(key), key1 = key) {
+        value = cache.update(key)
+    }
+}
+
+interface CachedState<K, V> {
+    fun cached(k: K): V?
+
+    suspend fun update(k: K): V?
+}
+
+abstract class GenericBaseCache<K, V>(capacity: Int) : CachedState<K, V> {
+    private val cache = LruCache<K, V>(capacity)
+
+    override fun cached(k: K): V? {
+        return cache[k]
+    }
+
+    override suspend fun update(k: K): V? {
+        cache[k]?.let { return it }
+
+        val v = compute(k)
+
+        cache.put(k, v)
+
+        return v
+    }
+
+    abstract suspend fun compute(key: K): V?
+}