From 3521497cf8cf37fe32383e69160c2ddc8ddfa9dd Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 2 Mar 2023 08:51:59 -0500 Subject: [PATCH 01/36] Activating the loading of long text form across the app --- .../vitorpamplona/amethyst/service/NostrGlobalDataSource.kt | 3 ++- .../amethyst/service/NostrSingleEventDataSource.kt | 5 +++-- .../amethyst/service/NostrUserProfileDataSource.kt | 3 ++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrGlobalDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrGlobalDataSource.kt index b4b3ddf55..a1ea5d004 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrGlobalDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrGlobalDataSource.kt @@ -1,6 +1,7 @@ package com.vitorpamplona.amethyst.service import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent +import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.TypedFilter import nostr.postr.JsonFilter @@ -10,7 +11,7 @@ object NostrGlobalDataSource: NostrDataSource("GlobalFeed") { fun createGlobalFilter() = TypedFilter( types = setOf(FeedType.GLOBAL), filter = JsonFilter( - kinds = listOf(TextNoteEvent.kind, ChannelMessageEvent.kind), + kinds = listOf(TextNoteEvent.kind, ChannelMessageEvent.kind, LongTextNoteEvent.kind), limit = 200 ) ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt index e7492022b..57f90eece 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt @@ -6,6 +6,7 @@ import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent 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.ReactionEvent import com.vitorpamplona.amethyst.service.model.ReportEvent import com.vitorpamplona.amethyst.service.model.RepostEvent @@ -35,7 +36,7 @@ object NostrSingleEventDataSource: NostrDataSource("SingleEventFeed") { types = FeedType.values().toSet(), filter = JsonFilter( kinds = listOf( - TextNoteEvent.kind, ReactionEvent.kind, RepostEvent.kind, ReportEvent.kind, LnZapEvent.kind, LnZapRequestEvent.kind + TextNoteEvent.kind, LongTextNoteEvent.kind, ReactionEvent.kind, RepostEvent.kind, ReportEvent.kind, LnZapEvent.kind, LnZapRequestEvent.kind ), tags = mapOf("e" to listOf(it.idHex)), since = it.lastReactionsDownloadTime @@ -69,7 +70,7 @@ object NostrSingleEventDataSource: NostrDataSource("SingleEventFeed") { types = FeedType.values().toSet(), filter = JsonFilter( kinds = listOf( - TextNoteEvent.kind, ReactionEvent.kind, RepostEvent.kind, LnZapEvent.kind, LnZapRequestEvent.kind, + TextNoteEvent.kind, LongTextNoteEvent.kind, ReactionEvent.kind, RepostEvent.kind, LnZapEvent.kind, LnZapRequestEvent.kind, ChannelMessageEvent.kind, ChannelCreateEvent.kind, ChannelMetadataEvent.kind ), ids = interestedEvents.toList() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt index 7da717a1a..c399f5e3c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt @@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.service import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.model.LnZapEvent +import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.TypedFilter import nostr.postr.JsonFilter @@ -38,7 +39,7 @@ object NostrUserProfileDataSource: NostrDataSource("UserProfileFeed") { TypedFilter( types = FeedType.values().toSet(), filter = JsonFilter( - kinds = listOf(TextNoteEvent.kind), + kinds = listOf(TextNoteEvent.kind, LongTextNoteEvent.kind), authors = listOf(it.pubkeyHex), limit = 200 ) From 10ce5a984e847fc1a575c6249a9dceccb1432841 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 2 Mar 2023 09:33:19 -0500 Subject: [PATCH 02/36] Simplifying logs --- .../main/java/com/vitorpamplona/amethyst/model/LocalCache.kt | 5 ++--- .../main/java/com/vitorpamplona/amethyst/service/Nip19.kt | 4 ++-- 2 files changed, 4 insertions(+), 5 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 8249c0507..371ed19e4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -276,9 +276,8 @@ object LocalCache { val pubKey = decodePublicKey(it.pubKeyHex) getOrCreateUser(pubKey.toHexKey()) } catch (e: Exception) { - println("Could not parse Hex key: ${it.pubKeyHex}") - println("UpdateFollows: " + event.toJson()) - e.printStackTrace() + Log.w("ContactList Parser", "Ignoring: Could not parse Hex key: ${it.pubKeyHex} in ${event.toJson()}") + //e.printStackTrace() null } }.filterNotNull().toSet(), diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip19.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip19.kt index 46d464af1..700afe405 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip19.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip19.kt @@ -36,8 +36,8 @@ class Nip19 { } } } catch (e: Throwable) { - println("Trying to Decode NIP19: ${uri}") - e.printStackTrace() + println("Issue trying to Decode NIP19 ${uri}: ${e.message}") + //e.printStackTrace() } return null From b95c01371486181665bb9c540189427a08df4cc8 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 2 Mar 2023 09:34:42 -0500 Subject: [PATCH 03/36] Restructuring the invalidateFeed events to happen quicker when users click the navigation row or when the app comes back from sleep. --- .../vitorpamplona/amethyst/model/LocalCache.kt | 2 +- .../amethyst/ui/screen/CardFeedViewModel.kt | 2 +- .../amethyst/ui/screen/FeedViewModel.kt | 4 +++- .../ui/screen/loggedIn/ChatroomListScreen.kt | 15 +++++++++++++++ .../amethyst/ui/screen/loggedIn/HomeScreen.kt | 17 +++++++++++++++++ .../ui/screen/loggedIn/NotificationScreen.kt | 18 ++++++++++++++++++ 6 files changed, 55 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 371ed19e4..b8cc49b01 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -798,7 +798,7 @@ class LocalCacheLiveData(val cache: LocalCache): LiveData(Local val scope = CoroutineScope(Job() + Dispatchers.Main) scope.launch { try { - delay(500) + delay(50) refresh() } finally { withContext(NonCancellable) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt index d1c1cfe91..4711e5582 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt @@ -129,7 +129,7 @@ open class CardFeedViewModel(val dataSource: FeedFilter): ViewModel() { var handlerWaiting = AtomicBoolean() - private fun invalidateData() { + fun invalidateData() { if (handlerWaiting.getAndSet(true)) return val scope = CoroutineScope(Job() + Dispatchers.Default) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt index 94c8cd364..e334b6b5e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt @@ -95,7 +95,9 @@ abstract class FeedViewModel(val localFilter: FeedFilter): ViewModel() { scope.launch { try { delay(50) - refresh() + // adds the time to perform the refresh into this delay + // holding off new updates in case of heavy refresh routines. + refreshSuspended() } finally { withContext(NonCancellable) { handlerWaiting.set(false) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomListScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomListScreen.kt index 4572dabd9..0087b96ef 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomListScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomListScreen.kt @@ -94,6 +94,21 @@ fun TabKnown(accountViewModel: AccountViewModel, navController: NavController) { feedViewModel.invalidateData() } + val lifeCycleOwner = LocalLifecycleOwner.current + DisposableEffect(accountViewModel) { + val observer = LifecycleEventObserver { source, event -> + if (event == Lifecycle.Event.ON_RESUME) { + NostrChatroomListDataSource.resetFilters() + feedViewModel.invalidateData() + } + } + + lifeCycleOwner.lifecycle.addObserver(observer) + onDispose { + lifeCycleOwner.lifecycle.removeObserver(observer) + } + } + Column(Modifier.fillMaxHeight()) { Column( modifier = Modifier.padding(vertical = 0.dp) 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 ab8cd67ae..5b91d4c31 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 @@ -27,6 +27,7 @@ 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.service.NostrChatroomListDataSource import com.vitorpamplona.amethyst.service.NostrHomeDataSource import com.vitorpamplona.amethyst.ui.dal.HomeConversationsFeedFilter import com.vitorpamplona.amethyst.ui.dal.HomeNewThreadFeedFilter @@ -58,6 +59,22 @@ fun HomeScreen(accountViewModel: AccountViewModel, navController: NavController) feedViewModelReplies.invalidateData() } + val lifeCycleOwner = LocalLifecycleOwner.current + DisposableEffect(accountViewModel) { + val observer = LifecycleEventObserver { source, event -> + if (event == Lifecycle.Event.ON_RESUME) { + NostrHomeDataSource.resetFilters() + feedViewModel.invalidateData() + feedViewModelReplies.invalidateData() + } + } + + lifeCycleOwner.lifecycle.addObserver(observer) + onDispose { + lifeCycleOwner.lifecycle.removeObserver(observer) + } + } + Column(Modifier.fillMaxHeight()) { Column( modifier = Modifier.padding(vertical = 0.dp) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt index 0ffd8bec6..6221913c7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt @@ -4,11 +4,15 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.padding 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.ui.Modifier +import androidx.compose.ui.platform.LocalLifecycleOwner 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.ui.dal.NotificationFeedFilter @@ -28,6 +32,20 @@ fun NotificationScreen(accountViewModel: AccountViewModel, navController: NavCon feedViewModel.refresh() } + val lifeCycleOwner = LocalLifecycleOwner.current + DisposableEffect(accountViewModel) { + val observer = LifecycleEventObserver { source, event -> + if (event == Lifecycle.Event.ON_RESUME) { + feedViewModel.invalidateData() + } + } + + lifeCycleOwner.lifecycle.addObserver(observer) + onDispose { + lifeCycleOwner.lifecycle.removeObserver(observer) + } + } + Column(Modifier.fillMaxHeight()) { Column( modifier = Modifier.padding(vertical = 0.dp) From 65d4d59e77c85aa13623132954c5f87802661e50 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 2 Mar 2023 09:35:43 -0500 Subject: [PATCH 04/36] Trying move the cursor to the first item when the list updates immediately after being first drawn --- .../java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt | 6 ++++++ .../amethyst/ui/screen/loggedIn/SearchScreen.kt | 2 -- 2 files changed, 6 insertions(+), 2 deletions(-) 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 2e554cee8..df931ea53 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 @@ -30,6 +30,7 @@ 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 +import kotlinx.coroutines.delay @Composable fun FeedView( @@ -96,6 +97,11 @@ private fun FeedLoaded( ) { val listState = rememberLazyListState() + LaunchedEffect(Unit) { + delay(500) + listState.animateScrollToItem(0) + } + LazyColumn( contentPadding = PaddingValues( top = 10.dp, 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 b90293bf6..7c66c1afe 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 @@ -88,8 +88,6 @@ fun SearchScreen(accountViewModel: AccountViewModel, navController: NavControlle LaunchedEffect(Unit) { feedViewModel.invalidateData() - delay(500) - feedViewModel.invalidateData() } DisposableEffect(accountViewModel) { From 4f9b1ab72c24cf4f88139fe9c9f750beea3e5f7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rio=20Nunes?= Date: Thu, 2 Mar 2023 14:40:44 -0300 Subject: [PATCH 05/36] Corrected translation! Changed from Mostar to Mostrar --- app/src/main/res/values-pt-rBR/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 505558701..12824f656 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -47,7 +47,7 @@ Perfil Filtros de segurança Sair - Mostar mais + Mostrar mais Fatura Lightning Pagar Doação Lightning @@ -173,4 +173,4 @@ Denunciar discurso de ódio Denunciar nudez / pornografia outros - \ No newline at end of file + From bab314ebc96698175663d41d9e44d19dc661b194 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 2 Mar 2023 13:38:05 -0500 Subject: [PATCH 06/36] Fixing Preview Error when the url is invalid --- .../vitorpamplona/amethyst/ui/components/UrlPreview.kt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreview.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreview.kt index bd993f872..d4adfc34d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreview.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreview.kt @@ -19,10 +19,16 @@ import kotlinx.coroutines.withContext @Composable fun UrlPreview(url: String, urlText: String) { - val default = UrlCachedPreviewer.cache[url]?.let { UrlPreviewState.Loaded(it) } ?: UrlPreviewState.Loading + val default = UrlCachedPreviewer.cache[url]?.let { + if (it.url == url) + UrlPreviewState.Loaded(it) + else + UrlPreviewState.Empty + + } ?: UrlPreviewState.Loading var context = LocalContext.current - var urlPreviewState by remember { mutableStateOf(default) } + var urlPreviewState by remember { mutableStateOf(UrlPreviewState.Loading) } // Doesn't use a viewModel because of viewModel reusing issues (too many UrlPreview are created). LaunchedEffect(url) { From 0369b453851882476018d444c179284e2932682e Mon Sep 17 00:00:00 2001 From: Oleg Koretsky Date: Thu, 2 Mar 2023 20:41:22 +0200 Subject: [PATCH 07/36] Add Russian (ru) translation --- .../vitorpamplona/amethyst/ui/MainActivity.kt | 1 - .../amethyst/ui/actions/ImageSaver.kt | 11 +- .../amethyst/ui/actions/ImageUploader.kt | 20 +- .../amethyst/ui/actions/NewPostViewModel.kt | 12 +- app/src/main/res/values-ar/strings.xml | 5 - app/src/main/res/values-pt-rBR/strings.xml | 5 - app/src/main/res/values-ru/strings.xml | 174 ++++++++++++++++++ app/src/main/res/values/strings.xml | 5 - 8 files changed, 186 insertions(+), 47 deletions(-) create mode 100644 app/src/main/res/values-ru/strings.xml 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 48ac5aee1..3b2b81287 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt @@ -21,7 +21,6 @@ import com.vitorpamplona.amethyst.service.relays.Client import com.vitorpamplona.amethyst.ui.screen.AccountScreen import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel import com.vitorpamplona.amethyst.ui.theme.AmethystTheme -import java.util.Locale class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { 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 400d728d7..5e1b20990 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 @@ -9,12 +9,11 @@ import android.os.Environment import android.provider.MediaStore import androidx.annotation.RequiresApi import androidx.core.net.toUri -import java.io.File -import com.vitorpamplona.amethyst.R import okhttp3.* import okio.BufferedSource import okio.IOException import okio.sink +import java.io.File object ImageSaver { @@ -50,7 +49,7 @@ object ImageSaver { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { val contentType = response.header("Content-Type") checkNotNull(contentType) { - context.getString(R.string.can_t_find_out_the_content_type) + "Can't find out the content type" } saveContentQ( @@ -58,7 +57,6 @@ object ImageSaver { contentType = contentType, contentSource = response.body.source(), contentResolver = context.contentResolver, - context = context ) } else { saveContentDefault( @@ -82,7 +80,6 @@ object ImageSaver { contentType: String, contentSource: BufferedSource, contentResolver: ContentResolver, - context: Context ) { val contentValues = ContentValues().apply { put(MediaStore.MediaColumns.DISPLAY_NAME, displayName) @@ -96,13 +93,13 @@ object ImageSaver { val uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues) checkNotNull(uri) { - context.getString(R.string.can_t_insert_the_new_content) + "Can't insert the new content" } try { val outputStream = contentResolver.openOutputStream(uri) checkNotNull(outputStream) { - context.getString(R.string.can_t_open_the_content_output_stream) + "Can't open the content output stream" } outputStream.use { 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 c426c3a71..ccb6b49cb 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 @@ -1,23 +1,14 @@ package com.vitorpamplona.amethyst.ui.actions import android.content.ContentResolver -import android.content.Context import android.net.Uri import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import java.io.IOException -import java.util.UUID -import okhttp3.Call -import okhttp3.Callback -import okhttp3.MediaType -import com.vitorpamplona.amethyst.R +import okhttp3.* import okhttp3.MediaType.Companion.toMediaType -import okhttp3.MultipartBody -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.RequestBody -import okhttp3.Response import okio.BufferedSink import okio.source +import java.io.IOException +import java.util.* object ImageUploader { fun uploadImage( @@ -25,7 +16,6 @@ object ImageUploader { contentResolver: ContentResolver, onSuccess: (String) -> Unit, onError: (Throwable) -> Unit, - context : Context ) { val contentType = contentResolver.getType(uri) @@ -43,7 +33,7 @@ object ImageUploader { override fun writeTo(sink: BufferedSink) { val imageInputStream = contentResolver.openInputStream(uri) checkNotNull(imageInputStream) { - context.getString(R.string.can_t_open_the_image_input_stream) + "Can't open the image input stream" } imageInputStream.source().use(sink::writeAll) @@ -66,7 +56,7 @@ object ImageUploader { val tree = jacksonObjectMapper().readTree(body.string()) val url = tree?.get("data")?.get("link")?.asText() checkNotNull(url) { - context.getString(R.string.there_must_be_an_uploaded_image_url_in_the_response) + "There must be an uploaded image URL in the response" } onSuccess(url) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt index 37a9114a4..de6470d22 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt @@ -9,12 +9,7 @@ import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.parseDirtyWordForKey -import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.* import com.vitorpamplona.amethyst.ui.components.isValidURL import com.vitorpamplona.amethyst.ui.components.noProtocolUrlValidator import kotlinx.coroutines.flow.MutableSharedFlow @@ -134,10 +129,9 @@ class NewPostViewModel: ViewModel() { onError = { isUploadingImage = false viewModelScope.launch { - imageUploadingError.emit(context.getString(R.string.failed_to_upload_the_image)) + imageUploadingError.emit("Failed to upload the image") } - }, - context = context + } ) } diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 829d4df40..4e0578e1b 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -59,11 +59,6 @@ خطأ في تحليل المعاينة لـ %1$s : %2$s معاينة صورة البطاقة لـ %1$s قناة جديدة - لا يمكن معرفة نوع المحتوى - لا يمكن إدراج المحتوى الجديد - لا يمكن فتح دفق إخراج المحتوى - لا يمكن فتح دفق إدخال الصورة - يجب أن يكون هناك عنوان URL للصورة التي تم تحميلها في الاستجابة اسم القناة مجموعتي الرائعة رابط الصورة diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 505558701..b8a45d4c4 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -59,11 +59,6 @@ Erro ao analisar a visualização para %1$s: %2$s Visualize a imagem do cartão para %1$s Novo Canal - Não é possível descobrir o tipo de conteúdo - Não é possível inserir o novo conteúdo - Não é possível abrir o fluxo de saída de conteúdo - Não é possível abrir o fluxo de entrada de imagem - Deve haver um URL de imagem na resposta Nome do Canal Meu grupo Url da foto diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml new file mode 100644 index 000000000..ec443deab --- /dev/null +++ b/app/src/main/res/values-ru/strings.xml @@ -0,0 +1,174 @@ + + Amethyst + Amethyst Debug + Наведите на QR код + Показать QR + Фото профиля + Сканировать QR + Показать + Запись была помечена как неуместная + запись не найдена + Фото канала + Связанное событие не найдено + Не удалось расшифровать сообщение + Фото группы + Откровенное содержание + Спам + Выдача себя за другое лицо + Незаконные действия + Неизвестно + Иконка релея + Неизвестный автор + Скопировать текст + Скопировать ключ пользователя + Скопировать ID записи + Разослать + Блокировать и скрыть пользователя + Сообщить о спаме / мошеничестве + Сообщить о выдаче себя за другое лицо + Сообщить об откровенном содержании + Сообщить о незаконных действиях + Войдите с приватным ключом чтобы ответить + Войдите с приватным ключом чтобы продвигать записи + Войдите с приватным ключом чтобы лайкать записи + Не настроены запы. Нажмите и удерживайте для настройки + Войдите с приватным ключом чтобы запать + Запы + Просмотры + Продвинуть + Цитировать + Новая сумма в sat + Добавить + "в ответ " + " и " + "в канале " + Баннер профиля + " Подписок" + " Подписчиков" + Профиль + Фильтры безопасности + Выйти + Больше + Lightning инвойс + Оплатить + Lightning чаевые + Заметка для получателя + Большое спасибо! + Сумма в sat + Отправить + "Не переводить с " + "Не удалось создать предпросмотр для %1$s : %2$s" + "Предпросмотр для %1$s" + Новый канал + Название канала + Моя новая группа + URL фотографии + Описание + "О нас.. " + Что нового? + Опубликовать + Сохранить + Создать + Отменить + Не удалось загрузить фото + Адрес релея + Записи + Ошибки + Домашняя лента + Лента личных сообщений + Лента чатов + Глобальная лента + Добавить релей + Отображаемое имя + Моё отображаемое имя + Никнейм + Мой никнейм + Обо мне + URL фотографии + URL баннера + URL сайта + LN адрес + LN URL (устаревш.) + Фото сохранено в галерею + Не удалось сохранить фото + Загрузить\nфото + Загрузка… + Пользователь не усталовил Lightning адрес для получения чаевых + "ответить.. " + Копирует ID записи для отправки + Скопировать ID канала (записи) + Редактирует метаданные канала + Войти + Известные + Новые запросы + Заблокированные пользователи + Новые треды + Обсуждения + Записи + Ответы + "Подписок" + "Жалоб" + Больше опций + " Релеев" + Сайт + Lightning адрес + Копирует Nsec ID (ваш пароль) для резервного копирования + Скопировать приватный ключ + Копирует публичный ключ для отправки + Скопировать публичный ключ (NPub) + Отправить сообщение + Редактирует метаданные пользователя + Подписаться + Разблокировать + Скопировать ID пользователя + Разблокировать пользователя + "npub, hex, никнейм " + Очистить + Логотип приложения + nsec / npub / приватный ключ в hex + Показать пароль + Скрыть пароль + Некорректный ключ + "Я принимаю " + условия использования + Нужно принять условия использования + Нужно ввести ключ + Войти + Сгенерировать ключ + Загрузка ленты + "Не удалось загрузить ответы: " + Повторить + Лента пуста. + Обновить + создал(а) + с описанием + и фото + сменил(а) название на + описание на + и фото на + Выйти + Отписаться + Канал создан + Информация о канале изменена на + Публичный чат + записей получено + Удалить + Автоматически + переведено с + на + Показать сперва на + + Всегда переводить на + NIP-05 + LNURL... + никогда + сейчас + ч + м + д + Нагота + Брань / Оскорбления + Сообщить об оскорблениях + Сообщить о наготе / порно + других + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 952cd426c..37cb35f12 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -60,11 +60,6 @@ "Error parsing preview for %1$s : %2$s" "Preview Card Image for %1$s" New Channel - Can\'t find out the content type - Can\'t insert the new content - Can\'t open the content output stream - Can\'t open the image input stream - There must be an uploaded image URL in the response Channel Name My Awesome Group Picture Url From c0bcb638c0422a3360075a3042b4eaa2d0bf219c Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 2 Mar 2023 16:14:06 -0500 Subject: [PATCH 08/36] Bringing TextNoteEvent from NostrPostr to Amethyst's codebase and Restructuring Addressable Notes. --- .../vitorpamplona/amethyst/model/Account.kt | 6 +- .../com/vitorpamplona/amethyst/model/Hex.kt | 1 + .../amethyst/model/LocalCache.kt | 18 ++---- .../com/vitorpamplona/amethyst/model/Note.kt | 17 +++++- .../com/vitorpamplona/amethyst/model/User.kt | 22 ++++++++ .../service/NostrAccountDataSource.kt | 2 +- .../amethyst/service/NostrDataSource.kt | 6 +- .../amethyst/service/NostrGlobalDataSource.kt | 2 +- .../amethyst/service/NostrHomeDataSource.kt | 2 +- .../service/NostrSingleEventDataSource.kt | 31 ++++++++++- .../service/NostrUserProfileDataSource.kt | 2 +- .../service/model/LnZapRequestEvent.kt | 6 +- .../service/model/LongTextNoteEvent.kt | 6 ++ .../amethyst/service/model/ReactionEvent.kt | 7 ++- .../amethyst/service/model/ReportEvent.kt | 7 ++- .../amethyst/service/model/RepostEvent.kt | 7 ++- .../amethyst/service/model/TextNoteEvent.kt | 45 +++++++++++++++ .../amethyst/ui/actions/NewPostView.kt | 2 +- .../amethyst/ui/actions/NewPostViewModel.kt | 9 ++- .../amethyst/ui/components/ClickableRoute.kt | 6 +- .../amethyst/ui/components/RichTextViewer.kt | 55 ++++++++++++++----- .../amethyst/ui/dal/GlobalFeedFilter.kt | 2 +- .../ui/dal/HomeConversationsFeedFilter.kt | 2 +- .../ui/dal/HomeNewThreadFeedFilter.kt | 2 +- .../amethyst/ui/dal/NotificationFeedFilter.kt | 2 +- .../ui/dal/UserProfileNewThreadFeedFilter.kt | 2 +- .../amethyst/ui/navigation/Routes.kt | 2 +- .../amethyst/ui/note/BoostSetCompose.kt | 2 +- .../amethyst/ui/note/ChatroomCompose.kt | 4 +- .../amethyst/ui/note/LikeSetCompose.kt | 2 +- .../amethyst/ui/note/MultiSetCompose.kt | 2 +- .../amethyst/ui/note/NoteCompose.kt | 9 +-- .../amethyst/ui/note/ZapSetCompose.kt | 2 +- .../amethyst/ui/screen/ThreadFeedView.kt | 7 ++- .../ui/screen/loggedIn/ChannelScreen.kt | 2 +- 35 files changed, 231 insertions(+), 70 deletions(-) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/service/model/TextNoteEvent.kt diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 50f1f4f00..1eb820b1d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -34,7 +34,7 @@ import nostr.postr.events.DeletionEvent import nostr.postr.events.Event import nostr.postr.events.MetadataEvent import nostr.postr.events.PrivateDmEvent -import nostr.postr.events.TextNoteEvent +import com.vitorpamplona.amethyst.service.model.TextNoteEvent import nostr.postr.toHex val DefaultChannels = setOf( @@ -290,18 +290,20 @@ class Account( val repliesToHex = replyTo?.map { it.idHex } val mentionsHex = mentions?.map { it.pubkeyHex } + val addressesHex = replyTo?.mapNotNull { it.address() } val signedEvent = TextNoteEvent.create( msg = message, replyTos = repliesToHex, mentions = mentionsHex, + addresses = addressesHex, privateKey = loggedIn.privKey!! ) Client.send(signedEvent) LocalCache.consume(signedEvent) } - fun sendChannelMeesage(message: String, toChannel: String, replyingTo: Note? = null, mentions: List?) { + fun sendChannelMessage(message: String, toChannel: String, replyingTo: Note? = null, mentions: List?) { if (!isWriteable()) return val repliesToHex = listOfNotNull(replyingTo?.idHex).ifEmpty { null } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Hex.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Hex.kt index 429e53b6b..c8fb14180 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Hex.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Hex.kt @@ -74,6 +74,7 @@ fun parseDirtyWordForKey(mightBeAKey: String): DirtyKeyInfo? { return DirtyKeyInfo("note", keyB32.bechToBytes().toHexKey(), restOfWord) } + } catch (e: Exception) { e.printStackTrace() } 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 b8cc49b01..da84d2fa4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -37,7 +37,7 @@ import nostr.postr.events.Event import nostr.postr.events.MetadataEvent import nostr.postr.events.PrivateDmEvent import nostr.postr.events.RecommendRelayEvent -import nostr.postr.events.TextNoteEvent +import com.vitorpamplona.amethyst.service.model.TextNoteEvent import nostr.postr.toHex import nostr.postr.toNpub @@ -212,7 +212,7 @@ object LocalCache { //Log.d("TN", "New Note (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${note.event?.content?.take(100)} ${formattedDateTime(event.createdAt)}") // Prepares user's profile view. - author.addNote(note) + author.addLongFormNote(note) // Adds notifications to users. mentions.forEach { @@ -222,11 +222,6 @@ object LocalCache { it.author?.addTaggedPost(note) } - // Counts the replies - replyTo.forEach { - it.addReply(note) - } - refreshObservers() } @@ -492,7 +487,6 @@ object LocalCache { val note = getOrCreateNote(event.id.toHex()) oldChannel.addNote(note) - note.channel = oldChannel note.loadEvent(event, author, emptyList(), emptyList()) refreshObservers() @@ -514,7 +508,6 @@ object LocalCache { val note = getOrCreateNote(event.id.toHex()) oldChannel.addNote(note) - note.channel = oldChannel note.loadEvent(event, author, emptyList(), emptyList()) refreshObservers() @@ -553,7 +546,6 @@ object LocalCache { .mapNotNull { checkGetOrCreateNote(it) } .filter { it.event !is ChannelCreateEvent } - note.channel = channel note.loadEvent(event, author, mentions, replyTo) //Log.d("CM", "New Note (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${note.event?.content} ${formattedDateTime(event.createdAt)}") @@ -658,8 +650,9 @@ object LocalCache { fun findNotesStartingWith(text: String): List { return notes.values.filter { - (it.event is TextNoteEvent && it.event?.content?.contains(text, true) ?: false) + (it.event is TextNoteEvent && it.event?.content?.contains(text, true) ?: false) || (it.event is ChannelMessageEvent && it.event?.content?.contains(text, true) ?: false) + || (it.event is LongTextNoteEvent && it.event?.content?.contains(text, true) ?: false) || it.idHex.startsWith(text, true) || it.idNote().startsWith(text, true) } @@ -745,7 +738,7 @@ object LocalCache { fun pruneHiddenMessages(account: Account) { val toBeRemoved = account.hiddenUsers.map { - users[it]?.notes ?: emptySet() + (users[it]?.notes ?: emptySet()) + (users[it]?.longFormNotes?.values?.flatten() ?: emptySet()) }.flatten() account.hiddenUsers.forEach { @@ -754,6 +747,7 @@ object LocalCache { toBeRemoved.forEach { it.author?.removeNote(it) + it.author?.removeLongFormNote(it) // reverts the add it.mentions?.forEach { user -> diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index d464fe9e1..14db2aa01 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -2,7 +2,11 @@ package com.vitorpamplona.amethyst.model import androidx.lifecycle.LiveData import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource +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.service.model.LnZapEvent +import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent import com.vitorpamplona.amethyst.service.model.ReactionEvent import com.vitorpamplona.amethyst.service.model.RepostEvent import com.vitorpamplona.amethyst.service.relays.Relay @@ -23,6 +27,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import nostr.postr.events.Event +import nostr.postr.toHex val tagSearch = Pattern.compile("(?:\\s|\\A)\\#\\[([0-9]+)\\]") @@ -49,14 +54,22 @@ class Note(val idHex: String) { var relays = setOf() private set - var channel: Channel? = null - var lastReactionsDownloadTime: Long? = null fun id() = Hex.decode(idHex) fun idNote() = id().toNote() fun idDisplayNote() = idNote().toShortenHex() + fun channel(): Channel? { + val channelHex = (event as? ChannelMessageEvent)?.channel ?: + (event as? ChannelMetadataEvent)?.channel ?: + (event as? ChannelCreateEvent)?.let { idHex } + + return channelHex?.let { LocalCache.checkGetOrCreateChannel(it) } + } + + fun address() = (event as? LongTextNoteEvent)?.address + fun loadEvent(event: Event, author: User, mentions: List, replyTo: List) { this.event = event this.author = author diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt index 67e656942..c201b76f5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt @@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.model import androidx.lifecycle.LiveData import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource import com.vitorpamplona.amethyst.service.model.LnZapEvent +import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent import com.vitorpamplona.amethyst.service.model.ReportEvent import com.vitorpamplona.amethyst.service.relays.Relay import com.vitorpamplona.amethyst.ui.note.toShortenHex @@ -37,6 +38,8 @@ class User(val pubkeyHex: String) { var notes = setOf() private set + var longFormNotes = mapOf>() + private set var taggedPosts = setOf() private set @@ -142,8 +145,27 @@ class User(val pubkeyHex: String) { notes = notes - note } + fun addLongFormNote(note: Note) { + val address = (note.event as LongTextNoteEvent).address + + if (address in longFormNotes.keys) { + if (longFormNotes[address]?.contains(note) == false) + longFormNotes = longFormNotes + Pair(address, (longFormNotes[address] ?: emptySet()) + note) + } else { + longFormNotes = longFormNotes + Pair(address, setOf(note)) + // No need for Listener yet + } + } + + fun removeLongFormNote(note: Note) { + val address = (note.event as LongTextNoteEvent).address ?: return + + longFormNotes = longFormNotes - address + } + fun clearNotes() { notes = setOf() + longFormNotes = mapOf>() } fun addReport(note: Note) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt index 64cf24171..e7958ff7c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt @@ -11,7 +11,7 @@ import com.vitorpamplona.amethyst.service.relays.TypedFilter import nostr.postr.JsonFilter import nostr.postr.events.ContactListEvent import nostr.postr.events.MetadataEvent -import nostr.postr.events.TextNoteEvent +import com.vitorpamplona.amethyst.service.model.TextNoteEvent object NostrAccountDataSource: NostrDataSource("AccountData") { lateinit var account: Account diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt index 7d59dc02e..d9493450b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt @@ -32,7 +32,7 @@ import nostr.postr.events.Event import nostr.postr.events.MetadataEvent import nostr.postr.events.PrivateDmEvent import nostr.postr.events.RecommendRelayEvent -import nostr.postr.events.TextNoteEvent +import com.vitorpamplona.amethyst.service.model.TextNoteEvent abstract class NostrDataSource(val debugName: String) { private var subscriptions = mapOf() @@ -63,12 +63,14 @@ abstract class NostrDataSource(val debugName: String) { try { when (event) { is MetadataEvent -> LocalCache.consume(event) - is TextNoteEvent -> LocalCache.consume(event, relay) + //is TextNoteEvent -> LocalCache.consume(event, relay) overrides default TextNote is RecommendRelayEvent -> LocalCache.consume(event) is ContactListEvent -> LocalCache.consume(event) is PrivateDmEvent -> LocalCache.consume(event, relay) is DeletionEvent -> LocalCache.consume(event) else -> when (event.kind) { + TextNoteEvent.kind -> LocalCache.consume(TextNoteEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig), relay) + RepostEvent.kind -> { val repostEvent = RepostEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrGlobalDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrGlobalDataSource.kt index a1ea5d004..d3ded698e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrGlobalDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrGlobalDataSource.kt @@ -5,7 +5,7 @@ import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.TypedFilter import nostr.postr.JsonFilter -import nostr.postr.events.TextNoteEvent +import com.vitorpamplona.amethyst.service.model.TextNoteEvent object NostrGlobalDataSource: NostrDataSource("GlobalFeed") { fun createGlobalFilter() = TypedFilter( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt index 48894e5cf..2ff7a001a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt @@ -9,7 +9,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import nostr.postr.JsonFilter -import nostr.postr.events.TextNoteEvent +import com.vitorpamplona.amethyst.service.model.TextNoteEvent object NostrHomeDataSource: NostrDataSource("HomeFeed") { lateinit var account: Account diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt index 57f90eece..c35d2284c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt @@ -14,11 +14,37 @@ import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.TypedFilter import java.util.Date import nostr.postr.JsonFilter -import nostr.postr.events.TextNoteEvent +import com.vitorpamplona.amethyst.service.model.TextNoteEvent object NostrSingleEventDataSource: NostrDataSource("SingleEventFeed") { private var eventsToWatch = setOf() + private fun createAddressFilter(): List? { + val addressesToWatch = eventsToWatch.map { LocalCache.getOrCreateNote(it) }.filter { it.address() != null } + + if (addressesToWatch.isEmpty()) { + return null + } + + val now = Date().time / 1000 + + return addressesToWatch.filter { + val lastTime = it.lastReactionsDownloadTime + lastTime == null || lastTime < (now - 10) + }.map { + TypedFilter( + types = FeedType.values().toSet(), + filter = JsonFilter( + kinds = listOf( + TextNoteEvent.kind, LongTextNoteEvent.kind, ReactionEvent.kind, RepostEvent.kind, ReportEvent.kind, LnZapEvent.kind, LnZapRequestEvent.kind + ), + tags = mapOf("a" to listOf(it.address()!!)), + since = it.lastReactionsDownloadTime + ) + ) + } + } + private fun createRepliesAndReactionsFilter(): List? { val reactionsToWatch = eventsToWatch.map { LocalCache.getOrCreateNote(it) } @@ -91,8 +117,9 @@ object NostrSingleEventDataSource: NostrDataSource("SingleEventFeed") { override fun updateChannelFilters() { val reactions = createRepliesAndReactionsFilter() val missing = createLoadEventsIfNotLoadedFilter() + val addresses = createAddressFilter() - singleEventChannel.typedFilters = listOfNotNull(reactions, missing).flatten().ifEmpty { null } + singleEventChannel.typedFilters = listOfNotNull(reactions, missing, addresses).flatten().ifEmpty { null } } fun add(eventId: String) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt index c399f5e3c..18fbafe2f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt @@ -9,7 +9,7 @@ import com.vitorpamplona.amethyst.service.relays.TypedFilter import nostr.postr.JsonFilter import nostr.postr.events.ContactListEvent import nostr.postr.events.MetadataEvent -import nostr.postr.events.TextNoteEvent +import com.vitorpamplona.amethyst.service.model.TextNoteEvent object NostrUserProfileDataSource: NostrDataSource("UserProfileFeed") { var user: User? = null diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt index c3d2c7568..9c6b57034 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt @@ -28,11 +28,15 @@ class LnZapRequestEvent ( fun create(originalNote: Event, relays: Set, privateKey: ByteArray, createdAt: Long = Date().time / 1000): LnZapRequestEvent { val content = "" val pubKey = Utils.pubkeyCreate(privateKey) - val tags = listOf( + var tags = listOf( listOf("e", originalNote.id.toHex()), listOf("p", originalNote.pubKey.toHex()), listOf("relays") + relays ) + if (originalNote is LongTextNoteEvent) { + tags = tags + listOf( listOf("a", originalNote.address) ) + } + val id = generateId(pubKey, createdAt, kind, tags, content) val sig = Utils.sign(id, privateKey) return LnZapRequestEvent(id, pubKey, createdAt, tags, content, sig) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LongTextNoteEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LongTextNoteEvent.kt index 72c1d106f..1c2d67558 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LongTextNoteEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LongTextNoteEvent.kt @@ -1,5 +1,6 @@ package com.vitorpamplona.amethyst.service.model +import com.vitorpamplona.amethyst.model.toHexKey import java.util.Date import nostr.postr.Utils import nostr.postr.events.Event @@ -20,11 +21,16 @@ class LongTextNoteEvent( @Transient val summary: String? @Transient val publishedAt: Long? @Transient val topics: List + @Transient val address: String + @Transient val dTag: String? init { replyTos = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } mentions = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } + dTag = tags.filter { it.firstOrNull() == "d" }.mapNotNull { it.getOrNull(1) }.firstOrNull() + + address = tags.filter { it.firstOrNull() == "a" }.mapNotNull { it.getOrNull(1) }.firstOrNull() ?: "$kind:${pubKey.toHexKey()}:$dTag" topics = tags.filter { it.firstOrNull() == "t" }.mapNotNull { it.getOrNull(1) } title = tags.filter { it.firstOrNull() == "title" }.mapNotNull { it.getOrNull(1) }.firstOrNull() image = tags.filter { it.firstOrNull() == "image" }.mapNotNull { it.getOrNull(1) }.firstOrNull() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt index a5ac96524..264ac4990 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt @@ -35,7 +35,12 @@ class ReactionEvent ( fun create(content: String, originalNote: Event, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReactionEvent { val pubKey = Utils.pubkeyCreate(privateKey) - val tags = listOf( listOf("e", originalNote.id.toHex()), listOf("p", originalNote.pubKey.toHex())) + + var tags = listOf( listOf("e", originalNote.id.toHex()), listOf("p", originalNote.pubKey.toHex())) + if (originalNote is LongTextNoteEvent) { + tags = tags + listOf( listOf("a", originalNote.address) ) + } + val id = generateId(pubKey, createdAt, kind, tags, content) val sig = Utils.sign(id, privateKey) return ReactionEvent(id, pubKey, createdAt, tags, content, sig) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt index cbee5cf0b..1129ff579 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt @@ -60,7 +60,12 @@ class ReportEvent ( val reportAuthorTag = listOf("p", reportedPost.pubKey.toHex(), type.name.toLowerCase()) val pubKey = Utils.pubkeyCreate(privateKey) - val tags:List> = listOf(reportPostTag, reportAuthorTag) + var tags:List> = listOf(reportPostTag, reportAuthorTag) + + if (reportedPost is LongTextNoteEvent) { + tags = tags + listOf( listOf("a", reportedPost.address) ) + } + val id = generateId(pubKey, createdAt, kind, tags, content) val sig = Utils.sign(id, privateKey) return ReportEvent(id, pubKey, createdAt, tags, content, sig) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/RepostEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/RepostEvent.kt index 3a1cc0a46..d0d405d5b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/RepostEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/RepostEvent.kt @@ -40,7 +40,12 @@ class RepostEvent ( val replyToAuthor = listOf("p", boostedPost.pubKey.toHex()) val pubKey = Utils.pubkeyCreate(privateKey) - val tags:List> = boostedPost.tags.plus(listOf(replyToPost, replyToAuthor)) + var tags:List> = boostedPost.tags.plus(listOf(replyToPost, replyToAuthor)) + + if (boostedPost is LongTextNoteEvent) { + tags = tags + listOf( listOf("a", boostedPost.address) ) + } + val id = generateId(pubKey, createdAt, kind, tags, content) val sig = Utils.sign(id, privateKey) return RepostEvent(id, pubKey, createdAt, tags, content, sig) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/TextNoteEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/TextNoteEvent.kt new file mode 100644 index 000000000..e22c0418d --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/TextNoteEvent.kt @@ -0,0 +1,45 @@ +package com.vitorpamplona.amethyst.service.model + +import java.util.Date +import nostr.postr.Utils +import nostr.postr.events.Event + +class TextNoteEvent( + id: ByteArray, + pubKey: ByteArray, + createdAt: Long, + tags: List>, + content: String, + sig: ByteArray +): Event(id, pubKey, createdAt, kind, tags, content, sig) { + @Transient val replyTos: List + @Transient val mentions: List + @Transient val longFormAddress: List + + init { + longFormAddress = tags.filter { it.firstOrNull() == "a" }.mapNotNull { it.getOrNull(1) } + replyTos = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } + mentions = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } + } + + companion object { + const val kind = 1 + + fun create(msg: String, replyTos: List?, mentions: List?, addresses: List?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): TextNoteEvent { + val pubKey = Utils.pubkeyCreate(privateKey) + val tags = mutableListOf>() + replyTos?.forEach { + tags.add(listOf("e", it)) + } + mentions?.forEach { + tags.add(listOf("p", it)) + } + addresses?.forEach { + tags.add(listOf("a", it)) + } + val id = generateId(pubKey, createdAt, kind, tags, msg) + val sig = Utils.sign(id, privateKey) + return TextNoteEvent(id, pubKey, createdAt, tags, msg, sig) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt index 2d76b76a9..8d1ca4633 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt @@ -41,7 +41,7 @@ import com.vitorpamplona.amethyst.ui.navigation.UploadFromGallery import com.vitorpamplona.amethyst.ui.note.ReplyInformation import com.vitorpamplona.amethyst.ui.screen.loggedIn.UserLine import kotlinx.coroutines.delay -import nostr.postr.events.TextNoteEvent +import com.vitorpamplona.amethyst.service.model.TextNoteEvent @OptIn(ExperimentalComposeUiApi::class) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt index 37a9114a4..fcc03314f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt @@ -46,7 +46,6 @@ class NewPostViewModel: ViewModel() { } else { this.mentions = currentMentions.plus(replyUser) } - } } @@ -68,12 +67,12 @@ class NewPostViewModel: ViewModel() { fun tagIndex(user: User): Int { // Postr Events assembles replies before mentions in the tag order - return (if (originalNote?.channel != null) 1 else 0) + (replyTos?.size ?: 0) + (mentions?.indexOf(user) ?: 0) + return (if (originalNote?.channel() != null) 1 else 0) + (replyTos?.size ?: 0) + (mentions?.indexOf(user) ?: 0) } fun tagIndex(note: Note): Int { // Postr Events assembles replies before mentions in the tag order - return (if (originalNote?.channel != null) 1 else 0) + (replyTos?.indexOf(note) ?: 0) + return (if (originalNote?.channel() != null) 1 else 0) + (replyTos?.indexOf(note) ?: 0) } fun sendPost() { @@ -108,8 +107,8 @@ class NewPostViewModel: ViewModel() { }.joinToString(" ") }.joinToString("\n") - if (originalNote?.channel != null) { - account?.sendChannelMeesage(newMessage, originalNote!!.channel!!.idHex, originalNote!!, mentions) + if (originalNote?.channel() != null) { + account?.sendChannelMessage(newMessage, originalNote!!.channel()!!.idHex, originalNote!!, mentions) } else { account?.sendPost(newMessage, replyTos, mentions) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt index 534548c84..6ef3abb29 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt @@ -42,10 +42,10 @@ fun ClickableRoute( onClick = { navController.navigate("Channel/${nip19.hex}") }, style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary) ) - } else if (note.channel != null) { + } else if (note.channel() != null) { ClickableText( - text = AnnotatedString("@${note.channel?.toBestDisplayName()} "), - onClick = { navController.navigate("Channel/${note.channel?.idHex}") }, + text = AnnotatedString("@${note.channel()?.toBestDisplayName()} "), + onClick = { navController.navigate("Channel/${note.channel()?.idHex}") }, style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary) ) } else { 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 09620d90f..f4fa3dac3 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 @@ -26,20 +26,20 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.compositeOver import androidx.compose.ui.text.Paragraph import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.navigation.NavController import com.google.accompanist.flowlayout.FlowRow import com.halilibo.richtext.markdown.Markdown import com.halilibo.richtext.markdown.MarkdownParseOptions -import com.halilibo.richtext.ui.RichText import com.halilibo.richtext.ui.RichTextStyle -import com.halilibo.richtext.ui.currentRichTextStyle import com.halilibo.richtext.ui.material.MaterialRichText import com.halilibo.richtext.ui.resolveDefaults -import com.halilibo.richtext.ui.string.RichTextString -import com.halilibo.richtext.ui.string.RichTextStringStyle import com.vitorpamplona.amethyst.service.lnurl.LnInvoiceUtil import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.Nip19 @@ -81,20 +81,47 @@ fun RichTextViewer( navController: NavController, ) { + val myMarkDownStyle = RichTextStyle().resolveDefaults().copy( + codeBlockStyle = RichTextStyle().resolveDefaults().codeBlockStyle?.copy( + textStyle = TextStyle( + fontFamily = FontFamily.Monospace, + fontSize = 14.sp + ), + modifier = Modifier + .padding(0.dp) + .fillMaxWidth() + .clip(shape = RoundedCornerShape(15.dp)) + .border( + 1.dp, + MaterialTheme.colors.onSurface.copy(alpha = 0.12f), + RoundedCornerShape(15.dp) + ) + .background(MaterialTheme.colors.onSurface.copy(alpha = 0.05f).compositeOver(backgroundColor)) + ), + stringStyle = RichTextStyle().resolveDefaults().stringStyle?.copy( + linkStyle = SpanStyle( + textDecoration = TextDecoration.Underline, + color = MaterialTheme.colors.primary + ), + codeStyle = SpanStyle( + fontFamily = FontFamily.Monospace, + fontSize = 14.sp, + background = MaterialTheme.colors.onSurface.copy(alpha = 0.22f).compositeOver(backgroundColor) + ) + ) + ) + Column(modifier = modifier.animateContentSize()) { - if (content.startsWith("# ") || content.contains("##") || content.contains("```")) { - var richTextStyle by remember { mutableStateOf(RichTextStyle().resolveDefaults()) } + if ( content.startsWith("# ") + || content.contains("##") + || content.contains("**") + || content.contains("__") + || content.contains("```") + ) { MaterialRichText( - style = RichTextStyle().resolveDefaults().copy( - stringStyle = richTextStyle.stringStyle?.copy( - linkStyle = SpanStyle( - textDecoration = TextDecoration.Underline, - color = MaterialTheme.colors.primary - ) - ) - ), + style = myMarkDownStyle, ) { Markdown( content = content, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/GlobalFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/GlobalFeedFilter.kt index e02ee666b..010e3ef53 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/GlobalFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/GlobalFeedFilter.kt @@ -4,7 +4,7 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent -import nostr.postr.events.TextNoteEvent +import com.vitorpamplona.amethyst.service.model.TextNoteEvent object GlobalFeedFilter: FeedFilter() { lateinit var account: Account diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeConversationsFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeConversationsFeedFilter.kt index 9a9efdbbb..1f96b3ee8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeConversationsFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeConversationsFeedFilter.kt @@ -4,7 +4,7 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.model.RepostEvent -import nostr.postr.events.TextNoteEvent +import com.vitorpamplona.amethyst.service.model.TextNoteEvent object HomeConversationsFeedFilter: FeedFilter() { lateinit var account: Account diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt index 275eb41d4..5276c2437 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt @@ -5,7 +5,7 @@ import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent import com.vitorpamplona.amethyst.service.model.RepostEvent -import nostr.postr.events.TextNoteEvent +import com.vitorpamplona.amethyst.service.model.TextNoteEvent object HomeNewThreadFeedFilter: FeedFilter() { lateinit var account: Account diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt index 06a7e534d..80f94cd27 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt @@ -7,7 +7,7 @@ import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent import com.vitorpamplona.amethyst.service.model.LnZapRequestEvent import com.vitorpamplona.amethyst.service.model.ReactionEvent import com.vitorpamplona.amethyst.service.model.RepostEvent -import nostr.postr.events.TextNoteEvent +import com.vitorpamplona.amethyst.service.model.TextNoteEvent object NotificationFeedFilter: FeedFilter() { lateinit var account: Account diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileNewThreadFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileNewThreadFeedFilter.kt index 6407840f9..fd5316df0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileNewThreadFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileNewThreadFeedFilter.kt @@ -15,7 +15,7 @@ object UserProfileNewThreadFeedFilter: FeedFilter() { } override fun feed(): List { - return user?.notes + return user?.notes?.plus(user?.longFormNotes?.values?.flatten() ?: emptySet()) ?.filter { account?.isAcceptable(it) == true && it.isNewThread() } ?.sortedBy { it.event?.createdAt } ?.reversed() 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 12595420b..8da81cfa0 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 @@ -122,7 +122,7 @@ private fun messagesHasNewItems(account: Account, cache: NotificationCache, cont ChatroomListKnownFeedFilter.account = account val note = ChatroomListKnownFeedFilter.feed().firstOrNull { - it.event?.createdAt != null && it.channel == null && it.author != account.userProfile() + it.event?.createdAt != null && it.channel() == null && it.author != account.userProfile() } ?: return false val lastTime = cache.load("Room/${note.author?.pubkeyHex}", context) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BoostSetCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BoostSetCompose.kt index 98f613684..5462d4ef4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BoostSetCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BoostSetCompose.kt @@ -72,7 +72,7 @@ fun BoostSetCompose(boostSetCard: BoostSetCard, isInnerNote: Boolean = false, ro launchSingleTop = true } } else { - note.channel?.let { + note.channel()?.let { navController.navigate("Channel/${it.idHex}") } } 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 204df89fe..91e97681b 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 @@ -63,11 +63,11 @@ fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navContr if (note?.event == null) { BlankNote(Modifier) - } else if (note.channel != null) { + } else if (note.channel() != null) { val authorState by note.author!!.live().metadata.observeAsState() val author = authorState?.user - val channelState by note.channel!!.live.observeAsState() + val channelState by note.channel()!!.live.observeAsState() val channel = channelState?.channel val noteEvent = note.event diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/LikeSetCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/LikeSetCompose.kt index ac9f329f2..fdbec03b7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/LikeSetCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/LikeSetCompose.kt @@ -72,7 +72,7 @@ fun LikeSetCompose(likeSetCard: LikeSetCard, modifier: Modifier = Modifier, isIn launchSingleTop = true } } else { - note.channel?.let { + note.channel()?.let { navController.navigate("Channel/${it.idHex}") } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt index ca121bb22..5cc2181d8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt @@ -78,7 +78,7 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, modifier: Modifier = Modifier, r launchSingleTop = true } } else { - note.channel?.let { + note.channel()?.let { navController.navigate("Channel/${it.idHex}") } } 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 ad043894f..c56c242f0 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 @@ -35,6 +35,7 @@ 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 import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent @@ -51,7 +52,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.Following import kotlin.time.ExperimentalTime import nostr.postr.events.PrivateDmEvent -import nostr.postr.events.TextNoteEvent +import com.vitorpamplona.amethyst.service.model.TextNoteEvent @OptIn(ExperimentalFoundationApi::class) @Composable @@ -133,7 +134,7 @@ fun NoteCompose( launchSingleTop = true } } else { - note.channel?.let { + note.channel()?.let { navController.navigate("Channel/${it.idHex}") } } @@ -175,7 +176,7 @@ fun NoteCompose( } // boosted picture - val baseChannel = note.channel + val baseChannel = note.channel() if (noteEvent is ChannelMessageEvent && baseChannel != null) { val channelState by baseChannel.live.observeAsState() val channel = channelState?.channel @@ -293,7 +294,7 @@ fun NoteCompose( } else if (noteEvent is ChannelMessageEvent && (note.replyTo != null || note.mentions != null)) { val sortedMentions = note.mentions?.toSet()?.sortedBy { account.userProfile().isFollowing(it) } - note.channel?.let { + note.channel()?.let { ReplyInformationChannel(note.replyTo, sortedMentions, it, navController) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapSetCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapSetCompose.kt index 1fd50f2c7..e1ecdcf77 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapSetCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapSetCompose.kt @@ -74,7 +74,7 @@ fun ZapSetCompose(zapSetCard: ZapSetCard, modifier: Modifier = Modifier, isInner launchSingleTop = true } } else { - note.channel?.let { + note.channel()?.let { navController.navigate("Channel/${it.idHex}") } } 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 ea386f65d..7f749f8d1 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 @@ -268,7 +268,7 @@ fun NoteMaster(baseNote: Note, } if (noteEvent is LongTextNoteEvent) { - Row(modifier = Modifier.padding(horizontal = 12.dp)) { + Row(modifier = Modifier.padding(start = 12.dp, end = 12.dp, top = 10.dp)) { Column { noteEvent.image?.let { AsyncImage( @@ -295,7 +295,10 @@ fun NoteMaster(baseNote: Note, noteEvent.summary?.let { Text( - text = it + text = it, + modifier = Modifier + .fillMaxWidth() + .padding(top = 10.dp) ) } } 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 d756c98ec..55f9d219f 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 @@ -193,7 +193,7 @@ fun ChannelScreen(channelId: String?, accountViewModel: AccountViewModel, accoun trailingIcon = { PostButton( onPost = { - account.sendChannelMeesage(newPost.value.text, channel.idHex, replyTo.value, null) + account.sendChannelMessage(newPost.value.text, channel.idHex, replyTo.value, null) newPost.value = TextFieldValue("") replyTo.value = null feedViewModel.refresh() // Don't wait a full second before updating From 3cd37b8658e04941a5fc8d30cafa43f414298cd2 Mon Sep 17 00:00:00 2001 From: Oleg Koretsky Date: Thu, 2 Mar 2023 23:28:31 +0200 Subject: [PATCH 09/36] Add Ukrainian (uk) translation --- app/src/main/res/values-uk/strings.xml | 174 +++++++++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 app/src/main/res/values-uk/strings.xml diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml new file mode 100644 index 000000000..9d587f90a --- /dev/null +++ b/app/src/main/res/values-uk/strings.xml @@ -0,0 +1,174 @@ + + Amethyst + Amethyst Debug + Наведіть на QR код + Показати QR + Фото профілю + Сканувати QR + Показати + Допис було відмічено як недоречний + допис не знайдено + Фото каналу + Пов\'язану подію не знайдено + Не вдалося розшифрувати повідомлення + Фото групи + Відвертий вміст + Спам + Видача себе за іншу особу + Незаконні дії + Невідомо + Іконка рілею + Невідомий автор + Скопіювати текст + Скопіювати ключ користувача + Скопіювати ID допису + Розіслати + Блокувати та приховати користувача + Повідомити про спам / шахрайство + Повідомити про видачу себе за іншу особу + Повідомити про відвертий вміст + Повідомити про незаконні дії + Увійдіть з приватним ключем щоб відповісти + Увійдіть з приватним ключем щоб просувати дописи + Увійдіть з приватним ключем щоб лайкати дописи + Не налаштовані запи. Натисніть та утримуйте для налаштування + Увійдіть з приватним ключем щоб запати + Запи + Перегляди + Просувати + Цитувати + Нова сума в sat + Додати + "у відповідь " + " та " + "в каналі " + Банер профілю + " Підписок" + " Підписників" + Профіль + Фільтри безпеки + Вийти + Більше + Lightning інвойс + Сплатити + Lightning чайові + Примітка для отримувача + Дуже дякуємо! + Сума в sat + Надіслати + "Не переводити з " + "Не вдалося створити перегляд для %1$s : %2$s" + "Перегляд для %1$s" + Новий канал + Назва каналу + Моя нова група + URL фотографії + Опис + "Про нас.. " + Що нового? + Опублікувати + Зберегти + Створити + Скасувати + Не вдалося завантажити фото + Адреса рілея + Дописи + Помилки + Домашня стрічка + Стрічка персональних повідомлень + Стрічка чатів + Глобальна стрічка + Додати рілей + Видиме ім\'я + Моє видиме ім\'я + Нікнейм + Мій нікнейм + Про мене + URL фотографії + URL банеру + URL сайту + LN адреса + LN URL (застарівш.) + Фото збережено до галереї + Не вдалося зберегти фото + Завантажити\nфото + Завантаження… + Користувач не встановив Lightning адресу для отримання чайових + "відповісти.. " + Копіює ID допису для відправки + Скопіювати ID каналу (допису) + Редагує метадані каналу + Увійти + Відомі + Нові запити + Заблоковані користувачі + Нові треди + Обговорення + Дописи + Відповіді + "Підписок" + "Скарг" + Більше опцій + " Рілеїв" + Сайт + Lightning адреса + Копіює Nsec ID (ваш пароль) для резервного копіювання + Скопіювати приватний ключ + Копіює публічний ключ для відправки + Скопіювати публічний ключ (NPub) + Відправити повідомлення + Редагує метадані користувача + Підписатися + Розблокувати + Скопіювати ID користувача + Розблокувати користувача + "npub, hex, нікнейм " + Очистити + Логотип додатка + nsec / npub / приватний ключ в hex + Показати пароль + Сховати пароль + Некоректний ключ + "Я приймаю " + умови користування + Треба прийняти умови користування + Треба ввести ключ + Увійти + Згенерувати ключ + Завантаження стрічки + "Не вдалося завантажити відповіді: " + Повторити + Стрічка порожня. + Оновити + створив(ла) + з описом + та фото + змінив(ла) назву на + опис на + та фото на + Вийти + Відписатися + Канал створено + Інформацію каналу змінено на + Публічний чат + дописів отримано + Видалити + Автоматично + перекладено з + на + Показати спершу на + + Завжди перекладати на + NIP-05 + LNURL... + ніколи + зараз + год + хв + дн + Нагота + Лайка / Образи + Повідомити про образи + Повідомити про наготу / порно + інших + \ No newline at end of file From 88ceb436ac453fd766b63c6bdcacbbb3b7dbb3e6 Mon Sep 17 00:00:00 2001 From: Oleg Koretsky Date: Thu, 2 Mar 2023 23:30:37 +0200 Subject: [PATCH 10/36] Fix typos --- app/src/main/res/values-ru/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index ec443deab..4f6e36f32 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -24,7 +24,7 @@ Скопировать ID записи Разослать Блокировать и скрыть пользователя - Сообщить о спаме / мошеничестве + Сообщить о спаме / мошенничестве Сообщить о выдаче себя за другое лицо Сообщить об откровенном содержании Сообщить о незаконных действиях @@ -93,7 +93,7 @@ Не удалось сохранить фото Загрузить\nфото Загрузка… - Пользователь не усталовил Lightning адрес для получения чаевых + Пользователь не установил Lightning адрес для получения чаевых "ответить.. " Копирует ID записи для отправки Скопировать ID канала (записи) From f1711719f47c7ba57221a70fc6fe0ad1a0372784 Mon Sep 17 00:00:00 2001 From: Rashed <38612386+rashedswen@users.noreply.github.com> Date: Fri, 3 Mar 2023 09:53:56 +0300 Subject: [PATCH 11/36] add follow directly from a note feature --- .../com/vitorpamplona/amethyst/ui/note/NoteCompose.kt | 11 +++++++++++ .../amethyst/ui/screen/loggedIn/AccountViewModel.kt | 4 +++- 2 files changed, 14 insertions(+), 1 deletion(-) 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 c56c242f0..a596c06c3 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 @@ -629,6 +629,17 @@ fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit, expanded = popupExpanded, onDismissRequest = onDismiss ) { + if (note.author != accountViewModel.accountLiveData.value?.account?.userProfile()) { + + DropdownMenuItem(onClick = { + accountViewModel.follow( + note.author ?: return@DropdownMenuItem + ); onDismiss() + }) { + Text(stringResource(R.string.follow)) + } + Divider() + } DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(accountViewModel.decrypt(note) ?: "")); onDismiss() }) { Text(stringResource(R.string.copy_text)) } 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 66e702358..2c7391156 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 @@ -113,7 +113,9 @@ class AccountViewModel(private val account: Account): ViewModel() { account.prefer(source, target, preference) } - + fun follow(user: User) { + account.follow(user) + } } \ No newline at end of file From 8e858996fe07ca5eac66c1d63108fa0ebae47dd3 Mon Sep 17 00:00:00 2001 From: Rashed <38612386+rashedswen@users.noreply.github.com> Date: Fri, 3 Mar 2023 10:13:15 +0300 Subject: [PATCH 12/36] hide follow button when the user already followed --- .../java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 a596c06c3..c6fd6212d 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 @@ -629,7 +629,8 @@ fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit, expanded = popupExpanded, onDismissRequest = onDismiss ) { - if (note.author != accountViewModel.accountLiveData.value?.account?.userProfile()) { + if (note.author != accountViewModel.accountLiveData.value?.account?.userProfile() && !accountViewModel.accountLiveData.value?.account?.userProfile() + !!.isFollowing(note.author!!)) { DropdownMenuItem(onClick = { accountViewModel.follow( From f4d5785710aac16309107b75af3d24786b57d34c Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 3 Mar 2023 11:35:29 -0500 Subject: [PATCH 13/36] Support for Replaceable Events (NIP-33) --- README.md | 3 +- .../vitorpamplona/amethyst/model/Account.kt | 4 +- .../vitorpamplona/amethyst/model/Channel.kt | 2 +- .../amethyst/model/LocalCache.kt | 120 ++++++++++++------ .../com/vitorpamplona/amethyst/model/Note.kt | 48 +++++-- .../amethyst/model/ThreadAssembler.kt | 12 +- .../com/vitorpamplona/amethyst/model/User.kt | 28 +--- .../vitorpamplona/amethyst/service/Nip19.kt | 69 +++++++--- .../amethyst/service/NostrDataSource.kt | 4 +- .../service/NostrSingleEventDataSource.kt | 49 ++++--- .../amethyst/service/model/ATag.kt | 72 +++++++++++ .../service/model/ChannelCreateEvent.kt | 14 +- .../service/model/ChannelHideMessageEvent.kt | 6 +- .../service/model/ChannelMessageEvent.kt | 11 +- .../service/model/ChannelMetadataEvent.kt | 21 ++- .../service/model/ChannelMuteUserEvent.kt | 6 +- .../amethyst/service/model/LnZapEvent.kt | 34 ++--- .../service/model/LnZapRequestEvent.kt | 13 +- .../service/model/LongTextNoteEvent.kt | 38 ++---- .../amethyst/service/model/ReactionEvent.kt | 12 +- .../amethyst/service/model/ReportEvent.kt | 47 ++++--- .../amethyst/service/model/RepostEvent.kt | 20 ++- .../amethyst/service/model/TextNoteEvent.kt | 16 +-- .../amethyst/ui/dal/ChannelFeedFilter.kt | 2 +- .../amethyst/ui/dal/ChatroomFeedFilter.kt | 2 +- .../ui/dal/ChatroomListKnownFeedFilter.kt | 6 +- .../ui/dal/ChatroomListNewFeedFilter.kt | 4 +- .../amethyst/ui/dal/GlobalFeedFilter.kt | 13 +- .../ui/dal/HomeConversationsFeedFilter.kt | 2 +- .../ui/dal/HomeNewThreadFeedFilter.kt | 15 ++- .../amethyst/ui/dal/NotificationFeedFilter.kt | 2 +- .../dal/UserProfileConversationsFeedFilter.kt | 2 +- .../ui/dal/UserProfileNewThreadFeedFilter.kt | 6 +- .../ui/dal/UserProfileReportsFeedFilter.kt | 2 +- .../amethyst/ui/navigation/Routes.kt | 8 +- .../amethyst/ui/note/ChatroomCompose.kt | 8 +- .../ui/note/ChatroomMessageCompose.kt | 16 +-- .../amethyst/ui/note/NoteCompose.kt | 101 ++++++++------- .../amethyst/ui/screen/CardFeedState.kt | 14 +- .../amethyst/ui/screen/FeedView.kt | 5 - .../amethyst/ui/screen/ThreadFeedView.kt | 12 +- app/src/main/res/values/strings.xml | 2 +- .../vitorpamplona/amethyst/NIP19ParserTest.kt | 33 +++++ 43 files changed, 533 insertions(+), 371 deletions(-) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/service/model/ATag.kt create mode 100644 app/src/test/java/com/vitorpamplona/amethyst/NIP19ParserTest.kt diff --git a/README.md b/README.md index 7a2e5c813..ae056b95a 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,8 @@ Amethyst brings the best social network to your Android phone. Just insert your - [x] URI Support (NIP-21) - [x] Event Deletion (NIP-09: like, boost, text notes and reports) - [x] Identity Verification (NIP-05) +- [x] Long-form Content (NIP-23) +- [x] Parameterized Replaceable Events (NIP-33) - [ ] Local Database - [ ] View Individual Reactions (Like, Boost, Zaps, Reports) per Post - [ ] Bookmarks, Pinned Posts, Muted Events (NIP-51) @@ -36,7 +38,6 @@ Amethyst brings the best social network to your Android phone. Just insert your - [ ] Generic Tags (NIP-12) - [ ] Proof of Work in the Phone (NIP-13, NIP-20) - [ ] Events with a Subject (NIP-14) -- [ ] Long-form Content (NIP-23) - [ ] Online Relay Search (NIP-50) - [ ] Workspaces - [ ] Expiration Support (NIP-40) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 1eb820b1d..ed433bebf 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -290,13 +290,13 @@ class Account( val repliesToHex = replyTo?.map { it.idHex } val mentionsHex = mentions?.map { it.pubkeyHex } - val addressesHex = replyTo?.mapNotNull { it.address() } + val addresses = replyTo?.mapNotNull { it.address() } val signedEvent = TextNoteEvent.create( msg = message, replyTos = repliesToHex, mentions = mentionsHex, - addresses = addressesHex, + addresses = addresses, privateKey = loggedIn.privKey!! ) Client.send(signedEvent) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt index a24445dcc..882b46efa 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt @@ -51,7 +51,7 @@ class Channel(val idHex: String) { fun pruneOldAndHiddenMessages(account: Account): Set { val important = notes.values .filter { it.author?.let { it1 -> account.isHidden(it1) } == false } - .sortedBy { it.event?.createdAt } + .sortedBy { it.createdAt() } .reversed() .take(1000) .toSet() 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 da84d2fa4..66b388368 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -5,6 +5,7 @@ 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.ChannelCreateEvent import com.vitorpamplona.amethyst.service.model.ChannelHideMessageEvent import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent @@ -52,6 +53,7 @@ object LocalCache { val users = ConcurrentHashMap() val notes = ConcurrentHashMap() val channels = ConcurrentHashMap() + val addressables = ConcurrentHashMap() fun checkGetOrCreateUser(key: String): User? { return try { @@ -111,6 +113,29 @@ object LocalCache { } } + fun checkGetOrCreateAddressableNote(key: String): AddressableNote? { + return try { + val addr = ATag.parse(key) + if (addr != null) + getOrCreateAddressableNote(addr) + else + null + } catch (e: IllegalArgumentException) { + Log.e("LocalCache", "Invalid Key to create channel: $key", e) + null + } + } + + @Synchronized + fun getOrCreateAddressableNote(key: ATag): AddressableNote { + return addressables[key.toNAddr()] ?: run { + val answer = AddressableNote(key) + answer.author = checkGetOrCreateUser(key.pubKeyHex) + addressables.put(key.toNAddr(), answer) + answer + } + } + fun consume(event: MetadataEvent) { // new event @@ -159,8 +184,9 @@ object LocalCache { // Already processed this event. if (note.event != null) return - val mentions = event.mentions.mapNotNull { checkGetOrCreateUser(it) } - val replyTo = replyToWithoutCitations(event).mapNotNull { checkGetOrCreateNote(it) } + val mentions = event.mentions().mapNotNull { checkGetOrCreateUser(it) } + val replyTo = replyToWithoutCitations(event).mapNotNull { checkGetOrCreateNote(it) } + + event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) } note.loadEvent(event, author, mentions, replyTo) @@ -193,7 +219,7 @@ object LocalCache { return } - val note = getOrCreateNote(event.id.toHex()) + val note = getOrCreateAddressableNote(event.address()) val author = getOrCreateUser(event.pubKey.toHexKey()) if (relay != null) { @@ -202,27 +228,26 @@ object LocalCache { } // Already processed this event. - if (note.event != null) return + if (note.event?.id?.toHex() == event.id.toHex()) return - val mentions = event.mentions.mapNotNull { checkGetOrCreateUser(it) } + val mentions = event.mentions().mapNotNull { checkGetOrCreateUser(it) } val replyTo = replyToWithoutCitations(event).mapNotNull { checkGetOrCreateNote(it) } - note.loadEvent(event, author, mentions, replyTo) + if (event.createdAt > (note.createdAt() ?: 0)) { + note.loadEvent(event, author, mentions, replyTo) - //Log.d("TN", "New Note (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${note.event?.content?.take(100)} ${formattedDateTime(event.createdAt)}") + author.addNote(note) - // Prepares user's profile view. - author.addLongFormNote(note) + // Adds notifications to users. + mentions.forEach { + it.addTaggedPost(note) + } + replyTo.forEach { + it.author?.addTaggedPost(note) + } - // Adds notifications to users. - mentions.forEach { - it.addTaggedPost(note) + refreshObservers() } - replyTo.forEach { - it.author?.addTaggedPost(note) - } - - refreshObservers() } private fun findCitations(event: Event): Set { @@ -245,13 +270,13 @@ object LocalCache { private fun replyToWithoutCitations(event: TextNoteEvent): List { val citations = findCitations(event) - return event.replyTos.filter { it !in citations } + return event.replyTos().filter { it !in citations } } private fun replyToWithoutCitations(event: LongTextNoteEvent): List { val citations = findCitations(event) - return event.replyTos.filter { it !in citations } + return event.replyTos().filter { it !in citations } } fun consume(event: RecommendRelayEvent) { @@ -378,8 +403,9 @@ object LocalCache { //Log.d("TN", "New Boost (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}") val author = getOrCreateUser(event.pubKey.toHexKey()) - val mentions = event.originalAuthor.mapNotNull { checkGetOrCreateUser(it) } - val repliesTo = event.boostedPost.mapNotNull { checkGetOrCreateNote(it) } + val mentions = event.originalAuthor().mapNotNull { checkGetOrCreateUser(it) } + val repliesTo = event.boostedPost().mapNotNull { checkGetOrCreateNote(it) } + + event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) } note.loadEvent(event, author, mentions, repliesTo) @@ -409,8 +435,9 @@ object LocalCache { if (note.event != null) return val author = getOrCreateUser(event.pubKey.toHexKey()) - val mentions = event.originalAuthor.mapNotNull { checkGetOrCreateUser(it) } - val repliesTo = event.originalPost.mapNotNull { checkGetOrCreateNote(it) } + val mentions = event.originalAuthor().mapNotNull { checkGetOrCreateUser(it) } + val repliesTo = event.originalPost().mapNotNull { checkGetOrCreateNote(it) } + + event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) } note.loadEvent(event, author, mentions, repliesTo) @@ -459,8 +486,9 @@ object LocalCache { // Already processed this event. if (note.event != null) return - val mentions = event.reportedAuthor.mapNotNull { checkGetOrCreateUser(it.key) } - val repliesTo = event.reportedPost.mapNotNull { checkGetOrCreateNote(it.key) } + val mentions = event.reportedAuthor().mapNotNull { checkGetOrCreateUser(it.key) } + val repliesTo = event.reportedPost().mapNotNull { checkGetOrCreateNote(it.key) } + + event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) } note.loadEvent(event, author, mentions, repliesTo) @@ -483,7 +511,7 @@ object LocalCache { val author = getOrCreateUser(event.pubKey.toHexKey()) if (event.createdAt > oldChannel.updatedMetadataAt) { if (oldChannel.creator == null || oldChannel.creator == author) { - oldChannel.updateChannelInfo(author, event.channelInfo, event.createdAt) + oldChannel.updateChannelInfo(author, event.channelInfo(), event.createdAt) val note = getOrCreateNote(event.id.toHex()) oldChannel.addNote(note) @@ -496,15 +524,16 @@ object LocalCache { } } fun consume(event: ChannelMetadataEvent) { + val channelId = event.channel() //Log.d("MT", "New User ${users.size} ${event.contactMetaData.name}") - if (event.channel.isNullOrBlank()) return + if (channelId.isNullOrBlank()) return // new event - val oldChannel = checkGetOrCreateChannel(event.channel) ?: return + val oldChannel = checkGetOrCreateChannel(channelId) ?: return val author = getOrCreateUser(event.pubKey.toHexKey()) if (event.createdAt > oldChannel.updatedMetadataAt) { if (oldChannel.creator == null || oldChannel.creator == author) { - oldChannel.updateChannelInfo(author, event.channelInfo, event.createdAt) + oldChannel.updateChannelInfo(author, event.channelInfo(), event.createdAt) val note = getOrCreateNote(event.id.toHex()) oldChannel.addNote(note) @@ -518,7 +547,9 @@ object LocalCache { } fun consume(event: ChannelMessageEvent, relay: Relay?) { - if (event.channel.isNullOrBlank()) return + val channelId = event.channel() + + if (channelId.isNullOrBlank()) return if (antiSpam.isSpam(event)) { relay?.let { it.spamCounter++ @@ -526,7 +557,7 @@ object LocalCache { return } - val channel = checkGetOrCreateChannel(event.channel) ?: return + val channel = checkGetOrCreateChannel(channelId) ?: return val note = getOrCreateNote(event.id.toHex()) channel.addNote(note) @@ -541,8 +572,8 @@ object LocalCache { // Already processed this event. if (note.event != null) return - val mentions = event.mentions.mapNotNull { checkGetOrCreateUser(it) } - val replyTo = event.replyTos + val mentions = event.mentions().mapNotNull { checkGetOrCreateUser(it) } + val replyTo = event.replyTos() .mapNotNull { checkGetOrCreateNote(it) } .filter { it.event !is ChannelCreateEvent } @@ -580,13 +611,16 @@ object LocalCache { // Already processed this event. if (note.event != null) return + val zapRequest = event.containedPost()?.id?.toHexKey()?.let { getOrCreateNote(it) } + val author = getOrCreateUser(event.pubKey.toHexKey()) - val mentions = event.zappedAuthor.mapNotNull { checkGetOrCreateUser(it) } - val repliesTo = event.zappedPost.mapNotNull { checkGetOrCreateNote(it) } + val mentions = event.zappedAuthor().mapNotNull { checkGetOrCreateUser(it) } + val repliesTo = event.zappedPost().mapNotNull { checkGetOrCreateNote(it) } + + event.taggedAddresses().map { getOrCreateAddressableNote(it) } + + ((zapRequest?.event as? LnZapRequestEvent)?.taggedAddresses()?.map { getOrCreateAddressableNote(it) } ?: emptySet()) note.loadEvent(event, author, mentions, repliesTo) - val zapRequest = event.containedPost?.id?.toHexKey()?.let { getOrCreateNote(it) } if (zapRequest == null) { Log.e("ZP","Zap Request not found. Unable to process Zap {${event.toJson()}}") return @@ -617,8 +651,9 @@ object LocalCache { if (note.event != null) return val author = getOrCreateUser(event.pubKey.toHexKey()) - val mentions = event.zappedAuthor.mapNotNull { checkGetOrCreateUser(it) } - val repliesTo = event.zappedPost.mapNotNull { checkGetOrCreateNote(it) } + val mentions = event.zappedAuthor().mapNotNull { checkGetOrCreateUser(it) } + val repliesTo = event.zappedPost().mapNotNull { checkGetOrCreateNote(it) } + + event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) } note.loadEvent(event, author, mentions, repliesTo) @@ -652,9 +687,13 @@ object LocalCache { return notes.values.filter { (it.event is TextNoteEvent && it.event?.content?.contains(text, true) ?: false) || (it.event is ChannelMessageEvent && it.event?.content?.contains(text, true) ?: false) - || (it.event is LongTextNoteEvent && it.event?.content?.contains(text, true) ?: false) || it.idHex.startsWith(text, true) || it.idNote().startsWith(text, true) + } + addressables.values.filter { + (it.event as? LongTextNoteEvent)?.content?.contains(text, true) ?: false + || (it.event as? LongTextNoteEvent)?.title()?.contains(text, true) ?: false + || (it.event as? LongTextNoteEvent)?.summary()?.contains(text, true) ?: false + || it.idHex.startsWith(text, true) } } @@ -738,7 +777,7 @@ object LocalCache { fun pruneHiddenMessages(account: Account) { val toBeRemoved = account.hiddenUsers.map { - (users[it]?.notes ?: emptySet()) + (users[it]?.longFormNotes?.values?.flatten() ?: emptySet()) + (users[it]?.notes ?: emptySet()) }.flatten() account.hiddenUsers.forEach { @@ -747,7 +786,6 @@ object LocalCache { toBeRemoved.forEach { it.author?.removeNote(it) - it.author?.removeLongFormNote(it) // reverts the add it.mentions?.forEach { user -> diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index 14db2aa01..12d130b8a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.model import androidx.lifecycle.LiveData import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource +import com.vitorpamplona.amethyst.service.model.ATag import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent @@ -27,11 +28,18 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import nostr.postr.events.Event -import nostr.postr.toHex val tagSearch = Pattern.compile("(?:\\s|\\A)\\#\\[([0-9]+)\\]") -class Note(val idHex: String) { + +class AddressableNote(val address: ATag): Note(address.toNAddr()) { + override fun idNote() = address.toNAddr() + override fun idDisplayNote() = idNote().toShortenHex() + override fun address() = address + override fun createdAt() = (event as? LongTextNoteEvent)?.publishedAt() ?: event?.createdAt +} + +open class Note(val idHex: String) { // These fields are only available after the Text Note event is received. // They are immutable after that. var event: Event? = null @@ -57,18 +65,21 @@ class Note(val idHex: String) { var lastReactionsDownloadTime: Long? = null fun id() = Hex.decode(idHex) - fun idNote() = id().toNote() - fun idDisplayNote() = idNote().toShortenHex() + open fun idNote() = id().toNote() + open fun idDisplayNote() = idNote().toShortenHex() fun channel(): Channel? { - val channelHex = (event as? ChannelMessageEvent)?.channel ?: - (event as? ChannelMetadataEvent)?.channel ?: - (event as? ChannelCreateEvent)?.let { idHex } + val channelHex = + (event as? ChannelMessageEvent)?.channel() ?: + (event as? ChannelMetadataEvent)?.channel() ?: + (event as? ChannelCreateEvent)?.let { it.id.toHexKey() } return channelHex?.let { LocalCache.checkGetOrCreateChannel(it) } } - fun address() = (event as? LongTextNoteEvent)?.address + open fun address() = (event as? LongTextNoteEvent)?.address() + + open fun createdAt() = event?.createdAt fun loadEvent(event: Event, author: User, mentions: List, replyTo: List) { this.event = event @@ -90,14 +101,14 @@ class Note(val idHex: String) { fun replyLevelSignature(cachedSignatures: MutableMap = mutableMapOf()): String { val replyTo = replyTo if (replyTo == null || replyTo.isEmpty()) { - return "/" + formattedDateTime(event?.createdAt ?: 0) + ";" + return "/" + formattedDateTime(createdAt() ?: 0) + ";" } return replyTo .map { cachedSignatures[it] ?: it.replyLevelSignature(cachedSignatures).apply { cachedSignatures.put(it, this) } } - .maxBy { it.length }.removeSuffix(";") + "/" + formattedDateTime(event?.createdAt ?: 0) + ";" + .maxBy { it.length }.removeSuffix(";") + "/" + formattedDateTime(createdAt() ?: 0) + ";" } fun replyLevel(cachedLevels: MutableMap = mutableMapOf()): Int { @@ -236,7 +247,7 @@ class Note(val idHex: String) { val dayAgo = Date().time / 1000 - 24*60*60 return reports.isNotEmpty() || (author?.reports?.values?.filter { - it.firstOrNull { ( it.event?.createdAt ?: 0 ) > dayAgo } != null + it.firstOrNull { ( it.createdAt() ?: 0 ) > dayAgo } != null }?.isNotEmpty() ?: false) } @@ -283,7 +294,7 @@ class Note(val idHex: String) { fun hasBoostedInTheLast5Minutes(loggedIn: User): Boolean { val currentTime = Date().time / 1000 - return boosts.firstOrNull { it.author == loggedIn && (it.event?.createdAt ?: 0) > currentTime - (60 * 5)} != null // 5 minute protection + return boosts.firstOrNull { it.author == loggedIn && (it.createdAt() ?: 0) > currentTime - (60 * 5)} != null // 5 minute protection } fun boostedBy(loggedIn: User): List { @@ -356,12 +367,21 @@ class NoteLiveData(val note: Note): LiveData(NoteState(note)) { override fun onActive() { super.onActive() - NostrSingleEventDataSource.add(note.idHex) + if (note is AddressableNote) { + NostrSingleEventDataSource.addAddress(note) + } else { + NostrSingleEventDataSource.add(note) + } + } override fun onInactive() { super.onInactive() - NostrSingleEventDataSource.remove(note.idHex) + if (note is AddressableNote) { + NostrSingleEventDataSource.removeAddress(note) + } else { + NostrSingleEventDataSource.remove(note) + } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/ThreadAssembler.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/ThreadAssembler.kt index f1b928928..be2366c66 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/ThreadAssembler.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/ThreadAssembler.kt @@ -1,5 +1,6 @@ package com.vitorpamplona.amethyst.model +import com.vitorpamplona.amethyst.service.model.ATag import kotlin.time.ExperimentalTime import kotlin.time.measureTimedValue @@ -34,7 +35,16 @@ class ThreadAssembler { @OptIn(ExperimentalTime::class) fun findThreadFor(noteId: String): Set { val (result, elapsed) = measureTimedValue { - val note = LocalCache.getOrCreateNote(noteId) + val note = if (noteId.startsWith("naddr")) { + val aTag = ATag.parse(noteId) + if (aTag != null) + LocalCache.getOrCreateAddressableNote(aTag) + else + return emptySet() + } else { + LocalCache.getOrCreateNote(noteId) + } + if (note.event != null) { val thread = mutableSetOf() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt index c201b76f5..1b7de0513 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt @@ -3,7 +3,6 @@ package com.vitorpamplona.amethyst.model import androidx.lifecycle.LiveData import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource import com.vitorpamplona.amethyst.service.model.LnZapEvent -import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent import com.vitorpamplona.amethyst.service.model.ReportEvent import com.vitorpamplona.amethyst.service.relays.Relay import com.vitorpamplona.amethyst.ui.note.toShortenHex @@ -38,8 +37,7 @@ class User(val pubkeyHex: String) { var notes = setOf() private set - var longFormNotes = mapOf>() - private set + var taggedPosts = setOf() private set @@ -145,27 +143,8 @@ class User(val pubkeyHex: String) { notes = notes - note } - fun addLongFormNote(note: Note) { - val address = (note.event as LongTextNoteEvent).address - - if (address in longFormNotes.keys) { - if (longFormNotes[address]?.contains(note) == false) - longFormNotes = longFormNotes + Pair(address, (longFormNotes[address] ?: emptySet()) + note) - } else { - longFormNotes = longFormNotes + Pair(address, setOf(note)) - // No need for Listener yet - } - } - - fun removeLongFormNote(note: Note) { - val address = (note.event as LongTextNoteEvent).address ?: return - - longFormNotes = longFormNotes - address - } - fun clearNotes() { notes = setOf() - longFormNotes = mapOf>() } fun addReport(note: Note) { @@ -179,7 +158,7 @@ class User(val pubkeyHex: String) { liveSet?.reports?.invalidateData() } - val reportTime = note.event?.createdAt ?: 0 + val reportTime = note.createdAt() ?: 0 if (reportTime > latestReportTime) { latestReportTime = reportTime } @@ -311,7 +290,7 @@ class User(val pubkeyHex: String) { fun hasReport(loggedIn: User, type: ReportEvent.ReportType): Boolean { return reports[loggedIn]?.firstOrNull() { - it.event is ReportEvent && (it.event as ReportEvent).reportedAuthor.any { it.reportType == type } + it.event is ReportEvent && (it.event as ReportEvent).reportedAuthor().any { it.reportType == type } } != null } @@ -364,6 +343,7 @@ data class RelayInfo ( data class Chatroom(var roomMessages: Set) + class UserMetadata { var name: String? = null var username: String? = null diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip19.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip19.kt index 700afe405..f4b551be1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip19.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip19.kt @@ -1,12 +1,18 @@ package com.vitorpamplona.amethyst.service +import com.vitorpamplona.amethyst.model.toByteArray import com.vitorpamplona.amethyst.model.toHexKey +import com.vitorpamplona.amethyst.service.model.ATag +import java.nio.ByteBuffer +import java.nio.ByteOrder +import nostr.postr.Bech32 import nostr.postr.bechToBytes +import nostr.postr.toByteArray class Nip19 { enum class Type { - USER, NOTE + USER, NOTE, RELAY, ADDRESS } data class Return(val type: Type, val hex: String) @@ -24,16 +30,31 @@ class Nip19 { } if (key.startsWith("nprofile")) { val tlv = parseTLV(bytes) - val hex = tlv.get(0)?.get(0)?.toHexKey() + val hex = tlv.get(NIP19TLVTypes.SPECIAL.id)?.get(0)?.toHexKey() if (hex != null) return Return(Type.USER, hex) } if (key.startsWith("nevent")) { val tlv = parseTLV(bytes) - val hex = tlv.get(0)?.get(0)?.toHexKey() + val hex = tlv.get(NIP19TLVTypes.SPECIAL.id)?.get(0)?.toHexKey() if (hex != null) return Return(Type.USER, hex) } + if (key.startsWith("nrelay")) { + val tlv = parseTLV(bytes) + val relayUrl = tlv.get(NIP19TLVTypes.SPECIAL.id)?.get(0)?.toString(Charsets.UTF_8) + if (relayUrl != null) + return Return(Type.RELAY, relayUrl) + } + if (key.startsWith("naddr")) { + val tlv = parseTLV(bytes) + val d = tlv.get(NIP19TLVTypes.SPECIAL.id)?.get(0)?.toString(Charsets.UTF_8) + val relay = tlv.get(NIP19TLVTypes.RELAY.id)?.get(0)?.toString(Charsets.UTF_8) + val author = tlv.get(NIP19TLVTypes.AUTHOR.id)?.get(0)?.toHexKey() + val kind = tlv.get(NIP19TLVTypes.KIND.id)?.get(0)?.let { toInt32(it) } + if (d != null) + return Return(Type.ADDRESS, "$kind:$author:$d") + } } } catch (e: Throwable) { println("Issue trying to Decode NIP19 ${uri}: ${e.message}") @@ -42,22 +63,34 @@ class Nip19 { return null } +} - fun parseTLV(data: ByteArray): Map> { - var result = mutableMapOf>() - var rest = data - while (rest.isNotEmpty()) { - val t = rest[0] - val l = rest[1] - val v = rest.sliceArray(IntRange(2, (2 + l) - 1)) - rest = rest.sliceArray(IntRange(2 + l, rest.size-1)) - if (v.size < l) continue +enum class NIP19TLVTypes(val id: Byte) { //classes should start with an uppercase letter in kotlin + SPECIAL(0), + RELAY(1), + AUTHOR(2), + KIND(3); +} - if (!result.containsKey(t)) { - result.put(t, mutableListOf()) - } - result.get(t)?.add(v) +fun toInt32(bytes: ByteArray): Int { + require(bytes.size == 4) { "length must be 4, got: ${bytes.size}" } + return ByteBuffer.wrap(bytes, 0, 4).order(ByteOrder.BIG_ENDIAN).int +} + +fun parseTLV(data: ByteArray): Map> { + var result = mutableMapOf>() + var rest = data + while (rest.isNotEmpty()) { + val t = rest[0] + val l = rest[1] + val v = rest.sliceArray(IntRange(2, (2 + l) - 1)) + rest = rest.sliceArray(IntRange(2 + l, rest.size-1)) + if (v.size < l) continue + + if (!result.containsKey(t)) { + result.put(t, mutableListOf()) } - return result + result.get(t)?.add(v) } -} \ No newline at end of file + return result +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt index d9493450b..2956fdd7c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt @@ -74,7 +74,7 @@ abstract class NostrDataSource(val debugName: String) { RepostEvent.kind -> { val repostEvent = RepostEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig) - repostEvent.containedPost?.let { onEvent(it, subscriptionId, relay) } + repostEvent.containedPost()?.let { onEvent(it, subscriptionId, relay) } LocalCache.consume(repostEvent) } ReactionEvent.kind -> LocalCache.consume(ReactionEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig)) @@ -83,7 +83,7 @@ abstract class NostrDataSource(val debugName: String) { LnZapEvent.kind -> { val zapEvent = LnZapEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig) - zapEvent.containedPost?.let { onEvent(it, subscriptionId, relay) } + zapEvent.containedPost()?.let { onEvent(it, subscriptionId, relay) } LocalCache.consume(zapEvent) } LnZapRequestEvent.kind -> LocalCache.consume(LnZapRequestEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig)) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt index c35d2284c..90947cca0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt @@ -1,6 +1,6 @@ package com.vitorpamplona.amethyst.service -import com.vitorpamplona.amethyst.model.LocalCache +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 @@ -17,10 +17,11 @@ import nostr.postr.JsonFilter import com.vitorpamplona.amethyst.service.model.TextNoteEvent object NostrSingleEventDataSource: NostrDataSource("SingleEventFeed") { - private var eventsToWatch = setOf() + private var eventsToWatch = setOf() + private var addressesToWatch = setOf() private fun createAddressFilter(): List? { - val addressesToWatch = eventsToWatch.map { LocalCache.getOrCreateNote(it) }.filter { it.address() != null } + val addressesToWatch = eventsToWatch.filter { it.address() != null } + addressesToWatch if (addressesToWatch.isEmpty()) { return null @@ -31,22 +32,24 @@ object NostrSingleEventDataSource: NostrDataSource("SingleEventFeed") { return addressesToWatch.filter { val lastTime = it.lastReactionsDownloadTime lastTime == null || lastTime < (now - 10) - }.map { - TypedFilter( - types = FeedType.values().toSet(), - filter = JsonFilter( - kinds = listOf( - TextNoteEvent.kind, LongTextNoteEvent.kind, ReactionEvent.kind, RepostEvent.kind, ReportEvent.kind, LnZapEvent.kind, LnZapRequestEvent.kind - ), - tags = mapOf("a" to listOf(it.address()!!)), - since = it.lastReactionsDownloadTime + }.mapNotNull { + it.address()?.let { aTag -> + TypedFilter( + types = FeedType.values().toSet(), + filter = JsonFilter( + kinds = listOf( + TextNoteEvent.kind, LongTextNoteEvent.kind, ReactionEvent.kind, RepostEvent.kind, ReportEvent.kind, LnZapEvent.kind, LnZapRequestEvent.kind + ), + tags = mapOf("a" to listOf(aTag.toTag())), + since = it.lastReactionsDownloadTime + ) ) - ) + } } } private fun createRepliesAndReactionsFilter(): List? { - val reactionsToWatch = eventsToWatch.map { LocalCache.getOrCreateNote(it) } + val reactionsToWatch = eventsToWatch if (reactionsToWatch.isEmpty()) { return null @@ -73,11 +76,9 @@ object NostrSingleEventDataSource: NostrDataSource("SingleEventFeed") { fun createLoadEventsIfNotLoadedFilter(): List? { val directEventsToLoad = eventsToWatch - .map { LocalCache.getOrCreateNote(it) } .filter { it.event == null } val threadingEventsToLoad = eventsToWatch - .map { LocalCache.getOrCreateNote(it) } .mapNotNull { it.replyTo } .flatten() .filter { it.event == null } @@ -107,7 +108,7 @@ object NostrSingleEventDataSource: NostrDataSource("SingleEventFeed") { val singleEventChannel = requestNewChannel { time -> eventsToWatch.forEach { - LocalCache.getOrCreateNote(it).lastReactionsDownloadTime = time + it.lastReactionsDownloadTime = time } // Many relays operate with limits in the amount of filters. // As information comes, the filters will be rotated to get more data. @@ -122,13 +123,23 @@ object NostrSingleEventDataSource: NostrDataSource("SingleEventFeed") { singleEventChannel.typedFilters = listOfNotNull(reactions, missing, addresses).flatten().ifEmpty { null } } - fun add(eventId: String) { + fun add(eventId: Note) { eventsToWatch = eventsToWatch.plus(eventId) invalidateFilters() } - fun remove(eventId: String) { + fun remove(eventId: Note) { eventsToWatch = eventsToWatch.minus(eventId) invalidateFilters() } + + fun addAddress(aTag: Note) { + addressesToWatch = addressesToWatch.plus(aTag) + invalidateFilters() + } + + fun removeAddress(aTag: Note) { + addressesToWatch = addressesToWatch.minus(aTag) + invalidateFilters() + } } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ATag.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ATag.kt new file mode 100644 index 000000000..0ecfd8f5d --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ATag.kt @@ -0,0 +1,72 @@ +package com.vitorpamplona.amethyst.service.model + +import android.util.Log +import com.vitorpamplona.amethyst.model.toByteArray +import com.vitorpamplona.amethyst.model.toHexKey +import com.vitorpamplona.amethyst.service.NIP19TLVTypes +import com.vitorpamplona.amethyst.service.parseTLV +import com.vitorpamplona.amethyst.service.toInt32 +import fr.acinq.secp256k1.Hex +import nostr.postr.Bech32 +import nostr.postr.bechToBytes +import nostr.postr.toByteArray + +data class ATag(val kind: Int, val pubKeyHex: String, val dTag: String) { + fun toTag() = "$kind:$pubKeyHex:$dTag" + + fun toNAddr(): String { + val kind = kind.toByteArray() + val addr = pubKeyHex.toByteArray() + val dTag = dTag.toByteArray(Charsets.UTF_8) + + val fullArray = + byteArrayOf(NIP19TLVTypes.SPECIAL.id, dTag.size.toByte()) + dTag + + byteArrayOf(NIP19TLVTypes.AUTHOR.id, addr.size.toByte()) + addr + + byteArrayOf(NIP19TLVTypes.KIND.id, kind.size.toByte()) + kind + + return Bech32.encodeBytes(hrp = "naddr", fullArray, Bech32.Encoding.Bech32) + } + + companion object { + fun parse(address: String): ATag? { + return if (address.startsWith("naddr") || address.startsWith("nostr:naddr")) + parseNAddr(address) + else + parseAtag(address) + } + + fun parseAtag(atag: String): ATag? { + return try { + val parts = atag.split(":") + Hex.decode(parts[1]) + ATag(parts[0].toInt(), parts[1], parts[2]) + } catch (t: Throwable) { + Log.w("Address", "Error parsing A Tag: ${atag}: ${t.message}") + null + } + } + + fun parseNAddr(naddr: String): ATag? { + try { + val key = naddr.removePrefix("nostr:") + + if (key.startsWith("naddr")) { + val tlv = parseTLV(key.bechToBytes()) + val d = tlv.get(NIP19TLVTypes.SPECIAL.id)?.get(0)?.toString(Charsets.UTF_8) ?: "" + val relay = tlv.get(NIP19TLVTypes.RELAY.id)?.get(0)?.toString(Charsets.UTF_8) + val author = tlv.get(NIP19TLVTypes.AUTHOR.id)?.get(0)?.toHexKey() + val kind = tlv.get(NIP19TLVTypes.KIND.id)?.get(0)?.let { toInt32(it) } + + if (kind != null && author != null) + return ATag(kind, author, d) + } + + } catch (e: Throwable) { + println("Issue trying to Decode NIP19 ${this}: ${e.message}") + //e.printStackTrace() + } + + return null + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelCreateEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelCreateEvent.kt index c63798393..791aafecf 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelCreateEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelCreateEvent.kt @@ -14,15 +14,11 @@ class ChannelCreateEvent ( content: String, sig: ByteArray ): Event(id, pubKey, createdAt, kind, tags, content, sig) { - @Transient val channelInfo: ChannelData - - init { - channelInfo = try { - MetadataEvent.gson.fromJson(content, ChannelData::class.java) - } catch (e: Exception) { - Log.e("ChannelMetadataEvent", "Can't parse channel info $content", e) - ChannelData(null, null, null) - } + fun channelInfo() = try { + MetadataEvent.gson.fromJson(content, ChannelData::class.java) + } catch (e: Exception) { + Log.e("ChannelMetadataEvent", "Can't parse channel info $content", e) + ChannelData(null, null, null) } companion object { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelHideMessageEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelHideMessageEvent.kt index c15562778..41c526e4f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelHideMessageEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelHideMessageEvent.kt @@ -12,11 +12,7 @@ class ChannelHideMessageEvent ( content: String, sig: ByteArray ): Event(id, pubKey, createdAt, kind, tags, content, sig) { - @Transient val eventsToHide: List - - init { - eventsToHide = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } - } + fun eventsToHide() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } companion object { const val kind = 43 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMessageEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMessageEvent.kt index 459fc4739..39f0e6bae 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMessageEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMessageEvent.kt @@ -12,15 +12,10 @@ class ChannelMessageEvent ( content: String, sig: ByteArray ): Event(id, pubKey, createdAt, kind, tags, content, sig) { - @Transient val channel: String? - @Transient val replyTos: List - @Transient val mentions: List - init { - channel = tags.firstOrNull { it[0] == "e" && it.size > 3 && it[3] == "root" }?.getOrNull(1) ?: tags.firstOrNull { it.firstOrNull() == "e" }?.getOrNull(1) - replyTos = tags.filter { it.getOrNull(1) != channel }.mapNotNull { it.getOrNull(1) } - mentions = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } - } + fun channel() = tags.firstOrNull { it[0] == "e" && it.size > 3 && it[3] == "root" }?.getOrNull(1) ?: tags.firstOrNull { it.firstOrNull() == "e" }?.getOrNull(1) + fun replyTos() = tags.filter { it.getOrNull(1) != channel() }.mapNotNull { it.getOrNull(1) } + fun mentions() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } companion object { const val kind = 42 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMetadataEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMetadataEvent.kt index 09bff41ec..2552ad89c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMetadataEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMetadataEvent.kt @@ -14,19 +14,14 @@ class ChannelMetadataEvent ( content: String, sig: ByteArray ): Event(id, pubKey, createdAt, kind, tags, content, sig) { - @Transient val channel: String? - @Transient val channelInfo: ChannelCreateEvent.ChannelData - - init { - channel = tags.firstOrNull { it.firstOrNull() == "e" }?.getOrNull(1) - channelInfo = - try { - MetadataEvent.gson.fromJson(content, ChannelCreateEvent.ChannelData::class.java) - } catch (e: Exception) { - Log.e("ChannelMetadataEvent", "Can't parse channel info $content", e) - ChannelCreateEvent.ChannelData(null, null, null) - } - } + fun channel() = tags.firstOrNull { it.firstOrNull() == "e" }?.getOrNull(1) + fun channelInfo() = + try { + MetadataEvent.gson.fromJson(content, ChannelCreateEvent.ChannelData::class.java) + } catch (e: Exception) { + Log.e("ChannelMetadataEvent", "Can't parse channel info $content", e) + ChannelCreateEvent.ChannelData(null, null, null) + } companion object { const val kind = 41 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMuteUserEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMuteUserEvent.kt index dcc52755d..23c1c52fd 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMuteUserEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMuteUserEvent.kt @@ -12,11 +12,9 @@ class ChannelMuteUserEvent ( content: String, sig: ByteArray ): Event(id, pubKey, createdAt, kind, tags, content, sig) { - @Transient val usersToMute: List - init { - usersToMute = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } - } + fun usersToMute() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } + companion object { const val kind = 44 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt index acaa2e9ab..2a38c8cf4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt @@ -14,31 +14,25 @@ class LnZapEvent ( sig: ByteArray ): Event(id, pubKey, createdAt, kind, tags, content, sig) { - @Transient val zappedPost: List - @Transient val zappedAuthor: List - @Transient val containedPost: Event? - @Transient val lnInvoice: String? - @Transient val preimage: String? - @Transient val amount: BigDecimal? + fun zappedPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } + fun zappedAuthor() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } - init { - zappedPost = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } - zappedAuthor = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } + fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { it.getOrNull(1) }.mapNotNull { ATag.parse(it) } - lnInvoice = tags.filter { it.firstOrNull() == "bolt11" }.mapNotNull { it.getOrNull(1) }.firstOrNull() - amount = lnInvoice?.let { LnInvoiceUtil.getAmountInSats(lnInvoice) } - preimage = tags.filter { it.firstOrNull() == "preimage" }.mapNotNull { it.getOrNull(1) }.firstOrNull() + fun lnInvoice() = tags.filter { it.firstOrNull() == "bolt11" }.mapNotNull { it.getOrNull(1) }.firstOrNull() + fun preimage() = tags.filter { it.firstOrNull() == "preimage" }.mapNotNull { it.getOrNull(1) }.firstOrNull() - val description = tags.filter { it.firstOrNull() == "description" }.mapNotNull { it.getOrNull(1) }.firstOrNull() + fun description() = tags.filter { it.firstOrNull() == "description" }.mapNotNull { it.getOrNull(1) }.firstOrNull() - containedPost = try { - if (description == null) - null - else - fromJson(description, Client.lenient) - } catch (e: Exception) { - null + // Keeps this as a field because it's a heavier function used everywhere. + val amount = lnInvoice()?.let { LnInvoiceUtil.getAmountInSats(it) } + + fun containedPost() = try { + description()?.let { + fromJson(it, Client.lenient) } + } catch (e: Exception) { + null } companion object { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt index 9c6b57034..049b4b434 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt @@ -13,14 +13,9 @@ class LnZapRequestEvent ( content: String, sig: ByteArray ): Event(id, pubKey, createdAt, kind, tags, content, sig) { - - @Transient val zappedPost: List - @Transient val zappedAuthor: List - - init { - zappedPost = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } - zappedAuthor = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } - } + fun zappedPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } + fun zappedAuthor() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } + fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { it.getOrNull(1) }.mapNotNull { ATag.parse(it) } companion object { const val kind = 9734 @@ -34,7 +29,7 @@ class LnZapRequestEvent ( listOf("relays") + relays ) if (originalNote is LongTextNoteEvent) { - tags = tags + listOf( listOf("a", originalNote.address) ) + tags = tags + listOf( listOf("a", originalNote.address().toTag()) ) } val id = generateId(pubKey, createdAt, kind, tags, content) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LongTextNoteEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LongTextNoteEvent.kt index 1c2d67558..037c23c83 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LongTextNoteEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LongTextNoteEvent.kt @@ -13,33 +13,21 @@ class LongTextNoteEvent( content: String, sig: ByteArray ): Event(id, pubKey, createdAt, kind, tags, content, sig) { - @Transient val replyTos: List - @Transient val mentions: List + fun replyTos() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } + fun mentions() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } - @Transient val title: String? - @Transient val image: String? - @Transient val summary: String? - @Transient val publishedAt: Long? - @Transient val topics: List - @Transient val address: String - @Transient val dTag: String? + fun dTag() = tags.filter { it.firstOrNull() == "d" }.mapNotNull { it.getOrNull(1) }.firstOrNull() ?: "" + fun address() = ATag(kind, pubKey.toHexKey(), dTag()) - init { - replyTos = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } - mentions = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } + fun topics() = tags.filter { it.firstOrNull() == "t" }.mapNotNull { it.getOrNull(1) } + fun title() = tags.filter { it.firstOrNull() == "title" }.mapNotNull { it.getOrNull(1) }.firstOrNull() + fun image() = tags.filter { it.firstOrNull() == "image" }.mapNotNull { it.getOrNull(1) }.firstOrNull() + fun summary() = tags.filter { it.firstOrNull() == "summary" }.mapNotNull { it.getOrNull(1) }.firstOrNull() - dTag = tags.filter { it.firstOrNull() == "d" }.mapNotNull { it.getOrNull(1) }.firstOrNull() - - address = tags.filter { it.firstOrNull() == "a" }.mapNotNull { it.getOrNull(1) }.firstOrNull() ?: "$kind:${pubKey.toHexKey()}:$dTag" - topics = tags.filter { it.firstOrNull() == "t" }.mapNotNull { it.getOrNull(1) } - title = tags.filter { it.firstOrNull() == "title" }.mapNotNull { it.getOrNull(1) }.firstOrNull() - image = tags.filter { it.firstOrNull() == "image" }.mapNotNull { it.getOrNull(1) }.firstOrNull() - summary = tags.filter { it.firstOrNull() == "summary" }.mapNotNull { it.getOrNull(1) }.firstOrNull() - publishedAt = try { - tags.filter { it.firstOrNull() == "published_at" }.mapNotNull { it.getOrNull(1) }.firstOrNull()?.toLong() - } catch (_: Exception) { - null - } + fun publishedAt() = try { + tags.filter { it.firstOrNull() == "published_at" }.mapNotNull { it.getOrNull(1) }.firstOrNull()?.toLong() + } catch (_: Exception) { + null } companion object { @@ -59,4 +47,4 @@ class LongTextNoteEvent( return LongTextNoteEvent(id, pubKey, createdAt, tags, msg, sig) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt index 264ac4990..6048a0978 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt @@ -14,13 +14,9 @@ class ReactionEvent ( sig: ByteArray ): Event(id, pubKey, createdAt, kind, tags, content, sig) { - @Transient val originalPost: List - @Transient val originalAuthor: List - - init { - originalPost = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } - originalAuthor = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } - } + fun originalPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } + fun originalAuthor() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } + fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { it.getOrNull(1) }.mapNotNull { ATag.parse(it) } companion object { const val kind = 7 @@ -38,7 +34,7 @@ class ReactionEvent ( var tags = listOf( listOf("e", originalNote.id.toHex()), listOf("p", originalNote.pubKey.toHex())) if (originalNote is LongTextNoteEvent) { - tags = tags + listOf( listOf("a", originalNote.address) ) + tags = tags + listOf( listOf("a", originalNote.address().toTag()) ) } val id = generateId(pubKey, createdAt, kind, tags, content) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt index 1129ff579..a1d83c155 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt @@ -17,12 +17,8 @@ class ReportEvent ( sig: ByteArray ): Event(id, pubKey, createdAt, kind, tags, content, sig) { - @Transient val reportedPost: List - @Transient val reportedAuthor: List - - init { + private fun defaultReportType(): ReportType { // Works with old and new structures for report. - var reportType = tags.filter { it.firstOrNull() == "report" }.mapNotNull { it.getOrNull(1) }.map { ReportType.valueOf(it.toUpperCase()) }.firstOrNull() if (reportType == null) { reportType = tags.mapNotNull { it.getOrNull(2) }.map { ReportType.valueOf(it.toUpperCase()) }.firstOrNull() @@ -30,26 +26,29 @@ class ReportEvent ( if (reportType == null) { reportType = ReportType.SPAM } - - reportedPost = tags - .filter { it.firstOrNull() == "e" && it.getOrNull(1) != null } - .map { - ReportedKey( - it[1], - it.getOrNull(2)?.toUpperCase()?.let { it1 -> ReportType.valueOf(it1) }?: reportType - ) - } - - reportedAuthor = tags - .filter { it.firstOrNull() == "p" && it.getOrNull(1) != null } - .map { - ReportedKey( - it[1], - it.getOrNull(2)?.toUpperCase()?.let { it1 -> ReportType.valueOf(it1) }?: reportType - ) - } + return reportType } + fun reportedPost() = tags + .filter { it.firstOrNull() == "e" && it.getOrNull(1) != null } + .map { + ReportedKey( + it[1], + it.getOrNull(2)?.toUpperCase()?.let { it1 -> ReportType.valueOf(it1) }?: defaultReportType() + ) + } + + fun reportedAuthor() = tags + .filter { it.firstOrNull() == "p" && it.getOrNull(1) != null } + .map { + ReportedKey( + it[1], + it.getOrNull(2)?.toUpperCase()?.let { it1 -> ReportType.valueOf(it1) }?: defaultReportType() + ) + } + + fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { it.getOrNull(1) }.mapNotNull { ATag.parse(it) } + companion object { const val kind = 1984 @@ -63,7 +62,7 @@ class ReportEvent ( var tags:List> = listOf(reportPostTag, reportAuthorTag) if (reportedPost is LongTextNoteEvent) { - tags = tags + listOf( listOf("a", reportedPost.address) ) + tags = tags + listOf( listOf("a", reportedPost.address().toTag()) ) } val id = generateId(pubKey, createdAt, kind, tags, content) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/RepostEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/RepostEvent.kt index d0d405d5b..3da165f2c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/RepostEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/RepostEvent.kt @@ -15,19 +15,15 @@ class RepostEvent ( sig: ByteArray ): Event(id, pubKey, createdAt, kind, tags, content, sig) { - @Transient val boostedPost: List - @Transient val originalAuthor: List - @Transient val containedPost: Event? - init { - boostedPost = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } - originalAuthor = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } + fun boostedPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } + fun originalAuthor() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } + fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { it.getOrNull(1) }.mapNotNull { ATag.parse(it) } - containedPost = try { - fromJson(content, Client.lenient) - } catch (e: Exception) { - null - } + fun containedPost() = try { + fromJson(content, Client.lenient) + } catch (e: Exception) { + null } companion object { @@ -43,7 +39,7 @@ class RepostEvent ( var tags:List> = boostedPost.tags.plus(listOf(replyToPost, replyToAuthor)) if (boostedPost is LongTextNoteEvent) { - tags = tags + listOf( listOf("a", boostedPost.address) ) + tags = tags + listOf( listOf("a", boostedPost.address().toTag()) ) } val id = generateId(pubKey, createdAt, kind, tags, content) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/TextNoteEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/TextNoteEvent.kt index e22c0418d..9e1634b16 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/TextNoteEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/TextNoteEvent.kt @@ -12,20 +12,14 @@ class TextNoteEvent( content: String, sig: ByteArray ): Event(id, pubKey, createdAt, kind, tags, content, sig) { - @Transient val replyTos: List - @Transient val mentions: List - @Transient val longFormAddress: List - - init { - longFormAddress = tags.filter { it.firstOrNull() == "a" }.mapNotNull { it.getOrNull(1) } - replyTos = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } - mentions = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } - } + fun mentions() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } + fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { it.getOrNull(1) }.mapNotNull { ATag.parse(it) } + fun replyTos() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } companion object { const val kind = 1 - fun create(msg: String, replyTos: List?, mentions: List?, addresses: List?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): TextNoteEvent { + fun create(msg: String, replyTos: List?, mentions: List?, addresses: List?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): TextNoteEvent { val pubKey = Utils.pubkeyCreate(privateKey) val tags = mutableListOf>() replyTos?.forEach { @@ -35,7 +29,7 @@ class TextNoteEvent( tags.add(listOf("p", it)) } addresses?.forEach { - tags.add(listOf("a", it)) + tags.add(listOf("a", it.toTag())) } val id = generateId(pubKey, createdAt, kind, tags, msg) val sig = Utils.sign(id, privateKey) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChannelFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChannelFeedFilter.kt index c334910fa..86a2424be 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChannelFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChannelFeedFilter.kt @@ -16,6 +16,6 @@ object ChannelFeedFilter: FeedFilter() { // returns the last Note of each user. override fun feed(): List { - return channel.notes?.values?.filter { account.isAcceptable(it) }?.sortedBy { it.event?.createdAt }?.reversed() ?: emptyList() + return channel.notes?.values?.filter { account.isAcceptable(it) }?.sortedBy { it.createdAt() }?.reversed() ?: emptyList() } } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomFeedFilter.kt index fe58f8031..ab198227e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomFeedFilter.kt @@ -23,6 +23,6 @@ object ChatroomFeedFilter: FeedFilter() { val messages = myAccount.userProfile().privateChatrooms[myUser] ?: return emptyList() - return messages.roomMessages.filter { myAccount.isAcceptable(it) }.sortedBy { it.event?.createdAt }.reversed() + return messages.roomMessages.filter { myAccount.isAcceptable(it) }.sortedBy { it.createdAt() }.reversed() } } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListKnownFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListKnownFeedFilter.kt index 678eace2c..c6b32ee05 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListKnownFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListKnownFeedFilter.kt @@ -17,17 +17,17 @@ object ChatroomListKnownFeedFilter: FeedFilter() { val privateMessages = messagingWith.mapNotNull { privateChatrooms[it]?.roomMessages?.sortedBy { - it.event?.createdAt + it.createdAt() }?.lastOrNull { it.event != null } } val publicChannels = account.followingChannels().map { - it.notes.values.filter { account.isAcceptable(it) }.sortedBy { it.event?.createdAt }.lastOrNull { it.event != null } + it.notes.values.filter { account.isAcceptable(it) }.sortedBy { it.createdAt() }.lastOrNull { it.event != null } } - return (privateMessages + publicChannels).filterNotNull().sortedBy { it.event?.createdAt }.reversed() + return (privateMessages + publicChannels).filterNotNull().sortedBy { it.createdAt() }.reversed() } } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListNewFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListNewFeedFilter.kt index 3bb7eb510..77e7ef015 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListNewFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListNewFeedFilter.kt @@ -17,13 +17,13 @@ object ChatroomListNewFeedFilter: FeedFilter() { val privateMessages = messagingWith.mapNotNull { privateChatrooms[it]?.roomMessages?.sortedBy { - it.event?.createdAt + it.createdAt() }?.lastOrNull { it.event != null } } - return privateMessages.sortedBy { it.event?.createdAt }.reversed() + return privateMessages.sortedBy { it.createdAt() }.reversed() } } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/GlobalFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/GlobalFeedFilter.kt index 010e3ef53..6ad966e23 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/GlobalFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/GlobalFeedFilter.kt @@ -4,6 +4,7 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent +import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent object GlobalFeedFilter: FeedFilter() { @@ -11,11 +12,17 @@ object GlobalFeedFilter: FeedFilter() { override fun feed() = LocalCache.notes.values .filter { - (it.event is TextNoteEvent && (it.event as TextNoteEvent).replyTos.isEmpty()) || - (it.event is ChannelMessageEvent && (it.event as ChannelMessageEvent).replyTos.isEmpty()) + (it.event is TextNoteEvent || it.event is LongTextNoteEvent || it.event is ChannelMessageEvent) + && it.replyTo.isNullOrEmpty() + } + .filter { + // does not show events already in the public chat list + (it.channel() == null || it.channel() !in account.followingChannels()) + // does not show people the user already follows + && (it.author !in account.userProfile().follows) } .filter { account.isAcceptable(it) } - .sortedBy { it.event?.createdAt } + .sortedBy { it.createdAt() } .reversed() } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeConversationsFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeConversationsFeedFilter.kt index 1f96b3ee8..e53d0d4c0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeConversationsFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeConversationsFeedFilter.kt @@ -20,7 +20,7 @@ object HomeConversationsFeedFilter: FeedFilter() { && it.author?.let { !HomeNewThreadFeedFilter.account.isHidden(it) } ?: true && !it.isNewThread() } - .sortedBy { it.event?.createdAt } + .sortedBy { it.createdAt() } .reversed() } } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt index 5276c2437..76307908c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt @@ -13,7 +13,7 @@ object HomeNewThreadFeedFilter: FeedFilter() { override fun feed(): List { val user = account.userProfile() - return LocalCache.notes.values + val notes = LocalCache.notes.values .filter { (it.event is TextNoteEvent || it.event is RepostEvent || it.event is LongTextNoteEvent) && it.author in user.follows @@ -21,7 +21,18 @@ object HomeNewThreadFeedFilter: FeedFilter() { && it.author?.let { !account.isHidden(it) } ?: true && it.isNewThread() } - .sortedBy { it.event?.createdAt } + + val longFormNotes = LocalCache.addressables.values + .filter { + (it.event is TextNoteEvent || it.event is RepostEvent || it.event is LongTextNoteEvent) + && it.author in user.follows + // && account.isAcceptable(it) // This filter follows only. No need to check if acceptable + && it.author?.let { !account.isHidden(it) } ?: true + && it.isNewThread() + } + + return (notes + longFormNotes) + .sortedBy { it.createdAt() } .reversed() } } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt index 80f94cd27..c3b8d7e70 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt @@ -63,7 +63,7 @@ object NotificationFeedFilter: FeedFilter() { ) } - .sortedBy { it.event?.createdAt } + .sortedBy { it.createdAt() } .reversed() } } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileConversationsFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileConversationsFeedFilter.kt index 8c0288806..3d8415581 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileConversationsFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileConversationsFeedFilter.kt @@ -17,7 +17,7 @@ object UserProfileConversationsFeedFilter: FeedFilter() { override fun feed(): List { return user?.notes ?.filter { account?.isAcceptable(it) == true && !it.isNewThread() } - ?.sortedBy { it.event?.createdAt } + ?.sortedBy { it.createdAt() } ?.reversed() ?: emptyList() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileNewThreadFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileNewThreadFeedFilter.kt index fd5316df0..3465375cf 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileNewThreadFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileNewThreadFeedFilter.kt @@ -15,9 +15,11 @@ object UserProfileNewThreadFeedFilter: FeedFilter() { } override fun feed(): List { - return user?.notes?.plus(user?.longFormNotes?.values?.flatten() ?: emptySet()) + val longFormNotes = LocalCache.addressables.values.filter { it.author == user } + + return user?.notes?.plus(longFormNotes) ?.filter { account?.isAcceptable(it) == true && it.isNewThread() } - ?.sortedBy { it.event?.createdAt } + ?.sortedBy { it.createdAt() } ?.reversed() ?: emptyList() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileReportsFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileReportsFeedFilter.kt index 2e527c002..fd4e383de 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileReportsFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileReportsFeedFilter.kt @@ -12,6 +12,6 @@ object UserProfileReportsFeedFilter: FeedFilter() { } override fun feed(): List { - return user?.reports?.values?.flatten()?.sortedBy { it.event?.createdAt }?.reversed() ?: emptyList() + return user?.reports?.values?.flatten()?.sortedBy { it.createdAt() }?.reversed() ?: emptyList() } } \ No newline at end of file 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 8da81cfa0..bf48cecf0 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 @@ -107,7 +107,7 @@ private fun homeHasNewItems(account: Account, cache: NotificationCache, context: HomeNewThreadFeedFilter.account = account - return (HomeNewThreadFeedFilter.feed().firstOrNull { it.event?.createdAt != null }?.event?.createdAt ?: 0) > lastTime + return (HomeNewThreadFeedFilter.feed().firstOrNull { it.createdAt() != null }?.createdAt() ?: 0) > lastTime } private fun notificationHasNewItems(account: Account, cache: NotificationCache, context: Context): Boolean { @@ -115,17 +115,17 @@ private fun notificationHasNewItems(account: Account, cache: NotificationCache, NotificationFeedFilter.account = account - return (NotificationFeedFilter.feed().firstOrNull { it.event?.createdAt != null }?.event?.createdAt ?: 0) > lastTime + return (NotificationFeedFilter.feed().firstOrNull { it.createdAt() != null }?.createdAt() ?: 0) > lastTime } private fun messagesHasNewItems(account: Account, cache: NotificationCache, context: Context): Boolean { ChatroomListKnownFeedFilter.account = account val note = ChatroomListKnownFeedFilter.feed().firstOrNull { - it.event?.createdAt != null && it.channel() == null && it.author != account.userProfile() + it.createdAt() != null && it.channel() == null && it.author != account.userProfile() } ?: return false val lastTime = cache.load("Room/${note.author?.pubkeyHex}", context) - return (note.event?.createdAt ?: 0) > lastTime + return (note.createdAt() ?: 0) > lastTime } \ No newline at end of file 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 91e97681b..926636feb 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 @@ -83,8 +83,8 @@ fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navContr var hasNewMessages by remember { mutableStateOf(false) } LaunchedEffect(key1 = notificationCache) { - noteEvent?.let { - hasNewMessages = it.createdAt > notificationCache.cache.load("Channel/${channel.idHex}", context) + note.createdAt()?.let { + hasNewMessages = it > notificationCache.cache.load("Channel/${channel.idHex}", context) } } @@ -103,7 +103,7 @@ fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navContr color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) ) }, - channelLastTime = note.event?.createdAt, + channelLastTime = note.createdAt(), channelLastContent = "${author?.toBestDisplayName()}: " + description, hasNewMessages = hasNewMessages, onClick = { navController.navigate("Channel/${channel.idHex}") }) @@ -134,7 +134,7 @@ fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navContr ChannelName( channelPicture = { UserPicture(userToComposeOn, account.userProfile(), size = 55.dp) }, channelTitle = { UsernameDisplay(userToComposeOn, it) }, - channelLastTime = noteEvent?.createdAt, + channelLastTime = note.createdAt(), channelLastContent = accountViewModel.decrypt(note), hasNewMessages = hasNewMessages, onClick = { navController.navigate("Room/${user.pubkeyHex}") }) 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 8b6fb5539..87f749abc 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 @@ -134,7 +134,7 @@ fun ChatroomMessageCompose( routeForLastRead?.let { val lastTime = NotificationCache.load(it, context) - val createdAt = note.event?.createdAt + val createdAt = note.createdAt() if (createdAt != null) { NotificationCache.markAsRead(it, createdAt, context) isNew = createdAt > lastTime @@ -241,16 +241,16 @@ fun ChatroomMessageCompose( val event = note.event if (event is ChannelCreateEvent) { Text(text = note.author?.toBestDisplayName() - .toString() + " ${stringResource(R.string.created)} " + (event.channelInfo.name - ?: "") +" ${stringResource(R.string.with_description_of)} '" + (event.channelInfo.about - ?: "") + "', ${stringResource(R.string.and_picture)} '" + (event.channelInfo.picture + .toString() + " ${stringResource(R.string.created)} " + (event.channelInfo().name + ?: "") +" ${stringResource(R.string.with_description_of)} '" + (event.channelInfo().about + ?: "") + "', ${stringResource(R.string.and_picture)} '" + (event.channelInfo().picture ?: "") + "'" ) } else if (event is ChannelMetadataEvent) { Text(text = note.author?.toBestDisplayName() - .toString() + " ${stringResource(R.string.changed_chat_name_to)} '" + (event.channelInfo.name - ?: "") + "$', {stringResource(R.string.description_to)} '" + (event.channelInfo.about - ?: "") + "', ${stringResource(R.string.and_picture_to)} '" + (event.channelInfo.picture + .toString() + " ${stringResource(R.string.changed_chat_name_to)} '" + (event.channelInfo().name + ?: "") + "$', {stringResource(R.string.description_to)} '" + (event.channelInfo().about + ?: "") + "', ${stringResource(R.string.and_picture_to)} '" + (event.channelInfo().picture ?: "") + "'" ) } else { @@ -295,7 +295,7 @@ fun ChatroomMessageCompose( ) { Row() { Text( - timeAgoShort(note.event?.createdAt, context), + timeAgoShort(note.createdAt(), context), color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f), fontSize = 12.sp ) 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 c56c242f0..77e332947 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 @@ -107,7 +107,7 @@ fun NoteCompose( routeForLastRead?.let { val lastTime = NotificationCache.load(it, context) - val createdAt = noteEvent.createdAt + val createdAt = note.createdAt() if (createdAt != null) { NotificationCache.markAsRead(it, createdAt, context) isNew = createdAt > lastTime @@ -241,7 +241,7 @@ fun NoteCompose( } Text( - timeAgo(noteEvent.createdAt, context = context), + timeAgo(note.createdAt(), context = context), color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f), maxLines = 1 ) @@ -322,7 +322,7 @@ fun NoteCompose( ) } } else if (noteEvent is ReportEvent) { - val reportType = (noteEvent.reportedPost + noteEvent.reportedAuthor).map { + val reportType = (noteEvent.reportedPost() + noteEvent.reportedAuthor()).map { when (it.reportType) { ReportEvent.ReportType.EXPLICIT -> stringResource(R.string.explicit_content) ReportEvent.ReportType.NUDITY -> stringResource(R.string.nudity) @@ -343,50 +343,7 @@ fun NoteCompose( thickness = 0.25.dp ) } else if (noteEvent is LongTextNoteEvent) { - Row( - modifier = Modifier - .clip(shape = RoundedCornerShape(15.dp)) - .border(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.12f), RoundedCornerShape(15.dp)) - ) { - Column { - noteEvent.image?.let { - AsyncImage( - model = noteEvent.image, - contentDescription = stringResource( - R.string.preview_card_image_for, - noteEvent.image - ), - contentScale = ContentScale.FillWidth, - modifier = Modifier.fillMaxWidth() - ) - } - - noteEvent.title?.let { - Text( - text = it, - style = MaterialTheme.typography.body2, - modifier = Modifier - .fillMaxWidth() - .padding(start = 10.dp, end = 10.dp, top = 10.dp), - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } - - noteEvent.summary?.let { - Text( - text = it, - style = MaterialTheme.typography.caption, - modifier = Modifier - .fillMaxWidth() - .padding(start = 10.dp, end = 10.dp, bottom = 10.dp), - color = Color.Gray, - maxLines = 3, - overflow = TextOverflow.Ellipsis - ) - } - } - } + LongFormHeader(noteEvent) ReactionsRow(note, accountViewModel) @@ -429,6 +386,56 @@ fun NoteCompose( } } +@Composable +private fun LongFormHeader(noteEvent: LongTextNoteEvent) { + Row( + modifier = Modifier + .clip(shape = RoundedCornerShape(15.dp)) + .border( + 1.dp, + MaterialTheme.colors.onSurface.copy(alpha = 0.12f), + RoundedCornerShape(15.dp) + ) + ) { + Column { + noteEvent.image()?.let { + AsyncImage( + model = it, + contentDescription = stringResource( + R.string.preview_card_image_for, + it + ), + contentScale = ContentScale.FillWidth, + modifier = Modifier.fillMaxWidth() + ) + } + + noteEvent.title()?.let { + Text( + text = it, + style = MaterialTheme.typography.body1, + modifier = Modifier + .fillMaxWidth() + .padding(start = 10.dp, end = 10.dp, top = 10.dp) + ) + } + + noteEvent.summary()?.let { + Text( + text = it, + style = MaterialTheme.typography.caption, + modifier = Modifier + .fillMaxWidth() + .padding(start = 10.dp, end = 10.dp, bottom = 10.dp), + color = Color.Gray, + maxLines = 3, + overflow = TextOverflow.Ellipsis + ) + } + } + } +} + @Composable private fun RelayBadges(baseNote: Note) { val noteRelaysState by baseNote.live().relays.observeAsState() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedState.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedState.kt index 7b636ef14..669bc0dad 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedState.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedState.kt @@ -10,14 +10,14 @@ abstract class Card() { class NoteCard(val note: Note): Card() { override fun createdAt(): Long { - return note.event?.createdAt ?: 0 + return note.createdAt() ?: 0 } override fun id() = note.idHex } class LikeSetCard(val note: Note, val likeEvents: List): Card() { - val createdAt = likeEvents.maxOf { it.event?.createdAt ?: 0 } + val createdAt = likeEvents.maxOf { it.createdAt() ?: 0 } override fun createdAt(): Long { return createdAt } @@ -25,7 +25,7 @@ class LikeSetCard(val note: Note, val likeEvents: List): Card() { } class ZapSetCard(val note: Note, val zapEvents: Map): Card() { - val createdAt = zapEvents.maxOf { it.value.event?.createdAt ?: 0 } + val createdAt = zapEvents.maxOf { it.value.createdAt() ?: 0 } override fun createdAt(): Long { return createdAt } @@ -34,9 +34,9 @@ class ZapSetCard(val note: Note, val zapEvents: Map): Card() { class MultiSetCard(val note: Note, val boostEvents: List, val likeEvents: List, val zapEvents: Map): Card() { val createdAt = maxOf( - zapEvents.maxOfOrNull { it.value.event?.createdAt ?: 0 } ?: 0 , - likeEvents.maxOfOrNull { it.event?.createdAt ?: 0 } ?: 0 , - boostEvents.maxOfOrNull { it.event?.createdAt ?: 0 } ?: 0 + zapEvents.maxOfOrNull { it.value.createdAt() ?: 0 } ?: 0 , + likeEvents.maxOfOrNull { it.createdAt() ?: 0 } ?: 0 , + boostEvents.maxOfOrNull { it.createdAt() ?: 0 } ?: 0 ) override fun createdAt(): Long { @@ -46,7 +46,7 @@ class MultiSetCard(val note: Note, val boostEvents: List, val likeEvents: } class BoostSetCard(val note: Note, val boostEvents: List): Card() { - val createdAt = boostEvents.maxOf { it.event?.createdAt ?: 0 } + val createdAt = boostEvents.maxOf { it.createdAt() ?: 0 } override fun createdAt(): Long { return createdAt 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 df931ea53..e78a451e9 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 @@ -97,11 +97,6 @@ private fun FeedLoaded( ) { val listState = rememberLazyListState() - LaunchedEffect(Unit) { - delay(500) - listState.animateScrollToItem(0) - } - LazyColumn( contentPadding = PaddingValues( top = 10.dp, 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 7f749f8d1..e0c7b07b2 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 @@ -243,7 +243,7 @@ fun NoteMaster(baseNote: Note, NoteUsernameDisplay(baseNote, Modifier.weight(1f)) Text( - timeAgo(noteEvent.createdAt, context = context), + timeAgo(note.createdAt(), context = context), color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f), maxLines = 1 ) @@ -270,19 +270,19 @@ fun NoteMaster(baseNote: Note, if (noteEvent is LongTextNoteEvent) { Row(modifier = Modifier.padding(start = 12.dp, end = 12.dp, top = 10.dp)) { Column { - noteEvent.image?.let { + noteEvent.image()?.let { AsyncImage( - model = noteEvent.image, + model = it, contentDescription = stringResource( R.string.preview_card_image_for, - noteEvent.image + it ), contentScale = ContentScale.FillWidth, modifier = Modifier.fillMaxWidth() ) } - noteEvent.title?.let { + noteEvent.title()?.let { Text( text = it, fontSize = 30.sp, @@ -293,7 +293,7 @@ fun NoteMaster(baseNote: Note, ) } - noteEvent.summary?.let { + noteEvent.summary()?.let { Text( text = it, modifier = Modifier diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 37cb35f12..1d6d7acef 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -7,7 +7,7 @@ Scan QR Show Anyway Post was flagged as inappropriate by - post not found + Post not found Channel Image Referenced event not found Could Not decrypt the message diff --git a/app/src/test/java/com/vitorpamplona/amethyst/NIP19ParserTest.kt b/app/src/test/java/com/vitorpamplona/amethyst/NIP19ParserTest.kt new file mode 100644 index 000000000..9389dc692 --- /dev/null +++ b/app/src/test/java/com/vitorpamplona/amethyst/NIP19ParserTest.kt @@ -0,0 +1,33 @@ +package com.vitorpamplona.amethyst + +import com.vitorpamplona.amethyst.service.Nip19 +import com.vitorpamplona.amethyst.service.model.ATag +import com.vitorpamplona.amethyst.service.toNAddr +import org.junit.Assert.assertEquals +import org.junit.Test + +class NIP19ParserTest { + @Test + fun nAddrParser() { + val result = Nip19().uriToRoute("nostr:naddr1qqqqygzxpsj7dqha57pjk5k37gkn6g4nzakewtmqmnwryyhd3jfwlpgxtspsgqqqw4rs3xyxus") + assertEquals("30023:460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c:", result?.hex) + } + + @Test + fun nAddrParser2() { + val result = Nip19().uriToRoute("nostr:naddr1qq8kwatfv3jj6amfwfjkwatpwfjqygxsm6lelvfda7qlg0tud9pfhduysy4vrexj65azqtdk4tr75j6xdspsgqqqw4rsg32ag8") + assertEquals("30023:d0debf9fb12def81f43d7c69429bb784812ac1e4d2d53a202db6aac7ea4b466c:guide-wireguard", result?.hex) + } + + @Test + fun nAddrFormatter() { + val address = ATag(30023, "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", "" ) + assertEquals("naddr1qqqqygzxpsj7dqha57pjk5k37gkn6g4nzakewtmqmnwryyhd3jfwlpgxtspsgqqqw4rs3xyxus", address.toNAddr()) + } + + @Test + fun nAddrFormatter2() { + val address = ATag(30023, "d0debf9fb12def81f43d7c69429bb784812ac1e4d2d53a202db6aac7ea4b466c", "guide-wireguard" ) + assertEquals("naddr1qq8kwatfv3jj6amfwfjkwatpwfjqygxsm6lelvfda7qlg0tud9pfhduysy4vrexj65azqtdk4tr75j6xdspsgqqqw4rsg32ag8", address.toNAddr()) + } +} \ No newline at end of file From 0aaf1f9f3413c4a40e6591620ff6dbe595c51a31 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 3 Mar 2023 11:55:25 -0500 Subject: [PATCH 14/36] v0.22.1 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index bad6bce74..74c477013 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,8 +11,8 @@ android { applicationId "com.vitorpamplona.amethyst" minSdk 26 targetSdk 33 - versionCode 83 - versionName "0.22.0" + versionCode 84 + versionName "0.22.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { From d05d0f0829e929b4c8e94e85391a55d2c2238178 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 3 Mar 2023 12:00:27 -0500 Subject: [PATCH 15/36] Fixing test cases --- app/src/test/java/com/vitorpamplona/amethyst/NIP19ParserTest.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/test/java/com/vitorpamplona/amethyst/NIP19ParserTest.kt b/app/src/test/java/com/vitorpamplona/amethyst/NIP19ParserTest.kt index 9389dc692..77d6a96c0 100644 --- a/app/src/test/java/com/vitorpamplona/amethyst/NIP19ParserTest.kt +++ b/app/src/test/java/com/vitorpamplona/amethyst/NIP19ParserTest.kt @@ -2,7 +2,6 @@ package com.vitorpamplona.amethyst import com.vitorpamplona.amethyst.service.Nip19 import com.vitorpamplona.amethyst.service.model.ATag -import com.vitorpamplona.amethyst.service.toNAddr import org.junit.Assert.assertEquals import org.junit.Test From 62d22bf4ca50d36fa3c7a9800a349c5340fad28c Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Fri, 3 Mar 2023 19:02:58 +0100 Subject: [PATCH 16/36] Add string translations for ES --- app/src/main/res/values-es/strings.xml | 176 +++++++++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 app/src/main/res/values-es/strings.xml diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml new file mode 100644 index 000000000..b23d5429c --- /dev/null +++ b/app/src/main/res/values-es/strings.xml @@ -0,0 +1,176 @@ + + + Amethyst + Amethyst Debug + Apunta al código QR + Mostrar el QR + Imagen de Perfil + Escanear el QR + Mostrar de todos modos + Post marcado como inapropiado por + Post no encontrado + Imagen del canal + Evento referenciado no encontrado + No se pudo desencriptar el mensaje + Imagen de grupo + Contenido explícito + Spam + Suplantación de identidad + Comportamiento ilegal + Desconodido + Icono de relé + Autor desconocido + Copiar texto + Copiar PubKey del usuario + Copiar ID de la nota + Transmisión + Bloquear y ocultar usuario> + Reportar Spam / Estafa + Reportar suplantación de identidad + Reportar contenido explicito + Reportar comportamiento ilegal + Inicie sesión con una clave privada para poder responder + Inicie sesión con una clave privada para poder publicar posts + Inicie sesión con una clave privada para dar me gusta a los posts + Falta la configuración de Zaps por defecto. Mantén pulsado unos segundos para cambiarla + Inicie sesión con una clave privada para poder enviar Zaps + Zaps + Total vistas + Aumentar + Cita + Nueva cantidad en Sats + Añadir + "respondiendo a " + " y " + "en canal " + Banner de perfil + " Siguiendo" + " Seguidores" + Peril + Filtros de seguridad + Cerrar sesión + Mostrar más + Factura Lightning + Pagar + Donación Lightning + Nota al receptor + ¡Muchas gracias! + Cantidad en Sats + Enviar Sats + "Nunca traducir desde " + "Error al analizar la vista previa de %1$s : %2$s" + "Imagen de vista previa para %1$s" + Nuevo canal + Nombre del canal + Mi grupo genial + URL de imagen + Descripción + "Sobre nosotros.. " + ¿Qué tienes en mente? + Publicación + Guardar + Crear + Cancelar + No se pudo cargar la imagen + Dirección del transmisor + Publicaciones + Errores + Tus noticias + Fuente de mensajes privados + Fuente de chat público + Fuente global + Anadir tramsmisor + Nombre para mostrar + Mi nombre para mostrar + Nombre de usuario + Mi nombre de usuario + Sobre mí + URL del avatar + URL del banner + Dirección web + Dirección LN + Dirección LN (outdated) + Imagen guardada en la galería + Error al guardar la imagen + Cargar imagen + Cargando… + El usuario no tiene una configuración de dirección Lightning para recibir sats + "responde aquí.. " + Copia la nota-ID para compartir + Copia la nota-ID del canal + Edita los metadatos del canal + Unirse + Conocido + Nuevas solicitudes + Usuarios bloqueados + Temas nuevos + Conversaciones + Notas + Respuestas + "Sigue" + "Reportes" + Más opciones + " Retrasmisores" + Sitio web + Dirección Lightning + Copia la ID de Nsec (su contraseña) en el portapapeles para hacer una copia de seguridad + Copiar clave privada al portapapeles + Copia la clave pública al portapapeles para compartir + Copie la clave pública (NPub) al portapapeles + Enviar un mensaje directo + Edita los metadatos del usuario + Seguir + Desbloquear + Copiar ID de usuario + Desbloquear usuario + "npub, hex, username " + Limpiar + Logo de la app + nsec / npub / hex private key + Mostrar contraseña + Ocultar contraseña + Clave invalida + "Acepto los " + terminos de uso + Se requiere la aceptación de los términos + Se requiere la clave + Acceso + Generar una nueva clave + Cargando el tablón + "Error al cargar las respuestas: " + Intentar otra vez + Tablón vacío + Actualizar + Creado + con descripción de + y foto + cambió el nombre del chat a + descripción a + e imagen a + Dejar + Dejar de seguir + Canal creado + "La información del canal cambió a" + Chat público + publicaciones recibidas + Eliminar + sats + Auto + traducido de + a + Mostrar en + primero + Traducir siempre a + NIP-05 + LNURL... + nunca + ahora + h + m + d + Desnudos + Blasfemias / Discurso de odio + Denuncia discurso de odio + Reportar desnudos / Porno + otros + From 10d6011009dd58b8a18e71bd0b607a07733e34fb Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Fri, 3 Mar 2023 19:29:08 +0100 Subject: [PATCH 17/36] Fix typos suggested by JesusValera --- app/src/main/res/values-es/strings.xml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index b23d5429c..70267bb78 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -17,8 +17,8 @@ Spam Suplantación de identidad Comportamiento ilegal - Desconodido - Icono de relé + Desconocido + Icono del transmisor Autor desconocido Copiar texto Copiar PubKey del usuario @@ -27,7 +27,7 @@ Bloquear y ocultar usuario> Reportar Spam / Estafa Reportar suplantación de identidad - Reportar contenido explicito + Reportar contenido explícito Reportar comportamiento ilegal Inicie sesión con una clave privada para poder responder Inicie sesión con una clave privada para poder publicar posts @@ -65,7 +65,7 @@ Mi grupo genial URL de imagen Descripción - "Sobre nosotros.. " + "Sobre nosotros… " ¿Qué tienes en mente? Publicación Guardar @@ -79,7 +79,7 @@ Fuente de mensajes privados Fuente de chat público Fuente global - Anadir tramsmisor + Anadir transmisor Nombre para mostrar Mi nombre para mostrar Nombre de usuario @@ -89,13 +89,13 @@ URL del banner Dirección web Dirección LN - Dirección LN (outdated) + Dirección LN (antiguo) Imagen guardada en la galería Error al guardar la imagen Cargar imagen Cargando… El usuario no tiene una configuración de dirección Lightning para recibir sats - "responde aquí.. " + "responde aquí… " Copia la nota-ID para compartir Copia la nota-ID del canal Edita los metadatos del canal @@ -110,7 +110,7 @@ "Sigue" "Reportes" Más opciones - " Retrasmisores" + " Retransmisores" Sitio web Dirección Lightning Copia la ID de Nsec (su contraseña) en el portapapeles para hacer una copia de seguridad @@ -131,7 +131,7 @@ Ocultar contraseña Clave invalida "Acepto los " - terminos de uso + términos de uso Se requiere la aceptación de los términos Se requiere la clave Acceso From 3e32a91e34691fccc29107d1bbd23fda64bbaa67 Mon Sep 17 00:00:00 2001 From: maxmoney21m Date: Sat, 4 Mar 2023 03:02:04 +0800 Subject: [PATCH 18/36] Move nsec backup to Drawer and Dialog, organize Drawer --- .../amethyst/ui/navigation/DrawerContent.kt | 182 ++++++++++-------- .../amethyst/ui/navigation/Routes.kt | 2 +- .../ui/screen/loggedIn/AccountBackupDialog.kt | 123 ++++++++++++ .../ui/screen/loggedIn/ProfileScreen.kt | 39 ---- app/src/main/res/drawable-anydpi/ic_key.xml | 11 ++ .../main/res/drawable-anydpi/ic_logout.xml | 12 ++ .../main/res/drawable-anydpi/ic_security.xml | 11 ++ app/src/main/res/drawable-hdpi/ic_key.png | Bin 0 -> 380 bytes app/src/main/res/drawable-hdpi/ic_logout.png | Bin 0 -> 326 bytes .../main/res/drawable-hdpi/ic_security.png | Bin 0 -> 510 bytes app/src/main/res/drawable-mdpi/ic_key.png | Bin 0 -> 250 bytes app/src/main/res/drawable-mdpi/ic_logout.png | Bin 0 -> 199 bytes .../main/res/drawable-mdpi/ic_security.png | Bin 0 -> 339 bytes app/src/main/res/drawable-xhdpi/ic_key.png | Bin 0 -> 443 bytes app/src/main/res/drawable-xhdpi/ic_logout.png | Bin 0 -> 361 bytes .../main/res/drawable-xhdpi/ic_security.png | Bin 0 -> 662 bytes app/src/main/res/drawable-xxhdpi/ic_key.png | Bin 0 -> 675 bytes .../main/res/drawable-xxhdpi/ic_logout.png | Bin 0 -> 509 bytes .../main/res/drawable-xxhdpi/ic_security.png | Bin 0 -> 991 bytes app/src/main/res/values/strings.xml | 10 +- 20 files changed, 271 insertions(+), 119 deletions(-) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountBackupDialog.kt create mode 100644 app/src/main/res/drawable-anydpi/ic_key.xml create mode 100644 app/src/main/res/drawable-anydpi/ic_logout.xml create mode 100644 app/src/main/res/drawable-anydpi/ic_security.xml create mode 100644 app/src/main/res/drawable-hdpi/ic_key.png create mode 100644 app/src/main/res/drawable-hdpi/ic_logout.png create mode 100644 app/src/main/res/drawable-hdpi/ic_security.png create mode 100644 app/src/main/res/drawable-mdpi/ic_key.png create mode 100644 app/src/main/res/drawable-mdpi/ic_logout.png create mode 100644 app/src/main/res/drawable-mdpi/ic_security.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_key.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_logout.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_security.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_key.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_logout.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_security.png 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 acdc717d0..5964847c8 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 @@ -7,12 +7,13 @@ import androidx.compose.foundation.clickable 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.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.lazy.LazyColumn import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.Divider import androidx.compose.material.Icon @@ -36,8 +37,8 @@ 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 +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.font.FontWeight.Companion.W500 import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavController @@ -45,14 +46,14 @@ import androidx.navigation.NavHostController 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.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 -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.platform.LocalContext @Composable fun DrawerContent(navController: NavHostController, @@ -88,7 +89,8 @@ fun DrawerContent(navController: NavHostController, modifier = Modifier .fillMaxWidth() .weight(1F), - accountStateViewModel + accountStateViewModel, + account, ) BottomContent(account.userProfile(), scaffoldState, navController) @@ -155,38 +157,44 @@ fun ProfileContent(baseAccountUser: User, modifier: Modifier = Modifier, scaffol if (accountUser.bestDisplayName() != null) { Text( accountUser.bestDisplayName() ?: "", - modifier = Modifier.padding(top = 7.dp).clickable(onClick = { - accountUser.let { - navController.navigate("User/${it.pubkeyHex}") - } - coroutineScope.launch { - scaffoldState.drawerState.close() - } - }), + modifier = Modifier + .padding(top = 7.dp) + .clickable(onClick = { + accountUser.let { + navController.navigate("User/${it.pubkeyHex}") + } + coroutineScope.launch { + scaffoldState.drawerState.close() + } + }), fontWeight = FontWeight.Bold, fontSize = 18.sp ) } if (accountUser.bestUsername() != null) { Text(" @${accountUser.bestUsername()}", color = Color.LightGray, - modifier = Modifier.padding(top = 15.dp).clickable(onClick = { - accountUser.let { - navController.navigate("User/${it.pubkeyHex}") - } - coroutineScope.launch { - scaffoldState.drawerState.close() - } - }) + modifier = Modifier + .padding(top = 15.dp) + .clickable(onClick = { + accountUser.let { + navController.navigate("User/${it.pubkeyHex}") + } + coroutineScope.launch { + scaffoldState.drawerState.close() + } + }) ) } - Row(modifier = Modifier.padding(top = 15.dp).clickable(onClick = { - accountUser.let { - navController.navigate("User/${it.pubkeyHex}") - } - coroutineScope.launch { - scaffoldState.drawerState.close() - } - })) { + Row(modifier = Modifier + .padding(top = 15.dp) + .clickable(onClick = { + accountUser.let { + navController.navigate("User/${it.pubkeyHex}") + } + coroutineScope.launch { + scaffoldState.drawerState.close() + } + })) { Row() { Text("${accountUserFollows.follows.size}", fontWeight = FontWeight.Bold) Text(stringResource(R.string.following)) @@ -206,66 +214,84 @@ fun ListContent( navController: NavHostController, scaffoldState: ScaffoldState, modifier: Modifier, - accountViewModel: AccountStateViewModel + accountViewModel: AccountStateViewModel, + account: Account, ) { val coroutineScope = rememberCoroutineScope() + var backupDialogOpen by remember { mutableStateOf(false) } - Column(modifier = modifier) { - LazyColumn() { - item { - if (accountUser != null) - NavigationRow(navController, - scaffoldState, - "User/${accountUser.pubkeyHex}", - Route.Profile.icon, - stringResource(R.string.profile) - ) + Column(modifier = modifier.fillMaxHeight()) { + if (accountUser != null) + NavigationRow( + title = stringResource(R.string.profile), + icon = Route.Profile.icon, + tint = MaterialTheme.colors.primary, + navController = navController, + scaffoldState = scaffoldState, + route = "User/${accountUser.pubkeyHex}", + ) - Divider( - modifier = Modifier.padding(bottom = 15.dp), - thickness = 0.25.dp - ) - Column(modifier = modifier.padding(horizontal = 25.dp)) { - Row(modifier = Modifier.clickable(onClick = { - navController.navigate(Route.Filters.route) - coroutineScope.launch { - scaffoldState.drawerState.close() - } - })) { - Text( - text = stringResource(R.string.security_filters), - fontSize = 18.sp, - fontWeight = W500 - ) - } - Row(modifier = Modifier.clickable(onClick = { accountViewModel.logOff() })) { - Text( - text = stringResource(R.string.log_out), - modifier = Modifier.padding(vertical = 15.dp), - fontSize = 18.sp, - fontWeight = W500 - ) - } - } - } - } + Divider(thickness = 0.25.dp) + + NavigationRow( + title = stringResource(R.string.security_filters), + icon = Route.Filters.icon, + tint = MaterialTheme.colors.onBackground, + navController = navController, + scaffoldState = scaffoldState, + route = Route.Filters.route, + ) + + Divider(thickness = 0.25.dp) + + IconRow( + title = "Backup Keys", + icon = R.drawable.ic_key, + tint = MaterialTheme.colors.onBackground, + onClick = { backupDialogOpen = true } + ) + + Spacer(modifier = Modifier.weight(1f)) + + IconRow( + "Logout", + R.drawable.ic_logout, + MaterialTheme.colors.onBackground, + onClick = { accountViewModel.logOff() } + ) + } + + if (backupDialogOpen) { + AccountBackupDialog(account, onClose = { backupDialogOpen = false }) } } @Composable -fun NavigationRow(navController: NavHostController, scaffoldState: ScaffoldState, route: String, icon: Int, title: String) { +fun NavigationRow( + title: String, + icon: Int, + tint: Color, + navController: NavHostController, + scaffoldState: ScaffoldState, + route: String, +) { val coroutineScope = rememberCoroutineScope() val currentRoute = currentRoute(navController) + IconRow(title, icon, tint, onClick = { + if (currentRoute != route) { + navController.navigate(route) + } + coroutineScope.launch { + scaffoldState.drawerState.close() + } + }) +} + +@Composable +fun IconRow(title: String, icon: Int, tint: Color, onClick: () -> Unit) { Row(modifier = Modifier .fillMaxWidth() - .clickable(onClick = { - if (currentRoute != route) { - navController.navigate(route) - } - coroutineScope.launch { - scaffoldState.drawerState.close() - } - }) + .clickable(onClick = onClick) ) { Row( modifier = Modifier @@ -276,7 +302,7 @@ fun NavigationRow(navController: NavHostController, scaffoldState: ScaffoldState Icon( painter = painterResource(icon), null, modifier = Modifier.size(22.dp), - tint = MaterialTheme.colors.primary + tint = tint ) Text( modifier = Modifier.padding(start = 16.dp), 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 12595420b..94fc1b4ff 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 @@ -53,7 +53,7 @@ sealed class Route( buildScreen = { acc, accSt, nav -> { _ -> ChatroomListScreen(acc, nav) }} ) - object Filters : Route("Filters", R.drawable.ic_dm, + object Filters : Route("Filters", R.drawable.ic_security, buildScreen = { acc, accSt, nav -> { _ -> FiltersScreen(acc, nav) }} ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountBackupDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountBackupDialog.kt new file mode 100644 index 000000000..e56431e08 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountBackupDialog.kt @@ -0,0 +1,123 @@ +package com.vitorpamplona.amethyst.ui.screen.loggedIn + +import android.widget.Toast +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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Key +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import com.halilibo.richtext.markdown.Markdown +import com.halilibo.richtext.ui.RichTextStyle +import com.halilibo.richtext.ui.material.MaterialRichText +import com.halilibo.richtext.ui.resolveDefaults +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.ui.actions.CloseButton +import kotlinx.coroutines.launch +import nostr.postr.toNsec + +@Composable +fun AccountBackupDialog(account: Account, onClose: () -> Unit) { + 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 = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + MaterialRichText( + style = RichTextStyle().resolveDefaults(), + ) { + Markdown( + content = stringResource(R.string.account_backup_tips_md), + ) + } + + Spacer(modifier = Modifier.height(15.dp)) + + NSecCopyButton(account) + } + } + } + } +} + +@Composable +private fun NSecCopyButton( + account: Account +) { + val clipboardManager = LocalClipboardManager.current + val context = LocalContext.current + val scope = rememberCoroutineScope() + + Button( + modifier = Modifier.padding(horizontal = 3.dp), + onClick = { + account.loggedIn.privKey?.let { + clipboardManager.setText(AnnotatedString(it.toNsec())) + scope.launch { + Toast.makeText( + context, + context.getString(R.string.secret_key_copied_to_clipboard), + Toast.LENGTH_SHORT + ).show() + } + } + }, + shape = RoundedCornerShape(20.dp), colors = ButtonDefaults.buttonColors( + backgroundColor = MaterialTheme.colors.primary + ) + ) { + Icon( + tint = Color.White, + imageVector = Icons.Default.Key, + contentDescription = stringResource(R.string.copies_the_nsec_id_your_password_to_the_clipboard_for_backup) + ) + Text("Copy Secret Key", color = MaterialTheme.colors.onPrimary) + } +} 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 b16677610..df52949e4 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 @@ -10,7 +10,6 @@ import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Bolt import androidx.compose.material.icons.filled.EditNote -import androidx.compose.material.icons.filled.Key import androidx.compose.material.icons.filled.Link import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.Share @@ -78,7 +77,6 @@ import com.vitorpamplona.amethyst.ui.screen.UserFeedView import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import nostr.postr.toNsec @OptIn(ExperimentalPagerApi::class) @Composable @@ -337,10 +335,6 @@ private fun ProfileHeader( .padding(bottom = 3.dp)) { MessageButton(baseUser, navController) - if (accountUser == baseUser && account.isWriteable()) { - NSecCopyButton(account) - } - NPubCopyButton(baseUser) if (accountUser == baseUser) { @@ -637,40 +631,7 @@ fun TabRelays(user: User, accountViewModel: AccountViewModel, navController: Nav } } -@Composable -private fun NSecCopyButton( - account: Account -) { - val clipboardManager = LocalClipboardManager.current - var popupExpanded by remember { mutableStateOf(false) } - Button( - modifier = Modifier - .padding(horizontal = 3.dp) - .width(50.dp), - onClick = { popupExpanded = true }, - shape = RoundedCornerShape(20.dp), - colors = ButtonDefaults - .buttonColors( - backgroundColor = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) - ) - ) { - Icon( - tint = Color.White, - imageVector = Icons.Default.Key, - contentDescription = stringResource(R.string.copies_the_nsec_id_your_password_to_the_clipboard_for_backup) - ) - - DropdownMenu( - expanded = popupExpanded, - onDismissRequest = { popupExpanded = false } - ) { - DropdownMenuItem(onClick = { account.loggedIn.privKey?.let { clipboardManager.setText(AnnotatedString(it.toNsec())) }; popupExpanded = false }) { - Text(stringResource(R.string.copy_private_key_to_the_clipboard)) - } - } - } -} @Composable private fun NPubCopyButton( diff --git a/app/src/main/res/drawable-anydpi/ic_key.xml b/app/src/main/res/drawable-anydpi/ic_key.xml new file mode 100644 index 000000000..0db74fa0d --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_key.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_logout.xml b/app/src/main/res/drawable-anydpi/ic_logout.xml new file mode 100644 index 000000000..6555606de --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_logout.xml @@ -0,0 +1,12 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_security.xml b/app/src/main/res/drawable-anydpi/ic_security.xml new file mode 100644 index 000000000..717b89c05 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_security.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable-hdpi/ic_key.png b/app/src/main/res/drawable-hdpi/ic_key.png new file mode 100644 index 0000000000000000000000000000000000000000..81ef03db80d47008d6b2394c207b9644fa73f7c2 GIT binary patch literal 380 zcmV-?0fYXDP)H_=P_mn#{ia_i^rzhvE$s2q08I;pvB^JE57@K<~WY? a&)flbHGXVp^eyB70000C9@!Z5gW-(B?BUa%rd}RXps!?7LvhtW*Oiu1oE(-sbxTf z5P%dPt0GFs%ufsPJ~S8ZazS3vYbm7(cex-BUn^2i9;_zZ>p zX5RSTV2D91=c`7N?j+$e3*|!2B6z?|@EBXpQQv3WADkTH+Mj(aTij&ND$5B-c;TH-*I_R#T84kYTZih4Nnu^0*e{ zaV^N>&lQwD4_lL9&p3}oJ3cWKe}+cDiNzwuc`Rh-e^qgQL4d^~#yLvxF@?uXtn<&D zagGwQT>HTv8a!?yfd>}!jB}LG4+;$)H^EVZ1wG>&OrqY5o+7@Wx4|`zXJ~d#Jnph| zmN;I+E#MvYOeE2fg)QUQ0m-pV>PQ^g3@xzEIFz$^$v75pJgyh_4udCbX1=?uf^yu8 zDh6lsE&ElG0=u{NZ_8gUv0XU|MCN8H`(Rc^n`)jJ|8u8?ELiL*y*%n3zHHZ7CJQuW zL1=>Px#07*qoM6N<$f?1a0 ATmS$7 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_key.png b/app/src/main/res/drawable-mdpi/ic_key.png new file mode 100644 index 0000000000000000000000000000000000000000..67341e7e157c2f11d8be2af586be98c575ceabe4 GIT binary patch literal 250 zcmV<22B6FRNAPjStw zNsXuvnLwH>&6$6qA~j|vI~jaNDI=N^?H7I+QNAZ#GT!|qE}3U-CX+ZwTz&+N&vc3P zZ=)b_S)AbP*G!lz?PoK1i)e)J9h_Hh*3q#$p5XwkU-a+l?EnA(07*qoM6N<$f}&1p A^Z)<= literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_logout.png b/app/src/main/res/drawable-mdpi/ic_logout.png new file mode 100644 index 0000000000000000000000000000000000000000..dc2dabde5eb1761a359e093eca5b725b84facc11 GIT binary patch literal 199 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjeV#6kAr*0Nryk^OP~dTuJR~d8 zP*liXRv>jg?(Vg32UuI{ygTG7Je@CH_&-VRz)nss!Jh($&ON`ms)&z`^Sw|Bd&ZwI zf#f?&PW|KUZg~IOX1SLRQ)6?uN0KPVge7Vx{+fT)`03MdVXG3aM$86|AC-{{3Jxj> xSXiul*c8fO9CP5e&88)xPi1&|{}rg2>XmQlFWYosMGVko44$rjF6*2UngCdjO`HG# literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_security.png b/app/src/main/res/drawable-mdpi/ic_security.png new file mode 100644 index 0000000000000000000000000000000000000000..3dc8abccedc4ff47494f035b24b59a6f4bcbb6be GIT binary patch literal 339 zcmV-Z0j&OsP)$-K$ z^8ff9+oDYcDinybh_A<6f zG~6IEcx{^b1{Mp~In5Z^6QP3K<8s&#*%MI|n16$kz%0GN(2ixYM}d`xIx@HxA?yCc zflT%&uo+QD2G=5Rws0VmJqkSmkioSGeasx$1Ejv(CVisxnph8r=ZISN^(abD!Fn09 zS}V?8Av4)?@K6IBhCUqW@T9dO^iN|Zb*vf38uj@Bm3K5JTiJecAv3002ovPDHLkV1oa-mAwD} literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_key.png b/app/src/main/res/drawable-xhdpi/ic_key.png new file mode 100644 index 0000000000000000000000000000000000000000..79ffc2e6744e9fb3ea053e9da09d10c587150232 GIT binary patch literal 443 zcmV;s0Yv_ZP)Tog4284qI@{bK63ixBF4ApthIX4R-S)QeQ*FsL^9NyoO8%s$pxA~tKQKO>9LI4S z$8nr2UDusM2m}3okUsmq|AFZGf&VymwspJ1#^1$oNyw#|H+!F5ATw7oHCmGu8mabX z@3$#15HU7OyA6V8eqv~eHXj7`e7|hlq%>~&PLy6)#7P&Gf2pZak7x(a{=@)8v2ShM zf*5jaovLsDi6s+*eXuBJUO^(_<~giR9Jyb`v)g+lCnp)K@f6FK>4TWD$?{F9eBZXSf=e@4^hLGB>ye>=?N zAlQ7e1>v?l*&t%&3h}&op|l0~L5o|7n8}KGoq2$n%C!V@g?rg%PMg%`6Y+ZULRhz* zzS{c(7>k(~I6mLJ5L?Q*dda{VVD^@5C+=MLH81ejBm0tSA=mEtL%;(WNOyf=)Aa-Y lA+|}+?>LU*IF94|;R$kXlb_%7NR$8o002ovPDHLkV1iw>(m((J literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_logout.png b/app/src/main/res/drawable-xhdpi/ic_logout.png new file mode 100644 index 0000000000000000000000000000000000000000..10fd5751c309573eb28cddc24d880b0cc876a40f GIT binary patch literal 361 zcmV-v0ha!WP)b;@6ht{hT2A40Lz_dSrA-b(3oWRvolzLj8CVcD2lgZ znx>DK@0eR_?b$Q*PDkP6;5&nC9r~v;&+`vM^bGyeX~uV7L;rMk570{$*qbV_f%xXT zYWVslMi)@<*IAYlBMU^}FP_(3f`-(cMA&ou@;`WHR|%R?cVjO>BkCDgOVEUR7Pb;J zpq`1P1UFMBT%R?oOK>A~!u6T6ssuMtCtRO7hc2)xyx>Oandn2@7H+6khZo#TJqt^C zK?CX;*uo2%PTP z-Wn;)6e~W|DfnKw|_}Wv6si=@mKl#d^jB5>ga#A?UM7(PATfD6n9$+ePQ8^ zylAZE(6c$Em|ZF6zENZ-Hg`wwr!)083u2-Nx5+wL>TeYDTE_jJdY#*s@+s>i+TXVG z8_wsrV8++UA=uy6O0#{;@x4i}6N(s#BWH;7vZ2?70)_n@fsSnGb)lfu-x27^hF+Ip zfk96;^tucS40^Jm*JW5>(31_lF2e$Yo^0rK85S7yWJ9mZu)v@v8+u)a1qMCY(CacR zFzCsKUYB8kK~FaHx(o{pda|L{h0p!m9)*QuqFU&6vB%^Ovfj`eK~K50L$z>>%Q+9A zJUITS9jOB-k~%sn4dOkqC zqfC6FtT%Le=v#?;zG~o@FqCCNrw1?Js)1v|=&dVxWl=q$a|wNHt>Gqk3e6_L^ z`j%*%{dC}VuT~oFeUiPg z(NohZUP;}!B4idc9TQgkAC6yT;V!Z7e8}T`p~cxc^Kx_S>e!hJ-=R6*TJghI^>VJ> z_jB|dL^Vfez4`8KTKt4|7+DkE&rx!0UVP2S^U~0B&YGw_N-=*~;9N9TbLcrcrMTTv wyV$n=pNqD}-Vy!UsKxnYufN_I|7HdL0m}ayRE`W09smFU07*qoM6N<$f__X;)Bpeg literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_key.png b/app/src/main/res/drawable-xxhdpi/ic_key.png new file mode 100644 index 0000000000000000000000000000000000000000..e0cdfef0ab6512e256235cec672d450d15b7766c GIT binary patch literal 675 zcmV;U0$lxxP)8qv5QJU3PV4&G10)@gJV@N8joY+w>v!@Svy90G*ja4%wt6k6B?=CQNu7!6i zp^sgfwB^cS!6fd#YH&RBB(_%S^OeU@C26oH9{dee|LGRpT6TZ)Bk3jQz-Rn#My|TI z8gArx&_ha-d2bz895E^OgWHd2GUwg!UU8TcIeF|vgrWcCVqpy2z6vl29&DeY=48yI z$V%E&IEZ+Rqo8JRk^#-$0%+Tl?`{g2l!D(l0c(xr@7>0?lG`NDz_H4>U6jB|y_sAl zHIB!M@+hARa&X=nG-(ny=o7b@3+Jsxqb6~1pSaCjIBzW)Eg_!!l!I@vVROKP7wPGF zJ@7F5xr42RK6xA^<)p{V$x!@y=47JVV~KKMo&po~n3J*i z_00ix@=QK{EEW2QoQPFnCT+HokbAf0lx$IxrfSaaXxIDsi|cjKZb?`E+&La>SuhuE zIp1MH?XXkg&1%WDpf|-&@DS5H(ogV&5JCtcgb+dqA%qYQ%0Ff(uFsf>j7I?ac{IX@Wu?lY^}{h|ihs9aQ=!VBLy-cQ4JQQ-ZwGXFva0EYUW< zQ0D(SxqG>Hn?41yUT(9F2=86|kZ+ss~*9(4X}fZf`{lDPTXeN=eOTPL_W#-U|I&Ww${b~W&lJ+MxZ;Iu!>Ixh{xWv0qk%Se zBv;6|e`E?@pl8uF-GZs~aNwR3A}jd&9;NmeC+}n z!SR@9iP((wMl!Av2rIat2FFUNcZs(z=Bup-(bu+r00!ck*B>K~$gYX5zZcYX oL^f~YzB{WHRFp%aVcK8jR<)bS-%Pre0i%_{)78&qol`;+0QG9&hX4Qo literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_security.png b/app/src/main/res/drawable-xxhdpi/ic_security.png new file mode 100644 index 0000000000000000000000000000000000000000..dba255c73a9a08bbb1d8a13f0e8abae84f49d4d3 GIT binary patch literal 991 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@Zgyv2U|#0w;uum9_jdOF{>u&mZo$i! zE%oabu@>4Ae%YzoR9AP)mU7+yy1xaZqiid$e|zI2q+WSRJz&$k=XY-;$Jf0n^Pgu^ z$>O*C^7F?Ze*|Cr|MQRYB(dcC-~W35`tEfn~pI zS7pD@ugrf~oLyX8d+JvFCA0ULM)!ZNDxPB+{x(?mZ|b`If|~4SHS4x~o4)0(_%)R~ z=L`#LR~5g~_kQqAFmkv5<^Y4~LG_VL*{*GQ#yT-w$A6xiHF4o>9&Tgr-=~Eltc^-$ zCq6WM_x#hkH(eZe(;{xfPH=Qjd0}?=(7PrcD<$Xcl451zDHjAKcb(m^VYfna--!#k zZLN1%B}-gD2ENfdu|OGQMq;@mn=IJi-P}emI(TScRLGZomUq*>Ow(cwT)11bw!oo% zk!9hUnY#J`_rK475fCG;;khqJKy+8M=G)zJ&m)X~X|y-3xNz6&<%{pXM4s8Nyy4EZ z$f;cW*P1>)*WyPv)HFJ{_Uc}Vi|ET;RCX;)TzBP(+`@d_t#K=BpI4M;Sw9f`buH+Xtye^`+C+E3*z6TwPjt)JDp#on z-P36Z=>J7KH4{L;=T{zMcFSRz208Aw6CbfVwaQ*clj#ah3TAIgX4Zn z)IHUjSoYw6Y! z#ilmD1+)DFUt69E{q4s&_qE*gx@%`8<2qhlTvfZH>b~Z~jUH<(FU`NdZ^oYUlEs(1 zjW6x;SW|9ue@a-tRH`tLyU8QDH}9zbhAYKpn=O=`_ecC?yZu4W#m& znxC^>bn8p*B$JuVe$0LsW-XlddVSBjK0oHWH;Xo9FP55ZxAfVC(9F!wM-T1EioUix zx~=un#Q>YHZL23AvpILj=i*%bm%juz{ByamXvN0+;r*4Lo}3i<&+x2NQS!l?6`(B4 N;OXk;vd$@?2>|%H?qmP} literal 0 HcmV?d00001 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 952cd426c..0871a623f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -118,7 +118,7 @@ Website Lightning Address Copies the Nsec ID (your password) to the clipboard for backup - Copy Private Key to the Clipboard + Copy Secret Key to the Clipboard Copies the public key to the clipboard for sharing Copy Public Key (NPub) to the Clipboard Send a Direct Message @@ -177,4 +177,12 @@ Report Hateful speech Report Nudity / Porn others + + ## 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 \ No newline at end of file From 3674f6b354458821486ded21c3144096c6dc4863 Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Fri, 3 Mar 2023 21:22:54 +0100 Subject: [PATCH 19/36] Fix some typos in ES translations --- app/src/main/res/values-es/strings.xml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 70267bb78..83c976168 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -24,7 +24,7 @@ Copiar PubKey del usuario Copiar ID de la nota Transmisión - Bloquear y ocultar usuario> + Bloquear y ocultar usuario Reportar Spam / Estafa Reportar suplantación de identidad Reportar contenido explícito @@ -46,7 +46,7 @@ Banner de perfil " Siguiendo" " Seguidores" - Peril + Perfil Filtros de seguridad Cerrar sesión Mostrar más @@ -67,7 +67,7 @@ Descripción "Sobre nosotros… " ¿Qué tienes en mente? - Publicación + Enviar Guardar Crear Cancelar @@ -100,15 +100,15 @@ Copia la nota-ID del canal Edita los metadatos del canal Unirse - Conocido + Conocidos Nuevas solicitudes Usuarios bloqueados Temas nuevos Conversaciones Notas Respuestas - "Sigue" - "Reportes" + Siguiendo + Reportes Más opciones " Retransmisores" Sitio web @@ -136,7 +136,7 @@ Se requiere la clave Acceso Generar una nueva clave - Cargando el tablón + Cargando el tablón… "Error al cargar las respuestas: " Intentar otra vez Tablón vacío @@ -150,7 +150,7 @@ Dejar Dejar de seguir Canal creado - "La información del canal cambió a" + La información del canal cambió a Chat público publicaciones recibidas Eliminar @@ -160,7 +160,7 @@ a Mostrar en primero - Traducir siempre a + "Traducir siempre a " NIP-05 LNURL... nunca From f3bc190b0b7be7959490513a059bfaf351900901 Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Fri, 3 Mar 2023 21:42:40 +0100 Subject: [PATCH 20/36] Use boost=impulsar --- app/src/main/res/values-es/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 83c976168..c135450e2 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -36,7 +36,7 @@ Inicie sesión con una clave privada para poder enviar Zaps Zaps Total vistas - Aumentar + Impulsar Cita Nueva cantidad en Sats Añadir From fe80b509e34312b40ff6c39a9368fdf14c3d31e5 Mon Sep 17 00:00:00 2001 From: Rashed <38612386+rashedswen@users.noreply.github.com> Date: Fri, 3 Mar 2023 23:44:39 +0300 Subject: [PATCH 21/36] temporary solution for mirroring arabic text --- .../ui/components/ExpandableRichTextViewer.kt | 15 ++++++++++++++- .../amethyst/ui/components/RichTextViewer.kt | 9 +++++++-- .../ui/components/TranslateableRichTextViewer.kt | 1 - .../vitorpamplona/amethyst/ui/note/NoteCompose.kt | 10 ++++++---- app/src/main/res/values-ar/strings.xml | 1 + app/src/main/res/values-es/strings.xml | 1 + app/src/main/res/values-pt-rBR/strings.xml | 1 + app/src/main/res/values-ru/strings.xml | 1 + app/src/main/res/values-uk/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 10 files changed, 33 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ExpandableRichTextViewer.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ExpandableRichTextViewer.kt index c7db09a6e..11c25cb62 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ExpandableRichTextViewer.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ExpandableRichTextViewer.kt @@ -13,6 +13,7 @@ import androidx.compose.material.ButtonDefaults import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -22,11 +23,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.compositeOver +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.dp import androidx.navigation.NavController import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.R import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.LayoutDirection @Composable fun ExpandableRichTextViewer( @@ -43,7 +46,17 @@ fun ExpandableRichTextViewer( val text = if (showFullText) content else content.take(350) Box(contentAlignment = Alignment.BottomCenter) { - RichTextViewer(text, canPreview, modifier, tags, backgroundColor, accountViewModel, navController) + CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) { + RichTextViewer( + text, + canPreview, + modifier, + tags, + backgroundColor, + accountViewModel, + navController + ) + } if (content.length > 350 && !showFullText) { Row( 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 f4fa3dac3..36b380cad 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 @@ -29,6 +29,7 @@ import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.unit.dp @@ -132,8 +133,8 @@ fun RichTextViewer( // FlowRow doesn't work well with paragraphs. So we need to split them content.split('\n').forEach { paragraph -> FlowRow() { - paragraph.split(' ').forEach { word: String -> - + val s = if (isArabic(paragraph)) paragraph.split(' ').reversed() else paragraph.split(' '); + s.forEach { word: String -> if (canPreview) { // Explicit URL val lnInvoice = LnInvoiceUtil.findInvoice(word) @@ -191,6 +192,10 @@ fun RichTextViewer( } } +private fun isArabic(text: String): Boolean { + return text.any { it in '\u0600'..'\u06FF' || it in '\u0750'..'\u077F' } +} + fun isBechLink(word: String): Boolean { return word.startsWith("nostr:", true) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/TranslateableRichTextViewer.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/TranslateableRichTextViewer.kt index 1d871fcfa..25c10a951 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/TranslateableRichTextViewer.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/TranslateableRichTextViewer.kt @@ -183,7 +183,6 @@ fun TranslateableRichTextViewer( Spacer(modifier = Modifier.size(10.dp)) - // TODO : Rashed translate this Text( "${stringResource(R.string.show_in)} ${Locale(source).displayName} ${ stringResource( 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 8c353220a..188ce2cc7 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 @@ -134,9 +134,11 @@ fun NoteCompose( launchSingleTop = true } } else { - note.channel()?.let { - navController.navigate("Channel/${it.idHex}") - } + note + .channel() + ?.let { + navController.navigate("Channel/${it.idHex}") + } } }, onLongClick = { popupExpanded = true } @@ -234,7 +236,7 @@ fun NoteCompose( if (noteEvent is RepostEvent) { Text( - " boosted", + " ${stringResource(id = R.string.boosted)}", fontWeight = FontWeight.Bold, color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) ) diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 4e0578e1b..760e72de5 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -35,6 +35,7 @@ Zaps مشاهدة العد تعزيز + معزز إقتباس مبلغ جديد في Sats إضافة diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 70267bb78..f8c1e7ef4 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -37,6 +37,7 @@ Zaps Total vistas Aumentar + boosted Cita Nueva cantidad en Sats Añadir diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index eb05636d9..2ae02417c 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -35,6 +35,7 @@ Zaps Contagem de visualizações Impulsionar + boosted Citar Novo Valor em Sats Adicionar diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 4f6e36f32..3c485cbaa 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -36,6 +36,7 @@ Запы Просмотры Продвинуть + boosted Цитировать Новая сумма в sat Добавить diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 9d587f90a..a8a541768 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -36,6 +36,7 @@ Запи Перегляди Просувати + boosted Цитувати Нова сума в sat Додати diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1d6d7acef..1af1f265b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -36,6 +36,7 @@ Zaps View count Boost + boosted Quote New Amount in Sats Add From 5ae552117ddb537f885220c03d2d3333689c3dfd Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 3 Mar 2023 16:00:47 -0500 Subject: [PATCH 22/36] Migrates Events to the App's source code as opposed to NostrPostr: - Changes them to remove all secondary fields and turn them into functions - Changes them to from being based in ByteArrays to String (since we use Hex everywhere and strings are immutable, we avoid duplicating memory with ByteArrays) --- app/proguard-rules.pro | 2 +- .../amethyst/LocalPreferences.kt | 6 +- .../vitorpamplona/amethyst/model/Account.kt | 96 ++------- .../amethyst/model/AntiSpamFilter.kt | 10 +- .../amethyst/model/LocalCache.kt | 86 ++++---- .../com/vitorpamplona/amethyst/model/Note.kt | 20 +- .../com/vitorpamplona/amethyst/model/User.kt | 4 +- .../service/NostrAccountDataSource.kt | 4 +- .../service/NostrChatroomDataSource.kt | 2 +- .../service/NostrChatroomListDataSource.kt | 2 +- .../amethyst/service/NostrDataSource.kt | 67 +++--- .../NostrSearchEventOrUserDataSource.kt | 2 +- .../service/NostrSingleUserDataSource.kt | 2 +- .../service/NostrUserProfileDataSource.kt | 4 +- .../amethyst/service/model/ATag.kt | 8 +- .../service/model/ChannelCreateEvent.kt | 14 +- .../service/model/ChannelHideMessageEvent.kt | 13 +- .../service/model/ChannelMessageEvent.kt | 13 +- .../service/model/ChannelMetadataEvent.kt | 14 +- .../service/model/ChannelMuteUserEvent.kt | 14 +- .../service/model/ContactListEvent.kt | 59 ++++++ .../amethyst/service/model/DeletionEvent.kt | 30 +++ .../amethyst/service/model/Event.kt | 197 ++++++++++++++++++ .../amethyst/service/model/LnZapEvent.kt | 9 +- .../service/model/LnZapRequestEvent.kt | 21 +- .../service/model/LongTextNoteEvent.kt | 14 +- .../amethyst/service/model/MetadataEvent.kt | 48 +++++ .../amethyst/service/model/PrivateDmEvent.kt | 86 ++++++++ .../amethyst/service/model/ReactionEvent.kt | 15 +- .../service/model/RecommendRelayEvent.kt | 37 ++++ .../amethyst/service/model/ReportEvent.kt | 21 +- .../amethyst/service/model/RepostEvent.kt | 17 +- .../amethyst/service/model/TextNoteEvent.kt | 13 +- .../amethyst/service/relays/Client.kt | 6 +- .../amethyst/service/relays/EventVerifier.kt | 16 -- .../amethyst/service/relays/Relay.kt | 2 +- .../amethyst/service/relays/RelayPool.kt | 2 +- .../amethyst/ui/note/NoteCompose.kt | 17 +- .../ui/screen/loggedIn/ChannelScreen.kt | 3 +- .../ui/screen/loggedIn/ChatroomListScreen.kt | 21 +- .../ui/screen/loggedIn/ChatroomScreen.kt | 4 +- .../amethyst/ui/screen/loggedIn/HomeScreen.kt | 8 +- .../ui/screen/loggedIn/NotificationScreen.kt | 2 +- .../ui/screen/loggedIn/SearchScreen.kt | 6 +- 44 files changed, 729 insertions(+), 308 deletions(-) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/service/model/ContactListEvent.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/service/model/DeletionEvent.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/service/model/MetadataEvent.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/service/model/PrivateDmEvent.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/service/model/RecommendRelayEvent.kt delete mode 100644 app/src/main/java/com/vitorpamplona/amethyst/service/relays/EventVerifier.kt diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index bbee40884..5acc1b972 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -25,7 +25,7 @@ -keep class fr.acinq.secp256k1.jni.** { *; } # For the NostrPostr library -keep class nostr.postr.** { *; } --keep class nostr.postr.events.** { *; } +-keep class com.vitorpamplona.amethyst.service.model.** { *; } # Json parsing -keep class com.google.gson.reflect.** { *; } -keep class * extends com.google.gson.reflect.TypeToken diff --git a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index 8684b11d1..38be7f0da 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -8,9 +8,9 @@ import com.vitorpamplona.amethyst.model.RelaySetupInfo import com.vitorpamplona.amethyst.model.toByteArray import java.util.Locale import nostr.postr.Persona -import nostr.postr.events.ContactListEvent -import nostr.postr.events.Event -import nostr.postr.events.Event.Companion.getRefinedEvent +import com.vitorpamplona.amethyst.service.model.ContactListEvent +import com.vitorpamplona.amethyst.service.model.Event +import com.vitorpamplona.amethyst.service.model.Event.Companion.getRefinedEvent import nostr.postr.toHex class LocalPreferences(context: Context) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index ed433bebf..b91282926 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.LiveData 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.service.model.Contact import com.vitorpamplona.amethyst.service.model.LnZapRequestEvent import com.vitorpamplona.amethyst.service.model.ReactionEvent import com.vitorpamplona.amethyst.service.model.ReportEvent @@ -26,16 +27,12 @@ import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import nostr.postr.Contact import nostr.postr.Persona -import nostr.postr.Utils -import nostr.postr.events.ContactListEvent -import nostr.postr.events.DeletionEvent -import nostr.postr.events.Event -import nostr.postr.events.MetadataEvent -import nostr.postr.events.PrivateDmEvent +import com.vitorpamplona.amethyst.service.model.ContactListEvent +import com.vitorpamplona.amethyst.service.model.DeletionEvent +import com.vitorpamplona.amethyst.service.model.MetadataEvent +import com.vitorpamplona.amethyst.service.model.PrivateDmEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent -import nostr.postr.toHex val DefaultChannels = setOf( "25e5c82273a271cb1a840d0060391a0bf4965cafeb029d5ab55350b418953fbb", // -> Anigma's Nostr @@ -89,10 +86,11 @@ class Account( if (!isWriteable()) return val contactList = userProfile().latestContactList + val follows = contactList?.follows() ?: emptyList() - if (contactList != null && contactList.follows.size > 0) { + if (contactList != null && follows.isNotEmpty()) { val event = ContactListEvent.create( - contactList.follows, + follows, relays, loggedIn.privKey!!) @@ -111,14 +109,7 @@ class Account( if (!isWriteable()) return loggedIn.privKey?.let { - val createdAt = Date().time / 1000 - val content = toString - val pubKey = Utils.pubkeyCreate(it) - val tags = listOf>() - val id = Event.generateId(pubKey, createdAt, MetadataEvent.kind, tags, content) - val sig = Utils.sign(id, it) - val event = MetadataEvent(id, pubKey, createdAt, tags, content, sig) - + val event = MetadataEvent.create(toString, loggedIn.privKey!!) Client.send(event) LocalCache.consume(event) } @@ -250,10 +241,11 @@ class Account( if (!isWriteable()) return val contactList = userProfile().latestContactList + val follows = contactList?.follows() ?: emptyList() - val event = if (contactList != null && contactList.follows.size > 0) { + val event = if (contactList != null && follows.isNotEmpty()) { ContactListEvent.create( - contactList.follows.plus(Contact(user.pubkeyHex, null)), + follows.plus(Contact(user.pubkeyHex, null)), userProfile().relays, loggedIn.privKey!!) } else { @@ -273,10 +265,11 @@ class Account( if (!isWriteable()) return val contactList = userProfile().latestContactList + val follows = contactList?.follows() ?: emptyList() - if (contactList != null && contactList.follows.size > 0) { + if (contactList != null && follows.isNotEmpty()) { val event = ContactListEvent.create( - contactList.follows.filter { it.pubKeyHex != user.pubkeyHex }, + follows.filter { it.pubKeyHex != user.pubkeyHex }, userProfile().relays, loggedIn.privKey!!) @@ -320,37 +313,6 @@ class Account( LocalCache.consume(signedEvent, null) } - fun createPrivateMessageWithReply( - recipientPubKey: ByteArray, - msg: String, - replyTos: List? = null, mentions: List? = null, - privateKey: ByteArray, - createdAt: Long = Date().time / 1000, - publishedRecipientPubKey: ByteArray? = null, - advertiseNip18: Boolean = true - ): PrivateDmEvent { - val content = Utils.encrypt( - if (advertiseNip18) { - PrivateDmEvent.nip18Advertisement - } else { "" } + msg, - privateKey, - recipientPubKey) - val pubKey = Utils.pubkeyCreate(privateKey) - val tags = mutableListOf>() - publishedRecipientPubKey?.let { - tags.add(listOf("p", publishedRecipientPubKey.toHex())) - } - replyTos?.forEach { - tags.add(listOf("e", it)) - } - mentions?.forEach { - tags.add(listOf("p", it)) - } - val id = Event.generateId(pubKey, createdAt, PrivateDmEvent.kind, tags, content) - val sig = Utils.sign(id, privateKey) - return PrivateDmEvent(id, pubKey, createdAt, tags, content, sig) - } - fun sendPrivateMeesage(message: String, toUser: String, replyingTo: Note? = null) { if (!isWriteable()) return val user = LocalCache.users[toUser] ?: return @@ -358,7 +320,7 @@ class Account( val repliesToHex = listOfNotNull(replyingTo?.idHex).ifEmpty { null } val mentionsHex = emptyList() - val signedEvent = createPrivateMessageWithReply( + val signedEvent = PrivateDmEvent.create( recipientPubKey = user.pubkey(), publishedRecipientPubKey = user.pubkey(), msg = message, @@ -386,7 +348,7 @@ class Account( Client.send(event) LocalCache.consume(event) - joinChannel(event.id.toHex()) + joinChannel(event.id) } fun joinChannel(idHex: String) { @@ -438,7 +400,7 @@ class Account( Client.send(event) LocalCache.consume(event) - joinChannel(event.id.toHex()) + joinChannel(event.id) } fun decryptContent(note: Note): String? { @@ -446,26 +408,12 @@ class Account( return if (event is PrivateDmEvent && loggedIn.privKey != null) { var pubkeyToUse = event.pubKey - val recepientPK = event.recipientPubKey + val recepientPK = event.recipientPubKey() if (note.author == userProfile() && recepientPK != null) pubkeyToUse = recepientPK - return try { - val sharedSecret = Utils.getSharedSecret(loggedIn.privKey!!, pubkeyToUse) - - val retVal = Utils.decrypt(event.content, sharedSecret) - - if (retVal.startsWith(PrivateDmEvent.nip18Advertisement)) { - retVal.substring(16) - } else { - retVal - } - - } catch (e: Exception) { - e.printStackTrace() - null - } + event.plainContent(loggedIn.privKey!!, pubkeyToUse.toByteArray()) } else { event?.content } @@ -495,10 +443,10 @@ class Account( } private fun updateContactListTo(newContactList: ContactListEvent?) { - if (newContactList?.follows.isNullOrEmpty()) return + if (newContactList?.follows().isNullOrEmpty()) return // Events might be different objects, we have to compare their ids. - if (backupContactList?.id?.toHex() != newContactList?.id?.toHex()) { + if (backupContactList?.id != newContactList?.id) { backupContactList = newContactList saveable.invalidateData() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt index 3fec4edf8..a4bb82eae 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt @@ -11,7 +11,7 @@ import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import nostr.postr.events.Event +import com.vitorpamplona.amethyst.service.model.Event import nostr.postr.toHex data class Spammer(val pubkeyHex: HexKey, var duplicatedMessages: Set) @@ -22,7 +22,7 @@ class AntiSpamFilter { @Synchronized fun isSpam(event: Event): Boolean { - val idHex = event.id.toHexKey() + val idHex = event.id // if already processed, ok if (LocalCache.notes[idHex] != null) return false @@ -37,15 +37,15 @@ class AntiSpamFilter { val hash = (event.content + event.tags.flatten().joinToString(",")).hashCode() if ((recentMessages[hash] != null && recentMessages[hash] != idHex) || spamMessages[hash] != null) { - Log.w("Potential SPAM Message", "${event.id.toHex()} ${recentMessages[hash]} ${spamMessages[hash] != null} ${event.content.replace("\n", " | ")}") + Log.w("Potential SPAM Message", "${event.id} ${recentMessages[hash]} ${spamMessages[hash] != null} ${event.content.replace("\n", " | ")}") // Log down offenders if (spamMessages.get(hash) == null) { - spamMessages.put(hash, Spammer(event.pubKey.toHexKey(), setOf(recentMessages[hash], event.id.toHex()))) + spamMessages.put(hash, Spammer(event.pubKey, setOf(recentMessages[hash], event.id))) liveSpam.invalidateData() } else { val spammer = spamMessages.get(hash) - spammer.duplicatedMessages = spammer.duplicatedMessages + event.id.toHex() + spammer.duplicatedMessages = spammer.duplicatedMessages + event.id liveSpam.invalidateData() } 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 66b388368..ae3f07104 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -17,6 +17,13 @@ import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent 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.ContactListEvent +import com.vitorpamplona.amethyst.service.model.DeletionEvent +import com.vitorpamplona.amethyst.service.model.Event +import com.vitorpamplona.amethyst.service.model.MetadataEvent +import com.vitorpamplona.amethyst.service.model.PrivateDmEvent +import com.vitorpamplona.amethyst.service.model.RecommendRelayEvent +import com.vitorpamplona.amethyst.service.model.TextNoteEvent import com.vitorpamplona.amethyst.service.relays.Relay import fr.acinq.secp256k1.Hex import java.io.ByteArrayInputStream @@ -32,13 +39,6 @@ import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import nostr.postr.events.ContactListEvent -import nostr.postr.events.DeletionEvent -import nostr.postr.events.Event -import nostr.postr.events.MetadataEvent -import nostr.postr.events.PrivateDmEvent -import nostr.postr.events.RecommendRelayEvent -import com.vitorpamplona.amethyst.service.model.TextNoteEvent import nostr.postr.toHex import nostr.postr.toNpub @@ -139,7 +139,7 @@ object LocalCache { fun consume(event: MetadataEvent) { // new event - val oldUser = getOrCreateUser(event.pubKey.toHexKey()) + val oldUser = getOrCreateUser(event.pubKey) if (oldUser.info == null || event.createdAt > oldUser.info!!.updatedMetadataAt) { val newUser = try { metadataParser.readValue( @@ -173,8 +173,8 @@ object LocalCache { return } - val note = getOrCreateNote(event.id.toHex()) - val author = getOrCreateUser(event.pubKey.toHexKey()) + val note = getOrCreateNote(event.id) + val author = getOrCreateUser(event.pubKey) if (relay != null) { author.addRelayBeingUsed(relay, event.createdAt) @@ -220,7 +220,7 @@ object LocalCache { } val note = getOrCreateAddressableNote(event.address()) - val author = getOrCreateUser(event.pubKey.toHexKey()) + val author = getOrCreateUser(event.pubKey) if (relay != null) { author.addRelayBeingUsed(relay, event.createdAt) @@ -228,7 +228,7 @@ object LocalCache { } // Already processed this event. - if (note.event?.id?.toHex() == event.id.toHex()) return + if (note.event?.id == event.id) return val mentions = event.mentions().mapNotNull { checkGetOrCreateUser(it) } val replyTo = replyToWithoutCitations(event).mapNotNull { checkGetOrCreateNote(it) } @@ -284,14 +284,15 @@ object LocalCache { } fun consume(event: ContactListEvent) { - val user = getOrCreateUser(event.pubKey.toHexKey()) + val user = getOrCreateUser(event.pubKey) + val follows = event.follows() - if (event.createdAt > user.updatedFollowsAt && event.follows.isNotEmpty()) { + if (event.createdAt > user.updatedFollowsAt && !follows.isNullOrEmpty()) { // Saves relay list only if it's a user that is currently been seen user.latestContactList = event user.updateFollows( - event.follows.map { + follows.map { try { val pubKey = decodePublicKey(it.pubKeyHex) getOrCreateUser(pubKey.toHexKey()) @@ -316,20 +317,17 @@ object LocalCache { user.updateRelays(relays) } } catch (e: Exception) { - println("relay import issue") + Log.w("Relay List Parser","Relay import issue ${e.message}", e) e.printStackTrace() } - Log.d( - "CL", - "AAA ${user.toBestDisplayName()} ${event.follows.size}" - ) + Log.d("CL", "AAA ${user.toBestDisplayName()} ${follows.size}") } } fun consume(event: PrivateDmEvent, relay: Relay?) { - val note = getOrCreateNote(event.id.toHex()) - val author = getOrCreateUser(event.pubKey.toHexKey()) + val note = getOrCreateNote(event.id) + val author = getOrCreateUser(event.pubKey) if (relay != null) { author.addRelayBeingUsed(relay, event.createdAt) @@ -339,7 +337,7 @@ object LocalCache { // Already processed this event. if (note.event != null) return - val recipient = event.recipientPubKey?.let { getOrCreateUser(it.toHexKey()) } + val recipient = event.recipientPubKey()?.let { getOrCreateUser(it) } //Log.d("PM", "${author.toBestDisplayName()} to ${recipient?.toBestDisplayName()}") @@ -359,9 +357,9 @@ object LocalCache { fun consume(event: DeletionEvent) { var deletedAtLeastOne = false - event.deleteEvents.mapNotNull { notes[it] }.forEach { deleteNote -> + event.deleteEvents().mapNotNull { notes[it] }.forEach { deleteNote -> // must be the same author - if (deleteNote.author?.pubkeyHex == event.pubKey.toHexKey()) { + if (deleteNote.author?.pubkeyHex == event.pubKey) { deleteNote.author?.removeNote(deleteNote) // reverts the add @@ -395,14 +393,14 @@ object LocalCache { } fun consume(event: RepostEvent) { - val note = getOrCreateNote(event.id.toHex()) + val note = getOrCreateNote(event.id) // Already processed this event. if (note.event != null) return //Log.d("TN", "New Boost (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}") - val author = getOrCreateUser(event.pubKey.toHexKey()) + val author = getOrCreateUser(event.pubKey) val mentions = event.originalAuthor().mapNotNull { checkGetOrCreateUser(it) } val repliesTo = event.boostedPost().mapNotNull { checkGetOrCreateNote(it) } + event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) } @@ -429,12 +427,12 @@ object LocalCache { } fun consume(event: ReactionEvent) { - val note = getOrCreateNote(event.id.toHexKey()) + val note = getOrCreateNote(event.id) // Already processed this event. if (note.event != null) return - val author = getOrCreateUser(event.pubKey.toHexKey()) + val author = getOrCreateUser(event.pubKey) val mentions = event.originalAuthor().mapNotNull { checkGetOrCreateUser(it) } val repliesTo = event.originalPost().mapNotNull { checkGetOrCreateNote(it) } + event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) } @@ -475,8 +473,8 @@ object LocalCache { } fun consume(event: ReportEvent, relay: Relay?) { - val note = getOrCreateNote(event.id.toHex()) - val author = getOrCreateUser(event.pubKey.toHexKey()) + val note = getOrCreateNote(event.id) + val author = getOrCreateUser(event.pubKey) if (relay != null) { author.addRelayBeingUsed(relay, event.createdAt) @@ -507,13 +505,13 @@ object LocalCache { fun consume(event: ChannelCreateEvent) { //Log.d("MT", "New Event ${event.content} ${event.id.toHex()}") // new event - val oldChannel = getOrCreateChannel(event.id.toHex()) - val author = getOrCreateUser(event.pubKey.toHexKey()) + 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) - val note = getOrCreateNote(event.id.toHex()) + val note = getOrCreateNote(event.id) oldChannel.addNote(note) note.loadEvent(event, author, emptyList(), emptyList()) @@ -530,12 +528,12 @@ object LocalCache { // new event val oldChannel = checkGetOrCreateChannel(channelId) ?: return - val author = getOrCreateUser(event.pubKey.toHexKey()) + val author = getOrCreateUser(event.pubKey) if (event.createdAt > oldChannel.updatedMetadataAt) { if (oldChannel.creator == null || oldChannel.creator == author) { oldChannel.updateChannelInfo(author, event.channelInfo(), event.createdAt) - val note = getOrCreateNote(event.id.toHex()) + val note = getOrCreateNote(event.id) oldChannel.addNote(note) note.loadEvent(event, author, emptyList(), emptyList()) @@ -559,10 +557,10 @@ object LocalCache { val channel = checkGetOrCreateChannel(channelId) ?: return - val note = getOrCreateNote(event.id.toHex()) + val note = getOrCreateNote(event.id) channel.addNote(note) - val author = getOrCreateUser(event.pubKey.toHexKey()) + val author = getOrCreateUser(event.pubKey) if (relay != null) { author.addRelayBeingUsed(relay, event.createdAt) @@ -606,14 +604,14 @@ object LocalCache { } fun consume(event: LnZapEvent) { - val note = getOrCreateNote(event.id.toHexKey()) + val note = getOrCreateNote(event.id) // Already processed this event. if (note.event != null) return - val zapRequest = event.containedPost()?.id?.toHexKey()?.let { getOrCreateNote(it) } + val zapRequest = event.containedPost()?.id?.let { getOrCreateNote(it) } - val author = getOrCreateUser(event.pubKey.toHexKey()) + val author = getOrCreateUser(event.pubKey) val mentions = event.zappedAuthor().mapNotNull { checkGetOrCreateUser(it) } val repliesTo = event.zappedPost().mapNotNull { checkGetOrCreateNote(it) } + event.taggedAddresses().map { getOrCreateAddressableNote(it) } + @@ -645,15 +643,15 @@ object LocalCache { } fun consume(event: LnZapRequestEvent) { - val note = getOrCreateNote(event.id.toHexKey()) + val note = getOrCreateNote(event.id) // Already processed this event. if (note.event != null) return - val author = getOrCreateUser(event.pubKey.toHexKey()) + val author = getOrCreateUser(event.pubKey) val mentions = event.zappedAuthor().mapNotNull { checkGetOrCreateUser(it) } val repliesTo = event.zappedPost().mapNotNull { checkGetOrCreateNote(it) } + - event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) } + event.taggedAddresses().map { getOrCreateAddressableNote(it) } note.loadEvent(event, author, mentions, repliesTo) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index 12d130b8a..892792119 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -27,7 +27,7 @@ import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import nostr.postr.events.Event +import com.vitorpamplona.amethyst.service.model.Event val tagSearch = Pattern.compile("(?:\\s|\\A)\\#\\[([0-9]+)\\]") @@ -72,7 +72,7 @@ open class Note(val idHex: String) { val channelHex = (event as? ChannelMessageEvent)?.channel() ?: (event as? ChannelMetadataEvent)?.channel() ?: - (event as? ChannelCreateEvent)?.let { it.id.toHexKey() } + (event as? ChannelCreateEvent)?.let { it.id } return channelHex?.let { LocalCache.checkGetOrCreateChannel(it) } } @@ -251,6 +251,22 @@ open class Note(val idHex: String) { }?.isNotEmpty() ?: false) } + fun directlyCiteUsersHex(): Set { + val matcher = tagSearch.matcher(event?.content ?: "") + val returningList = mutableSetOf() + while (matcher.find()) { + try { + val tag = matcher.group(1)?.let { event?.tags?.get(it.toInt()) } + if (tag != null && tag[0] == "p") { + returningList.add(tag[1]) + } + } catch (e: Exception) { + + } + } + return returningList + } + fun directlyCiteUsers(): Set { val matcher = tagSearch.matcher(event?.content ?: "") val returningList = mutableSetOf() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt index 1b7de0513..d0f0e41f0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt @@ -18,8 +18,8 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import nostr.postr.Bech32 -import nostr.postr.events.ContactListEvent -import nostr.postr.events.MetadataEvent +import com.vitorpamplona.amethyst.service.model.ContactListEvent +import com.vitorpamplona.amethyst.service.model.MetadataEvent import nostr.postr.toNpub val lnurlpPattern = Pattern.compile("(?i:http|https):\\/\\/((.+)\\/)*\\.well-known\\/lnurlp\\/(.*)") diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt index e7958ff7c..daa2fe6b6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt @@ -9,8 +9,8 @@ import com.vitorpamplona.amethyst.service.model.RepostEvent import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.TypedFilter import nostr.postr.JsonFilter -import nostr.postr.events.ContactListEvent -import nostr.postr.events.MetadataEvent +import com.vitorpamplona.amethyst.service.model.ContactListEvent +import com.vitorpamplona.amethyst.service.model.MetadataEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent object NostrAccountDataSource: NostrDataSource("AccountData") { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomDataSource.kt index 1e08e1bea..7ad19edad 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomDataSource.kt @@ -6,7 +6,7 @@ import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.TypedFilter import nostr.postr.JsonFilter -import nostr.postr.events.PrivateDmEvent +import com.vitorpamplona.amethyst.service.model.PrivateDmEvent object NostrChatroomDataSource: NostrDataSource("ChatroomFeed") { lateinit var account: Account diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomListDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomListDataSource.kt index e1e721344..cdf92ad24 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomListDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomListDataSource.kt @@ -7,7 +7,7 @@ import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.TypedFilter import nostr.postr.JsonFilter -import nostr.postr.events.PrivateDmEvent +import com.vitorpamplona.amethyst.service.model.PrivateDmEvent object NostrChatroomListDataSource: NostrDataSource("MailBoxFeed") { lateinit var account: Account diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt index 2956fdd7c..ffdde932b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt @@ -1,5 +1,6 @@ package com.vitorpamplona.amethyst.service +import android.util.Log import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent import com.vitorpamplona.amethyst.service.model.ChannelHideMessageEvent @@ -15,7 +16,6 @@ import com.vitorpamplona.amethyst.service.model.RepostEvent import com.vitorpamplona.amethyst.service.relays.Client import com.vitorpamplona.amethyst.service.relays.Relay import com.vitorpamplona.amethyst.service.relays.Subscription -import com.vitorpamplona.amethyst.service.relays.hasValidSignature import java.util.Date import java.util.UUID import java.util.concurrent.atomic.AtomicBoolean @@ -26,12 +26,12 @@ import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import nostr.postr.events.ContactListEvent -import nostr.postr.events.DeletionEvent -import nostr.postr.events.Event -import nostr.postr.events.MetadataEvent -import nostr.postr.events.PrivateDmEvent -import nostr.postr.events.RecommendRelayEvent +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.MetadataEvent +import com.vitorpamplona.amethyst.service.model.PrivateDmEvent +import com.vitorpamplona.amethyst.service.model.RecommendRelayEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent abstract class NostrDataSource(val debugName: String) { @@ -62,39 +62,32 @@ abstract class NostrDataSource(val debugName: String) { try { when (event) { - is MetadataEvent -> LocalCache.consume(event) - //is TextNoteEvent -> LocalCache.consume(event, relay) overrides default TextNote - is RecommendRelayEvent -> LocalCache.consume(event) + is ChannelCreateEvent -> LocalCache.consume(event) + is ChannelHideMessageEvent -> LocalCache.consume(event) + is ChannelMessageEvent -> LocalCache.consume(event, relay) + is ChannelMetadataEvent -> LocalCache.consume(event) + is ChannelMuteUserEvent -> LocalCache.consume(event) is ContactListEvent -> LocalCache.consume(event) - is PrivateDmEvent -> LocalCache.consume(event, relay) is DeletionEvent -> LocalCache.consume(event) - else -> when (event.kind) { - TextNoteEvent.kind -> LocalCache.consume(TextNoteEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig), relay) - RepostEvent.kind -> { - val repostEvent = RepostEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig) - - repostEvent.containedPost()?.let { onEvent(it, subscriptionId, relay) } - LocalCache.consume(repostEvent) - } - ReactionEvent.kind -> LocalCache.consume(ReactionEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig)) - ReportEvent.kind -> LocalCache.consume(ReportEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig), relay) - - LnZapEvent.kind -> { - val zapEvent = LnZapEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig) - - zapEvent.containedPost()?.let { onEvent(it, subscriptionId, relay) } - LocalCache.consume(zapEvent) - } - LnZapRequestEvent.kind -> LocalCache.consume(LnZapRequestEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig)) - - ChannelCreateEvent.kind -> LocalCache.consume(ChannelCreateEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig)) - ChannelMetadataEvent.kind -> LocalCache.consume(ChannelMetadataEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig)) - ChannelMessageEvent.kind -> LocalCache.consume(ChannelMessageEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig), relay) - ChannelHideMessageEvent.kind -> LocalCache.consume(ChannelHideMessageEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig)) - ChannelMuteUserEvent.kind -> LocalCache.consume(ChannelMuteUserEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig)) - - LongTextNoteEvent.kind -> LocalCache.consume(LongTextNoteEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig), relay) + is LnZapEvent -> { + event.containedPost()?.let { onEvent(it, subscriptionId, relay) } + LocalCache.consume(event) + } + is LnZapRequestEvent -> LocalCache.consume(event) + is LongTextNoteEvent -> LocalCache.consume(event, relay) + is MetadataEvent -> LocalCache.consume(event) + is PrivateDmEvent -> LocalCache.consume(event, relay) + is ReactionEvent -> LocalCache.consume(event) + is RecommendRelayEvent -> LocalCache.consume(event) + is ReportEvent -> LocalCache.consume(event, relay) + is RepostEvent -> { + event.containedPost()?.let { onEvent(it, subscriptionId, relay) } + LocalCache.consume(event) + } + is TextNoteEvent -> LocalCache.consume(event, relay) + else -> { + Log.w("Event Not Supported", event.toJson()) } } } catch (e: Exception) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt index dee1eea3a..6f3abc42f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt @@ -5,7 +5,7 @@ import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.TypedFilter import nostr.postr.JsonFilter import nostr.postr.bechToBytes -import nostr.postr.events.MetadataEvent +import com.vitorpamplona.amethyst.service.model.MetadataEvent import nostr.postr.toHex object NostrSearchEventOrUserDataSource: NostrDataSource("SingleEventFeed") { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleUserDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleUserDataSource.kt index 387b08a1c..823d1d159 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleUserDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleUserDataSource.kt @@ -5,7 +5,7 @@ import com.vitorpamplona.amethyst.service.model.ReportEvent import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.TypedFilter import nostr.postr.JsonFilter -import nostr.postr.events.MetadataEvent +import com.vitorpamplona.amethyst.service.model.MetadataEvent object NostrSingleUserDataSource: NostrDataSource("SingleUserFeed") { var usersToWatch = setOf() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt index 18fbafe2f..04ce33011 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt @@ -7,8 +7,8 @@ import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.TypedFilter import nostr.postr.JsonFilter -import nostr.postr.events.ContactListEvent -import nostr.postr.events.MetadataEvent +import com.vitorpamplona.amethyst.service.model.ContactListEvent +import com.vitorpamplona.amethyst.service.model.MetadataEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent object NostrUserProfileDataSource: NostrDataSource("UserProfileFeed") { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ATag.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ATag.kt index 0ecfd8f5d..4a8ae0374 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ATag.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ATag.kt @@ -21,8 +21,8 @@ data class ATag(val kind: Int, val pubKeyHex: String, val dTag: String) { val fullArray = byteArrayOf(NIP19TLVTypes.SPECIAL.id, dTag.size.toByte()) + dTag + - byteArrayOf(NIP19TLVTypes.AUTHOR.id, addr.size.toByte()) + addr + - byteArrayOf(NIP19TLVTypes.KIND.id, kind.size.toByte()) + kind + byteArrayOf(NIP19TLVTypes.AUTHOR.id, addr.size.toByte()) + addr + + byteArrayOf(NIP19TLVTypes.KIND.id, kind.size.toByte()) + kind return Bech32.encodeBytes(hrp = "naddr", fullArray, Bech32.Encoding.Bech32) } @@ -41,7 +41,7 @@ data class ATag(val kind: Int, val pubKeyHex: String, val dTag: String) { Hex.decode(parts[1]) ATag(parts[0].toInt(), parts[1], parts[2]) } catch (t: Throwable) { - Log.w("Address", "Error parsing A Tag: ${atag}: ${t.message}") + Log.w("ATag", "Error parsing A Tag: ${atag}: ${t.message}") null } } @@ -62,7 +62,7 @@ data class ATag(val kind: Int, val pubKeyHex: String, val dTag: String) { } } catch (e: Throwable) { - println("Issue trying to Decode NIP19 ${this}: ${e.message}") + Log.w( "ATag", "Issue trying to Decode NIP19 ${this}: ${e.message}") //e.printStackTrace() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelCreateEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelCreateEvent.kt index 791aafecf..6a1d93f93 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelCreateEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelCreateEvent.kt @@ -1,18 +1,18 @@ package com.vitorpamplona.amethyst.service.model import android.util.Log +import com.vitorpamplona.amethyst.model.HexKey +import com.vitorpamplona.amethyst.model.toHexKey import java.util.Date import nostr.postr.Utils -import nostr.postr.events.Event -import nostr.postr.events.MetadataEvent class ChannelCreateEvent ( - id: ByteArray, - pubKey: ByteArray, + id: HexKey, + pubKey: HexKey, createdAt: Long, tags: List>, content: String, - sig: ByteArray + sig: HexKey ): Event(id, pubKey, createdAt, kind, tags, content, sig) { fun channelInfo() = try { MetadataEvent.gson.fromJson(content, ChannelData::class.java) @@ -35,11 +35,11 @@ class ChannelCreateEvent ( "" } - val pubKey = Utils.pubkeyCreate(privateKey) + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val tags = emptyList>() val id = generateId(pubKey, createdAt, kind, tags, content) val sig = Utils.sign(id, privateKey) - return ChannelCreateEvent(id, pubKey, createdAt, tags, content, sig) + return ChannelCreateEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelHideMessageEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelHideMessageEvent.kt index 41c526e4f..4f8b57f18 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelHideMessageEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelHideMessageEvent.kt @@ -1,16 +1,17 @@ package com.vitorpamplona.amethyst.service.model +import com.vitorpamplona.amethyst.model.HexKey +import com.vitorpamplona.amethyst.model.toHexKey import java.util.Date import nostr.postr.Utils -import nostr.postr.events.Event class ChannelHideMessageEvent ( - id: ByteArray, - pubKey: ByteArray, + id: HexKey, + pubKey: HexKey, createdAt: Long, tags: List>, content: String, - sig: ByteArray + sig: HexKey ): Event(id, pubKey, createdAt, kind, tags, content, sig) { fun eventsToHide() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } @@ -19,7 +20,7 @@ class ChannelHideMessageEvent ( fun create(reason: String, messagesToHide: List?, mentions: List?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ChannelHideMessageEvent { val content = reason - val pubKey = Utils.pubkeyCreate(privateKey) + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val tags = messagesToHide?.map { listOf("e", it) @@ -27,7 +28,7 @@ class ChannelHideMessageEvent ( val id = generateId(pubKey, createdAt, kind, tags, content) val sig = Utils.sign(id, privateKey) - return ChannelHideMessageEvent(id, pubKey, createdAt, tags, content, sig) + return ChannelHideMessageEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) } } } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMessageEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMessageEvent.kt index 39f0e6bae..411a258d8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMessageEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMessageEvent.kt @@ -1,16 +1,17 @@ package com.vitorpamplona.amethyst.service.model +import com.vitorpamplona.amethyst.model.HexKey +import com.vitorpamplona.amethyst.model.toHexKey import java.util.Date import nostr.postr.Utils -import nostr.postr.events.Event class ChannelMessageEvent ( - id: ByteArray, - pubKey: ByteArray, + id: HexKey, + pubKey: HexKey, createdAt: Long, tags: List>, content: String, - sig: ByteArray + sig: HexKey ): Event(id, pubKey, createdAt, kind, tags, content, sig) { fun channel() = tags.firstOrNull { it[0] == "e" && it.size > 3 && it[3] == "root" }?.getOrNull(1) ?: tags.firstOrNull { it.firstOrNull() == "e" }?.getOrNull(1) @@ -22,7 +23,7 @@ class ChannelMessageEvent ( fun create(message: String, channel: String, replyTos: List? = null, mentions: List? = null, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ChannelMessageEvent { val content = message - val pubKey = Utils.pubkeyCreate(privateKey) + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val tags = mutableListOf( listOf("e", channel, "", "root") ) @@ -35,7 +36,7 @@ class ChannelMessageEvent ( val id = generateId(pubKey, createdAt, kind, tags, content) val sig = Utils.sign(id, privateKey) - return ChannelMessageEvent(id, pubKey, createdAt, tags, content, sig) + return ChannelMessageEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) } } } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMetadataEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMetadataEvent.kt index 2552ad89c..d84ea2ab8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMetadataEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMetadataEvent.kt @@ -1,18 +1,18 @@ package com.vitorpamplona.amethyst.service.model import android.util.Log +import com.vitorpamplona.amethyst.model.HexKey +import com.vitorpamplona.amethyst.model.toHexKey import java.util.Date import nostr.postr.Utils -import nostr.postr.events.Event -import nostr.postr.events.MetadataEvent class ChannelMetadataEvent ( - id: ByteArray, - pubKey: ByteArray, + id: HexKey, + pubKey: HexKey, createdAt: Long, tags: List>, content: String, - sig: ByteArray + sig: HexKey ): Event(id, pubKey, createdAt, kind, tags, content, sig) { fun channel() = tags.firstOrNull { it.firstOrNull() == "e" }?.getOrNull(1) fun channelInfo() = @@ -33,11 +33,11 @@ class ChannelMetadataEvent ( else "" - val pubKey = Utils.pubkeyCreate(privateKey) + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val tags = listOf( listOf("e", originalChannelIdHex, "", "root") ) val id = generateId(pubKey, createdAt, kind, tags, content) val sig = Utils.sign(id, privateKey) - return ChannelMetadataEvent(id, pubKey, createdAt, tags, content, sig) + return ChannelMetadataEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) } } } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMuteUserEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMuteUserEvent.kt index 23c1c52fd..6b12e96bf 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMuteUserEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMuteUserEvent.kt @@ -1,27 +1,27 @@ package com.vitorpamplona.amethyst.service.model +import com.vitorpamplona.amethyst.model.HexKey +import com.vitorpamplona.amethyst.model.toHexKey import java.util.Date import nostr.postr.Utils -import nostr.postr.events.Event class ChannelMuteUserEvent ( - id: ByteArray, - pubKey: ByteArray, + id: HexKey, + pubKey: HexKey, createdAt: Long, tags: List>, content: String, - sig: ByteArray + sig: HexKey ): Event(id, pubKey, createdAt, kind, tags, content, sig) { fun usersToMute() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } - companion object { const val kind = 44 fun create(reason: String, usersToMute: List?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ChannelMuteUserEvent { val content = reason - val pubKey = Utils.pubkeyCreate(privateKey) + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val tags = usersToMute?.map { listOf("p", it) @@ -29,7 +29,7 @@ class ChannelMuteUserEvent ( val id = generateId(pubKey, createdAt, kind, tags, content) val sig = Utils.sign(id, privateKey) - return ChannelMuteUserEvent(id, pubKey, createdAt, tags, content, sig) + return ChannelMuteUserEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) } } } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ContactListEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ContactListEvent.kt new file mode 100644 index 000000000..502fb7551 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ContactListEvent.kt @@ -0,0 +1,59 @@ +package com.vitorpamplona.amethyst.service.model + +import android.util.Log +import com.google.gson.reflect.TypeToken +import com.vitorpamplona.amethyst.model.HexKey +import com.vitorpamplona.amethyst.model.toHexKey +import java.util.Date +import nostr.postr.Utils + +data class Contact(val pubKeyHex: String, val relayUri: String?) + +class ContactListEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: List>, + content: String, + sig: HexKey +): Event(id, pubKey, createdAt, kind, tags, content, sig) { + fun follows() = try { + tags.filter { it[0] == "p" }.map { Contact(it[1], it.getOrNull(2)) } + } catch (e: Exception) { + Log.e("ContactListEvent", "can't parse tags as follows: $tags", e) + null + } + + fun relayUse() = try { + if (content.isNotEmpty()) + gson.fromJson(content, object: TypeToken>() {}.type) + else + null + } catch (e: Exception) { + Log.e("ContactListEvent", "can't parse content as relay lists: $tags", e) + null + } + + companion object { + const val kind = 3 + + fun create(follows: List, relayUse: Map?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ContactListEvent { + val content = if (relayUse != null) + gson.toJson(relayUse) + else + "" + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() + val tags = follows.map { + if (it.relayUri != null) + listOf("p", it.pubKeyHex, it.relayUri) + else + listOf("p", it.pubKeyHex) + } + val id = generateId(pubKey, createdAt, kind, tags, content) + val sig = Utils.sign(id, privateKey) + return ContactListEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) + } + } + + data class ReadWrite(val read: Boolean, val write: Boolean) +} \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/DeletionEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/DeletionEvent.kt new file mode 100644 index 000000000..a29cd6261 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/DeletionEvent.kt @@ -0,0 +1,30 @@ +package com.vitorpamplona.amethyst.service.model + +import com.vitorpamplona.amethyst.model.HexKey +import com.vitorpamplona.amethyst.model.toHexKey +import java.util.Date +import nostr.postr.Utils + +class DeletionEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: List>, + content: String, + sig: HexKey +): Event(id, pubKey, createdAt, kind, tags, content, sig) { + fun deleteEvents() = tags.map { it[1] } + + companion object { + const val kind = 5 + + fun create(deleteEvents: List, privateKey: ByteArray, createdAt: Long = Date().time / 1000): DeletionEvent { + val content = "" + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() + val tags = deleteEvents.map { listOf("e", it) } + val id = generateId(pubKey, createdAt, kind, tags, content) + val sig = Utils.sign(id, privateKey) + return DeletionEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt new file mode 100644 index 000000000..6ef8f2e24 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt @@ -0,0 +1,197 @@ +package com.vitorpamplona.amethyst.service.model + +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import com.google.gson.JsonArray +import com.google.gson.JsonDeserializationContext +import com.google.gson.JsonDeserializer +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import com.google.gson.JsonPrimitive +import com.google.gson.JsonSerializationContext +import com.google.gson.JsonSerializer +import com.google.gson.annotations.SerializedName +import com.vitorpamplona.amethyst.model.HexKey +import com.vitorpamplona.amethyst.model.toHexKey +import fr.acinq.secp256k1.Hex +import fr.acinq.secp256k1.Secp256k1 +import java.lang.reflect.Type +import java.security.MessageDigest +import java.util.Date +import nostr.postr.Utils +import nostr.postr.toHex + +open class Event( + val id: HexKey, + @SerializedName("pubkey") val pubKey: HexKey, + @SerializedName("created_at") val createdAt: Long, + val kind: Int, + val tags: List>, + val content: String, + val sig: HexKey +) { + fun toJson(): String = gson.toJson(this) + + fun generateId(): String { + val rawEvent = listOf( + 0, + pubKey, + createdAt, + kind, + tags, + content + ) + val rawEventJson = gson.toJson(rawEvent) + return sha256.digest(rawEventJson.toByteArray()).toHexKey() + } + + /** + * Checks if the ID is correct and then if the pubKey's secret key signed the event. + */ + fun checkSignature() { + if (!id.contentEquals(generateId())) { + throw Exception( + """|Unexpected ID. + | Event: ${toJson()} + | Actual ID: ${id} + | Generated: ${generateId()}""".trimIndent() + ) + } + if (!secp256k1.verifySchnorr(Hex.decode(sig), Hex.decode(id), Hex.decode(pubKey))) { + throw Exception("""Bad signature!""") + } + } + + fun hasValidSignature(): Boolean { + if (!id.contentEquals(generateId())) { + return false + } + if (!Secp256k1.get().verifySchnorr(Hex.decode(sig), Hex.decode(id), Hex.decode(pubKey))) { + return false + } + + return true + } + + class EventDeserializer : JsonDeserializer { + override fun deserialize( + json: JsonElement, + typeOfT: Type?, + context: JsonDeserializationContext? + ): Event { + val jsonObject = json.asJsonObject + return Event( + id = jsonObject.get("id").asString, + pubKey = jsonObject.get("pubkey").asString, + createdAt = jsonObject.get("created_at").asLong, + kind = jsonObject.get("kind").asInt, + tags = jsonObject.get("tags").asJsonArray.map { + it.asJsonArray.map { s -> s.asString } + }, + content = jsonObject.get("content").asString, + sig = jsonObject.get("sig").asString + ) + } + } + + class EventSerializer : JsonSerializer { + override fun serialize( + src: Event, + typeOfSrc: Type?, + context: JsonSerializationContext? + ): JsonElement { + return JsonObject().apply { + addProperty("id", src.id) + addProperty("pubkey", src.pubKey) + addProperty("created_at", src.createdAt) + addProperty("kind", src.kind) + add("tags", JsonArray().also { jsonTags -> + src.tags.forEach { tag -> + jsonTags.add(JsonArray().also { jsonTagElement -> + tag.forEach { tagElement -> + jsonTagElement.add(tagElement) + } + }) + } + }) + addProperty("content", src.content) + addProperty("sig", src.sig) + } + } + } + + class ByteArrayDeserializer : JsonDeserializer { + override fun deserialize( + json: JsonElement, + typeOfT: Type?, + context: JsonDeserializationContext? + ): ByteArray = Hex.decode(json.asString) + } + + class ByteArraySerializer : JsonSerializer { + override fun serialize( + src: ByteArray, + typeOfSrc: Type?, + context: JsonSerializationContext? + ) = JsonPrimitive(src.toHex()) + } + + companion object { + private val secp256k1 = Secp256k1.get() + + val sha256: MessageDigest = MessageDigest.getInstance("SHA-256") + val gson: Gson = GsonBuilder() + .disableHtmlEscaping() + .registerTypeAdapter(Event::class.java, EventSerializer()) + .registerTypeAdapter(Event::class.java, EventDeserializer()) + .registerTypeAdapter(ByteArray::class.java, ByteArraySerializer()) + .registerTypeAdapter(ByteArray::class.java, ByteArrayDeserializer()) + .create() + + fun fromJson(json: String, lenient: Boolean = false): Event = gson.fromJson(json, Event::class.java).getRefinedEvent(lenient) + + fun fromJson(json: JsonElement, lenient: Boolean = false): Event = gson.fromJson(json, Event::class.java).getRefinedEvent(lenient) + + fun Event.getRefinedEvent(lenient: Boolean = false): Event = when (kind) { + ChannelCreateEvent.kind -> ChannelCreateEvent(id, pubKey, createdAt, tags, content, sig) + ChannelHideMessageEvent.kind -> ChannelHideMessageEvent(id, pubKey, createdAt, tags, content, sig) + ChannelMessageEvent.kind -> ChannelMessageEvent(id, pubKey, createdAt, tags, content, sig) + ChannelMetadataEvent.kind -> ChannelMetadataEvent(id, pubKey, createdAt, tags, content, sig) + ChannelMuteUserEvent.kind -> ChannelMuteUserEvent(id, pubKey, createdAt, tags, content, sig) + ContactListEvent.kind -> ContactListEvent(id, pubKey, createdAt, tags, content, sig) + DeletionEvent.kind -> DeletionEvent(id, pubKey, createdAt, tags, content, sig) + + LnZapEvent.kind -> LnZapEvent(id, pubKey, createdAt, tags, content, sig) + LnZapRequestEvent.kind -> LnZapRequestEvent(id, pubKey, createdAt, tags, content, sig) + LongTextNoteEvent.kind -> LongTextNoteEvent(id, pubKey, createdAt, tags, content, sig) + MetadataEvent.kind -> MetadataEvent(id, pubKey, createdAt, tags, content, sig) + PrivateDmEvent.kind -> PrivateDmEvent(id, pubKey, createdAt, tags, content, sig) + ReactionEvent.kind -> ReactionEvent(id, pubKey, createdAt, tags, content, sig) + RecommendRelayEvent.kind -> RecommendRelayEvent(id, pubKey, createdAt, tags, content, sig, lenient) + ReportEvent.kind -> ReportEvent(id, pubKey, createdAt, tags, content, sig) + RepostEvent.kind -> RepostEvent(id, pubKey, createdAt, tags, content, sig) + TextNoteEvent.kind -> TextNoteEvent(id, pubKey, createdAt, tags, content, sig) + else -> this + } + + fun generateId(pubKey: HexKey, createdAt: Long, kind: Int, tags: List>, content: String): ByteArray { + val rawEvent = listOf( + 0, + pubKey, + createdAt, + kind, + tags, + content + ) + val rawEventJson = gson.toJson(rawEvent) + return sha256.digest(rawEventJson.toByteArray()) + } + + fun create(privateKey: ByteArray, kind: Int, tags: List> = emptyList(), content: String = "", createdAt: Long = Date().time / 1000): Event { + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() + val id = Companion.generateId(pubKey, createdAt, kind, tags, content) + val sig = Utils.sign(id, privateKey).toHexKey() + return Event(id.toHexKey(), pubKey, createdAt, kind, tags, content, sig) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt index 2a38c8cf4..6b43b627b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt @@ -1,17 +1,16 @@ package com.vitorpamplona.amethyst.service.model +import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.service.lnurl.LnInvoiceUtil import com.vitorpamplona.amethyst.service.relays.Client -import java.math.BigDecimal -import nostr.postr.events.Event class LnZapEvent ( - id: ByteArray, - pubKey: ByteArray, + id: HexKey, + pubKey: HexKey, createdAt: Long, tags: List>, content: String, - sig: ByteArray + sig: HexKey ): Event(id, pubKey, createdAt, kind, tags, content, sig) { fun zappedPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt index 049b4b434..e17c6e672 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt @@ -1,17 +1,18 @@ package com.vitorpamplona.amethyst.service.model +import com.vitorpamplona.amethyst.model.HexKey +import com.vitorpamplona.amethyst.model.toHexKey import java.util.Date import nostr.postr.Utils -import nostr.postr.events.Event import nostr.postr.toHex class LnZapRequestEvent ( - id: ByteArray, - pubKey: ByteArray, + id: HexKey, + pubKey: HexKey, createdAt: Long, tags: List>, content: String, - sig: ByteArray + sig: HexKey ): Event(id, pubKey, createdAt, kind, tags, content, sig) { fun zappedPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } fun zappedAuthor() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } @@ -22,10 +23,10 @@ class LnZapRequestEvent ( fun create(originalNote: Event, relays: Set, privateKey: ByteArray, createdAt: Long = Date().time / 1000): LnZapRequestEvent { val content = "" - val pubKey = Utils.pubkeyCreate(privateKey) + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() var tags = listOf( - listOf("e", originalNote.id.toHex()), - listOf("p", originalNote.pubKey.toHex()), + listOf("e", originalNote.id), + listOf("p", originalNote.pubKey), listOf("relays") + relays ) if (originalNote is LongTextNoteEvent) { @@ -34,19 +35,19 @@ class LnZapRequestEvent ( val id = generateId(pubKey, createdAt, kind, tags, content) val sig = Utils.sign(id, privateKey) - return LnZapRequestEvent(id, pubKey, createdAt, tags, content, sig) + return LnZapRequestEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) } fun create(userHex: String, relays: Set, privateKey: ByteArray, createdAt: Long = Date().time / 1000): LnZapRequestEvent { val content = "" - val pubKey = Utils.pubkeyCreate(privateKey) + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val tags = listOf( listOf("p", userHex), listOf("relays") + relays ) val id = generateId(pubKey, createdAt, kind, tags, content) val sig = Utils.sign(id, privateKey) - return LnZapRequestEvent(id, pubKey, createdAt, tags, content, sig) + return LnZapRequestEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LongTextNoteEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LongTextNoteEvent.kt index 037c23c83..14fd66d47 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LongTextNoteEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LongTextNoteEvent.kt @@ -1,23 +1,23 @@ package com.vitorpamplona.amethyst.service.model +import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.toHexKey import java.util.Date import nostr.postr.Utils -import nostr.postr.events.Event class LongTextNoteEvent( - id: ByteArray, - pubKey: ByteArray, + id: HexKey, + pubKey: HexKey, createdAt: Long, tags: List>, content: String, - sig: ByteArray + sig: HexKey ): Event(id, pubKey, createdAt, kind, tags, content, sig) { fun replyTos() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } fun mentions() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } fun dTag() = tags.filter { it.firstOrNull() == "d" }.mapNotNull { it.getOrNull(1) }.firstOrNull() ?: "" - fun address() = ATag(kind, pubKey.toHexKey(), dTag()) + fun address() = ATag(kind, pubKey, dTag()) fun topics() = tags.filter { it.firstOrNull() == "t" }.mapNotNull { it.getOrNull(1) } fun title() = tags.filter { it.firstOrNull() == "title" }.mapNotNull { it.getOrNull(1) }.firstOrNull() @@ -34,7 +34,7 @@ class LongTextNoteEvent( const val kind = 30023 fun create(msg: String, replyTos: List?, mentions: List?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): LongTextNoteEvent { - val pubKey = Utils.pubkeyCreate(privateKey) + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val tags = mutableListOf>() replyTos?.forEach { tags.add(listOf("e", it)) @@ -44,7 +44,7 @@ class LongTextNoteEvent( } val id = generateId(pubKey, createdAt, kind, tags, msg) val sig = Utils.sign(id, privateKey) - return LongTextNoteEvent(id, pubKey, createdAt, tags, msg, sig) + return LongTextNoteEvent(id.toHexKey(), pubKey, createdAt, tags, msg, sig.toHexKey()) } } } 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 new file mode 100644 index 000000000..bd4c4c6a6 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/MetadataEvent.kt @@ -0,0 +1,48 @@ +package com.vitorpamplona.amethyst.service.model + +import android.util.Log +import com.google.gson.Gson +import com.vitorpamplona.amethyst.model.HexKey +import com.vitorpamplona.amethyst.model.toHexKey +import java.util.Date +import nostr.postr.Utils + +data class ContactMetaData( + val name: String, + val picture: String, + val about: String, + val nip05: String?) + +class MetadataEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: List>, + content: String, + sig: HexKey +): Event(id, pubKey, createdAt, kind, tags, content, sig) { + fun contactMetaData() = try { + gson.fromJson(content, ContactMetaData::class.java) + } catch (e: Exception) { + Log.e("MetadataEvent", "Can't parse $content", e) + null + } + + companion object { + const val kind = 0 + val gson = Gson() + + fun create(contactMetaData: ContactMetaData, privateKey: ByteArray, createdAt: Long = Date().time / 1000): MetadataEvent { + return create(gson.toJson(contactMetaData), privateKey, createdAt = createdAt) + } + + fun create(contactMetaData: String, privateKey: ByteArray, createdAt: Long = Date().time / 1000): MetadataEvent { + val content = contactMetaData + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() + val tags = listOf>() + val id = generateId(pubKey, createdAt, kind, tags, content) + val sig = Utils.sign(id, privateKey) + return MetadataEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/PrivateDmEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/PrivateDmEvent.kt new file mode 100644 index 000000000..d0f5111f1 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/PrivateDmEvent.kt @@ -0,0 +1,86 @@ +package com.vitorpamplona.amethyst.service.model + +import android.util.Log +import com.vitorpamplona.amethyst.model.HexKey +import com.vitorpamplona.amethyst.model.toHexKey +import fr.acinq.secp256k1.Hex +import java.util.Date +import nostr.postr.Utils +import nostr.postr.toHex + +class PrivateDmEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: List>, + content: String, + sig: HexKey +) : Event(id, pubKey, createdAt, kind, tags, content, sig) { + /** + * This may or may not be the actual recipient's pub key. The event is intended to look like a + * nip-04 EncryptedDmEvent but may omit the recipient, too. This value can be queried and used + * for initial messages. + */ + fun recipientPubKey() = tags.firstOrNull { it.firstOrNull() == "p" }?.run { Hex.decode(this[1]).toHexKey() } // makes sure its a valid one + + /** + * To be fully compatible with nip-04, we read e-tags that are in violation to nip-18. + * + * Nip-18 messages should refer to other events by inline references in the content like + * `[](e/c06f795e1234a9a1aecc731d768d4f3ca73e80031734767067c82d67ce82e506). + */ + fun replyTo() = tags.firstOrNull { it.firstOrNull() == "e" }?.getOrNull(1) + + fun plainContent(privKey: ByteArray, pubKey: ByteArray): String? { + return try { + val sharedSecret = Utils.getSharedSecret(privKey, pubKey) + + val retVal = Utils.decrypt(content, sharedSecret) + + if (retVal.startsWith(nip18Advertisement)) { + retVal.substring(16) + } else { + retVal + } + } catch (e: Exception) { + Log.w("PrivateDM", "Error decrypting the message ${e.message}") + null + } + } + + + companion object { + const val kind = 4 + + const val nip18Advertisement = "[//]: # (nip18)\n" + + fun create( + recipientPubKey: ByteArray, + msg: String, + replyTos: List? = null, mentions: List? = null, + privateKey: ByteArray, + createdAt: Long = Date().time / 1000, + publishedRecipientPubKey: ByteArray? = null, + advertiseNip18: Boolean = true + ): PrivateDmEvent { + val content = Utils.encrypt( + if (advertiseNip18) { nip18Advertisement } else { "" } + msg, + privateKey, + recipientPubKey) + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() + val tags = mutableListOf>() + publishedRecipientPubKey?.let { + tags.add(listOf("p", publishedRecipientPubKey.toHex())) + } + replyTos?.forEach { + tags.add(listOf("e", it)) + } + mentions?.forEach { + tags.add(listOf("p", it)) + } + val id = generateId(pubKey, createdAt, kind, tags, content) + val sig = Utils.sign(id, privateKey) + return PrivateDmEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt index 6048a0978..c01972758 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt @@ -1,17 +1,18 @@ package com.vitorpamplona.amethyst.service.model +import com.vitorpamplona.amethyst.model.HexKey +import com.vitorpamplona.amethyst.model.toHexKey import java.util.Date import nostr.postr.Utils -import nostr.postr.events.Event import nostr.postr.toHex class ReactionEvent ( - id: ByteArray, - pubKey: ByteArray, + id: HexKey, + pubKey: HexKey, createdAt: Long, tags: List>, content: String, - sig: ByteArray + sig: HexKey ): Event(id, pubKey, createdAt, kind, tags, content, sig) { fun originalPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } @@ -30,16 +31,16 @@ class ReactionEvent ( } fun create(content: String, originalNote: Event, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReactionEvent { - val pubKey = Utils.pubkeyCreate(privateKey) + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() - var tags = listOf( listOf("e", originalNote.id.toHex()), listOf("p", originalNote.pubKey.toHex())) + var tags = listOf( listOf("e", originalNote.id), listOf("p", originalNote.pubKey)) if (originalNote is LongTextNoteEvent) { tags = tags + listOf( listOf("a", originalNote.address().toTag()) ) } val id = generateId(pubKey, createdAt, kind, tags, content) val sig = Utils.sign(id, privateKey) - return ReactionEvent(id, pubKey, createdAt, tags, content, sig) + return ReactionEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) } } } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/RecommendRelayEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/RecommendRelayEvent.kt new file mode 100644 index 000000000..a2ef2eaec --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/RecommendRelayEvent.kt @@ -0,0 +1,37 @@ +package com.vitorpamplona.amethyst.service.model + +import com.vitorpamplona.amethyst.model.HexKey +import com.vitorpamplona.amethyst.model.toHexKey +import java.net.URI +import java.util.Date +import nostr.postr.Utils + +class RecommendRelayEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: List>, + content: String, + sig: HexKey, + val lenient: Boolean = false +): Event(id, pubKey, createdAt, kind, tags, content, sig) { + + fun relay() = if (lenient) + URI.create(content.trim()) + else + URI.create(content) + + + companion object { + const val kind = 2 + + fun create(relay: URI, privateKey: ByteArray, createdAt: Long = Date().time / 1000): RecommendRelayEvent { + val content = relay.toString() + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() + val tags = listOf>() + val id = generateId(pubKey, createdAt, kind, tags, content) + val sig = Utils.sign(id, privateKey) + return RecommendRelayEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt index a1d83c155..4eebabae6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt @@ -1,20 +1,21 @@ package com.vitorpamplona.amethyst.service.model +import com.vitorpamplona.amethyst.model.HexKey +import com.vitorpamplona.amethyst.model.toHexKey import java.util.Date import nostr.postr.Utils -import nostr.postr.events.Event import nostr.postr.toHex data class ReportedKey(val key: String, val reportType: ReportEvent.ReportType) // NIP 56 event. class ReportEvent ( - id: ByteArray, - pubKey: ByteArray, + id: HexKey, + pubKey: HexKey, createdAt: Long, tags: List>, content: String, - sig: ByteArray + sig: HexKey ): Event(id, pubKey, createdAt, kind, tags, content, sig) { private fun defaultReportType(): ReportType { @@ -55,10 +56,10 @@ class ReportEvent ( fun create(reportedPost: Event, type: ReportType, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReportEvent { val content = "" - val reportPostTag = listOf("e", reportedPost.id.toHex(), type.name.toLowerCase()) - val reportAuthorTag = listOf("p", reportedPost.pubKey.toHex(), type.name.toLowerCase()) + val reportPostTag = listOf("e", reportedPost.id, type.name.toLowerCase()) + val reportAuthorTag = listOf("p", reportedPost.pubKey, type.name.toLowerCase()) - val pubKey = Utils.pubkeyCreate(privateKey) + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() var tags:List> = listOf(reportPostTag, reportAuthorTag) if (reportedPost is LongTextNoteEvent) { @@ -67,7 +68,7 @@ class ReportEvent ( val id = generateId(pubKey, createdAt, kind, tags, content) val sig = Utils.sign(id, privateKey) - return ReportEvent(id, pubKey, createdAt, tags, content, sig) + return ReportEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) } fun create(reportedUser: String, type: ReportType, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReportEvent { @@ -75,11 +76,11 @@ class ReportEvent ( val reportAuthorTag = listOf("p", reportedUser, type.name.toLowerCase()) - val pubKey = Utils.pubkeyCreate(privateKey) + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val tags:List> = listOf(reportAuthorTag) val id = generateId(pubKey, createdAt, kind, tags, content) val sig = Utils.sign(id, privateKey) - return ReportEvent(id, pubKey, createdAt, tags, content, sig) + return ReportEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/RepostEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/RepostEvent.kt index 3da165f2c..74756c6d0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/RepostEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/RepostEvent.kt @@ -1,18 +1,19 @@ package com.vitorpamplona.amethyst.service.model +import com.vitorpamplona.amethyst.model.HexKey +import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.service.relays.Client import java.util.Date import nostr.postr.Utils -import nostr.postr.events.Event import nostr.postr.toHex class RepostEvent ( - id: ByteArray, - pubKey: ByteArray, + id: HexKey, + pubKey: HexKey, createdAt: Long, tags: List>, content: String, - sig: ByteArray + sig: HexKey ): Event(id, pubKey, createdAt, kind, tags, content, sig) { @@ -32,10 +33,10 @@ class RepostEvent ( fun create(boostedPost: Event, privateKey: ByteArray, createdAt: Long = Date().time / 1000): RepostEvent { val content = boostedPost.toJson() - val replyToPost = listOf("e", boostedPost.id.toHex()) - val replyToAuthor = listOf("p", boostedPost.pubKey.toHex()) + val replyToPost = listOf("e", boostedPost.id) + val replyToAuthor = listOf("p", boostedPost.pubKey) - val pubKey = Utils.pubkeyCreate(privateKey) + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() var tags:List> = boostedPost.tags.plus(listOf(replyToPost, replyToAuthor)) if (boostedPost is LongTextNoteEvent) { @@ -44,7 +45,7 @@ class RepostEvent ( val id = generateId(pubKey, createdAt, kind, tags, content) val sig = Utils.sign(id, privateKey) - return RepostEvent(id, pubKey, createdAt, tags, content, sig) + return RepostEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) } } } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/TextNoteEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/TextNoteEvent.kt index 9e1634b16..63839802b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/TextNoteEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/TextNoteEvent.kt @@ -1,16 +1,17 @@ package com.vitorpamplona.amethyst.service.model +import com.vitorpamplona.amethyst.model.HexKey +import com.vitorpamplona.amethyst.model.toHexKey import java.util.Date import nostr.postr.Utils -import nostr.postr.events.Event class TextNoteEvent( - id: ByteArray, - pubKey: ByteArray, + id: HexKey, + pubKey: HexKey, createdAt: Long, tags: List>, content: String, - sig: ByteArray + sig: HexKey ): Event(id, pubKey, createdAt, kind, tags, content, sig) { fun mentions() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { it.getOrNull(1) }.mapNotNull { ATag.parse(it) } @@ -20,7 +21,7 @@ class TextNoteEvent( const val kind = 1 fun create(msg: String, replyTos: List?, mentions: List?, addresses: List?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): TextNoteEvent { - val pubKey = Utils.pubkeyCreate(privateKey) + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val tags = mutableListOf>() replyTos?.forEach { tags.add(listOf("e", it)) @@ -33,7 +34,7 @@ class TextNoteEvent( } val id = generateId(pubKey, createdAt, kind, tags, msg) val sig = Utils.sign(id, privateKey) - return TextNoteEvent(id, pubKey, createdAt, tags, msg, sig) + return TextNoteEvent(id.toHexKey(), pubKey, createdAt, tags, msg, sig.toHexKey()) } } } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt index 555d5d83a..776a670fb 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt @@ -4,7 +4,7 @@ import java.util.UUID import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch -import nostr.postr.events.Event +import com.vitorpamplona.amethyst.service.model.Event /** * The Nostr Client manages multiple personae the user may switch between. Events are received and @@ -38,9 +38,7 @@ object Client: RelayPool.Listener { if (relays.size != newRelayConfig.size) return false relays.forEach { oldRelayInfo -> - val newRelayInfo = newRelayConfig.find { it.url == oldRelayInfo.url } - - if (newRelayInfo == null) return false + val newRelayInfo = newRelayConfig.find { it.url == oldRelayInfo.url } ?: return false if (!oldRelayInfo.isSameRelayConfig(newRelayInfo)) return false } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/EventVerifier.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/EventVerifier.kt deleted file mode 100644 index 3bbab0e16..000000000 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/EventVerifier.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.vitorpamplona.amethyst.service.relays - -import fr.acinq.secp256k1.Secp256k1 -import nostr.postr.events.Event -import nostr.postr.events.generateId - -fun Event.hasValidSignature(): Boolean { - if (!id.contentEquals(generateId())) { - return false - } - if (!Secp256k1.get().verifySchnorr(sig, id, pubKey)) { - return false - } - - return true -} \ No newline at end of file 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 214f5c1ae..dc28b944e 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 @@ -3,7 +3,7 @@ package com.vitorpamplona.amethyst.service.relays import android.util.Log import com.google.gson.JsonElement import java.util.Date -import nostr.postr.events.Event +import com.vitorpamplona.amethyst.service.model.Event import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayPool.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayPool.kt index 7b96ca7ea..bb9952a82 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayPool.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayPool.kt @@ -5,7 +5,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch -import nostr.postr.events.Event +import com.vitorpamplona.amethyst.service.model.Event /** * RelayPool manages the connection to multiple Relays and lets consumers deal with simple events. 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 8c353220a..9fd223e2b 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 @@ -38,7 +38,9 @@ import com.vitorpamplona.amethyst.RoboHashCache import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User +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.service.model.LongTextNoteEvent import com.vitorpamplona.amethyst.service.model.ReactionEvent import com.vitorpamplona.amethyst.service.model.ReportEvent @@ -51,8 +53,9 @@ import com.vitorpamplona.amethyst.ui.components.UrlPreviewCard import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.Following import kotlin.time.ExperimentalTime -import nostr.postr.events.PrivateDmEvent +import com.vitorpamplona.amethyst.service.model.PrivateDmEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent +import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader @OptIn(ExperimentalFoundationApi::class) @Composable @@ -85,6 +88,7 @@ fun NoteCompose( var moreActionsExpanded by remember { mutableStateOf(false) } val noteEvent = note?.event + val baseChannel = note?.channel() if (noteEvent == null) { BlankNote(modifier.combinedClickable( @@ -100,6 +104,8 @@ fun NoteCompose( navController, onClick = { showHiddenNote = true } ) + } else if ((noteEvent is ChannelCreateEvent || noteEvent is ChannelMetadataEvent) && baseChannel != null) { + ChannelHeader(baseChannel = baseChannel, account = account, navController = navController) } else { var isNew by remember { mutableStateOf(false) } @@ -134,9 +140,11 @@ fun NoteCompose( launchSingleTop = true } } else { - note.channel()?.let { - navController.navigate("Channel/${it.idHex}") - } + note + .channel() + ?.let { + navController.navigate("Channel/${it.idHex}") + } } }, onLongClick = { popupExpanded = true } @@ -176,7 +184,6 @@ fun NoteCompose( } // boosted picture - val baseChannel = note.channel() if (noteEvent is ChannelMessageEvent && baseChannel != null) { val channelState by baseChannel.live.observeAsState() val channel = channelState?.channel 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 55f9d219f..7fc6aff8b 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 @@ -121,7 +121,6 @@ fun ChannelScreen(channelId: String?, accountViewModel: AccountViewModel, accoun Column(Modifier.fillMaxHeight()) { ChannelHeader( channel, account, - accountStateViewModel = accountStateViewModel, navController = navController ) @@ -213,7 +212,7 @@ fun ChannelScreen(channelId: String?, accountViewModel: AccountViewModel, accoun } @Composable -fun ChannelHeader(baseChannel: Channel, account: Account, accountStateViewModel: AccountStateViewModel, navController: NavController) { +fun ChannelHeader(baseChannel: Channel, account: Account, navController: NavController) { val channelState by baseChannel.live.observeAsState() val channel = channelState?.channel ?: return diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomListScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomListScreen.kt index 0087b96ef..d8959fd62 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomListScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomListScreen.kt @@ -91,7 +91,7 @@ fun TabKnown(accountViewModel: AccountViewModel, navController: NavController) { LaunchedEffect(accountViewModel) { NostrChatroomListDataSource.resetFilters() - feedViewModel.invalidateData() + feedViewModel.refresh() } val lifeCycleOwner = LocalLifecycleOwner.current @@ -99,7 +99,7 @@ fun TabKnown(accountViewModel: AccountViewModel, navController: NavController) { val observer = LifecycleEventObserver { source, event -> if (event == Lifecycle.Event.ON_RESUME) { NostrChatroomListDataSource.resetFilters() - feedViewModel.invalidateData() + feedViewModel.refresh() } } @@ -128,7 +128,22 @@ fun TabNew(accountViewModel: AccountViewModel, navController: NavController) { LaunchedEffect(accountViewModel) { NostrChatroomListDataSource.resetFilters() - feedViewModel.invalidateData() // refresh view + feedViewModel.refresh() + } + + val lifeCycleOwner = LocalLifecycleOwner.current + DisposableEffect(accountViewModel) { + val observer = LifecycleEventObserver { source, event -> + if (event == Lifecycle.Event.ON_RESUME) { + NostrChatroomListDataSource.resetFilters() + feedViewModel.refresh() + } + } + + lifeCycleOwner.lifecycle.addObserver(observer) + onDispose { + lifeCycleOwner.lifecycle.removeObserver(observer) + } } Column(Modifier.fillMaxHeight()) { 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 1edbc6155..17bda6fcf 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 @@ -79,7 +79,7 @@ fun ChatroomScreen(userId: String?, accountViewModel: AccountViewModel, navContr val lifeCycleOwner = LocalLifecycleOwner.current LaunchedEffect(userId) { - feedViewModel.invalidateData() + feedViewModel.refresh() } DisposableEffect(userId) { @@ -87,7 +87,7 @@ fun ChatroomScreen(userId: String?, accountViewModel: AccountViewModel, navContr if (event == Lifecycle.Event.ON_RESUME) { println("Private Message Start") NostrChatroomDataSource.start() - feedViewModel.invalidateData() + feedViewModel.refresh() } if (event == Lifecycle.Event.ON_PAUSE) { println("Private Message Stop") 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 5b91d4c31..466ad2959 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 @@ -55,8 +55,8 @@ fun HomeScreen(accountViewModel: AccountViewModel, navController: NavController) LaunchedEffect(accountViewModel) { NostrHomeDataSource.resetFilters() - feedViewModel.invalidateData() - feedViewModelReplies.invalidateData() + feedViewModel.refresh() + feedViewModelReplies.refresh() } val lifeCycleOwner = LocalLifecycleOwner.current @@ -64,8 +64,8 @@ fun HomeScreen(accountViewModel: AccountViewModel, navController: NavController) val observer = LifecycleEventObserver { source, event -> if (event == Lifecycle.Event.ON_RESUME) { NostrHomeDataSource.resetFilters() - feedViewModel.invalidateData() - feedViewModelReplies.invalidateData() + feedViewModel.refresh() + feedViewModelReplies.refresh() } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt index 6221913c7..ca0724285 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt @@ -36,7 +36,7 @@ fun NotificationScreen(accountViewModel: AccountViewModel, navController: NavCon DisposableEffect(accountViewModel) { val observer = LifecycleEventObserver { source, event -> if (event == Lifecycle.Event.ON_RESUME) { - feedViewModel.invalidateData() + feedViewModel.refresh() } } 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 7c66c1afe..65e5eb519 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 @@ -86,8 +86,8 @@ fun SearchScreen(accountViewModel: AccountViewModel, navController: NavControlle val feedViewModel: NostrGlobalFeedViewModel = viewModel() val lifeCycleOwner = LocalLifecycleOwner.current - LaunchedEffect(Unit) { - feedViewModel.invalidateData() + LaunchedEffect(accountViewModel) { + feedViewModel.refresh() } DisposableEffect(accountViewModel) { @@ -95,7 +95,7 @@ fun SearchScreen(accountViewModel: AccountViewModel, navController: NavControlle if (event == Lifecycle.Event.ON_RESUME) { println("Global Start") NostrGlobalDataSource.start() - feedViewModel.invalidateData() + feedViewModel.refresh() } if (event == Lifecycle.Event.ON_PAUSE) { println("Global Stop") From b4699159054700b6fa266d77c4eb117f21093501 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 3 Mar 2023 16:12:11 -0500 Subject: [PATCH 23/36] v0.22.2 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 74c477013..36a0f7cc2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,8 +11,8 @@ android { applicationId "com.vitorpamplona.amethyst" minSdk 26 targetSdk 33 - versionCode 84 - versionName "0.22.1" + versionCode 85 + versionName "0.22.2" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { From b6e16ad4707a3a7d0d857354cc58903354bf86c0 Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Fri, 3 Mar 2023 22:40:46 +0100 Subject: [PATCH 24/36] Create Nip19Test testing toInt32() --- .../amethyst/service/Nip19Test.kt | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 app/src/test/java/com/vitorpamplona/amethyst/service/Nip19Test.kt diff --git a/app/src/test/java/com/vitorpamplona/amethyst/service/Nip19Test.kt b/app/src/test/java/com/vitorpamplona/amethyst/service/Nip19Test.kt new file mode 100644 index 000000000..dd38517f2 --- /dev/null +++ b/app/src/test/java/com/vitorpamplona/amethyst/service/Nip19Test.kt @@ -0,0 +1,23 @@ +package com.vitorpamplona.amethyst.service + +import org.junit.Assert +import org.junit.Test + +class Nip19Test { + + @Test(expected = IllegalArgumentException::class) + fun to_int_32_length_smaller_than_4() { + toInt32(ByteArray(3)) + } + + @Test(expected = IllegalArgumentException::class) + fun to_int_32_length_bigger_than_4() { + toInt32(ByteArray(5)) + } + + @Test() + fun to_int_32_length_4() { + val actual = toInt32(ByteArray(4)) + Assert.assertEquals(0, actual) + } +} From 47f3fe5cc62227727ba0f8f8a7131b99ba0b0149 Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Fri, 3 Mar 2023 22:46:15 +0100 Subject: [PATCH 25/36] refactor Nip19Test introduce byteArrayOfInts() --- .../com/vitorpamplona/amethyst/service/Nip19Test.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/src/test/java/com/vitorpamplona/amethyst/service/Nip19Test.kt b/app/src/test/java/com/vitorpamplona/amethyst/service/Nip19Test.kt index dd38517f2..02e6a5695 100644 --- a/app/src/test/java/com/vitorpamplona/amethyst/service/Nip19Test.kt +++ b/app/src/test/java/com/vitorpamplona/amethyst/service/Nip19Test.kt @@ -7,17 +7,19 @@ class Nip19Test { @Test(expected = IllegalArgumentException::class) fun to_int_32_length_smaller_than_4() { - toInt32(ByteArray(3)) + toInt32(byteArrayOfInts(1, 2, 3)) } @Test(expected = IllegalArgumentException::class) fun to_int_32_length_bigger_than_4() { - toInt32(ByteArray(5)) + toInt32(byteArrayOfInts(1, 2, 3, 4, 5)) } @Test() fun to_int_32_length_4() { - val actual = toInt32(ByteArray(4)) - Assert.assertEquals(0, actual) + val actual = toInt32(byteArrayOfInts(1, 2, 3, 4)) + Assert.assertEquals(16909060, actual) } + + private fun byteArrayOfInts(vararg ints: Int) = ByteArray(ints.size) { pos -> ints[pos].toByte() } } From bd3d7e1aa3b6a971d183596cda25935035161f01 Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Fri, 3 Mar 2023 22:58:24 +0100 Subject: [PATCH 26/36] Prepare test parse_TLV --- .../main/java/com/vitorpamplona/amethyst/service/Nip19.kt | 6 +++--- .../java/com/vitorpamplona/amethyst/service/Nip19Test.kt | 8 ++++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip19.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip19.kt index f4b551be1..d2d20b6df 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip19.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip19.kt @@ -78,7 +78,7 @@ fun toInt32(bytes: ByteArray): Int { } fun parseTLV(data: ByteArray): Map> { - var result = mutableMapOf>() + val result = mutableMapOf>() var rest = data while (rest.isNotEmpty()) { val t = rest[0] @@ -88,9 +88,9 @@ fun parseTLV(data: ByteArray): Map> { if (v.size < l) continue if (!result.containsKey(t)) { - result.put(t, mutableListOf()) + result[t] = mutableListOf() } - result.get(t)?.add(v) + result[t]?.add(v) } return result } diff --git a/app/src/test/java/com/vitorpamplona/amethyst/service/Nip19Test.kt b/app/src/test/java/com/vitorpamplona/amethyst/service/Nip19Test.kt index 02e6a5695..414422dd3 100644 --- a/app/src/test/java/com/vitorpamplona/amethyst/service/Nip19Test.kt +++ b/app/src/test/java/com/vitorpamplona/amethyst/service/Nip19Test.kt @@ -1,6 +1,7 @@ package com.vitorpamplona.amethyst.service import org.junit.Assert +import org.junit.Ignore import org.junit.Test class Nip19Test { @@ -18,8 +19,15 @@ class Nip19Test { @Test() fun to_int_32_length_4() { val actual = toInt32(byteArrayOfInts(1, 2, 3, 4)) + Assert.assertEquals(16909060, actual) } + @Ignore("Not implemented yet") + @Test() + fun parse_TLV() { + // TODO + } + private fun byteArrayOfInts(vararg ints: Int) = ByteArray(ints.size) { pos -> ints[pos].toByte() } } From c1113f9df940a7ff3ea09260fbb31fe0128d70d3 Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Fri, 3 Mar 2023 23:19:05 +0100 Subject: [PATCH 27/36] Add test uri_to_route_npub --- .../amethyst/service/Nip19Test.kt | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/app/src/test/java/com/vitorpamplona/amethyst/service/Nip19Test.kt b/app/src/test/java/com/vitorpamplona/amethyst/service/Nip19Test.kt index 414422dd3..68bfe38b8 100644 --- a/app/src/test/java/com/vitorpamplona/amethyst/service/Nip19Test.kt +++ b/app/src/test/java/com/vitorpamplona/amethyst/service/Nip19Test.kt @@ -6,6 +6,8 @@ import org.junit.Test class Nip19Test { + private val nip19 = Nip19(); + @Test(expected = IllegalArgumentException::class) fun to_int_32_length_smaller_than_4() { toInt32(byteArrayOfInts(1, 2, 3)) @@ -29,5 +31,27 @@ class Nip19Test { // TODO } + @Test() + fun uri_to_route_null() { + val actual = nip19.uriToRoute(null) + + Assert.assertEquals(null, actual) + } + + @Test() + fun uri_to_route_unknown() { + val actual = nip19.uriToRoute("nostr:unknown") + + Assert.assertEquals(null, actual) + } + + @Test() + fun uri_to_route_npub() { + val actual = nip19.uriToRoute("nostr:npub1hv7k2s755n697sptva8vkh9jz40lzfzklnwj6ekewfmxp5crwdjs27007y") + + Assert.assertEquals(Nip19.Type.USER, actual?.type) + Assert.assertEquals("bb3d6543d4a4f45f402b674ecb5cb2155ff12456fcdd2d66d9727660d3037365", actual?.hex) + } + private fun byteArrayOfInts(vararg ints: Int) = ByteArray(ints.size) { pos -> ints[pos].toByte() } } From b35a59372c4838ffbd7eb8184d3aa4df41839402 Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Fri, 3 Mar 2023 23:23:42 +0100 Subject: [PATCH 28/36] Prepare move unit tests for uri_to_route behaviour --- .../amethyst/service/Nip19Test.kt | 56 ++++++++++++++++++- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/app/src/test/java/com/vitorpamplona/amethyst/service/Nip19Test.kt b/app/src/test/java/com/vitorpamplona/amethyst/service/Nip19Test.kt index 68bfe38b8..df6d5800f 100644 --- a/app/src/test/java/com/vitorpamplona/amethyst/service/Nip19Test.kt +++ b/app/src/test/java/com/vitorpamplona/amethyst/service/Nip19Test.kt @@ -47,10 +47,62 @@ class Nip19Test { @Test() fun uri_to_route_npub() { - val actual = nip19.uriToRoute("nostr:npub1hv7k2s755n697sptva8vkh9jz40lzfzklnwj6ekewfmxp5crwdjs27007y") + val actual = + nip19.uriToRoute("nostr:npub1hv7k2s755n697sptva8vkh9jz40lzfzklnwj6ekewfmxp5crwdjs27007y") Assert.assertEquals(Nip19.Type.USER, actual?.type) - Assert.assertEquals("bb3d6543d4a4f45f402b674ecb5cb2155ff12456fcdd2d66d9727660d3037365", actual?.hex) + Assert.assertEquals( + "bb3d6543d4a4f45f402b674ecb5cb2155ff12456fcdd2d66d9727660d3037365", + actual?.hex + ) + } + + @Test() + fun uri_to_route_note() { + val actual = + nip19.uriToRoute("nostr:note1stqea6wmwezg9x6yyr6qkukw95ewtdukyaztycws65l8wppjmtpscawevv") + + Assert.assertEquals(Nip19.Type.NOTE, actual?.type) + Assert.assertEquals( + "82c19ee9db7644829b4420f40b72ce2d32e5b7962744b261d0d53e770432dac3", + actual?.hex + ) + } + + @Ignore("Not implemented yet") + @Test() + fun uri_to_route_nprofile() { + val actual = nip19.uriToRoute("nostr:nprofile") + + Assert.assertEquals(Nip19.Type.USER, actual?.type) + Assert.assertEquals("*", actual?.hex) + } + + @Ignore("Not implemented yet") + @Test() + fun uri_to_route_nevent() { + val actual = nip19.uriToRoute("nostr:nevent") + + Assert.assertEquals(Nip19.Type.USER, actual?.type) + Assert.assertEquals("*", actual?.hex) + } + + @Ignore("Not implemented yet") + @Test() + fun uri_to_route_nrelay() { + val actual = nip19.uriToRoute("nostr:nrelay") + + Assert.assertEquals(Nip19.Type.RELAY, actual?.type) + Assert.assertEquals("*", actual?.hex) + } + + @Ignore("Not implemented yet") + @Test() + fun uri_to_route_naddr() { + val actual = nip19.uriToRoute("nostr:naddr") + + Assert.assertEquals(Nip19.Type.ADDRESS, actual?.type) + Assert.assertEquals("*", actual?.hex) } private fun byteArrayOfInts(vararg ints: Int) = ByteArray(ints.size) { pos -> ints[pos].toByte() } From fd58da2a93b3bfa24b7b6a6c65b458b533fd55a8 Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Fri, 3 Mar 2023 23:26:35 +0100 Subject: [PATCH 29/36] Remove 1 indentation level from uriToRoute() --- .../vitorpamplona/amethyst/service/Nip19.kt | 73 +++++++++---------- 1 file changed, 36 insertions(+), 37 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip19.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip19.kt index d2d20b6df..db73fbc5f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip19.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip19.kt @@ -14,47 +14,46 @@ class Nip19 { enum class Type { USER, NOTE, RELAY, ADDRESS } + data class Return(val type: Type, val hex: String) fun uriToRoute(uri: String?): Return? { try { - val key = uri?.removePrefix("nostr:") + val key = uri?.removePrefix("nostr:") ?: return null - if (key != null) { - val bytes = key.bechToBytes() - if (key.startsWith("npub")) { - return Return(Type.USER, bytes.toHexKey()) - } - if (key.startsWith("note")) { - return Return(Type.NOTE, bytes.toHexKey()) - } - if (key.startsWith("nprofile")) { - val tlv = parseTLV(bytes) - val hex = tlv.get(NIP19TLVTypes.SPECIAL.id)?.get(0)?.toHexKey() - if (hex != null) - return Return(Type.USER, hex) - } - if (key.startsWith("nevent")) { - val tlv = parseTLV(bytes) - val hex = tlv.get(NIP19TLVTypes.SPECIAL.id)?.get(0)?.toHexKey() - if (hex != null) - return Return(Type.USER, hex) - } - if (key.startsWith("nrelay")) { - val tlv = parseTLV(bytes) - val relayUrl = tlv.get(NIP19TLVTypes.SPECIAL.id)?.get(0)?.toString(Charsets.UTF_8) - if (relayUrl != null) - return Return(Type.RELAY, relayUrl) - } - if (key.startsWith("naddr")) { - val tlv = parseTLV(bytes) - val d = tlv.get(NIP19TLVTypes.SPECIAL.id)?.get(0)?.toString(Charsets.UTF_8) - val relay = tlv.get(NIP19TLVTypes.RELAY.id)?.get(0)?.toString(Charsets.UTF_8) - val author = tlv.get(NIP19TLVTypes.AUTHOR.id)?.get(0)?.toHexKey() - val kind = tlv.get(NIP19TLVTypes.KIND.id)?.get(0)?.let { toInt32(it) } - if (d != null) - return Return(Type.ADDRESS, "$kind:$author:$d") - } + val bytes = key.bechToBytes() + if (key.startsWith("npub")) { + return Return(Type.USER, bytes.toHexKey()) + } + if (key.startsWith("note")) { + return Return(Type.NOTE, bytes.toHexKey()) + } + if (key.startsWith("nprofile")) { + val tlv = parseTLV(bytes) + val hex = tlv.get(NIP19TLVTypes.SPECIAL.id)?.get(0)?.toHexKey() + if (hex != null) + return Return(Type.USER, hex) + } + if (key.startsWith("nevent")) { + val tlv = parseTLV(bytes) + val hex = tlv.get(NIP19TLVTypes.SPECIAL.id)?.get(0)?.toHexKey() + if (hex != null) + return Return(Type.USER, hex) + } + if (key.startsWith("nrelay")) { + val tlv = parseTLV(bytes) + val relayUrl = tlv.get(NIP19TLVTypes.SPECIAL.id)?.get(0)?.toString(Charsets.UTF_8) + if (relayUrl != null) + return Return(Type.RELAY, relayUrl) + } + if (key.startsWith("naddr")) { + val tlv = parseTLV(bytes) + val d = tlv.get(NIP19TLVTypes.SPECIAL.id)?.get(0)?.toString(Charsets.UTF_8) + val relay = tlv.get(NIP19TLVTypes.RELAY.id)?.get(0)?.toString(Charsets.UTF_8) + val author = tlv.get(NIP19TLVTypes.AUTHOR.id)?.get(0)?.toHexKey() + val kind = tlv.get(NIP19TLVTypes.KIND.id)?.get(0)?.let { toInt32(it) } + if (d != null) + return Return(Type.ADDRESS, "$kind:$author:$d") } } catch (e: Throwable) { println("Issue trying to Decode NIP19 ${uri}: ${e.message}") @@ -84,7 +83,7 @@ fun parseTLV(data: ByteArray): Map> { val t = rest[0] val l = rest[1] val v = rest.sliceArray(IntRange(2, (2 + l) - 1)) - rest = rest.sliceArray(IntRange(2 + l, rest.size-1)) + rest = rest.sliceArray(IntRange(2 + l, rest.size - 1)) if (v.size < l) continue if (!result.containsKey(t)) { From 91591abd140dd1c3fafd661ca5dbe19f2b4a9728 Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Fri, 3 Mar 2023 23:36:16 +0100 Subject: [PATCH 30/36] Extract method refactoring in Nip19::uriToRoute() --- .../vitorpamplona/amethyst/service/Nip19.kt | 85 ++++++++++++------- 1 file changed, 54 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip19.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip19.kt index db73fbc5f..ee373c06f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip19.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip19.kt @@ -23,37 +23,17 @@ class Nip19 { val bytes = key.bechToBytes() if (key.startsWith("npub")) { - return Return(Type.USER, bytes.toHexKey()) - } - if (key.startsWith("note")) { - return Return(Type.NOTE, bytes.toHexKey()) - } - if (key.startsWith("nprofile")) { - val tlv = parseTLV(bytes) - val hex = tlv.get(NIP19TLVTypes.SPECIAL.id)?.get(0)?.toHexKey() - if (hex != null) - return Return(Type.USER, hex) - } - if (key.startsWith("nevent")) { - val tlv = parseTLV(bytes) - val hex = tlv.get(NIP19TLVTypes.SPECIAL.id)?.get(0)?.toHexKey() - if (hex != null) - return Return(Type.USER, hex) - } - if (key.startsWith("nrelay")) { - val tlv = parseTLV(bytes) - val relayUrl = tlv.get(NIP19TLVTypes.SPECIAL.id)?.get(0)?.toString(Charsets.UTF_8) - if (relayUrl != null) - return Return(Type.RELAY, relayUrl) - } - if (key.startsWith("naddr")) { - val tlv = parseTLV(bytes) - val d = tlv.get(NIP19TLVTypes.SPECIAL.id)?.get(0)?.toString(Charsets.UTF_8) - val relay = tlv.get(NIP19TLVTypes.RELAY.id)?.get(0)?.toString(Charsets.UTF_8) - val author = tlv.get(NIP19TLVTypes.AUTHOR.id)?.get(0)?.toHexKey() - val kind = tlv.get(NIP19TLVTypes.KIND.id)?.get(0)?.let { toInt32(it) } - if (d != null) - return Return(Type.ADDRESS, "$kind:$author:$d") + return npub(bytes) + } else if (key.startsWith("note")) { + return note(bytes) + } else if (key.startsWith("nprofile")) { + return nprofile(bytes) + } else if (key.startsWith("nevent")) { + return nevent(bytes) + } else if (key.startsWith("nrelay")) { + return nrelay(bytes) + } else if (key.startsWith("naddr")) { + return naddr(bytes) } } catch (e: Throwable) { println("Issue trying to Decode NIP19 ${uri}: ${e.message}") @@ -62,6 +42,49 @@ class Nip19 { return null } + + private fun npub(bytes: ByteArray): Return { + return Return(Type.USER, bytes.toHexKey()) + } + + private fun note(bytes: ByteArray): Return { + return Return(Type.NOTE, bytes.toHexKey()); + } + + private fun nprofile(bytes: ByteArray): Return? { + val tlv = parseTLV(bytes) + val hex = tlv.get(NIP19TLVTypes.SPECIAL.id)?.get(0)?.toHexKey() ?: return null + + return Return(Type.USER, hex) + } + + private fun nevent(bytes: ByteArray): Return? { + val hex = parseTLV(bytes) + .get(NIP19TLVTypes.SPECIAL.id) + ?.get(0) + ?.toHexKey() ?: return null + + return Return(Type.USER, hex) + } + + private fun nrelay(bytes: ByteArray): Return? { + val relayUrl = parseTLV(bytes) + .get(NIP19TLVTypes.SPECIAL.id) + ?.get(0) + ?.toString(Charsets.UTF_8) ?: return null + + return Return(Type.RELAY, relayUrl) + } + + private fun naddr(bytes: ByteArray): Return? { + val tlv = parseTLV(bytes) + val d = tlv.get(NIP19TLVTypes.SPECIAL.id)?.get(0)?.toString(Charsets.UTF_8) ?: return null + val relay = tlv.get(NIP19TLVTypes.RELAY.id)?.get(0)?.toString(Charsets.UTF_8) + val author = tlv.get(NIP19TLVTypes.AUTHOR.id)?.get(0)?.toHexKey() + val kind = tlv.get(NIP19TLVTypes.KIND.id)?.get(0)?.let { toInt32(it) } + + return Return(Type.ADDRESS, "$kind:$author:$d") + } } enum class NIP19TLVTypes(val id: Byte) { //classes should start with an uppercase letter in kotlin From 657f99a65a315b0a369876d9d5c2468b47e8ee4a Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Fri, 3 Mar 2023 23:36:37 +0100 Subject: [PATCH 31/36] Remove non-used imports in Nip19 --- .../vitorpamplona/amethyst/service/Nip19.kt | 36 ++++++++++++------- .../amethyst/service/Nip19Test.kt | 12 +++---- 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip19.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip19.kt index ee373c06f..54d765940 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip19.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip19.kt @@ -1,13 +1,9 @@ package com.vitorpamplona.amethyst.service -import com.vitorpamplona.amethyst.model.toByteArray import com.vitorpamplona.amethyst.model.toHexKey -import com.vitorpamplona.amethyst.service.model.ATag +import nostr.postr.bechToBytes import java.nio.ByteBuffer import java.nio.ByteOrder -import nostr.postr.Bech32 -import nostr.postr.bechToBytes -import nostr.postr.toByteArray class Nip19 { @@ -37,7 +33,6 @@ class Nip19 { } } catch (e: Throwable) { println("Issue trying to Decode NIP19 ${uri}: ${e.message}") - //e.printStackTrace() } return null @@ -52,8 +47,10 @@ class Nip19 { } private fun nprofile(bytes: ByteArray): Return? { - val tlv = parseTLV(bytes) - val hex = tlv.get(NIP19TLVTypes.SPECIAL.id)?.get(0)?.toHexKey() ?: return null + val hex = parseTLV(bytes) + .get(NIP19TLVTypes.SPECIAL.id) + ?.get(0) + ?.toHexKey() ?: return null return Return(Type.USER, hex) } @@ -78,16 +75,29 @@ class Nip19 { private fun naddr(bytes: ByteArray): Return? { val tlv = parseTLV(bytes) - val d = tlv.get(NIP19TLVTypes.SPECIAL.id)?.get(0)?.toString(Charsets.UTF_8) ?: return null - val relay = tlv.get(NIP19TLVTypes.RELAY.id)?.get(0)?.toString(Charsets.UTF_8) - val author = tlv.get(NIP19TLVTypes.AUTHOR.id)?.get(0)?.toHexKey() - val kind = tlv.get(NIP19TLVTypes.KIND.id)?.get(0)?.let { toInt32(it) } + + val d = tlv.get(NIP19TLVTypes.SPECIAL.id) + ?.get(0) + ?.toString(Charsets.UTF_8) ?: return null + + val relay = tlv.get(NIP19TLVTypes.RELAY.id) + ?.get(0) + ?.toString(Charsets.UTF_8) + + val author = tlv.get(NIP19TLVTypes.AUTHOR.id) + ?.get(0) + ?.toHexKey() + + val kind = tlv.get(NIP19TLVTypes.KIND.id) + ?.get(0) + ?.let { toInt32(it) } return Return(Type.ADDRESS, "$kind:$author:$d") } } -enum class NIP19TLVTypes(val id: Byte) { //classes should start with an uppercase letter in kotlin +// Classes should start with an uppercase letter in kotlin +enum class NIP19TLVTypes(val id: Byte) { SPECIAL(0), RELAY(1), AUTHOR(2), diff --git a/app/src/test/java/com/vitorpamplona/amethyst/service/Nip19Test.kt b/app/src/test/java/com/vitorpamplona/amethyst/service/Nip19Test.kt index df6d5800f..53de03970 100644 --- a/app/src/test/java/com/vitorpamplona/amethyst/service/Nip19Test.kt +++ b/app/src/test/java/com/vitorpamplona/amethyst/service/Nip19Test.kt @@ -25,10 +25,10 @@ class Nip19Test { Assert.assertEquals(16909060, actual) } - @Ignore("Not implemented yet") + @Ignore("Test not implemented yet") @Test() fun parse_TLV() { - // TODO + // TODO: I don't know how to test this (?) } @Test() @@ -69,7 +69,7 @@ class Nip19Test { ) } - @Ignore("Not implemented yet") + @Ignore("Test not implemented yet") @Test() fun uri_to_route_nprofile() { val actual = nip19.uriToRoute("nostr:nprofile") @@ -78,7 +78,7 @@ class Nip19Test { Assert.assertEquals("*", actual?.hex) } - @Ignore("Not implemented yet") + @Ignore("Test not implemented yet") @Test() fun uri_to_route_nevent() { val actual = nip19.uriToRoute("nostr:nevent") @@ -87,7 +87,7 @@ class Nip19Test { Assert.assertEquals("*", actual?.hex) } - @Ignore("Not implemented yet") + @Ignore("Test not implemented yet") @Test() fun uri_to_route_nrelay() { val actual = nip19.uriToRoute("nostr:nrelay") @@ -96,7 +96,7 @@ class Nip19Test { Assert.assertEquals("*", actual?.hex) } - @Ignore("Not implemented yet") + @Ignore("Test not implemented yet") @Test() fun uri_to_route_naddr() { val actual = nip19.uriToRoute("nostr:naddr") From 8627fd4b4cc64bbb10861f9c6aae6ddd439d8a75 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 3 Mar 2023 18:08:09 -0500 Subject: [PATCH 32/36] Delete Origin Header (snort doesn't need it anymore) --- .../java/com/vitorpamplona/amethyst/service/relays/Relay.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 dc28b944e..2de931f3a 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 @@ -54,7 +54,7 @@ class Relay( if (socket != null) return try { - val request = Request.Builder().header("Origin", "amethyst.social").url(url.trim()).build() + val request = Request.Builder().url(url.trim()).build() val listener = object : WebSocketListener() { override fun onOpen(webSocket: WebSocket, response: Response) { From 5f98a54452a80edb7e28085e7e84d478862396b9 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 3 Mar 2023 18:11:50 -0500 Subject: [PATCH 33/36] Reverting change to activate RTL given some characters, it was picking up some english chars --- .../vitorpamplona/amethyst/ui/components/RichTextViewer.kt | 4 +++- 1 file changed, 3 insertions(+), 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 36b380cad..c67ddcd8b 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 @@ -133,7 +133,9 @@ fun RichTextViewer( // FlowRow doesn't work well with paragraphs. So we need to split them content.split('\n').forEach { paragraph -> FlowRow() { - val s = if (isArabic(paragraph)) paragraph.split(' ').reversed() else paragraph.split(' '); + // Did not work: if (isArabic(paragraph)) paragraph.split(' ').reversed() else + // English is not Right side + val s = paragraph.split(' '); s.forEach { word: String -> if (canPreview) { // Explicit URL From ea905ef6edea2369d8db5f4c30375bced68f00d8 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 3 Mar 2023 18:14:51 -0500 Subject: [PATCH 34/36] Removes the element that was forcing everything to be right aligned --- .../amethyst/ui/components/ExpandableRichTextViewer.kt | 4 ++-- .../vitorpamplona/amethyst/ui/components/RichTextViewer.kt | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ExpandableRichTextViewer.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ExpandableRichTextViewer.kt index 11c25cb62..70bdc114a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ExpandableRichTextViewer.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ExpandableRichTextViewer.kt @@ -46,7 +46,7 @@ fun ExpandableRichTextViewer( val text = if (showFullText) content else content.take(350) Box(contentAlignment = Alignment.BottomCenter) { - CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) { + //CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) { RichTextViewer( text, canPreview, @@ -56,7 +56,7 @@ fun ExpandableRichTextViewer( accountViewModel, navController ) - } + //} if (content.length > 350 && !showFullText) { Row( 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 c67ddcd8b..36b380cad 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 @@ -133,9 +133,7 @@ fun RichTextViewer( // FlowRow doesn't work well with paragraphs. So we need to split them content.split('\n').forEach { paragraph -> FlowRow() { - // Did not work: if (isArabic(paragraph)) paragraph.split(' ').reversed() else - // English is not Right side - val s = paragraph.split(' '); + val s = if (isArabic(paragraph)) paragraph.split(' ').reversed() else paragraph.split(' '); s.forEach { word: String -> if (canPreview) { // Explicit URL From 5e5c3934723ccf8827e376a1003c00f2a069af24 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 3 Mar 2023 19:40:26 -0500 Subject: [PATCH 35/36] Bringing back the idea of checking for change before updating the screen. The check happens between 500us and 1ms and the screen update generally in 10-100ms --- .../com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt | 4 ++-- .../vitorpamplona/amethyst/ui/screen/LnZapFeedViewModel.kt | 4 +++- .../com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt | 4 +++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt index e334b6b5e..98de7538d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt @@ -63,9 +63,9 @@ abstract class FeedViewModel(val localFilter: FeedFilter): ViewModel() { val oldNotesState = feedContent.value if (oldNotesState is FeedState.Loaded) { // Using size as a proxy for has changed. - //if (notes != oldNotesState.feed.value) { + if (notes != oldNotesState.feed.value) { updateFeed(notes) - //} + } } else { updateFeed(notes) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LnZapFeedViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LnZapFeedViewModel.kt index b24c82828..5b1a70da9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LnZapFeedViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LnZapFeedViewModel.kt @@ -38,7 +38,9 @@ open class LnZapFeedViewModel(val dataSource: FeedFilter>): Vie val oldNotesState = feedContent.value if (oldNotesState is LnZapFeedState.Loaded) { // Using size as a proxy for has changed. - updateFeed(notes) + if (notes != oldNotesState.feed.value) { + updateFeed(notes) + } } else { updateFeed(notes) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt index 1f6acfccd..d2ffb23ed 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt @@ -42,7 +42,9 @@ open class UserFeedViewModel(val dataSource: FeedFilter): ViewModel() { val oldNotesState = feedContent.value if (oldNotesState is UserFeedState.Loaded) { // Using size as a proxy for has changed. - updateFeed(notes) + if (notes != oldNotesState.feed.value) { + updateFeed(notes) + } } else { updateFeed(notes) } From 308a79a3052f430bcfeac83eccaf97b24952afea Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 3 Mar 2023 19:43:44 -0500 Subject: [PATCH 36/36] 0.22.3 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 36a0f7cc2..4c2169e58 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,8 +11,8 @@ android { applicationId "com.vitorpamplona.amethyst" minSdk 26 targetSdk 33 - versionCode 85 - versionName "0.22.2" + versionCode 86 + versionName "0.22.3" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables {