From 7ad81d1ac391de425b02464cc65dc060fbafe390 Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Fri, 12 Dec 2025 07:39:52 -0300 Subject: [PATCH] Implement pagination for History and Logs - Integrate Jetpack Paging 3 and Room Paging dependencies. - Update `LogDao` and `HistoryDao` to return `PagingSource` instead of `Flow`. - Refactor `ActivitiesScreen`, `ActivityScreen`, `LogsScreen`, and `RelayLogScreen` to use `Pager` and `collectAsLazyPagingItems`. - Update `LogDatabase` to version 2, adding an index on the `time` column for query performance. - Extract `ActivityRow` composable for reuse. - Remove explicit `VACUUM` commands from database setup. --- app/build.gradle | 5 + .../1.json | 19 ++- .../2.json | 68 +++++++++ .../nostrsigner/database/AppDatabase.kt | 1 - .../nostrsigner/database/HistoryDao.kt | 19 ++- .../nostrsigner/database/HistoryDatabase.kt | 1 - .../nostrsigner/database/LogDao.kt | 10 ++ .../nostrsigner/database/LogDatabase.kt | 12 +- .../nostrsigner/database/LogEntity.kt | 9 ++ .../nostrsigner/ui/ActivitiesScreen.kt | 138 +++++++++++------- .../greenart7c3/nostrsigner/ui/LogsScreen.kt | 94 +++++++----- .../greenart7c3/nostrsigner/ui/MainScreen.kt | 1 + .../nostrsigner/ui/actions/ActivityScreen.kt | 112 +++++--------- .../ui/actions/EditRelaysDialog.kt | 86 ++++++----- gradle/libs.versions.toml | 6 + 15 files changed, 366 insertions(+), 215 deletions(-) create mode 100644 app/schemas/com.greenart7c3.nostrsigner.database.LogDatabase/2.json diff --git a/app/build.gradle b/app/build.gradle index 143dfb4c..d642f45f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -205,6 +205,9 @@ dependencies { implementation libs.work.runtime.ktx implementation libs.material3.window implementation libs.adaptive.android + implementation libs.paging.common + implementation libs.paging.compose + implementation libs.paging.runtime testImplementation libs.junit androidTestImplementation libs.ext.junit @@ -234,6 +237,8 @@ dependencies { ksp(libs.room.compiler) implementation(libs.room.ktx) + implementation(libs.room.runtime) + implementation(libs.room.paging) freeImplementation libs.okhttp freeImplementation libs.okhttpCoroutines diff --git a/app/schemas/com.greenart7c3.nostrsigner.database.LogDatabase/1.json b/app/schemas/com.greenart7c3.nostrsigner.database.LogDatabase/1.json index b53587e8..345becb6 100644 --- a/app/schemas/com.greenart7c3.nostrsigner.database.LogDatabase/1.json +++ b/app/schemas/com.greenart7c3.nostrsigner.database.LogDatabase/1.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "3d70a45c6ee30a3f8c9d79b6d3a55cf0", + "identityHash": "22c4d1bdcd7396ef577313c1cf4627d7", "entities": [ { "tableName": "amber_log", @@ -44,12 +44,25 @@ "columnNames": [ "id" ] - } + }, + "indices": [ + { + "name": "log_by_time_key", + "unique": false, + "columnNames": [ + "time" + ], + "orders": [ + "DESC" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `log_by_time_key` ON `${TABLE_NAME}` (`time` DESC)" + } + ] } ], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '3d70a45c6ee30a3f8c9d79b6d3a55cf0')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '22c4d1bdcd7396ef577313c1cf4627d7')" ] } } \ No newline at end of file diff --git a/app/schemas/com.greenart7c3.nostrsigner.database.LogDatabase/2.json b/app/schemas/com.greenart7c3.nostrsigner.database.LogDatabase/2.json new file mode 100644 index 00000000..ef7f0900 --- /dev/null +++ b/app/schemas/com.greenart7c3.nostrsigner.database.LogDatabase/2.json @@ -0,0 +1,68 @@ +{ + "formatVersion": 1, + "database": { + "version": 2, + "identityHash": "22c4d1bdcd7396ef577313c1cf4627d7", + "entities": [ + { + "tableName": "amber_log", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `type` TEXT NOT NULL, `message` TEXT NOT NULL, `time` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "log_by_time_key", + "unique": false, + "columnNames": [ + "time" + ], + "orders": [ + "DESC" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `log_by_time_key` ON `${TABLE_NAME}` (`time` DESC)" + } + ] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '22c4d1bdcd7396ef577313c1cf4627d7')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/database/AppDatabase.kt b/app/src/main/java/com/greenart7c3/nostrsigner/database/AppDatabase.kt index c2ba046e..f1e9e7f8 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/database/AppDatabase.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/database/AppDatabase.kt @@ -191,7 +191,6 @@ abstract class AppDatabase : RoomDatabase() { .addMigrations(MIGRATION_14_15) .addMigrations(MIGRATION_15_16) .build() - instance.openHelper.writableDatabase.execSQL("VACUUM") instance } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/database/HistoryDao.kt b/app/src/main/java/com/greenart7c3/nostrsigner/database/HistoryDao.kt index 108547c8..f89444f1 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/database/HistoryDao.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/database/HistoryDao.kt @@ -3,6 +3,7 @@ package com.greenart7c3.nostrsigner.database import android.util.Log import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.toLowerCase +import androidx.paging.PagingSource import androidx.room.Dao import androidx.room.Delete import androidx.room.Insert @@ -11,21 +12,27 @@ import androidx.room.Query import androidx.room.Transaction import com.greenart7c3.nostrsigner.Amber import com.greenart7c3.nostrsigner.models.Permission -import kotlinx.coroutines.flow.Flow @Dao interface HistoryDao { @Query("SELECT * FROM history where pkKey = :pk ORDER BY time DESC") - fun getAllHistory(pk: String): Flow> + fun getAllHistoryPaging(pk: String): PagingSource @Query("SELECT * FROM history where (kind = :query OR LOWER(type) LIKE '%' || :query || '%' OR LOWER(translatedPermission) LIKE '%' || :query || '%') AND pkKey = :pk ORDER BY time DESC") - fun searchAllHistory(pk: String, query: String): Flow> + fun searchAllHistoryPaging(pk: String, query: String): PagingSource @Query("SELECT * FROM history ORDER BY time DESC") - fun getAllHistory(): Flow> + fun getAllHistoryPaging(): PagingSource - @Query("SELECT * FROM history where (kind = :query OR LOWER(type) LIKE '%' || :query || '%' OR LOWER(translatedPermission) LIKE '%' || :query || '%') ORDER BY time DESC") - fun searchAllHistory(query: String): Flow> + @Query( + """ + SELECT * FROM history + WHERE (kind = :query OR LOWER(type) LIKE '%' || :query || '%' + OR LOWER(translatedPermission) LIKE '%' || :query || '%') + ORDER BY time DESC + """, + ) + fun searchAllHistoryPaging(query: String): PagingSource @Query("DELETE FROM history where pkKey = :pk") suspend fun deleteHistory(pk: String) diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/database/HistoryDatabase.kt b/app/src/main/java/com/greenart7c3/nostrsigner/database/HistoryDatabase.kt index 42e7c908..c8a4cd9e 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/database/HistoryDatabase.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/database/HistoryDatabase.kt @@ -43,7 +43,6 @@ abstract class HistoryDatabase : RoomDatabase() { .setTransactionExecutor(transactionExecutor) .addMigrations(migration_1_2) .build() - instance.openHelper.writableDatabase.execSQL("VACUUM") instance } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/database/LogDao.kt b/app/src/main/java/com/greenart7c3/nostrsigner/database/LogDao.kt index 064c6db5..3d50f4b6 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/database/LogDao.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/database/LogDao.kt @@ -1,5 +1,6 @@ package com.greenart7c3.nostrsigner.database +import androidx.paging.PagingSource import androidx.room.Dao import androidx.room.Delete import androidx.room.Insert @@ -9,6 +10,9 @@ import kotlinx.coroutines.flow.Flow @Dao interface LogDao { + @Insert + fun insertAll(items: List) + @Insert @Transaction suspend fun insertLog(logEntity: LogEntity) @@ -16,9 +20,15 @@ interface LogDao { @Query("SELECT * FROM amber_log ORDER BY time DESC") fun getLogs(): Flow> + @Query("SELECT * FROM amber_log ORDER BY time DESC") + fun getLogsPaging(): PagingSource + @Query("SELECT * FROM amber_log where url = :url ORDER BY time DESC") fun getLogsByUrl(url: String): Flow> + @Query("SELECT * FROM amber_log where url = :url ORDER BY time DESC") + fun getLogsByUrlPaging(url: String): PagingSource + @Query("DELETE FROM amber_log") @Transaction suspend fun clearLogs() diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/database/LogDatabase.kt b/app/src/main/java/com/greenart7c3/nostrsigner/database/LogDatabase.kt index 442ce78d..59e50be8 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/database/LogDatabase.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/database/LogDatabase.kt @@ -5,19 +5,27 @@ import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.TypeConverters +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase import java.util.concurrent.Executors @Database( entities = [ LogEntity::class, ], - version = 1, + version = 2, ) @TypeConverters(Converters::class) abstract class LogDatabase : RoomDatabase() { abstract fun dao(): LogDao companion object { + val migration_1_2 = object : Migration(1, 2) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("CREATE INDEX IF NOT EXISTS `log_by_time_key` ON `amber_log` (`time` DESC)") + } + } + fun getDatabase( context: Context, npub: String, @@ -33,8 +41,8 @@ abstract class LogDatabase : RoomDatabase() { ) .setQueryExecutor(executor) .setTransactionExecutor(transactionExecutor) + .addMigrations(migration_1_2) .build() - instance.openHelper.writableDatabase.execSQL("VACUUM") instance } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/database/LogEntity.kt b/app/src/main/java/com/greenart7c3/nostrsigner/database/LogEntity.kt index 4c55332d..b1169b8b 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/database/LogEntity.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/database/LogEntity.kt @@ -1,10 +1,19 @@ package com.greenart7c3.nostrsigner.database import androidx.room.Entity +import androidx.room.Index +import androidx.room.Index.Order import androidx.room.PrimaryKey @Entity( tableName = "amber_log", + indices = [ + Index( + value = ["time"], + name = "log_by_time_key", + orders = [Order.DESC], + ), + ], ) data class LogEntity( @PrimaryKey(autoGenerate = true) diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/ActivitiesScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/ActivitiesScreen.kt index c6c9e2cf..b6cc67b7 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/ActivitiesScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/ActivitiesScreen.kt @@ -1,5 +1,6 @@ package com.greenart7c3.nostrsigner.ui +import android.util.Log import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -9,7 +10,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check @@ -31,18 +31,20 @@ 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.text.intl.Locale import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.text.toLowerCase import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.paging.LoadState +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.compose.collectAsLazyPagingItems import com.greenart7c3.nostrsigner.Amber import com.greenart7c3.nostrsigner.R import com.greenart7c3.nostrsigner.database.AppDatabase +import com.greenart7c3.nostrsigner.database.HistoryEntity import com.greenart7c3.nostrsigner.models.Account import com.greenart7c3.nostrsigner.models.TimeUtils import com.greenart7c3.nostrsigner.models.supportedKindNumbers @@ -63,10 +65,24 @@ fun ActivitiesScreen( val database = Amber.instance.getHistoryDatabase(account.npub) val context = LocalContext.current - // State for the search query var searchQuery by remember { mutableStateOf("") } - val filteredActivities = if (searchQuery.isEmpty()) database.dao().getAllHistory().collectAsStateWithLifecycle(emptyList()) else database.dao().searchAllHistory(searchQuery.toLowerCase(Locale.current)).collectAsStateWithLifecycle(emptyList()) + val pager = remember(searchQuery) { + Pager( + PagingConfig( + pageSize = 20, + enablePlaceholders = false, + ), + ) { + if (searchQuery.isEmpty()) { + database.dao().getAllHistoryPaging() + } else { + database.dao().searchAllHistoryPaging(searchQuery.lowercase()) + } + } + } + + val lazyPagingItems = pager.flow.collectAsLazyPagingItems() val textFieldState by remember { mutableStateOf(TextFieldState(initialText = searchQuery)) } @@ -92,7 +108,7 @@ fun ActivitiesScreen( ), ) { item { - if (filteredActivities.value.isEmpty()) { + if (lazyPagingItems.itemCount == 0) { Text( stringResource(R.string.no_activities_found), Modifier @@ -105,58 +121,78 @@ fun ActivitiesScreen( } } - items(filteredActivities.value) { activity -> - Column { - Row( - modifier = Modifier - .fillMaxSize() - .padding(vertical = 4.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Column( - modifier = Modifier.fillMaxWidth(0.9f), - verticalArrangement = Arrangement.Center, - ) { - ApplicationName( - key = activity.pkKey, - accepted = activity.accepted, - database = Amber.instance.getDatabase(account.npub), - account = account, - ) + items(lazyPagingItems.itemCount) { index -> + val activity = lazyPagingItems[index] + if (activity != null) { + ActivityRow(activity = activity, account = account) + } + } - Text( - text = activity.translatedPermission, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - color = if (activity.accepted) Color.Unspecified else Color.Gray, - ) - - Text( - modifier = Modifier.padding(top = 4.dp, bottom = 16.dp), - text = TimeUtils.formatLongToCustomDateTimeWithSeconds(activity.time * 1000), - fontSize = 16.sp, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - color = Color.Gray, - ) - } - Icon( - if (activity.accepted) Icons.Default.Check else Icons.Default.Close, - contentDescription = if (activity.accepted) stringResource(R.string.accepted) else stringResource(R.string.rejected), - tint = if (activity.accepted) Color(0xFF1D8802) else Color(0xFFFF6B00), - modifier = Modifier.padding(start = 10.dp, top = 4.dp, bottom = 16.dp), - ) + lazyPagingItems.apply { + when (loadState.refresh) { + is LoadState.Loading -> item { + Log.d("ActivitiesScreen", "Loading...") + Text("Loading...", Modifier.padding(16.dp)) } - Spacer(Modifier.weight(1f)) - HorizontalDivider( - color = MaterialTheme.colorScheme.primary, - ) + is LoadState.Error -> item { + Log.d("ActivitiesScreen", "Error loading data") + Text("Error loading data", Modifier.padding(16.dp)) + } + is LoadState.NotLoading -> { } } } } } } +@Composable +fun ActivityRow(activity: HistoryEntity, account: Account) { + Column { + Row( + modifier = Modifier + .fillMaxSize() + .padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Column( + modifier = Modifier.fillMaxWidth(0.9f), + verticalArrangement = Arrangement.Center, + ) { + ApplicationName( + key = activity.pkKey, + accepted = activity.accepted, + database = Amber.instance.getDatabase(account.npub), + account = account, + ) + + Text( + text = activity.translatedPermission, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = if (activity.accepted) Color.Unspecified else Color.Gray, + ) + + Text( + modifier = Modifier.padding(top = 4.dp, bottom = 16.dp), + text = TimeUtils.formatLongToCustomDateTimeWithSeconds(activity.time * 1000), + fontSize = 16.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = Color.Gray, + ) + } + Icon( + if (activity.accepted) Icons.Default.Check else Icons.Default.Close, + contentDescription = null, + tint = if (activity.accepted) Color(0xFF1D8802) else Color(0xFFFF6B00), + modifier = Modifier.padding(start = 10.dp, top = 4.dp, bottom = 16.dp), + ) + } + Spacer(Modifier.weight(1f)) + HorizontalDivider(color = MaterialTheme.colorScheme.primary) + } +} + @Composable fun ApplicationName( key: String, diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/LogsScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/LogsScreen.kt index b5d0cf67..c3024a53 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/LogsScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/LogsScreen.kt @@ -21,7 +21,9 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.compose.collectAsLazyPagingItems import com.greenart7c3.nostrsigner.Amber import com.greenart7c3.nostrsigner.R import com.greenart7c3.nostrsigner.models.Account @@ -36,8 +38,17 @@ fun LogsScreen( account: Account, ) { val scope = rememberCoroutineScope() - val logsFlow = Amber.instance.getLogDatabase(account.npub).dao().getLogs() - val logs = logsFlow.collectAsStateWithLifecycle(initialValue = emptyList()) + val pager = + Pager( + PagingConfig( + pageSize = 20, + enablePlaceholders = false, + ), + ) { + Amber.instance.getLogDatabase(account.npub).dao().getLogsPaging() + } + + val lazyPagingItems = pager.flow.collectAsLazyPagingItems() val state = rememberLazyListState() LazyColumn( @@ -57,45 +68,48 @@ fun LogsScreen( text = stringResource(R.string.clear_logs), ) } - items(logs.value) { log -> - Row( - modifier = Modifier - .fillMaxSize() - .padding(vertical = 4.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Column( - verticalArrangement = Arrangement.Center, + items(lazyPagingItems.itemCount) { index -> + val log = lazyPagingItems[index] + log?.let { + Row( + modifier = Modifier + .fillMaxSize() + .padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, ) { - Text( - modifier = Modifier.padding(top = 16.dp), - text = TimeUtils.formatLongToCustomDateTimeWithSeconds(log.time), - fontSize = 14.sp, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - Text( - modifier = Modifier.padding(top = 4.dp), - text = log.url, - fontSize = 20.sp, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - Text( - modifier = Modifier.padding(top = 4.dp), - text = log.type, - fontSize = 20.sp, - ) - Text( - modifier = Modifier.padding(top = 4.dp, bottom = 16.dp), - text = log.message, - fontSize = 20.sp, - ) + Column( + verticalArrangement = Arrangement.Center, + ) { + Text( + modifier = Modifier.padding(top = 16.dp), + text = TimeUtils.formatLongToCustomDateTimeWithSeconds(log.time), + fontSize = 14.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + modifier = Modifier.padding(top = 4.dp), + text = log.url, + fontSize = 20.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + modifier = Modifier.padding(top = 4.dp), + text = log.type, + fontSize = 20.sp, + ) + Text( + modifier = Modifier.padding(top = 4.dp, bottom = 16.dp), + text = log.message, + fontSize = 20.sp, + ) - Spacer(Modifier.weight(1f)) - HorizontalDivider( - color = MaterialTheme.colorScheme.primary, - ) + Spacer(Modifier.weight(1f)) + HorizontalDivider( + color = MaterialTheme.colorScheme.primary, + ) + } } } } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/MainScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/MainScreen.kt index 545f723d..112e22f4 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/MainScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/MainScreen.kt @@ -696,6 +696,7 @@ fun MainScreen( val localUrl = Base64.getDecoder().decode(url).toString(Charsets.UTF_8) RelayLogScreen( url = localUrl, + account = account, paddingValues = PaddingValues( top = padding.calculateTopPadding() + (verticalPadding * 1.5f), bottom = padding.calculateBottomPadding(), diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/ActivityScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/ActivityScreen.kt index a33339d2..d1b11a7d 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/ActivityScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/ActivityScreen.kt @@ -1,49 +1,34 @@ package com.greenart7c3.nostrsigner.ui.actions -import androidx.compose.foundation.layout.Arrangement +import android.util.Log import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.text.input.TextFieldState -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Check -import androidx.compose.material.icons.filled.Close import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.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.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.text.intl.Locale -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.text.toLowerCase import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.paging.LoadState +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.compose.collectAsLazyPagingItems import com.greenart7c3.nostrsigner.Amber -import com.greenart7c3.nostrsigner.R import com.greenart7c3.nostrsigner.models.Account -import com.greenart7c3.nostrsigner.models.TimeUtils import com.greenart7c3.nostrsigner.models.supportedKindNumbers +import com.greenart7c3.nostrsigner.ui.ActivityRow import com.greenart7c3.nostrsigner.ui.components.SimpleSearchBar @OptIn(ExperimentalMaterial3Api::class) @@ -55,9 +40,28 @@ fun ActivityScreen( account: Account, key: String, ) { + val database = Amber.instance.getHistoryDatabase(account.npub) + val context = LocalContext.current var searchQuery by remember { mutableStateOf("") } - val filteredActivities = if (searchQuery.isEmpty()) Amber.instance.getHistoryDatabase(account.npub).dao().getAllHistory(key).collectAsStateWithLifecycle(emptyList()) else Amber.instance.getHistoryDatabase(account.npub).dao().searchAllHistory(key, searchQuery.toLowerCase(Locale.current)).collectAsStateWithLifecycle(emptyList()) + + val pager = remember(searchQuery) { + Pager( + PagingConfig( + pageSize = 20, + enablePlaceholders = false, + ), + ) { + if (searchQuery.isEmpty()) { + database.dao().getAllHistoryPaging() + } else { + database.dao().searchAllHistoryPaging(searchQuery.lowercase()) + } + } + } + + val lazyPagingItems = pager.flow.collectAsLazyPagingItems() + val textFieldState by remember { mutableStateOf(TextFieldState(initialText = searchQuery)) } Column( @@ -81,60 +85,24 @@ fun ActivityScreen( bottom = paddingValues.calculateBottomPadding(), ), ) { - item { - if (filteredActivities.value.isEmpty()) { - Text( - stringResource(R.string.no_activities_found), - Modifier - .fillMaxWidth() - .padding(8.dp), - fontWeight = FontWeight.Bold, - textAlign = TextAlign.Center, - fontSize = 18.sp, - ) + items(lazyPagingItems.itemCount) { index -> + val activity = lazyPagingItems[index] + activity?.let { + ActivityRow(activity = activity, account = account) } } - items(filteredActivities.value) { activity -> - Column { - Row( - modifier = Modifier - .fillMaxSize() - .padding(vertical = 4.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Column( - modifier = Modifier.fillMaxWidth(0.9f), - verticalArrangement = Arrangement.Center, - ) { - Text( - modifier = Modifier.padding(top = 16.dp), - text = activity.translatedPermission, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - color = if (activity.accepted) Color.Unspecified else Color.Gray, - ) - - Text( - modifier = Modifier.padding(top = 4.dp, bottom = 16.dp), - text = TimeUtils.formatLongToCustomDateTimeWithSeconds(activity.time * 1000), - fontSize = 16.sp, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - color = Color.Gray, - ) - } - Icon( - if (activity.accepted) Icons.Default.Check else Icons.Default.Close, - contentDescription = if (activity.accepted) stringResource(R.string.accepted) else stringResource(R.string.rejected), - tint = if (activity.accepted) Color(0xFF1D8802) else Color(0xFFFF6B00), - modifier = Modifier.padding(start = 10.dp, top = 4.dp, bottom = 16.dp), - ) + lazyPagingItems.apply { + when (loadState.refresh) { + is LoadState.Loading -> item { + Log.d("ActivitiesScreen", "Loading...") + Text("Loading...", Modifier.padding(16.dp)) } - Spacer(Modifier.weight(1f)) - HorizontalDivider( - color = MaterialTheme.colorScheme.primary, - ) + is LoadState.Error -> item { + Log.d("ActivitiesScreen", "Error loading data") + Text("Error loading data", Modifier.padding(16.dp)) + } + is LoadState.NotLoading -> { } } } } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/EditRelaysDialog.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/EditRelaysDialog.kt index 0d68697c..3b7d3a28 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/EditRelaysDialog.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/EditRelaysDialog.kt @@ -13,7 +13,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.HorizontalDivider @@ -43,6 +42,9 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.compose.collectAsLazyPagingItems import com.greenart7c3.nostrsigner.Amber import com.greenart7c3.nostrsigner.BuildConfig import com.greenart7c3.nostrsigner.LocalPreferences @@ -76,7 +78,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.merge import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeoutOrNull @@ -438,52 +439,59 @@ fun onAddRelay( @Composable fun RelayLogScreen( paddingValues: PaddingValues, + account: Account, url: String, ) { - val context = LocalContext.current - - val flows = LocalPreferences.allSavedAccounts(context).map { - Amber.instance.getLogDatabase(it.npub).dao().getLogsByUrl(url) - }.merge() - - val logs = flows.collectAsStateWithLifecycle(initialValue = emptyList()) + val pager = + Pager( + PagingConfig( + pageSize = 20, + enablePlaceholders = false, + ), + ) { + Amber.instance.getLogDatabase(account.npub).dao().getLogsByUrlPaging(url) + } + val lazyPagingItems = pager.flow.collectAsLazyPagingItems() LazyColumn( Modifier.fillMaxSize(), contentPadding = paddingValues, ) { - itemsIndexed(logs.value) { _, log -> - Row( - modifier = Modifier - .fillMaxSize() - .padding(vertical = 4.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Column( - verticalArrangement = Arrangement.Center, + items(lazyPagingItems.itemCount) { index -> + val log = lazyPagingItems[index] + log?.let { + Row( + modifier = Modifier + .fillMaxSize() + .padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, ) { - Text( - modifier = Modifier.padding(top = 16.dp), - text = formatLongToCustomDateTimeWithSeconds(log.time), - fontSize = 14.sp, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - Text( - modifier = Modifier.padding(top = 4.dp), - text = log.type, - fontSize = 20.sp, - ) - Text( - modifier = Modifier.padding(top = 4.dp, bottom = 16.dp), - text = log.message, - fontSize = 20.sp, - ) + Column( + verticalArrangement = Arrangement.Center, + ) { + Text( + modifier = Modifier.padding(top = 16.dp), + text = formatLongToCustomDateTimeWithSeconds(log.time), + fontSize = 14.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + modifier = Modifier.padding(top = 4.dp), + text = log.type, + fontSize = 20.sp, + ) + Text( + modifier = Modifier.padding(top = 4.dp, bottom = 16.dp), + text = log.message, + fontSize = 20.sp, + ) - Spacer(Modifier.weight(1f)) - HorizontalDivider( - color = MaterialTheme.colorScheme.primary, - ) + Spacer(Modifier.weight(1f)) + HorizontalDivider( + color = MaterialTheme.colorScheme.primary, + ) + } } } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4af6c92e..a812d6c4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -30,6 +30,7 @@ jna = "5.18.1" adaptiveAndroid = "1.2.0" materialIconsExtended = "1.7.8" collections = "0.4.0" +pagingCommon = "3.3.6" [libraries] datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" } @@ -57,6 +58,8 @@ richtext-ui = { module = "com.github.vitorpamplona.compose-richtext:richtext-ui" richtext-ui-material3 = { module = "com.github.vitorpamplona.compose-richtext:richtext-ui-material3", version.ref = "richtextUi" } room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomKtx" } room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomKtx" } +room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomKtx" } +room-paging = { module = "androidx.room:room-paging", version.ref = "roomKtx" } runtime-livedata = { module = "androidx.compose.runtime:runtime-livedata", version.ref = "compose_ui" } security-crypto = { module = "androidx.security:security-crypto", version.ref = "securityCryptoKtx" } security-crypto-ktx = { module = "androidx.security:security-crypto-ktx", version.ref = "securityCryptoKtx" } @@ -76,6 +79,9 @@ coil-okhttp = { group = "io.coil-kt.coil3", name = "coil-network-okhttp", versio storage = { module = "com.anggrayudi:storage", version.ref = "storage" } material3-window = { group = "androidx.compose.material3", name = "material3-window-size-class", version.ref = "material3" } adaptive-android = { group = "androidx.compose.material3.adaptive", name = "adaptive-android", version.ref = "adaptiveAndroid" } +paging-common = { group = "androidx.paging", name = "paging-common", version.ref = "pagingCommon" } +paging-compose = { group = "androidx.paging", name = "paging-compose", version.ref = "pagingCommon" } +paging-runtime = { group = "androidx.paging", name = "paging-runtime", version.ref = "pagingCommon" } [plugins] jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }