From 78a3261b4e1691df21f8f72da342090b9c9857a8 Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Wed, 22 Jan 2025 11:42:37 -0300 Subject: [PATCH] Restore the restore follows button --- .../2.json | 192 ++++++++++ .../com/greenart7c3/citrine/MainActivity.kt | 116 ++++-- .../citrine/database/AppDatabase.kt | 36 ++ .../greenart7c3/citrine/database/EventDao.kt | 2 +- .../citrine/server/CustomWebSocketServer.kt | 3 + .../greenart7c3/citrine/ui/ContactsScreen.kt | 335 +++++++++++++++++ .../citrine/ui/dialogs/ContactsDialog.kt | 349 ------------------ .../citrine/ui/navigation/Route.kt | 6 + 8 files changed, 661 insertions(+), 378 deletions(-) create mode 100644 app/schemas/com.greenart7c3.citrine.database.HistoryDatabase/2.json create mode 100644 app/src/main/java/com/greenart7c3/citrine/ui/ContactsScreen.kt delete mode 100644 app/src/main/java/com/greenart7c3/citrine/ui/dialogs/ContactsDialog.kt diff --git a/app/schemas/com.greenart7c3.citrine.database.HistoryDatabase/2.json b/app/schemas/com.greenart7c3.citrine.database.HistoryDatabase/2.json new file mode 100644 index 0000000..b762a39 --- /dev/null +++ b/app/schemas/com.greenart7c3.citrine.database.HistoryDatabase/2.json @@ -0,0 +1,192 @@ +{ + "formatVersion": 1, + "database": { + "version": 2, + "identityHash": "bf90c485bbfb3751761eb581ab24cf07", + "entities": [ + { + "tableName": "EventEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `pubkey` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `kind` INTEGER NOT NULL, `content` TEXT NOT NULL, `sig` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubkey", + "columnName": "pubkey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sig", + "columnName": "sig", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "id_is_hash", + "unique": true, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `id_is_hash` ON `${TABLE_NAME}` (`id`)" + }, + { + "name": "most_common_search_is_pubkey_kind", + "unique": false, + "columnNames": [ + "pubkey", + "kind" + ], + "orders": [ + "ASC", + "ASC" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `most_common_search_is_pubkey_kind` ON `${TABLE_NAME}` (`pubkey` ASC, `kind` ASC)" + }, + { + "name": "most_common_search_is_kind", + "unique": false, + "columnNames": [ + "kind" + ], + "orders": [ + "ASC" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `most_common_search_is_kind` ON `${TABLE_NAME}` (`kind` ASC)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "TagEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`pk` INTEGER PRIMARY KEY AUTOINCREMENT, `pkEvent` TEXT, `position` INTEGER NOT NULL, `col0Name` TEXT, `col1Value` TEXT, `col2Differentiator` TEXT, `col3Amount` TEXT, `col4Plus` TEXT NOT NULL, FOREIGN KEY(`pkEvent`) REFERENCES `EventEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "pk", + "columnName": "pk", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pkEvent", + "columnName": "pkEvent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "col0Name", + "columnName": "col0Name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "col1Value", + "columnName": "col1Value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "col2Differentiator", + "columnName": "col2Differentiator", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "col3Amount", + "columnName": "col3Amount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "col4Plus", + "columnName": "col4Plus", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "pk" + ] + }, + "indices": [ + { + "name": "tags_by_pk_event", + "unique": false, + "columnNames": [ + "pkEvent" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `tags_by_pk_event` ON `${TABLE_NAME}` (`pkEvent`)" + }, + { + "name": "tags_by_tags_on_person_or_events", + "unique": false, + "columnNames": [ + "col0Name", + "col1Value" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `tags_by_tags_on_person_or_events` ON `${TABLE_NAME}` (`col0Name`, `col1Value`)" + } + ], + "foreignKeys": [ + { + "table": "EventEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "pkEvent" + ], + "referencedColumns": [ + "id" + ] + } + ] + } + ], + "views": [], + "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, 'bf90c485bbfb3751761eb581ab24cf07')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/greenart7c3/citrine/MainActivity.kt b/app/src/main/java/com/greenart7c3/citrine/MainActivity.kt index 0495bac..e808323 100644 --- a/app/src/main/java/com/greenart7c3/citrine/MainActivity.kt +++ b/app/src/main/java/com/greenart7c3/citrine/MainActivity.kt @@ -77,6 +77,7 @@ import com.greenart7c3.citrine.database.toTags import com.greenart7c3.citrine.server.Settings import com.greenart7c3.citrine.service.CustomWebSocketService import com.greenart7c3.citrine.service.LocalPreferences +import com.greenart7c3.citrine.ui.ContactsScreen import com.greenart7c3.citrine.ui.HomeViewModel import com.greenart7c3.citrine.ui.LogcatScreen import com.greenart7c3.citrine.ui.SettingsScreen @@ -154,7 +155,7 @@ class MainActivity : ComponentActivity() { Scaffold( bottomBar = { - if (destinationRoute != Route.Logs.route && !destinationRoute.startsWith("Feed") && destinationRoute != Route.DatabaseInfo.route) { + if (destinationRoute != Route.Logs.route && !destinationRoute.startsWith("Feed") && destinationRoute != Route.DatabaseInfo.route && !destinationRoute.startsWith("Contacts")) { NavigationBar(tonalElevation = 0.dp) { items.forEach { val selected = destinationRoute == it.route @@ -177,7 +178,7 @@ class MainActivity : ComponentActivity() { ) } } - } else if (destinationRoute.startsWith("Feed") || destinationRoute == Route.DatabaseInfo.route) { + } else if (destinationRoute.startsWith("Feed") || destinationRoute == Route.DatabaseInfo.route || destinationRoute.startsWith("Contacts")) { BottomAppBar { IconRow( center = true, @@ -192,14 +193,16 @@ class MainActivity : ComponentActivity() { } }, topBar = { - if (destinationRoute.startsWith("Feed") || destinationRoute == Route.DatabaseInfo.route) { + if (destinationRoute.startsWith("Feed") || destinationRoute == Route.DatabaseInfo.route || destinationRoute.startsWith("Contacts")) { CenterAlignedTopAppBar( title = { Text( text = if (destinationRoute.startsWith("Feed")) { stringResource(R.string.feed) - } else { + } else if (destinationRoute == Route.DatabaseInfo.route) { stringResource(R.string.database) + } else { + stringResource(R.string.restore_follows) }, ) }, @@ -294,6 +297,45 @@ class MainActivity : ComponentActivity() { } } + val launcherLoginContacts = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult(), + onResult = { result -> + if (result.resultCode != RESULT_OK) { + Toast.makeText( + context, + getString(R.string.sign_request_rejected), + Toast.LENGTH_SHORT, + ).show() + } else { + result.data?.let { + try { + val key = it.getStringExtra("signature") ?: "" + val packageName = it.getStringExtra("package") ?: "" + + val returnedKey = if (key.startsWith("npub")) { + when (val parsed = Nip19Bech32.uriToRoute(key)?.entity) { + is Nip19Bech32.NPub -> parsed.hex + else -> "" + } + } else { + key + } + + homeViewModel.signer = NostrSignerExternal( + returnedKey, + ExternalSignerLauncher(returnedKey, packageName), + ) + + homeViewModel.setPubKey(returnedKey) + navController.navigate(Route.ContactsScreen.route.replace("{pubkey}", returnedKey)) + } catch (e: Exception) { + Log.d(Citrine.TAG, e.message ?: "", e) + } + } + } + }, + ) + val launcherLogin = rememberLauncherForActivityResult( contract = ActivityResultContracts.StartActivityForResult(), onResult = { result -> @@ -528,30 +570,31 @@ class MainActivity : ComponentActivity() { ) } -// ElevatedButton( -// onClick = { -// try { -// val intent = Intent(Intent.ACTION_VIEW, Uri.parse("nostrsigner:")) -// val signerType = "get_public_key" -// intent.putExtra("type", signerType) -// intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP) -// launcherLogin.launch(intent) -// } catch (e: Exception) { -// Log.d(Citrine.TAG, e.message ?: "", e) -// coroutineScope.launch(Dispatchers.Main) { -// Toast.makeText( -// context, -// getString(R.string.no_external_signer_installed), -// Toast.LENGTH_SHORT, -// ).show() -// } -// val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/greenart7c3/Amber/releases")) -// launcherLogin.launch(intent) -// } -// }, -// ) { -// Text(stringResource(R.string.restore_follows)) -// } + ElevatedButton( + modifier = Modifier.fillMaxWidth(), + onClick = { + try { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse("nostrsigner:")) + val signerType = "get_public_key" + intent.putExtra("type", signerType) + intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP) + launcherLoginContacts.launch(intent) + } catch (e: Exception) { + Log.d(Citrine.TAG, e.message ?: "", e) + coroutineScope.launch(Dispatchers.Main) { + Toast.makeText( + context, + getString(R.string.no_external_signer_installed), + Toast.LENGTH_SHORT, + ).show() + } + val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/greenart7c3/Amber/releases")) + launcherLoginContacts.launch(intent) + } + }, + ) { + Text(stringResource(R.string.restore_follows)) + } ElevatedButton( modifier = Modifier.fillMaxWidth(), @@ -711,6 +754,23 @@ class MainActivity : ComponentActivity() { }, ) + composable( + Route.ContactsScreen.route, + arguments = listOf(navArgument("pubkey") { type = NavType.StringType }), + content = { + it.arguments?.getString("pubkey")?.let { pubkey -> + ContactsScreen( + modifier = Modifier + .fillMaxSize() + .padding(padding) + .padding(16.dp), + pubKey = pubkey, + navController = navController, + ) + } + }, + ) + composable(Route.Settings.route) { val context = LocalContext.current SettingsScreen( diff --git a/app/src/main/java/com/greenart7c3/citrine/database/AppDatabase.kt b/app/src/main/java/com/greenart7c3/citrine/database/AppDatabase.kt index de35e98..a56517a 100644 --- a/app/src/main/java/com/greenart7c3/citrine/database/AppDatabase.kt +++ b/app/src/main/java/com/greenart7c3/citrine/database/AppDatabase.kt @@ -23,6 +23,9 @@ abstract class AppDatabase : RoomDatabase() { @Volatile private var database: AppDatabase? = null + @Volatile + private var historyDatabase: AppDatabase? = null + fun getDatabase(context: Context): AppDatabase { return database ?: synchronized(this) { val instance = Room.databaseBuilder( @@ -41,6 +44,39 @@ abstract class AppDatabase : RoomDatabase() { } } +@Database( + entities = [EventEntity::class, TagEntity::class], + version = 2, +) +@TypeConverters(Converters::class) +abstract class HistoryDatabase : RoomDatabase() { + abstract fun eventDao(): EventDao + + companion object { + @Volatile + private var database: AppDatabase? = null + + @Volatile + private var historyDatabase: AppDatabase? = null + + fun getDatabase(context: Context): AppDatabase { + return database ?: synchronized(this) { + val instance = Room.databaseBuilder( + context, + AppDatabase::class.java, + "citrine_history_database", + ) + // .setQueryCallback(AppDatabaseCallback(), Executors.newSingleThreadExecutor()) + .addMigrations(MIGRATION_1_2) + .build() + + database = instance + instance + } + } + } +} + val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(db: SupportSQLiteDatabase) { db.execSQL("CREATE INDEX IF NOT EXISTS `most_common_search_is_kind` ON `EventEntity` (`kind` ASC)") diff --git a/app/src/main/java/com/greenart7c3/citrine/database/EventDao.kt b/app/src/main/java/com/greenart7c3/citrine/database/EventDao.kt index 56c033d..b61fc54 100644 --- a/app/src/main/java/com/greenart7c3/citrine/database/EventDao.kt +++ b/app/src/main/java/com/greenart7c3/citrine/database/EventDao.kt @@ -50,7 +50,7 @@ interface EventDao { @Transaction suspend fun countEventsWithExpirations(now: Long): List - @Query("SELECT * FROM EventEntity WHERE pubkey = :pubkey and kind = 3 ORDER BY createdAt DESC, id ASC LIMIT 5") + @Query("SELECT * FROM EventEntity WHERE pubkey = :pubkey and kind = 3 ORDER BY createdAt DESC, id ASC") @Transaction suspend fun getContactLists(pubkey: String): List diff --git a/app/src/main/java/com/greenart7c3/citrine/server/CustomWebSocketServer.kt b/app/src/main/java/com/greenart7c3/citrine/server/CustomWebSocketServer.kt index 5476a7f..fcbbbbe 100644 --- a/app/src/main/java/com/greenart7c3/citrine/server/CustomWebSocketServer.kt +++ b/app/src/main/java/com/greenart7c3/citrine/server/CustomWebSocketServer.kt @@ -8,6 +8,7 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.greenart7c3.citrine.BuildConfig import com.greenart7c3.citrine.Citrine import com.greenart7c3.citrine.database.AppDatabase +import com.greenart7c3.citrine.database.HistoryDatabase import com.greenart7c3.citrine.database.toEventWithTags import com.greenart7c3.citrine.service.CustomWebSocketService import com.greenart7c3.citrine.service.LocalPreferences @@ -283,6 +284,7 @@ class CustomWebSocketServer( save(event, connection) val ids = appDatabase.eventDao().getOldestReplaceable(event.kind, event.pubKey, event.tags.firstOrNull { it.size > 1 && it[0] == "d" }?.get(1) ?: "") appDatabase.eventDao().delete(ids, event.pubKey) + HistoryDatabase.getDatabase(Citrine.getInstance()).eventDao().insertEventWithTags(event.toEventWithTags(), connection = connection, sendEventToSubscriptions = false) } private suspend fun override(event: Event, connection: Connection?) { @@ -290,6 +292,7 @@ class CustomWebSocketServer( val ids = appDatabase.eventDao().getByKind(event.kind, event.pubKey).drop(1) if (ids.isEmpty()) return appDatabase.eventDao().delete(ids, event.pubKey) + HistoryDatabase.getDatabase(Citrine.getInstance()).eventDao().insertEventWithTags(event.toEventWithTags(), connection = connection, sendEventToSubscriptions = false) } private suspend fun save(event: Event, connection: Connection?) { diff --git a/app/src/main/java/com/greenart7c3/citrine/ui/ContactsScreen.kt b/app/src/main/java/com/greenart7c3/citrine/ui/ContactsScreen.kt new file mode 100644 index 0000000..1af7ee0 --- /dev/null +++ b/app/src/main/java/com/greenart7c3/citrine/ui/ContactsScreen.kt @@ -0,0 +1,335 @@ +package com.greenart7c3.citrine.ui + +import android.app.Activity +import android.util.Log +import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +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 +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.material3.Card +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ElevatedButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +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.res.stringResource +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import com.greenart7c3.citrine.Citrine +import com.greenart7c3.citrine.R +import com.greenart7c3.citrine.database.AppDatabase +import com.greenart7c3.citrine.database.HistoryDatabase +import com.greenart7c3.citrine.database.toEvent +import com.greenart7c3.citrine.okhttp.HttpClientManager +import com.greenart7c3.citrine.service.CustomWebSocketService +import com.greenart7c3.citrine.utils.toDateString +import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES +import com.vitorpamplona.ammolite.relays.RelaySetupInfo +import com.vitorpamplona.ammolite.relays.RelaySetupInfoToConnect +import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent +import com.vitorpamplona.quartz.events.ContactListEvent +import com.vitorpamplona.quartz.signers.ExternalSignerLauncher +import com.vitorpamplona.quartz.signers.NostrSignerExternal +import kotlin.coroutines.cancellation.CancellationException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +@Composable +fun ContactsScreen( + modifier: Modifier, + pubKey: String, + navController: NavController, +) { + var loading by remember { + mutableStateOf(true) + } + val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() + + val signer = NostrSignerExternal( + pubKey, + ExternalSignerLauncher(pubKey, ""), + ) + val launcher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult(), + onResult = { result -> + if (result.resultCode != Activity.RESULT_OK) { + Toast.makeText( + context, + context.getString(R.string.sign_request_rejected), + Toast.LENGTH_SHORT, + ).show() + } else { + result.data?.let { + coroutineScope.launch(Dispatchers.IO) { + signer.launcher.newResult(it) + } + } + } + }, + ) + signer.launcher.registerLauncher( + launcher = { + try { + launcher.launch(it) + } catch (e: Exception) { + if (e is CancellationException) throw e + Log.e("Signer", "Error opening Signer app", e) + coroutineScope.launch { + Toast.makeText( + context, + context.getString(R.string.make_sure_the_signer_application_has_authorized_this_transaction), + Toast.LENGTH_LONG, + ).show() + } + } + }, + contentResolver = { context.contentResolver }, + ) + + val events = remember { mutableListOf() } + var outboxRelays: AdvertisedRelayListEvent? = null + LaunchedEffect(Unit) { + coroutineScope.launch(Dispatchers.IO) { + loading = true + val dataBaseEvents = HistoryDatabase.getDatabase(context).eventDao().getContactLists(pubKey) + for (it in dataBaseEvents) { + val contactListEvent = it.toEvent() as ContactListEvent + Log.d("ContactsScreen", "ContactListEvent: $contactListEvent") + events.add(contactListEvent) + } + val dataBaseOutboxRelays = AppDatabase.getDatabase(context).eventDao().getAdvertisedRelayList(pubKey) + outboxRelays = dataBaseOutboxRelays?.toEvent() as? AdvertisedRelayListEvent + + loading = false + } + } + + if (loading) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + CircularProgressIndicator() + } + } else { + var useProxy by remember { + mutableStateOf(false) + } + var proxyPort by remember { + mutableStateOf(TextFieldValue("9050")) + } + + Column( + modifier = modifier + .background(MaterialTheme.colorScheme.background) + .fillMaxSize(), + ) { + if (events.isEmpty()) { + Column( + Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + stringResource(R.string.no_follow_list_found), + textAlign = TextAlign.Center, + ) + } + } + + LazyColumn( + Modifier.fillMaxSize(), + ) { + item { + Column { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 4.dp) + .clickable { + useProxy = !useProxy + }, + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + modifier = Modifier.weight(1f), + text = stringResource(R.string.use_proxy), + ) + Switch( + checked = useProxy, + onCheckedChange = { + useProxy = !useProxy + }, + ) + } + OutlinedTextField( + proxyPort, + modifier = Modifier + .fillMaxWidth() + .padding(4.dp), + onValueChange = { + if (it.text.toIntOrNull() == null) { + Toast.makeText( + context, + "Invalid port", + Toast.LENGTH_SHORT, + ).show() + return@OutlinedTextField + } + proxyPort = it + }, + label = { + Text(stringResource(R.string.proxy_port)) + }, + ) + } + } + items(events.size) { + val event = events[it] + Card( + Modifier + .fillMaxWidth() + .padding(4.dp), + ) { + Text( + "Date: ${event.createdAt.toDateString()}", + modifier = Modifier.padding( + start = 6.dp, + end = 6.dp, + top = 6.dp, + ), + ) + Text( + "Following: ${event.verifiedFollowKeySet().size}", + modifier = Modifier.padding(horizontal = 6.dp), + ) + Text( + "Communities: ${event.verifiedFollowAddressSet().size}", + modifier = Modifier.padding(horizontal = 6.dp), + ) + Text( + "Hashtags: ${event.countFollowTags()}", + modifier = Modifier.padding(horizontal = 6.dp), + ) + Text( + "Relays: ${event.relays()?.keys?.size ?: 0}", + modifier = Modifier.padding(horizontal = 6.dp), + ) + Row( + Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + ) { + ElevatedButton( + onClick = { + val relays = event.relays() + if (relays.isNullOrEmpty() && outboxRelays == null) { + coroutineScope.launch { + Toast.makeText( + context, + context.getString(R.string.no_relays_found), + Toast.LENGTH_SHORT, + ).show() + } + return@ElevatedButton + } + + ContactListEvent.create( + event.content, + event.tags, + signer, + ) { signedEvent -> + val outbox = outboxRelays?.writeRelays()?.map { relay -> + RelaySetupInfo( + url = relay, + read = true, + write = true, + feedTypes = COMMON_FEED_TYPES, + ) + } + val localRelays = outbox ?: relays?.mapNotNull { relay -> + if (relay.value.write) { + RelaySetupInfo( + relay.key, + relay.value.read, + relay.value.write, + COMMON_FEED_TYPES, + ) + } else { + null + } + } + + if (localRelays == null) return@create + + coroutineScope.launch(Dispatchers.IO) { + loading = true + if (useProxy) { + HttpClientManager.setDefaultProxyOnPort(proxyPort.text.toInt()) + } else { + HttpClientManager.setDefaultProxy(null) + } + + Citrine.getInstance().client.reconnect( + localRelays.map { + RelaySetupInfoToConnect( + it.url, + it.read, + it.write, + useProxy, + it.feedTypes, + ) + }.toTypedArray(), + ) + delay(1000) + Citrine.getInstance().client.sendAndWaitForResponse(signedEvent, forceProxy = useProxy, relayList = localRelays) + CustomWebSocketService.server?.innerProcessEvent(signedEvent, null) + Citrine.getInstance().client.getAll().forEach { + it.disconnect() + } + loading = false + coroutineScope.launch(Dispatchers.Main) { + navController.navigateUp() + } + } + } + }, + modifier = Modifier.padding( + start = 6.dp, + end = 6.dp, + bottom = 6.dp, + ), + ) { + Text(stringResource(R.string.restore)) + } + } + } + } + } + } + } +} diff --git a/app/src/main/java/com/greenart7c3/citrine/ui/dialogs/ContactsDialog.kt b/app/src/main/java/com/greenart7c3/citrine/ui/dialogs/ContactsDialog.kt deleted file mode 100644 index b5b21b3..0000000 --- a/app/src/main/java/com/greenart7c3/citrine/ui/dialogs/ContactsDialog.kt +++ /dev/null @@ -1,349 +0,0 @@ -package com.greenart7c3.citrine.ui.dialogs - -import android.app.Activity -import android.util.Log -import android.widget.Toast -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts -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 -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.material3.Card -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.ElevatedButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Surface -import androidx.compose.material3.Switch -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -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.res.stringResource -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog -import androidx.compose.ui.window.DialogProperties -import com.greenart7c3.citrine.Citrine -import com.greenart7c3.citrine.R -import com.greenart7c3.citrine.database.AppDatabase -import com.greenart7c3.citrine.database.toEvent -import com.greenart7c3.citrine.okhttp.HttpClientManager -import com.greenart7c3.citrine.ui.CloseButton -import com.greenart7c3.citrine.utils.toDateString -import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES -import com.vitorpamplona.ammolite.relays.RelaySetupInfo -import com.vitorpamplona.ammolite.relays.RelaySetupInfoToConnect -import com.vitorpamplona.quartz.encoders.bechToBytes -import com.vitorpamplona.quartz.encoders.toHexKey -import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent -import com.vitorpamplona.quartz.events.ContactListEvent -import com.vitorpamplona.quartz.signers.ExternalSignerLauncher -import com.vitorpamplona.quartz.signers.NostrSignerExternal -import kotlin.coroutines.cancellation.CancellationException -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch - -@Composable -fun ContactsDialog(pubKey: String, onClose: () -> Unit) { - var loading by remember { - mutableStateOf(true) - } - val context = LocalContext.current - val coroutineScope = rememberCoroutineScope() - - val signer = NostrSignerExternal( - pubKey.bechToBytes().toHexKey(), - ExternalSignerLauncher(pubKey, ""), - ) - val launcher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.StartActivityForResult(), - onResult = { result -> - if (result.resultCode != Activity.RESULT_OK) { - Toast.makeText( - context, - context.getString(R.string.sign_request_rejected), - Toast.LENGTH_SHORT, - ).show() - } else { - result.data?.let { - coroutineScope.launch(Dispatchers.IO) { - signer.launcher.newResult(it) - } - } - } - }, - ) - signer.launcher.registerLauncher( - launcher = { - try { - launcher.launch(it) - } catch (e: Exception) { - if (e is CancellationException) throw e - Log.e("Signer", "Error opening Signer app", e) - coroutineScope.launch { - Toast.makeText( - context, - context.getString(R.string.make_sure_the_signer_application_has_authorized_this_transaction), - Toast.LENGTH_LONG, - ).show() - } - } - }, - contentResolver = { context.contentResolver }, - ) - - val events = mutableListOf() - var outboxRelays: AdvertisedRelayListEvent? = null - LaunchedEffect(Unit) { - coroutineScope.launch(Dispatchers.IO) { - loading = true - val dataBaseEvents = AppDatabase.getDatabase(context).eventDao().getContactLists(pubKey.bechToBytes().toHexKey()) - dataBaseEvents.forEach { - events.add(it.toEvent() as ContactListEvent) - } - val dataBaseOutboxRelays = AppDatabase.getDatabase(context).eventDao().getAdvertisedRelayList(pubKey.bechToBytes().toHexKey()) - outboxRelays = dataBaseOutboxRelays?.toEvent() as? AdvertisedRelayListEvent - - loading = false - } - } - - Dialog( - onDismissRequest = onClose, - properties = DialogProperties(usePlatformDefaultWidth = false), - ) { - Surface( - Modifier.fillMaxSize(), - ) { - if (loading) { - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - CircularProgressIndicator() - } - } else { - var useProxy by remember { - mutableStateOf(false) - } - var proxyPort by remember { - mutableStateOf(TextFieldValue("9050")) - } - - Column( - modifier = Modifier - .background(MaterialTheme.colorScheme.background) - .fillMaxSize(), - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(10.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - CloseButton( - onCancel = onClose, - ) - } - - if (events.isEmpty()) { - Column( - Modifier.fillMaxSize(), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Text( - stringResource(R.string.no_follow_list_found), - textAlign = TextAlign.Center, - ) - } - } - - LazyColumn( - Modifier.fillMaxSize(), - ) { - item { - Column { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 4.dp) - .clickable { - useProxy = !useProxy - }, - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - modifier = Modifier.weight(1f), - text = stringResource(R.string.use_proxy), - ) - Switch( - checked = useProxy, - onCheckedChange = { - useProxy = !useProxy - }, - ) - } - OutlinedTextField( - proxyPort, - modifier = Modifier - .fillMaxWidth() - .padding(4.dp), - onValueChange = { - if (it.text.toIntOrNull() == null) { - Toast.makeText( - context, - "Invalid port", - Toast.LENGTH_SHORT, - ).show() - return@OutlinedTextField - } - proxyPort = it - }, - label = { - Text(stringResource(R.string.proxy_port)) - }, - ) - } - } - items(events.size) { - val event = events[it] - Card( - Modifier - .fillMaxWidth() - .padding(4.dp), - ) { - Text( - "Date: ${event.createdAt.toDateString()}", - modifier = Modifier.padding( - start = 6.dp, - end = 6.dp, - top = 6.dp, - ), - ) - Text( - "Following: ${event.verifiedFollowKeySet().size}", - modifier = Modifier.padding(horizontal = 6.dp), - ) - Text( - "Communities: ${event.verifiedFollowAddressSet().size}", - modifier = Modifier.padding(horizontal = 6.dp), - ) - Text( - "Hashtags: ${event.countFollowTags()}", - modifier = Modifier.padding(horizontal = 6.dp), - ) - Text( - "Relays: ${event.relays()?.keys?.size ?: 0}", - modifier = Modifier.padding(horizontal = 6.dp), - ) - Row( - Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center, - ) { - ElevatedButton( - onClick = { - val relays = event.relays() - if (relays.isNullOrEmpty() && outboxRelays == null) { - coroutineScope.launch { - Toast.makeText( - context, - context.getString(R.string.no_relays_found), - Toast.LENGTH_SHORT, - ).show() - } - return@ElevatedButton - } - - ContactListEvent.create( - event.content, - event.tags, - signer, - ) { signedEvent -> - val outbox = outboxRelays?.writeRelays()?.map { relay -> - RelaySetupInfo( - url = relay, - read = true, - write = true, - feedTypes = COMMON_FEED_TYPES, - ) - } - val localRelays = outbox ?: relays?.mapNotNull { relay -> - if (relay.value.write) { - RelaySetupInfo( - relay.key, - relay.value.read, - relay.value.write, - COMMON_FEED_TYPES, - ) - } else { - null - } - } - - if (localRelays == null) return@create - - coroutineScope.launch(Dispatchers.IO) { - loading = true - if (useProxy) { - HttpClientManager.setDefaultProxyOnPort(proxyPort.text.toInt()) - } else { - HttpClientManager.setDefaultProxy(null) - } - - Citrine.getInstance().client.reconnect( - localRelays.map { - RelaySetupInfoToConnect( - it.url, - it.read, - it.write, - useProxy, - it.feedTypes, - ) - }.toTypedArray(), - ) - delay(1000) - Citrine.getInstance().client.sendAndWaitForResponse(signedEvent, forceProxy = useProxy, relayList = localRelays) - Citrine.getInstance().client.getAll().forEach { - it.disconnect() - } - loading = false - onClose() - } - } - }, - modifier = Modifier.padding( - start = 6.dp, - end = 6.dp, - bottom = 6.dp, - ), - ) { - Text(stringResource(R.string.restore)) - } - } - } - } - } - } - } - } - } -} diff --git a/app/src/main/java/com/greenart7c3/citrine/ui/navigation/Route.kt b/app/src/main/java/com/greenart7c3/citrine/ui/navigation/Route.kt index c744f3d..e04ec37 100644 --- a/app/src/main/java/com/greenart7c3/citrine/ui/navigation/Route.kt +++ b/app/src/main/java/com/greenart7c3/citrine/ui/navigation/Route.kt @@ -43,4 +43,10 @@ sealed class Route( icon = Icons.Outlined.Settings, selectedIcon = Icons.Default.Settings, ) + + data object ContactsScreen : Route( + route = "Contacts/{pubkey}", + icon = Icons.Outlined.Settings, + selectedIcon = Icons.Default.Settings, + ) }