From d6298e63da06d73e14665461792f46883722c2c3 Mon Sep 17 00:00:00 2001 From: Marko Kocic Date: Tue, 17 Dec 2024 16:55:58 +0100 Subject: [PATCH 01/14] WIP: Implement quote and comment functionality but stuck on trying to generate valid nevent string. --- .../android/editor/NoteEditorContract.kt | 2 + .../android/editor/NoteEditorViewModel.kt | 18 +++++ .../android/editor/domain/NoteEditorArgs.kt | 7 ++ .../android/editor/ui/NoteEditorScreen.kt | 21 ++++++ .../android/highlights/db/HighlightDao.kt | 4 ++ .../repository/HighlightRepository.kt | 10 ++- .../android/navigation/PrimalAppNavigation.kt | 9 ++- .../primal/android/nostr/utils/Nip19TLV.kt | 67 ++++++++++++++----- .../android/notes/db/ReferencedHighlight.kt | 13 ++++ .../notes/feed/note/ui/ReferencedHighlight.kt | 3 +- .../feed/note/ui/events/NoteCallbacks.kt | 2 + .../articles/details/ArticleDetailsScreen.kt | 2 + .../ui/HighlightActivityBottomSheet.kt | 32 ++++++++- 13 files changed, 169 insertions(+), 21 deletions(-) diff --git a/app/src/main/kotlin/net/primal/android/editor/NoteEditorContract.kt b/app/src/main/kotlin/net/primal/android/editor/NoteEditorContract.kt index 704faa06..c8322cfd 100644 --- a/app/src/main/kotlin/net/primal/android/editor/NoteEditorContract.kt +++ b/app/src/main/kotlin/net/primal/android/editor/NoteEditorContract.kt @@ -8,6 +8,7 @@ import net.primal.android.attachments.domain.CdnImage import net.primal.android.core.compose.profile.model.UserProfileItemUi import net.primal.android.editor.domain.NoteAttachment import net.primal.android.editor.domain.NoteTaggedUser +import net.primal.android.highlights.model.HighlightUi import net.primal.android.notes.feed.model.FeedPostUi import net.primal.android.premium.legend.LegendaryCustomization @@ -17,6 +18,7 @@ interface NoteEditorContract { val content: TextFieldValue = TextFieldValue(), val conversation: List = emptyList(), val replyToArticle: FeedArticleUi? = null, + val replyToHighlight: HighlightUi? = null, val publishing: Boolean = false, val error: NoteEditorError? = null, val activeAccountAvatarCdnImage: CdnImage? = null, diff --git a/app/src/main/kotlin/net/primal/android/editor/NoteEditorViewModel.kt b/app/src/main/kotlin/net/primal/android/editor/NoteEditorViewModel.kt index e5cb973d..3a8d335e 100644 --- a/app/src/main/kotlin/net/primal/android/editor/NoteEditorViewModel.kt +++ b/app/src/main/kotlin/net/primal/android/editor/NoteEditorViewModel.kt @@ -17,6 +17,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter @@ -41,6 +42,8 @@ import net.primal.android.editor.domain.NoteAttachment import net.primal.android.editor.domain.NoteEditorArgs import net.primal.android.editor.domain.NoteTaggedUser import net.primal.android.explore.repository.ExploreRepository +import net.primal.android.highlights.model.asHighlightUi +import net.primal.android.highlights.repository.HighlightRepository import net.primal.android.networking.primal.upload.PrimalFileUploader import net.primal.android.networking.primal.upload.UnsuccessfulFileUpload import net.primal.android.networking.primal.upload.domain.UploadJob @@ -65,6 +68,7 @@ class NoteEditorViewModel @AssistedInject constructor( private val feedRepository: FeedRepository, private val notePublishHandler: NotePublishHandler, private val attachmentRepository: AttachmentsRepository, + private val highlightRepository: HighlightRepository, private val exploreRepository: ExploreRepository, private val profileRepository: ProfileRepository, private val articleRepository: ArticleRepository, @@ -72,6 +76,7 @@ class NoteEditorViewModel @AssistedInject constructor( private val replyToNoteId = args.replyToNoteId private val replyToArticleNaddr = args.replyToArticleNaddr?.let(Nip19TLV::parseUriAsNaddrOrNull) + private val replyToHighlightId = args.replyToHighlightId private val _state = MutableStateFlow(UiState()) val state = _state.asStateFlow() @@ -108,6 +113,10 @@ class NoteEditorViewModel @AssistedInject constructor( observeArticleByNaddr(naddr = replyToArticleNaddr) } + if (replyToHighlightId != null) { + observeHighlight(highlightId = replyToHighlightId) + } + if (args.mediaUris.isNotEmpty()) { importPhotos(args.mediaUris.mapNotNull { Uri.parse(it) }) } @@ -162,6 +171,15 @@ class NoteEditorViewModel @AssistedInject constructor( } } + private fun observeHighlight(highlightId: String) = + viewModelScope.launch { + highlightRepository.observeHighlightById(highlightId = highlightId) + .collect { + setState { copy(replyToHighlight = it.asHighlightUi()) } + } + + } + private fun subscribeToActiveAccount() = viewModelScope.launch { activeAccountStore.activeAccountState diff --git a/app/src/main/kotlin/net/primal/android/editor/domain/NoteEditorArgs.kt b/app/src/main/kotlin/net/primal/android/editor/domain/NoteEditorArgs.kt index 4c49231c..73989e7f 100644 --- a/app/src/main/kotlin/net/primal/android/editor/domain/NoteEditorArgs.kt +++ b/app/src/main/kotlin/net/primal/android/editor/domain/NoteEditorArgs.kt @@ -10,6 +10,7 @@ import net.primal.android.core.serialization.json.decodeFromStringOrNull data class NoteEditorArgs( val replyToNoteId: String? = null, val replyToArticleNaddr: String? = null, + val replyToHighlightId: String? = null, val mediaUris: List = emptyList(), val content: String = "", val contentSelectionStart: Int = 0, @@ -19,6 +20,12 @@ data class NoteEditorArgs( fun toJson(): String = NostrJson.encodeToString(this) companion object { + fun List.toNostrUriInNoteEditorArgs(): NoteEditorArgs { + val preFillContent = + TextFieldValue(text = this.joinToString(separator = "\n\n", prefix = "\n\n") { "nostr:$it" }) + return preFillContent.asNoteEditorArgs() + } + fun String.toNostrUriInNoteEditorArgs(): NoteEditorArgs { val preFillContent = TextFieldValue(text = "\n\nnostr:$this") return preFillContent.asNoteEditorArgs() diff --git a/app/src/main/kotlin/net/primal/android/editor/ui/NoteEditorScreen.kt b/app/src/main/kotlin/net/primal/android/editor/ui/NoteEditorScreen.kt index 0fa7f425..08a1e9fb 100644 --- a/app/src/main/kotlin/net/primal/android/editor/ui/NoteEditorScreen.kt +++ b/app/src/main/kotlin/net/primal/android/editor/ui/NoteEditorScreen.kt @@ -70,6 +70,7 @@ import net.primal.android.core.compose.ReplyingToText import net.primal.android.core.compose.TakePhotoIconButton import net.primal.android.core.compose.UniversalAvatarThumbnail import net.primal.android.core.compose.button.PrimalLoadingButton +import net.primal.android.core.compose.foundation.isAppInDarkPrimalTheme import net.primal.android.core.compose.icons.PrimalIcons import net.primal.android.core.compose.icons.primaliconpack.ImportPhotoFromCamera import net.primal.android.core.compose.icons.primaliconpack.ImportPhotoFromGallery @@ -79,11 +80,16 @@ import net.primal.android.editor.NoteEditorContract.UiState.NoteEditorError import net.primal.android.editor.NoteEditorViewModel import net.primal.android.editor.domain.NoteAttachment import net.primal.android.notes.feed.model.FeedPostUi +import net.primal.android.notes.feed.model.NoteContentUi import net.primal.android.notes.feed.model.toNoteContentUi import net.primal.android.notes.feed.note.ui.FeedNoteHeader import net.primal.android.notes.feed.note.ui.NoteContent +import net.primal.android.notes.feed.note.ui.ReferencedHighlight +import net.primal.android.notes.db.ReferencedHighlight +import net.primal.android.notes.db.toReferencedHighlight import net.primal.android.notes.feed.note.ui.events.NoteCallbacks import net.primal.android.theme.AppTheme +import timber.log.Timber @Composable fun NoteEditorScreen(viewModel: NoteEditorViewModel, onClose: () -> Unit) { @@ -223,6 +229,20 @@ private fun NoteEditorBox( .onSizeChanged { noteEditorMaxHeightPx = it.height }, state = editorListState, ) { + if (state.replyToHighlight != null) { + item( + key = state.replyToHighlight.highlightId, + contentType = "MentionedHighlight", + ) { + ReferencedHighlight( + modifier = Modifier.padding(all = 16.dp), + highlight = state.replyToHighlight.toReferencedHighlight(), + isDarkTheme = isAppInDarkPrimalTheme(), + onClick = {}, + ) + PrimalDivider() + } + } if (state.replyToArticle != null) { item( key = state.replyToArticle.eventId, @@ -338,6 +358,7 @@ private fun NoteEditor( legendaryCustomization = state.activeAccountLegendaryCustomization, ) + Timber.tag("content").i(state.content.text) NoteOutlinedTextField( modifier = Modifier .offset(x = (-8).dp) diff --git a/app/src/main/kotlin/net/primal/android/highlights/db/HighlightDao.kt b/app/src/main/kotlin/net/primal/android/highlights/db/HighlightDao.kt index cef7c62f..b89363d7 100644 --- a/app/src/main/kotlin/net/primal/android/highlights/db/HighlightDao.kt +++ b/app/src/main/kotlin/net/primal/android/highlights/db/HighlightDao.kt @@ -3,6 +3,7 @@ package net.primal.android.highlights.db import androidx.room.Dao import androidx.room.Query import androidx.room.Upsert +import kotlinx.coroutines.flow.Flow @Dao interface HighlightDao { @@ -14,4 +15,7 @@ interface HighlightDao { @Query("DELETE FROM HighlightData WHERE highlightId = :highlightId") fun deleteById(highlightId: String) + + @Query("SELECT * FROM HighlightData WHERE highlightId = :highlightId LIMIT 1") + fun observeById(highlightId: String): Flow } diff --git a/app/src/main/kotlin/net/primal/android/highlights/repository/HighlightRepository.kt b/app/src/main/kotlin/net/primal/android/highlights/repository/HighlightRepository.kt index 890aec00..c1aeafe0 100644 --- a/app/src/main/kotlin/net/primal/android/highlights/repository/HighlightRepository.kt +++ b/app/src/main/kotlin/net/primal/android/highlights/repository/HighlightRepository.kt @@ -1,5 +1,7 @@ package net.primal.android.highlights.repository +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filterNotNull import java.time.Instant import javax.inject.Inject import kotlinx.coroutines.withContext @@ -22,9 +24,15 @@ class HighlightRepository @Inject constructor( private val nostrPublisher: NostrPublisher, ) { companion object { - const val DEFAULT_ALT_TAG = "This is a highlight created in https://primal.net Android application" + const val DEFAULT_ALT_TAG = + "This is a highlight created in https://primal.net Android application" } + fun observeHighlightById(highlightId: String) = + database.highlights().observeById(highlightId = highlightId) + .distinctUntilChanged() + .filterNotNull() + suspend fun publishAndSaveHighlight( userId: String, content: String, diff --git a/app/src/main/kotlin/net/primal/android/navigation/PrimalAppNavigation.kt b/app/src/main/kotlin/net/primal/android/navigation/PrimalAppNavigation.kt index 4de2c383..e7549578 100644 --- a/app/src/main/kotlin/net/primal/android/navigation/PrimalAppNavigation.kt +++ b/app/src/main/kotlin/net/primal/android/navigation/PrimalAppNavigation.kt @@ -45,6 +45,7 @@ import net.primal.android.core.compose.LockToOrientationPortrait import net.primal.android.core.compose.PrimalTopLevelDestination import net.primal.android.core.compose.findActivity import net.primal.android.core.serialization.json.NostrJson +import net.primal.android.crypto.hexToHighlightHrp import net.primal.android.crypto.hexToNoteHrp import net.primal.android.drawer.DrawerScreenDestination import net.primal.android.editor.di.noteEditorViewModel @@ -313,6 +314,12 @@ fun noteCallbacksHandler(navController: NavController) = noteId.hexToNoteHrp().toNostrUriInNoteEditorArgs(), ) }, + onHighlightReplyClick = { id -> + navController.navigateToNoteEditor(args = NoteEditorArgs(replyToHighlightId = id)) + }, + onHighlightQuoteClick = { nevent, naddr -> + navController.navigateToNoteEditor(listOf(nevent, naddr).toNostrUriInNoteEditorArgs()) + }, onArticleClick = { naddr -> navController.navigateToArticleDetails(naddr = naddr) }, onArticleReplyClick = { naddr -> navController.navigateToNoteEditor( @@ -1584,7 +1591,7 @@ private fun NavGraphBuilder.mediaItem( sharedTransitionScope = sharedTransitionScope, animatedVisibilityScope = this@composable, - ) + ) } } diff --git a/app/src/main/kotlin/net/primal/android/nostr/utils/Nip19TLV.kt b/app/src/main/kotlin/net/primal/android/nostr/utils/Nip19TLV.kt index 054a5b6d..7821ad51 100644 --- a/app/src/main/kotlin/net/primal/android/nostr/utils/Nip19TLV.kt +++ b/app/src/main/kotlin/net/primal/android/nostr/utils/Nip19TLV.kt @@ -108,34 +108,42 @@ object Nip19TLV { } } + fun Nevent.asNeventString(): String { + val tlv = mutableListOf() + + this.eventId.addIdentifierBytes(tlv::addAll) + +// if (relays.isNotEmpty()) { +// this.relays.addRelayBytes(tlv::addAll) +// } + +// this.userId.addAuthorBytes(tlv::addAll) + +// this.kind.toKindBytes(tlv::addAll) + + return Bech32.encodeBytes( + hrp = "nevent", + data = tlv.toByteArray(), + encoding = Bech32.Encoding.Bech32, + ) + } + fun Naddr.toNaddrString(): String { val tlv = mutableListOf() // Add SPECIAL type - val identifierBytes = this.identifier.toByteArray(Charsets.US_ASCII) - tlv.add(Type.SPECIAL.id) - tlv.add(identifierBytes.size.toByte()) - tlv.addAll(identifierBytes.toList()) + this.identifier.addIdentifierBytes(tlv::addAll) // Add RELAY type if not empty if (this.relays.isNotEmpty()) { - val relaysBytes = this.relays.joinToString(",").toByteArray(Charsets.US_ASCII) - tlv.add(Type.RELAY.id) - tlv.add(relaysBytes.size.toByte()) - tlv.addAll(relaysBytes.toList()) + this.relays.addRelayBytes(tlv::addAll) } // Add AUTHOR type - val authorBytes = this.userId.hexToBytes() - tlv.add(Type.AUTHOR.id) - tlv.add(authorBytes.size.toByte()) - tlv.addAll(authorBytes.toList()) + this.userId.addAuthorBytes(tlv::addAll) // Add KIND type - val kindBytes = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(this.kind).array() - tlv.add(Type.KIND.id) - tlv.add(kindBytes.size.toByte()) - tlv.addAll(kindBytes.toList()) + this.kind.toKindBytes(tlv::addAll) return Bech32.encodeBytes( hrp = "naddr", @@ -144,6 +152,33 @@ object Nip19TLV { ) } + private fun Int.toKindBytes(addAll: (List) -> Boolean) { + val kindBytes = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(this).array() + addAll(kindBytes.toTLVBytes(type = Type.KIND)) + } + + private fun String.addAuthorBytes(addAll: (List) -> Boolean) { + val authorBytes = this.hexToBytes() + addAll(authorBytes.toTLVBytes(type = Type.AUTHOR)) + } + + private fun String.addIdentifierBytes(addAll: (List) -> Boolean) { + val identifierBytes = this.toByteArray(Charsets.US_ASCII) + addAll(identifierBytes.toTLVBytes(type = Type.SPECIAL)) + } + + private fun List.addRelayBytes(addAll: (List) -> Boolean) { + val relaysBytes = this.joinToString(",").toByteArray(Charsets.US_ASCII) + addAll(relaysBytes.toTLVBytes(type = Type.RELAY)) + } + + private fun ByteArray.toTLVBytes(type: Type) = + listOf( + type.id, + this.size.toByte(), + ) + this.toList() + + @Suppress("MagicNumber") private fun String.hexToBytes(): ByteArray { val cleanedInput = this.replace(Regex("[^0-9A-Fa-f]"), "") diff --git a/app/src/main/kotlin/net/primal/android/notes/db/ReferencedHighlight.kt b/app/src/main/kotlin/net/primal/android/notes/db/ReferencedHighlight.kt index 46cb3cbc..4fc9492f 100644 --- a/app/src/main/kotlin/net/primal/android/notes/db/ReferencedHighlight.kt +++ b/app/src/main/kotlin/net/primal/android/notes/db/ReferencedHighlight.kt @@ -2,6 +2,11 @@ package net.primal.android.notes.db import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.buildJsonArray +import net.primal.android.highlights.model.HighlightUi +import net.primal.android.nostr.ext.asEventIdTag +import net.primal.android.nostr.ext.asReplaceableEventTag +import net.primal.android.nostr.ext.parseEventTags @Serializable data class ReferencedHighlight( @@ -10,3 +15,11 @@ data class ReferencedHighlight( val authorId: String?, val aTag: JsonArray, ) + +fun HighlightUi.toReferencedHighlight() = + ReferencedHighlight( + text = this.content, + eventId = this.highlightId, + authorId = this.referencedEventAuthorId, + aTag = this.referencedEventATag?.asReplaceableEventTag() ?: buildJsonArray { }, + ) diff --git a/app/src/main/kotlin/net/primal/android/notes/feed/note/ui/ReferencedHighlight.kt b/app/src/main/kotlin/net/primal/android/notes/feed/note/ui/ReferencedHighlight.kt index f8aa175d..ac1cf1a1 100644 --- a/app/src/main/kotlin/net/primal/android/notes/feed/note/ui/ReferencedHighlight.kt +++ b/app/src/main/kotlin/net/primal/android/notes/feed/note/ui/ReferencedHighlight.kt @@ -22,13 +22,14 @@ val HighlightBackgroundLight = Color(0xFFE8F3E8) @Composable fun ReferencedHighlight( + modifier: Modifier = Modifier, highlight: ReferencedHighlight, isDarkTheme: Boolean, onClick: (naddr: String) -> Unit, ) { val naddr = highlight.aTag.aTagToNaddr()?.toNaddrString() Text( - modifier = Modifier + modifier = modifier .padding(top = 2.dp) .clickable( enabled = naddr != null, diff --git a/app/src/main/kotlin/net/primal/android/notes/feed/note/ui/events/NoteCallbacks.kt b/app/src/main/kotlin/net/primal/android/notes/feed/note/ui/events/NoteCallbacks.kt index 764d64f4..482e3a6d 100644 --- a/app/src/main/kotlin/net/primal/android/notes/feed/note/ui/events/NoteCallbacks.kt +++ b/app/src/main/kotlin/net/primal/android/notes/feed/note/ui/events/NoteCallbacks.kt @@ -4,6 +4,8 @@ data class NoteCallbacks( val onNoteClick: ((noteId: String) -> Unit)? = null, val onNoteReplyClick: ((noteId: String) -> Unit)? = null, val onNoteQuoteClick: ((noteId: String) -> Unit)? = null, + val onHighlightReplyClick: ((highlightId: String) -> Unit)? = null, + val onHighlightQuoteClick: ((nevent: String, naddr: String) -> Unit)? = null, val onArticleClick: ((naddr: String) -> Unit)? = null, val onArticleReplyClick: ((naddr: String) -> Unit)? = null, val onArticleQuoteClick: ((naddr: String) -> Unit)? = null, diff --git a/app/src/main/kotlin/net/primal/android/thread/articles/details/ArticleDetailsScreen.kt b/app/src/main/kotlin/net/primal/android/thread/articles/details/ArticleDetailsScreen.kt index f9391d7b..7c97ce9c 100644 --- a/app/src/main/kotlin/net/primal/android/thread/articles/details/ArticleDetailsScreen.kt +++ b/app/src/main/kotlin/net/primal/android/thread/articles/details/ArticleDetailsScreen.kt @@ -218,12 +218,14 @@ private fun ArticleDetailsScreen( } HighlightActivityBottomSheetHandler( + articleNaddr = detailsState.naddr, selectedHighlight = detailsState.selectedHighlight, dismissSelection = { detailsEventPublisher(UiEvent.DismissSelectedHighlight) }, isHighlighted = detailsState.isHighlighted, onSaveHighlightClick = { detailsEventPublisher(UiEvent.PublishSelectedHighlight) }, onDeleteHighlightClick = { detailsEventPublisher(UiEvent.DeleteSelectedHighlight) }, isWorking = detailsState.isWorking, + noteCallbacks = noteCallbacks, ) if (articleState.shouldApproveBookmark && detailsState.article != null) { diff --git a/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/HighlightActivityBottomSheet.kt b/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/HighlightActivityBottomSheet.kt index 5f0ccec0..0fa77a7a 100644 --- a/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/HighlightActivityBottomSheet.kt +++ b/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/HighlightActivityBottomSheet.kt @@ -48,29 +48,40 @@ import net.primal.android.core.compose.icons.primaliconpack.Highlight import net.primal.android.core.compose.icons.primaliconpack.Quote import net.primal.android.core.compose.icons.primaliconpack.RemoveHighlight import net.primal.android.core.compose.profile.model.ProfileDetailsUi +import net.primal.android.core.utils.ifNotNull import net.primal.android.highlights.model.CommentUi import net.primal.android.highlights.model.JoinedHighlightsUi +import net.primal.android.nostr.model.NostrEventKind +import net.primal.android.nostr.utils.Naddr +import net.primal.android.nostr.utils.Nevent +import net.primal.android.nostr.utils.Nip19TLV.asNeventString +import net.primal.android.nostr.utils.Nip19TLV.toNaddrString +import net.primal.android.notes.feed.note.ui.events.NoteCallbacks import net.primal.android.theme.AppTheme internal val DARK_THEME_ACTION_BUTTON_COLOR = Color(0xFF282828) @Composable fun HighlightActivityBottomSheetHandler( + articleNaddr: Naddr?, selectedHighlight: JoinedHighlightsUi?, isHighlighted: Boolean, isWorking: Boolean, dismissSelection: () -> Unit, onSaveHighlightClick: () -> Unit, onDeleteHighlightClick: () -> Unit, + noteCallbacks: NoteCallbacks, ) { if (selectedHighlight != null) { HighlightActivityBottomSheet( onDismissRequest = dismissSelection, + articleNaddr = articleNaddr, selectedHighlight = selectedHighlight, isHighlighted = isHighlighted, isWorking = isWorking, onSaveHighlightClick = onSaveHighlightClick, onDeleteHighlightClick = onDeleteHighlightClick, + noteCallbacks = noteCallbacks, ) } } @@ -79,12 +90,14 @@ fun HighlightActivityBottomSheetHandler( @Composable fun HighlightActivityBottomSheet( modifier: Modifier = Modifier, + articleNaddr: Naddr?, selectedHighlight: JoinedHighlightsUi, isHighlighted: Boolean, isWorking: Boolean, onDismissRequest: () -> Unit, onSaveHighlightClick: () -> Unit, onDeleteHighlightClick: () -> Unit, + noteCallbacks: NoteCallbacks, ) { ModalBottomSheet( tonalElevation = 0.dp, @@ -117,8 +130,23 @@ fun HighlightActivityBottomSheet( } } HighlightActionButtons( - onQuoteClick = {}, - onCommentClick = {}, + onQuoteClick = { + ifNotNull( + noteCallbacks.onHighlightQuoteClick, + selectedHighlight.referencedEventATag, + articleNaddr, + ) { quoteCb, aTag, naddr -> + quoteCb( + Nevent( + kind = NostrEventKind.Highlight.value, + userId = selectedHighlight.authors.first().pubkey, + eventId = aTag, + ).asNeventString(), + naddr.toNaddrString(), + ) + } + }, + onCommentClick = { noteCallbacks.onHighlightReplyClick?.invoke(selectedHighlight.highlightId) }, onSaveHighlightClick = onSaveHighlightClick, onDeleteHighlightClick = onDeleteHighlightClick, isHighlighted = isHighlighted, From 2fa780c16162350fc6df98f368a7e07af64eddbc Mon Sep 17 00:00:00 2001 From: Aleksandar Ilic Date: Tue, 17 Dec 2024 18:33:09 +0100 Subject: [PATCH 02/14] Fix toNeventString() and add some tests --- .../primal/android/nostr/utils/Nip19TLV.kt | 31 ++++++++++++---- .../android/nostr/utils/Nip19TLVTest.kt | 37 +++++++++++++++++++ 2 files changed, 60 insertions(+), 8 deletions(-) diff --git a/app/src/main/kotlin/net/primal/android/nostr/utils/Nip19TLV.kt b/app/src/main/kotlin/net/primal/android/nostr/utils/Nip19TLV.kt index 7821ad51..b1bf6ef7 100644 --- a/app/src/main/kotlin/net/primal/android/nostr/utils/Nip19TLV.kt +++ b/app/src/main/kotlin/net/primal/android/nostr/utils/Nip19TLV.kt @@ -108,18 +108,34 @@ object Nip19TLV { } } - fun Nevent.asNeventString(): String { + fun Nevent.toNeventString(): String { val tlv = mutableListOf() - this.eventId.addIdentifierBytes(tlv::addAll) + // Add EVENT_ID type + val identifierBytes = this.eventId.hexToBytes() + tlv.add(Type.SPECIAL.id) + tlv.add(identifierBytes.size.toByte()) + tlv.addAll(identifierBytes.toList()) -// if (relays.isNotEmpty()) { -// this.relays.addRelayBytes(tlv::addAll) -// } + // Add RELAY type if not empty + if (this.relays.isNotEmpty()) { + val relaysBytes = this.relays.joinToString(",").toByteArray(Charsets.US_ASCII) + tlv.add(Type.RELAY.id) + tlv.add(relaysBytes.size.toByte()) + tlv.addAll(relaysBytes.toList()) + } -// this.userId.addAuthorBytes(tlv::addAll) + // Add AUTHOR type + val authorBytes = this.userId.hexToBytes() + tlv.add(Type.AUTHOR.id) + tlv.add(authorBytes.size.toByte()) + tlv.addAll(authorBytes.toList()) -// this.kind.toKindBytes(tlv::addAll) + // Add KIND type + val kindBytes = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(this.kind).array() + tlv.add(Type.KIND.id) + tlv.add(kindBytes.size.toByte()) + tlv.addAll(kindBytes.toList()) return Bech32.encodeBytes( hrp = "nevent", @@ -178,7 +194,6 @@ object Nip19TLV { this.size.toByte(), ) + this.toList() - @Suppress("MagicNumber") private fun String.hexToBytes(): ByteArray { val cleanedInput = this.replace(Regex("[^0-9A-Fa-f]"), "") diff --git a/app/src/test/kotlin/net/primal/android/nostr/utils/Nip19TLVTest.kt b/app/src/test/kotlin/net/primal/android/nostr/utils/Nip19TLVTest.kt index f0ac895b..40f97b52 100644 --- a/app/src/test/kotlin/net/primal/android/nostr/utils/Nip19TLVTest.kt +++ b/app/src/test/kotlin/net/primal/android/nostr/utils/Nip19TLVTest.kt @@ -6,6 +6,7 @@ import io.kotest.matchers.types.instanceOf import net.primal.android.crypto.toHex import net.primal.android.crypto.toNpub import net.primal.android.nostr.utils.Nip19TLV.toNaddrString +import net.primal.android.nostr.utils.Nip19TLV.toNeventString import org.junit.Test class Nip19TLVTest { @@ -149,4 +150,40 @@ class Nip19TLVTest { naddr.toNaddrString() shouldBe expectedNaddr } + + @Test + fun parseUriAsNeventOrNull_returnsProperValuesForNaddr1Uri() { + val nevent = "nostr:nevent1qvzqqqpxfgpzp4sl80zm866yqrha4esknfwp0j4lxfrt29pkrh5nnnj2rgx6dm62qyvhwumn8g" + + "hj7urjv4kkjatd9ec8y6tdv9kzumn9wshszymhwden5te0wp6hyurvv4cxzeewv4ej7qgkwaehxw309aex2mrp0yhx6mmnw" + + "3ezuur4vghsqgz5fxdagtjhp4pgecdvl4vy9fs7p8jhpgeec6qetl7vea5umx6gaswmqppq" + + val expectedEventId = "54499bd42e570d428ce1acfd5842a61e09e570a339c68195ffcccf69cd9b48ec" + val expectedRelays = listOf("wss://premium.primal.net/") + val expectedUserId = "d61f3bc5b3eb4400efdae6169a5c17cabf3246b514361de939ce4a1a0da6ef4a" + val expectedKind = 9802 + + val result = Nip19TLV.parseUriAsNeventOrNull(nevent) + result.shouldNotBeNull() + println(result) + + result.eventId shouldBe expectedEventId + result.relays shouldBe expectedRelays + result.userId shouldBe expectedUserId + result.kind shouldBe expectedKind + } + + @Test + fun toNeventString_createsProperNevent_forGivenNeventStructureWithSingleRelay() { + val expectedNevent = "nevent1qqs9gjvm6sh9wr2z3ns6el2cg2npuz09wz3nn35pjhluenmfekd53mqpr9mhxue69uhhqun" + + "9d45h2mfwwpexjmtpdshxuet59upzp4sl80zm866yqrha4esknfwp0j4lxfrt29pkrh5nnnj2rgx6dm62qvzqqqpxfg8l385v" + + val nevent = Nevent( + eventId = "54499bd42e570d428ce1acfd5842a61e09e570a339c68195ffcccf69cd9b48ec", + relays = listOf("wss://premium.primal.net/"), + userId = "d61f3bc5b3eb4400efdae6169a5c17cabf3246b514361de939ce4a1a0da6ef4a", + kind = 9802, + ) + + nevent.toNeventString() shouldBe expectedNevent + } } From e36a67d83fb4dded1cf08c37e4b2095accad9e66 Mon Sep 17 00:00:00 2001 From: Marko Kocic Date: Wed, 18 Dec 2024 00:12:38 +0100 Subject: [PATCH 03/14] Implement quote highlight --- .../android/navigation/PrimalAppNavigation.kt | 1 - .../primal/android/nostr/utils/Nip19TLV.kt | 49 +++++------- .../ui/HighlightActivityBottomSheet.kt | 10 +-- .../android/nostr/utils/Nip19TLVTest.kt | 76 ++++++++++++++++++- 4 files changed, 100 insertions(+), 36 deletions(-) diff --git a/app/src/main/kotlin/net/primal/android/navigation/PrimalAppNavigation.kt b/app/src/main/kotlin/net/primal/android/navigation/PrimalAppNavigation.kt index e7549578..1dba3c6b 100644 --- a/app/src/main/kotlin/net/primal/android/navigation/PrimalAppNavigation.kt +++ b/app/src/main/kotlin/net/primal/android/navigation/PrimalAppNavigation.kt @@ -45,7 +45,6 @@ import net.primal.android.core.compose.LockToOrientationPortrait import net.primal.android.core.compose.PrimalTopLevelDestination import net.primal.android.core.compose.findActivity import net.primal.android.core.serialization.json.NostrJson -import net.primal.android.crypto.hexToHighlightHrp import net.primal.android.crypto.hexToNoteHrp import net.primal.android.drawer.DrawerScreenDestination import net.primal.android.editor.di.noteEditorViewModel diff --git a/app/src/main/kotlin/net/primal/android/nostr/utils/Nip19TLV.kt b/app/src/main/kotlin/net/primal/android/nostr/utils/Nip19TLV.kt index b1bf6ef7..4f885596 100644 --- a/app/src/main/kotlin/net/primal/android/nostr/utils/Nip19TLV.kt +++ b/app/src/main/kotlin/net/primal/android/nostr/utils/Nip19TLV.kt @@ -112,30 +112,18 @@ object Nip19TLV { val tlv = mutableListOf() // Add EVENT_ID type - val identifierBytes = this.eventId.hexToBytes() - tlv.add(Type.SPECIAL.id) - tlv.add(identifierBytes.size.toByte()) - tlv.addAll(identifierBytes.toList()) + tlv.addAll(this.eventId.constructNeventSpecialBytes()) // Add RELAY type if not empty if (this.relays.isNotEmpty()) { - val relaysBytes = this.relays.joinToString(",").toByteArray(Charsets.US_ASCII) - tlv.add(Type.RELAY.id) - tlv.add(relaysBytes.size.toByte()) - tlv.addAll(relaysBytes.toList()) + tlv.addAll(this.relays.constructRelayBytes()) } // Add AUTHOR type - val authorBytes = this.userId.hexToBytes() - tlv.add(Type.AUTHOR.id) - tlv.add(authorBytes.size.toByte()) - tlv.addAll(authorBytes.toList()) + tlv.addAll(this.userId.constructAuthorBytes()) // Add KIND type - val kindBytes = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(this.kind).array() - tlv.add(Type.KIND.id) - tlv.add(kindBytes.size.toByte()) - tlv.addAll(kindBytes.toList()) + tlv.addAll(this.kind.constructKindBytes()) return Bech32.encodeBytes( hrp = "nevent", @@ -148,18 +136,18 @@ object Nip19TLV { val tlv = mutableListOf() // Add SPECIAL type - this.identifier.addIdentifierBytes(tlv::addAll) + tlv.addAll(this.identifier.constructNaddrIdentifierBytes()) // Add RELAY type if not empty if (this.relays.isNotEmpty()) { - this.relays.addRelayBytes(tlv::addAll) + tlv.addAll(this.relays.constructRelayBytes()) } // Add AUTHOR type - this.userId.addAuthorBytes(tlv::addAll) + tlv.addAll(this.userId.constructAuthorBytes()) // Add KIND type - this.kind.toKindBytes(tlv::addAll) + tlv.addAll(this.kind.constructKindBytes()) return Bech32.encodeBytes( hrp = "naddr", @@ -168,24 +156,29 @@ object Nip19TLV { ) } - private fun Int.toKindBytes(addAll: (List) -> Boolean) { + private fun Int.constructKindBytes(): List { val kindBytes = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(this).array() - addAll(kindBytes.toTLVBytes(type = Type.KIND)) + return kindBytes.toTLVBytes(type = Type.KIND) } - private fun String.addAuthorBytes(addAll: (List) -> Boolean) { + private fun String.constructNeventSpecialBytes(): List { val authorBytes = this.hexToBytes() - addAll(authorBytes.toTLVBytes(type = Type.AUTHOR)) + return authorBytes.toTLVBytes(type = Type.SPECIAL) } - private fun String.addIdentifierBytes(addAll: (List) -> Boolean) { + private fun String.constructAuthorBytes(): List { + val authorBytes = this.hexToBytes() + return authorBytes.toTLVBytes(type = Type.AUTHOR) + } + + private fun String.constructNaddrIdentifierBytes(): List { val identifierBytes = this.toByteArray(Charsets.US_ASCII) - addAll(identifierBytes.toTLVBytes(type = Type.SPECIAL)) + return identifierBytes.toTLVBytes(type = Type.SPECIAL) } - private fun List.addRelayBytes(addAll: (List) -> Boolean) { + private fun List.constructRelayBytes(): List { val relaysBytes = this.joinToString(",").toByteArray(Charsets.US_ASCII) - addAll(relaysBytes.toTLVBytes(type = Type.RELAY)) + return relaysBytes.toTLVBytes(type = Type.RELAY) } private fun ByteArray.toTLVBytes(type: Type) = diff --git a/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/HighlightActivityBottomSheet.kt b/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/HighlightActivityBottomSheet.kt index 0fa77a7a..9c419498 100644 --- a/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/HighlightActivityBottomSheet.kt +++ b/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/HighlightActivityBottomSheet.kt @@ -54,7 +54,7 @@ import net.primal.android.highlights.model.JoinedHighlightsUi import net.primal.android.nostr.model.NostrEventKind import net.primal.android.nostr.utils.Naddr import net.primal.android.nostr.utils.Nevent -import net.primal.android.nostr.utils.Nip19TLV.asNeventString +import net.primal.android.nostr.utils.Nip19TLV.toNeventString import net.primal.android.nostr.utils.Nip19TLV.toNaddrString import net.primal.android.notes.feed.note.ui.events.NoteCallbacks import net.primal.android.theme.AppTheme @@ -133,15 +133,15 @@ fun HighlightActivityBottomSheet( onQuoteClick = { ifNotNull( noteCallbacks.onHighlightQuoteClick, - selectedHighlight.referencedEventATag, + selectedHighlight.highlightId, articleNaddr, - ) { quoteCb, aTag, naddr -> + ) { quoteCb, highlightId, naddr -> quoteCb( Nevent( kind = NostrEventKind.Highlight.value, userId = selectedHighlight.authors.first().pubkey, - eventId = aTag, - ).asNeventString(), + eventId = highlightId, + ).toNeventString(), naddr.toNaddrString(), ) } diff --git a/app/src/test/kotlin/net/primal/android/nostr/utils/Nip19TLVTest.kt b/app/src/test/kotlin/net/primal/android/nostr/utils/Nip19TLVTest.kt index 40f97b52..e61467e9 100644 --- a/app/src/test/kotlin/net/primal/android/nostr/utils/Nip19TLVTest.kt +++ b/app/src/test/kotlin/net/primal/android/nostr/utils/Nip19TLVTest.kt @@ -5,8 +5,8 @@ import io.kotest.matchers.shouldBe import io.kotest.matchers.types.instanceOf import net.primal.android.crypto.toHex import net.primal.android.crypto.toNpub -import net.primal.android.nostr.utils.Nip19TLV.toNaddrString import net.primal.android.nostr.utils.Nip19TLV.toNeventString +import net.primal.android.nostr.utils.Nip19TLV.toNaddrString import org.junit.Test class Nip19TLVTest { @@ -152,7 +152,27 @@ class Nip19TLVTest { } @Test - fun parseUriAsNeventOrNull_returnsProperValuesForNaddr1Uri() { + fun parseUriAsNeventOrNull_returnsProperValuesForNeventNoUris() { + val nevent = "nostr:nevent1qqs9gjvm6sh9wr2z3ns6el2cg2npuz09wz3nn35pjhluenmfekd53mqzyrtp7w" + + "79k045gq80mtnpdxjuzl9t7vjxk52rv80f888y5xsd5mh55qcyqqqzvjsk2whrp" + + val expectedEventId = "54499bd42e570d428ce1acfd5842a61e09e570a339c68195ffcccf69cd9b48ec" + val expectedRelays = emptyList() + val expectedUserId = "d61f3bc5b3eb4400efdae6169a5c17cabf3246b514361de939ce4a1a0da6ef4a" + val expectedKind = 9802 + + val result = Nip19TLV.parseUriAsNeventOrNull(nevent) + result.shouldNotBeNull() + println(result) + + result.eventId shouldBe expectedEventId + result.relays shouldBe expectedRelays + result.userId shouldBe expectedUserId + result.kind shouldBe expectedKind + } + + @Test + fun parseUriAsNeventOrNull_returnsProperValuesForNeventSingleUri() { val nevent = "nostr:nevent1qvzqqqpxfgpzp4sl80zm866yqrha4esknfwp0j4lxfrt29pkrh5nnnj2rgx6dm62qyvhwumn8g" + "hj7urjv4kkjatd9ec8y6tdv9kzumn9wshszymhwden5te0wp6hyurvv4cxzeewv4ej7qgkwaehxw309aex2mrp0yhx6mmnw" + "3ezuur4vghsqgz5fxdagtjhp4pgecdvl4vy9fs7p8jhpgeec6qetl7vea5umx6gaswmqppq" @@ -172,6 +192,42 @@ class Nip19TLVTest { result.kind shouldBe expectedKind } + @Test + fun parseUriAsNeventOrNull_returnsProperValuesForNeventMultipleUris() { + val nevent = "nostr:nevent1qqs9gjvm6sh9wr2z3ns6el2cg2npuz09wz3nn35pjhluenmfekd53mqp" + + "94mhxue69uhhyetvv9ujuerpd46hxtnfduk8wumn8ghj7urjv4kkjatd9ec8y6tdv9kzumn9wspzp4" + + "sl80zm866yqrha4esknfwp0j4lxfrt29pkrh5nnnj2rgx6dm62qvzqqqpxfgvudpun" + + val expectedEventId = "54499bd42e570d428ce1acfd5842a61e09e570a339c68195ffcccf69cd9b48ec" + val expectedRelays = listOf("wss://relay.damus.io", "wss://premium.primal.net") + val expectedUserId = "d61f3bc5b3eb4400efdae6169a5c17cabf3246b514361de939ce4a1a0da6ef4a" + val expectedKind = 9802 + + val result = Nip19TLV.parseUriAsNeventOrNull(nevent) + result.shouldNotBeNull() + println(result) + + result.eventId shouldBe expectedEventId + result.relays shouldBe expectedRelays + result.userId shouldBe expectedUserId + result.kind shouldBe expectedKind + } + + @Test + fun toNeventString_createsProperNevent_forGivenNeventStructureWithoutRelays() { + val expectedNevent = "nevent1qqs9gjvm6sh9wr2z3ns6el2cg2npuz09wz3nn35pjhluenmfekd53mqzyrtp7w79k045g" + + "q80mtnpdxjuzl9t7vjxk52rv80f888y5xsd5mh55qcyqqqzvjsk2whrp" + + val nevent = Nevent( + eventId = "54499bd42e570d428ce1acfd5842a61e09e570a339c68195ffcccf69cd9b48ec", + relays = emptyList(), + userId = "d61f3bc5b3eb4400efdae6169a5c17cabf3246b514361de939ce4a1a0da6ef4a", + kind = 9802, + ) + + nevent.toNeventString() shouldBe expectedNevent + } + @Test fun toNeventString_createsProperNevent_forGivenNeventStructureWithSingleRelay() { val expectedNevent = "nevent1qqs9gjvm6sh9wr2z3ns6el2cg2npuz09wz3nn35pjhluenmfekd53mqpr9mhxue69uhhqun" + @@ -186,4 +242,20 @@ class Nip19TLVTest { nevent.toNeventString() shouldBe expectedNevent } + + @Test + fun toNeventString_createsProperNevent_forGivenNeventStructureWithMultipleRelays() { + val expectedNevent = "nevent1qqs9gjvm6sh9wr2z3ns6el2cg2npuz09wz3nn35pjhluenmfekd53mqp94mhxu" + + "e69uhhyetvv9ujuerpd46hxtnfduk8wumn8ghj7urjv4kkjatd9ec8y6tdv9kzumn9wspzp4sl80zm866yqrha" + + "4esknfwp0j4lxfrt29pkrh5nnnj2rgx6dm62qvzqqqpxfgvudpun" + + val nevent = Nevent( + eventId = "54499bd42e570d428ce1acfd5842a61e09e570a339c68195ffcccf69cd9b48ec", + relays = listOf("wss://relay.damus.io", "wss://premium.primal.net"), + userId = "d61f3bc5b3eb4400efdae6169a5c17cabf3246b514361de939ce4a1a0da6ef4a", + kind = 9802, + ) + + nevent.toNeventString() shouldBe expectedNevent + } } From beda8bb3d4807e0924477e692cb11be063854c43 Mon Sep 17 00:00:00 2001 From: Marko Kocic Date: Wed, 18 Dec 2024 00:20:00 +0100 Subject: [PATCH 04/14] Disable only remove/highlight button while working --- .../thread/articles/details/ui/HighlightActivityBottomSheet.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/HighlightActivityBottomSheet.kt b/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/HighlightActivityBottomSheet.kt index 9c419498..09f1a883 100644 --- a/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/HighlightActivityBottomSheet.kt +++ b/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/HighlightActivityBottomSheet.kt @@ -229,13 +229,11 @@ fun HighlightActionButtons( icon = PrimalIcons.Quote, onClick = onQuoteClick, text = stringResource(id = R.string.article_details_highlight_activity_quote), - isWorking = isWorking, ) ActionButton( icon = PrimalIcons.FeedRepliesFilled, onClick = onCommentClick, text = stringResource(id = R.string.article_details_highlight_activity_comment), - isWorking = isWorking, ) ActionButton( icon = if (isHighlighted) { From cfca49b8349cd5a2528784acbc8117ddade6cce9 Mon Sep 17 00:00:00 2001 From: Marko Kocic Date: Thu, 19 Dec 2024 21:21:20 +0100 Subject: [PATCH 05/14] Implement highlight comments --- .../primal/android/core/utils/NullUtils.kt | 13 ++++++++++ .../android/editor/NoteEditorViewModel.kt | 3 ++- .../android/editor/NotePublishHandler.kt | 26 ++++++++++++++----- 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/app/src/main/kotlin/net/primal/android/core/utils/NullUtils.kt b/app/src/main/kotlin/net/primal/android/core/utils/NullUtils.kt index 9c485266..f2d472f0 100644 --- a/app/src/main/kotlin/net/primal/android/core/utils/NullUtils.kt +++ b/app/src/main/kotlin/net/primal/android/core/utils/NullUtils.kt @@ -20,3 +20,16 @@ inline fun ifNotNull( block(u, v, t) } } + +fun assertOnlyOneNotNull( + message: Any, + vararg args: Any?, +) { + args.sumOf { (it != null).toInt() }.apply { + if (this != 1) { + error(message = message) + } + } +} + +private fun Boolean.toInt() = if (this) 1 else 0 diff --git a/app/src/main/kotlin/net/primal/android/editor/NoteEditorViewModel.kt b/app/src/main/kotlin/net/primal/android/editor/NoteEditorViewModel.kt index 3a8d335e..d9115cc4 100644 --- a/app/src/main/kotlin/net/primal/android/editor/NoteEditorViewModel.kt +++ b/app/src/main/kotlin/net/primal/android/editor/NoteEditorViewModel.kt @@ -17,7 +17,6 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter @@ -269,6 +268,8 @@ class NoteEditorViewModel @AssistedInject constructor( rootArticleAuthorId = article?.authorId, rootPostId = if (article == null) rootPost?.postId else null, replyToPostId = replyToPost?.postId, + rootHighlightId = replyToHighlightId, + rootHighlightAuthorId = _state.value.replyToHighlight?.author?.pubkey, replyToAuthorId = replyToPost?.authorId, ) diff --git a/app/src/main/kotlin/net/primal/android/editor/NotePublishHandler.kt b/app/src/main/kotlin/net/primal/android/editor/NotePublishHandler.kt index 9be814e0..5bce3c77 100644 --- a/app/src/main/kotlin/net/primal/android/editor/NotePublishHandler.kt +++ b/app/src/main/kotlin/net/primal/android/editor/NotePublishHandler.kt @@ -1,12 +1,12 @@ package net.primal.android.editor -import java.util.* import javax.inject.Inject import kotlinx.coroutines.withContext import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.jsonPrimitive import net.primal.android.core.coroutines.CoroutineDispatcherProvider +import net.primal.android.core.utils.assertOnlyOneNotNull import net.primal.android.db.PrimalDatabase import net.primal.android.editor.domain.NoteAttachment import net.primal.android.networking.relays.errors.NostrPublishException @@ -35,13 +35,18 @@ class NotePublishHandler @Inject constructor( rootArticleEventId: String? = null, rootArticleId: String? = null, rootArticleAuthorId: String? = null, + rootHighlightId: String? = null, + rootHighlightAuthorId: String? = null, rootPostId: String? = null, replyToPostId: String? = null, replyToAuthorId: String? = null, ): Boolean { - if (rootArticleId != null && rootPostId != null) { - error("You can not have both article and post as root events.") - } + assertOnlyOneNotNull( + "You can have only one root event.", + rootArticleId, + rootPostId, + rootHighlightId, + ) val replyPostData = replyToPostId?.let { withContext(dispatcherProvider.io()) { @@ -49,6 +54,14 @@ class NotePublishHandler @Inject constructor( } } + val rootHighlightTags = if (rootHighlightId != null && rootHighlightAuthorId != null) { + listOf( + rootHighlightId.asEventIdTag(marker = "root"), + ) + } else { + null + } + /* Article tag */ val rootArticleTags = if (rootArticleId != null && rootArticleAuthorId != null && rootArticleEventId != null) { val tagContent = "${NostrEventKind.LongFormContent.value}:$rootArticleAuthorId:$rootArticleId" @@ -68,7 +81,7 @@ class NotePublishHandler @Inject constructor( } else { null } - val rootEventTags = rootArticleTags ?: listOf(rootPostTag) + val rootEventTags = rootHighlightTags ?: rootArticleTags ?: listOf(rootPostTag) val eventTags = setOfNotNull(*rootEventTags.toTypedArray(), replyEventTag) + mentionEventTags val relayHintsMap = withContext(dispatcherProvider.io()) { @@ -85,10 +98,11 @@ class NotePublishHandler @Inject constructor( /* Pubkey tags */ val existingPubkeyTags = replyPostData?.tags?.filter { it.isPubKeyTag() }?.toSet() ?: setOf() val replyAuthorPubkeyTag = replyToAuthorId?.asPubkeyTag() + val replyHighlightAuthorPubkeyTag = rootHighlightAuthorId?.asPubkeyTag() val rootArticleAuthorPubkeyTag = rootArticleAuthorId?.asPubkeyTag() val mentionPubkeyTags = content.parsePubkeyTags(marker = "mention").toSet() val pubkeyTags = existingPubkeyTags + mentionPubkeyTags + - setOfNotNull(replyAuthorPubkeyTag, rootArticleAuthorPubkeyTag) + setOfNotNull(replyAuthorPubkeyTag, rootArticleAuthorPubkeyTag, replyHighlightAuthorPubkeyTag) /* Hashtag tags */ val hashtagTags = content.parseHashtagTags().toSet() From c4f446913b60eb27b5a436aa9fd5337c6ed0a5f0 Mon Sep 17 00:00:00 2001 From: Marko Kocic Date: Thu, 19 Dec 2024 21:22:11 +0100 Subject: [PATCH 06/14] Auto-format code --- .../main/kotlin/net/primal/android/core/utils/NullUtils.kt | 5 +---- .../kotlin/net/primal/android/editor/NoteEditorViewModel.kt | 1 - .../kotlin/net/primal/android/editor/ui/NoteEditorScreen.kt | 5 ++--- .../android/highlights/repository/HighlightRepository.kt | 4 ++-- .../net/primal/android/navigation/PrimalAppNavigation.kt | 2 +- .../net/primal/android/notes/db/ReferencedHighlight.kt | 4 +--- .../articles/details/ui/HighlightActivityBottomSheet.kt | 2 +- .../kotlin/net/primal/android/nostr/utils/Nip19TLVTest.kt | 2 +- 8 files changed, 9 insertions(+), 16 deletions(-) diff --git a/app/src/main/kotlin/net/primal/android/core/utils/NullUtils.kt b/app/src/main/kotlin/net/primal/android/core/utils/NullUtils.kt index f2d472f0..57c244df 100644 --- a/app/src/main/kotlin/net/primal/android/core/utils/NullUtils.kt +++ b/app/src/main/kotlin/net/primal/android/core/utils/NullUtils.kt @@ -21,10 +21,7 @@ inline fun ifNotNull( } } -fun assertOnlyOneNotNull( - message: Any, - vararg args: Any?, -) { +fun assertOnlyOneNotNull(message: Any, vararg args: Any?) { args.sumOf { (it != null).toInt() }.apply { if (this != 1) { error(message = message) diff --git a/app/src/main/kotlin/net/primal/android/editor/NoteEditorViewModel.kt b/app/src/main/kotlin/net/primal/android/editor/NoteEditorViewModel.kt index d9115cc4..f44ba889 100644 --- a/app/src/main/kotlin/net/primal/android/editor/NoteEditorViewModel.kt +++ b/app/src/main/kotlin/net/primal/android/editor/NoteEditorViewModel.kt @@ -176,7 +176,6 @@ class NoteEditorViewModel @AssistedInject constructor( .collect { setState { copy(replyToHighlight = it.asHighlightUi()) } } - } private fun subscribeToActiveAccount() = diff --git a/app/src/main/kotlin/net/primal/android/editor/ui/NoteEditorScreen.kt b/app/src/main/kotlin/net/primal/android/editor/ui/NoteEditorScreen.kt index 08a1e9fb..1298724b 100644 --- a/app/src/main/kotlin/net/primal/android/editor/ui/NoteEditorScreen.kt +++ b/app/src/main/kotlin/net/primal/android/editor/ui/NoteEditorScreen.kt @@ -79,14 +79,13 @@ import net.primal.android.editor.NoteEditorContract.UiEvent import net.primal.android.editor.NoteEditorContract.UiState.NoteEditorError import net.primal.android.editor.NoteEditorViewModel import net.primal.android.editor.domain.NoteAttachment +import net.primal.android.notes.db.ReferencedHighlight +import net.primal.android.notes.db.toReferencedHighlight import net.primal.android.notes.feed.model.FeedPostUi -import net.primal.android.notes.feed.model.NoteContentUi import net.primal.android.notes.feed.model.toNoteContentUi import net.primal.android.notes.feed.note.ui.FeedNoteHeader import net.primal.android.notes.feed.note.ui.NoteContent import net.primal.android.notes.feed.note.ui.ReferencedHighlight -import net.primal.android.notes.db.ReferencedHighlight -import net.primal.android.notes.db.toReferencedHighlight import net.primal.android.notes.feed.note.ui.events.NoteCallbacks import net.primal.android.theme.AppTheme import timber.log.Timber diff --git a/app/src/main/kotlin/net/primal/android/highlights/repository/HighlightRepository.kt b/app/src/main/kotlin/net/primal/android/highlights/repository/HighlightRepository.kt index c1aeafe0..4e0b3199 100644 --- a/app/src/main/kotlin/net/primal/android/highlights/repository/HighlightRepository.kt +++ b/app/src/main/kotlin/net/primal/android/highlights/repository/HighlightRepository.kt @@ -1,9 +1,9 @@ package net.primal.android.highlights.repository -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.filterNotNull import java.time.Instant import javax.inject.Inject +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.withContext import net.primal.android.core.coroutines.CoroutineDispatcherProvider import net.primal.android.db.PrimalDatabase diff --git a/app/src/main/kotlin/net/primal/android/navigation/PrimalAppNavigation.kt b/app/src/main/kotlin/net/primal/android/navigation/PrimalAppNavigation.kt index 1dba3c6b..62721a64 100644 --- a/app/src/main/kotlin/net/primal/android/navigation/PrimalAppNavigation.kt +++ b/app/src/main/kotlin/net/primal/android/navigation/PrimalAppNavigation.kt @@ -1590,7 +1590,7 @@ private fun NavGraphBuilder.mediaItem( sharedTransitionScope = sharedTransitionScope, animatedVisibilityScope = this@composable, - ) + ) } } diff --git a/app/src/main/kotlin/net/primal/android/notes/db/ReferencedHighlight.kt b/app/src/main/kotlin/net/primal/android/notes/db/ReferencedHighlight.kt index 4fc9492f..ff0a62c6 100644 --- a/app/src/main/kotlin/net/primal/android/notes/db/ReferencedHighlight.kt +++ b/app/src/main/kotlin/net/primal/android/notes/db/ReferencedHighlight.kt @@ -4,9 +4,7 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.buildJsonArray import net.primal.android.highlights.model.HighlightUi -import net.primal.android.nostr.ext.asEventIdTag import net.primal.android.nostr.ext.asReplaceableEventTag -import net.primal.android.nostr.ext.parseEventTags @Serializable data class ReferencedHighlight( @@ -21,5 +19,5 @@ fun HighlightUi.toReferencedHighlight() = text = this.content, eventId = this.highlightId, authorId = this.referencedEventAuthorId, - aTag = this.referencedEventATag?.asReplaceableEventTag() ?: buildJsonArray { }, + aTag = this.referencedEventATag?.asReplaceableEventTag() ?: buildJsonArray { }, ) diff --git a/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/HighlightActivityBottomSheet.kt b/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/HighlightActivityBottomSheet.kt index 09f1a883..ed9aae38 100644 --- a/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/HighlightActivityBottomSheet.kt +++ b/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/HighlightActivityBottomSheet.kt @@ -54,8 +54,8 @@ import net.primal.android.highlights.model.JoinedHighlightsUi import net.primal.android.nostr.model.NostrEventKind import net.primal.android.nostr.utils.Naddr import net.primal.android.nostr.utils.Nevent -import net.primal.android.nostr.utils.Nip19TLV.toNeventString import net.primal.android.nostr.utils.Nip19TLV.toNaddrString +import net.primal.android.nostr.utils.Nip19TLV.toNeventString import net.primal.android.notes.feed.note.ui.events.NoteCallbacks import net.primal.android.theme.AppTheme diff --git a/app/src/test/kotlin/net/primal/android/nostr/utils/Nip19TLVTest.kt b/app/src/test/kotlin/net/primal/android/nostr/utils/Nip19TLVTest.kt index e61467e9..5094dc3d 100644 --- a/app/src/test/kotlin/net/primal/android/nostr/utils/Nip19TLVTest.kt +++ b/app/src/test/kotlin/net/primal/android/nostr/utils/Nip19TLVTest.kt @@ -5,8 +5,8 @@ import io.kotest.matchers.shouldBe import io.kotest.matchers.types.instanceOf import net.primal.android.crypto.toHex import net.primal.android.crypto.toNpub -import net.primal.android.nostr.utils.Nip19TLV.toNeventString import net.primal.android.nostr.utils.Nip19TLV.toNaddrString +import net.primal.android.nostr.utils.Nip19TLV.toNeventString import org.junit.Test class Nip19TLVTest { From dcaccecec9ba0d559051dfcd52e360e72cd1d495 Mon Sep 17 00:00:00 2001 From: Marko Kocic Date: Thu, 19 Dec 2024 21:30:45 +0100 Subject: [PATCH 07/14] Fix detekt CyclomaticComplexMethod and LongMethod --- .../android/editor/NotePublishHandler.kt | 65 ++++++++++++------- 1 file changed, 42 insertions(+), 23 deletions(-) diff --git a/app/src/main/kotlin/net/primal/android/editor/NotePublishHandler.kt b/app/src/main/kotlin/net/primal/android/editor/NotePublishHandler.kt index 5bce3c77..4e89f124 100644 --- a/app/src/main/kotlin/net/primal/android/editor/NotePublishHandler.kt +++ b/app/src/main/kotlin/net/primal/android/editor/NotePublishHandler.kt @@ -20,6 +20,7 @@ import net.primal.android.nostr.ext.parseHashtagTags import net.primal.android.nostr.ext.parsePubkeyTags import net.primal.android.nostr.model.NostrEventKind import net.primal.android.nostr.publish.NostrPublisher +import net.primal.android.notes.db.PostData class NotePublishHandler @Inject constructor( private val dispatcherProvider: CoroutineDispatcherProvider, @@ -43,9 +44,7 @@ class NotePublishHandler @Inject constructor( ): Boolean { assertOnlyOneNotNull( "You can have only one root event.", - rootArticleId, - rootPostId, - rootHighlightId, + rootArticleId, rootPostId, rootHighlightId, ) val replyPostData = replyToPostId?.let { @@ -96,13 +95,8 @@ class NotePublishHandler @Inject constructor( } /* Pubkey tags */ - val existingPubkeyTags = replyPostData?.tags?.filter { it.isPubKeyTag() }?.toSet() ?: setOf() - val replyAuthorPubkeyTag = replyToAuthorId?.asPubkeyTag() - val replyHighlightAuthorPubkeyTag = rootHighlightAuthorId?.asPubkeyTag() - val rootArticleAuthorPubkeyTag = rootArticleAuthorId?.asPubkeyTag() - val mentionPubkeyTags = content.parsePubkeyTags(marker = "mention").toSet() - val pubkeyTags = existingPubkeyTags + mentionPubkeyTags + - setOfNotNull(replyAuthorPubkeyTag, rootArticleAuthorPubkeyTag, replyHighlightAuthorPubkeyTag) + val pubkeyTags = + constructPubkeyTags(replyPostData, replyToAuthorId, rootHighlightAuthorId, rootArticleAuthorId, content) /* Hashtag tags */ val hashtagTags = content.parseHashtagTags().toSet() @@ -112,19 +106,7 @@ class NotePublishHandler @Inject constructor( val iMetaTags = attachments.filter { it.isImageAttachment }.map { it.asIMetaTag() } /* Content */ - val refinedContent = if (attachmentUrls.isEmpty()) { - content - } else { - StringBuilder().apply { - append(content) - appendLine() - appendLine() - attachmentUrls.forEach { - append(it) - appendLine() - } - }.toString() - } + val refinedContent = buildRefinedContent(attachmentUrls, content) val outboxRelays = replyToPostId?.let { noteId -> relayHintsMap[noteId]?.let { relayUrl -> listOf(relayUrl) } @@ -139,4 +121,41 @@ class NotePublishHandler @Inject constructor( ) } } + + private fun constructPubkeyTags( + replyPostData: PostData?, + replyToAuthorId: String?, + rootHighlightAuthorId: String?, + rootArticleAuthorId: String?, + content: String, + ): Set { + val existingPubkeyTags = replyPostData?.tags?.filter { it.isPubKeyTag() }?.toSet() ?: emptySet() + val replyAuthorPubkeyTag = replyToAuthorId?.asPubkeyTag() + val replyHighlightAuthorPubkeyTag = rootHighlightAuthorId?.asPubkeyTag() + val rootArticleAuthorPubkeyTag = rootArticleAuthorId?.asPubkeyTag() + val mentionPubkeyTags = content.parsePubkeyTags(marker = "mention").toSet() + + return existingPubkeyTags + mentionPubkeyTags + + setOfNotNull(replyAuthorPubkeyTag, rootArticleAuthorPubkeyTag, replyHighlightAuthorPubkeyTag) + } + + private fun buildRefinedContent( + attachmentUrls: List, + content: String, + ): String { + val refinedContent = if (attachmentUrls.isEmpty()) { + content + } else { + StringBuilder().apply { + append(content) + appendLine() + appendLine() + attachmentUrls.forEach { + append(it) + appendLine() + } + }.toString() + } + return refinedContent + } } From e0cd0b7649f9b822e6977c3bfb9098ca6b8f0a75 Mon Sep 17 00:00:00 2001 From: Marko Kocic Date: Thu, 19 Dec 2024 21:31:23 +0100 Subject: [PATCH 08/14] Update detekt baseline --- app/detekt-baseline.xml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/detekt-baseline.xml b/app/detekt-baseline.xml index 6df3048b..31d90f99 100644 --- a/app/detekt-baseline.xml +++ b/app/detekt-baseline.xml @@ -17,7 +17,6 @@ CyclomaticComplexMethod:NoteActionsRow.kt$@Composable fun FeedNoteActionsRow( modifier: Modifier, eventStats: EventStatsUi, isBookmarked: Boolean, highlightedNote: Boolean = false, showBookmark: Boolean = false, showCounts: Boolean = true, onPostAction: ((FeedPostAction) -> Unit)? = null, onPostLongPressAction: ((FeedPostAction) -> Unit)? = null, ) CyclomaticComplexMethod:NoteContent.kt$@OptIn(ExperimentalFoundationApi::class) @Composable fun NoteContent( modifier: Modifier = Modifier, data: NoteContentUi, expanded: Boolean, noteCallbacks: NoteCallbacks, maxLines: Int = Int.MAX_VALUE, overflow: TextOverflow = TextOverflow.Clip, enableTweetsMode: Boolean = false, textSelectable: Boolean = false, referencedEventsHaveBorder: Boolean = false, highlightColor: Color = AppTheme.colorScheme.secondary, contentColor: Color = AppTheme.colorScheme.onSurface, referencedEventsContainerColor: Color = AppTheme.extraColorScheme.surfaceVariantAlt1, onClick: ((offset: Offset) -> Unit)? = null, onUrlClick: ((url: String) -> Unit)? = null, ) CyclomaticComplexMethod:NoteFeedLazyColumn.kt$@ExperimentalMaterial3Api @ExperimentalFoundationApi @Composable fun NoteFeedLazyColumn( modifier: Modifier = Modifier, pagingItems: LazyPagingItems<FeedPostUi>, listState: LazyListState, showPaywall: Boolean, noteCallbacks: NoteCallbacks, onGoToWallet: () -> Unit, showTopZaps: Boolean = false, shouldShowLoadingState: Boolean = true, shouldShowNoContentState: Boolean = true, showReplyTo: Boolean = true, noContentVerticalArrangement: Arrangement.Vertical = Arrangement.Center, noContentPaddingValues: PaddingValues = PaddingValues(all = 0.dp), noContentText: String = stringResource(id = R.string.feed_no_content), contentPadding: PaddingValues = PaddingValues(all = 0.dp), header: @Composable (LazyItemScope.() -> Unit)? = null, stickyHeader: @Composable (LazyItemScope.() -> Unit)? = null, onUiError: ((UiError) -> Unit)? = null, ) - CyclomaticComplexMethod:NotePublishHandler.kt$NotePublishHandler$@Throws(NostrPublishException::class) suspend fun publishShortTextNote( userId: String, content: String, attachments: List<NoteAttachment> = emptyList(), rootArticleEventId: String? = null, rootArticleId: String? = null, rootArticleAuthorId: String? = null, rootPostId: String? = null, replyToPostId: String? = null, replyToAuthorId: String? = null, ): Boolean CyclomaticComplexMethod:NotificationEvents.kt$private fun ContentPrimalNotification.parseActionPostId(type: NotificationType): String? CyclomaticComplexMethod:NotificationEvents.kt$private fun ContentPrimalNotification.parseActionUserId(type: NotificationType): String? CyclomaticComplexMethod:NotificationListItem.kt$@Composable private fun NotificationType.toSuffixText(usersZappedCount: Int = 0, totalSatsZapped: String? = null): String @@ -95,7 +94,7 @@ LongParameterList:NostrResources.kt$( eventId: String, eventIdToNostrEvent: Map<String, NostrEvent>, postIdToPostDataMap: Map<String, PostData>, articleIdToArticle: Map<String, ArticleData>, profileIdToProfileDataMap: Map<String, ProfileData>, cdnResources: Map<String, CdnResource>, linkPreviews: Map<String, LinkPreviewData>, videoThumbnails: Map<String, String>, ) LongParameterList:NostrResources.kt$( eventIdToNostrEvent: Map<String, NostrEvent>, postIdToPostDataMap: Map<String, PostData>, articleIdToArticle: Map<String, ArticleData>, profileIdToProfileDataMap: Map<String, ProfileData>, cdnResources: Map<String, CdnResource>, linkPreviews: Map<String, LinkPreviewData>, videoThumbnails: Map<String, String>, ) LongParameterList:NostrResources.kt$( refNote: PostData?, refPostAuthor: ProfileData?, cdnResources: Map<String, CdnResource>, linkPreviews: Map<String, LinkPreviewData>, videoThumbnails: Map<String, String>, eventIdToNostrEvent: Map<String, NostrEvent>, postIdToPostDataMap: Map<String, PostData>, articleIdToArticle: Map<String, ArticleData>, profileIdToProfileDataMap: Map<String, ProfileData>, ) - LongParameterList:NoteEditorViewModel.kt$NoteEditorViewModel$( @Assisted private val args: NoteEditorArgs, private val dispatcherProvider: CoroutineDispatcherProvider, private val fileAnalyser: FileAnalyser, private val activeAccountStore: ActiveAccountStore, private val feedRepository: FeedRepository, private val notePublishHandler: NotePublishHandler, private val attachmentRepository: AttachmentsRepository, private val exploreRepository: ExploreRepository, private val profileRepository: ProfileRepository, private val articleRepository: ArticleRepository, ) + LongParameterList:NoteEditorViewModel.kt$NoteEditorViewModel$( @Assisted private val args: NoteEditorArgs, private val dispatcherProvider: CoroutineDispatcherProvider, private val fileAnalyser: FileAnalyser, private val activeAccountStore: ActiveAccountStore, private val feedRepository: FeedRepository, private val notePublishHandler: NotePublishHandler, private val attachmentRepository: AttachmentsRepository, private val highlightRepository: HighlightRepository, private val exploreRepository: ExploreRepository, private val profileRepository: ProfileRepository, private val articleRepository: ArticleRepository, ) LongParameterList:ProfileDetailsViewModel.kt$ProfileDetailsViewModel$( savedStateHandle: SavedStateHandle, private val dispatcherProvider: CoroutineDispatcherProvider, private val activeAccountStore: ActiveAccountStore, private val feedsRepository: FeedsRepository, private val profileRepository: ProfileRepository, private val mutedUserRepository: MutedUserRepository, private val zapHandler: ZapHandler, ) LongParameterList:SubscriptionsManager.kt$SubscriptionsManager$( dispatcherProvider: CoroutineDispatcherProvider, private val activeAccountStore: ActiveAccountStore, private val userRepository: UserRepository, private val nostrNotary: NostrNotary, private val appConfigProvider: AppConfigProvider, @PrimalCacheApiClient private val cacheApiClient: PrimalApiClient, @PrimalWalletApiClient private val walletApiClient: PrimalApiClient, ) LongParameterList:UserDataUpdater.kt$UserDataUpdater$( @Assisted val userId: String, private val settingsRepository: SettingsRepository, private val userRepository: UserRepository, private val walletRepository: WalletRepository, private val relayRepository: RelayRepository, private val bookmarksRepository: BookmarksRepository, private val premiumRepository: PremiumRepository, ) From 61e3ed53de35ad286b997dee50b8b64960589789 Mon Sep 17 00:00:00 2001 From: Marko Kocic Date: Thu, 19 Dec 2024 21:31:53 +0100 Subject: [PATCH 09/14] Auto-format code --- .../net/primal/android/editor/NotePublishHandler.kt | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/app/src/main/kotlin/net/primal/android/editor/NotePublishHandler.kt b/app/src/main/kotlin/net/primal/android/editor/NotePublishHandler.kt index 4e89f124..947462d9 100644 --- a/app/src/main/kotlin/net/primal/android/editor/NotePublishHandler.kt +++ b/app/src/main/kotlin/net/primal/android/editor/NotePublishHandler.kt @@ -44,7 +44,9 @@ class NotePublishHandler @Inject constructor( ): Boolean { assertOnlyOneNotNull( "You can have only one root event.", - rootArticleId, rootPostId, rootHighlightId, + rootArticleId, + rootPostId, + rootHighlightId, ) val replyPostData = replyToPostId?.let { @@ -139,10 +141,7 @@ class NotePublishHandler @Inject constructor( setOfNotNull(replyAuthorPubkeyTag, rootArticleAuthorPubkeyTag, replyHighlightAuthorPubkeyTag) } - private fun buildRefinedContent( - attachmentUrls: List, - content: String, - ): String { + private fun buildRefinedContent(attachmentUrls: List, content: String): String { val refinedContent = if (attachmentUrls.isEmpty()) { content } else { From 0bcdff8f5d3d2e0bc3c9fdf631a1b5b2b28acbe5 Mon Sep 17 00:00:00 2001 From: Marko Kocic Date: Fri, 20 Dec 2024 13:27:53 +0100 Subject: [PATCH 10/14] Remove logging --- .../kotlin/net/primal/android/editor/ui/NoteEditorScreen.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/src/main/kotlin/net/primal/android/editor/ui/NoteEditorScreen.kt b/app/src/main/kotlin/net/primal/android/editor/ui/NoteEditorScreen.kt index 1298724b..6997004a 100644 --- a/app/src/main/kotlin/net/primal/android/editor/ui/NoteEditorScreen.kt +++ b/app/src/main/kotlin/net/primal/android/editor/ui/NoteEditorScreen.kt @@ -79,7 +79,6 @@ import net.primal.android.editor.NoteEditorContract.UiEvent import net.primal.android.editor.NoteEditorContract.UiState.NoteEditorError import net.primal.android.editor.NoteEditorViewModel import net.primal.android.editor.domain.NoteAttachment -import net.primal.android.notes.db.ReferencedHighlight import net.primal.android.notes.db.toReferencedHighlight import net.primal.android.notes.feed.model.FeedPostUi import net.primal.android.notes.feed.model.toNoteContentUi @@ -88,7 +87,6 @@ import net.primal.android.notes.feed.note.ui.NoteContent import net.primal.android.notes.feed.note.ui.ReferencedHighlight import net.primal.android.notes.feed.note.ui.events.NoteCallbacks import net.primal.android.theme.AppTheme -import timber.log.Timber @Composable fun NoteEditorScreen(viewModel: NoteEditorViewModel, onClose: () -> Unit) { @@ -357,7 +355,6 @@ private fun NoteEditor( legendaryCustomization = state.activeAccountLegendaryCustomization, ) - Timber.tag("content").i(state.content.text) NoteOutlinedTextField( modifier = Modifier .offset(x = (-8).dp) From 4d7c7668efd0322ae0846519775c456ce9ef33c0 Mon Sep 17 00:00:00 2001 From: Marko Kocic Date: Fri, 20 Dec 2024 13:29:16 +0100 Subject: [PATCH 11/14] Reformat lines --- .../net/primal/android/editor/NotePublishHandler.kt | 13 ++++++++----- .../primal/android/editor/domain/NoteEditorArgs.kt | 5 +++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/app/src/main/kotlin/net/primal/android/editor/NotePublishHandler.kt b/app/src/main/kotlin/net/primal/android/editor/NotePublishHandler.kt index 947462d9..33a971d3 100644 --- a/app/src/main/kotlin/net/primal/android/editor/NotePublishHandler.kt +++ b/app/src/main/kotlin/net/primal/android/editor/NotePublishHandler.kt @@ -56,9 +56,7 @@ class NotePublishHandler @Inject constructor( } val rootHighlightTags = if (rootHighlightId != null && rootHighlightAuthorId != null) { - listOf( - rootHighlightId.asEventIdTag(marker = "root"), - ) + listOf(rootHighlightId.asEventIdTag(marker = "root")) } else { null } @@ -97,8 +95,13 @@ class NotePublishHandler @Inject constructor( } /* Pubkey tags */ - val pubkeyTags = - constructPubkeyTags(replyPostData, replyToAuthorId, rootHighlightAuthorId, rootArticleAuthorId, content) + val pubkeyTags = constructPubkeyTags( + replyPostData = replyPostData, + replyToAuthorId = replyToAuthorId, + rootHighlightAuthorId = rootHighlightAuthorId, + rootArticleAuthorId = rootArticleAuthorId, + content = content, + ) /* Hashtag tags */ val hashtagTags = content.parseHashtagTags().toSet() diff --git a/app/src/main/kotlin/net/primal/android/editor/domain/NoteEditorArgs.kt b/app/src/main/kotlin/net/primal/android/editor/domain/NoteEditorArgs.kt index 73989e7f..71446707 100644 --- a/app/src/main/kotlin/net/primal/android/editor/domain/NoteEditorArgs.kt +++ b/app/src/main/kotlin/net/primal/android/editor/domain/NoteEditorArgs.kt @@ -21,8 +21,9 @@ data class NoteEditorArgs( companion object { fun List.toNostrUriInNoteEditorArgs(): NoteEditorArgs { - val preFillContent = - TextFieldValue(text = this.joinToString(separator = "\n\n", prefix = "\n\n") { "nostr:$it" }) + val preFillContent = TextFieldValue( + text = this.joinToString(separator = "\n\n", prefix = "\n\n") { "nostr:$it" }, + ) return preFillContent.asNoteEditorArgs() } From 681567af562758b5be821e1715959b4239e8fc96 Mon Sep 17 00:00:00 2001 From: Marko Kocic Date: Fri, 20 Dec 2024 13:29:39 +0100 Subject: [PATCH 12/14] Change argument order --- .../main/kotlin/net/primal/android/core/utils/NullUtils.kt | 4 ++-- .../kotlin/net/primal/android/editor/NotePublishHandler.kt | 7 +------ 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/app/src/main/kotlin/net/primal/android/core/utils/NullUtils.kt b/app/src/main/kotlin/net/primal/android/core/utils/NullUtils.kt index 57c244df..52cdcd0c 100644 --- a/app/src/main/kotlin/net/primal/android/core/utils/NullUtils.kt +++ b/app/src/main/kotlin/net/primal/android/core/utils/NullUtils.kt @@ -21,10 +21,10 @@ inline fun ifNotNull( } } -fun assertOnlyOneNotNull(message: Any, vararg args: Any?) { +fun assertOnlyOneNotNull(vararg args: Any?, message: () -> Any) { args.sumOf { (it != null).toInt() }.apply { if (this != 1) { - error(message = message) + error(message = message()) } } } diff --git a/app/src/main/kotlin/net/primal/android/editor/NotePublishHandler.kt b/app/src/main/kotlin/net/primal/android/editor/NotePublishHandler.kt index 33a971d3..fa8ba1b5 100644 --- a/app/src/main/kotlin/net/primal/android/editor/NotePublishHandler.kt +++ b/app/src/main/kotlin/net/primal/android/editor/NotePublishHandler.kt @@ -42,12 +42,7 @@ class NotePublishHandler @Inject constructor( replyToPostId: String? = null, replyToAuthorId: String? = null, ): Boolean { - assertOnlyOneNotNull( - "You can have only one root event.", - rootArticleId, - rootPostId, - rootHighlightId, - ) + assertOnlyOneNotNull(rootArticleId, rootPostId, rootHighlightId) { "You can have only one root event." } val replyPostData = replyToPostId?.let { withContext(dispatcherProvider.io()) { From ece98a900d21c35b1f2958f8651ef4dc53bb94d6 Mon Sep 17 00:00:00 2001 From: Marko Kocic Date: Fri, 20 Dec 2024 14:00:15 +0100 Subject: [PATCH 13/14] Use Sequence to generate set of pubkey tags --- .../net/primal/android/editor/NotePublishHandler.kt | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/app/src/main/kotlin/net/primal/android/editor/NotePublishHandler.kt b/app/src/main/kotlin/net/primal/android/editor/NotePublishHandler.kt index fa8ba1b5..15ac3ab4 100644 --- a/app/src/main/kotlin/net/primal/android/editor/NotePublishHandler.kt +++ b/app/src/main/kotlin/net/primal/android/editor/NotePublishHandler.kt @@ -135,8 +135,15 @@ class NotePublishHandler @Inject constructor( val rootArticleAuthorPubkeyTag = rootArticleAuthorId?.asPubkeyTag() val mentionPubkeyTags = content.parsePubkeyTags(marker = "mention").toSet() - return existingPubkeyTags + mentionPubkeyTags + - setOfNotNull(replyAuthorPubkeyTag, rootArticleAuthorPubkeyTag, replyHighlightAuthorPubkeyTag) + return sequenceOf( + existingPubkeyTags, + mentionPubkeyTags, + listOf( + replyAuthorPubkeyTag, + rootArticleAuthorPubkeyTag, + replyHighlightAuthorPubkeyTag, + ), + ).flatten().filterNotNull().toSet() } private fun buildRefinedContent(attachmentUrls: List, content: String): String { From 284af6d8562c7d3fa72c327a05cfdc9695b48966 Mon Sep 17 00:00:00 2001 From: Marko Kocic Date: Fri, 20 Dec 2024 14:37:56 +0100 Subject: [PATCH 14/14] Render highlight and article when commenting on highlight --- .../android/editor/NotePublishHandler.kt | 28 ++++++++----------- .../android/navigation/PrimalAppNavigation.kt | 9 ++++-- .../feed/note/ui/events/NoteCallbacks.kt | 2 +- .../ui/HighlightActivityBottomSheet.kt | 9 +++++- 4 files changed, 27 insertions(+), 21 deletions(-) diff --git a/app/src/main/kotlin/net/primal/android/editor/NotePublishHandler.kt b/app/src/main/kotlin/net/primal/android/editor/NotePublishHandler.kt index 15ac3ab4..3e9cd890 100644 --- a/app/src/main/kotlin/net/primal/android/editor/NotePublishHandler.kt +++ b/app/src/main/kotlin/net/primal/android/editor/NotePublishHandler.kt @@ -42,7 +42,7 @@ class NotePublishHandler @Inject constructor( replyToPostId: String? = null, replyToAuthorId: String? = null, ): Boolean { - assertOnlyOneNotNull(rootArticleId, rootPostId, rootHighlightId) { "You can have only one root event." } + assertOnlyOneNotNull(rootArticleId, rootPostId) { "You can have only one root event." } val replyPostData = replyToPostId?.let { withContext(dispatcherProvider.io()) { @@ -50,32 +50,27 @@ class NotePublishHandler @Inject constructor( } } - val rootHighlightTags = if (rootHighlightId != null && rootHighlightAuthorId != null) { - listOf(rootHighlightId.asEventIdTag(marker = "root")) + /* Note tags */ + val mentionEventTags = content.parseEventTags(marker = "mention") + val rootPostTag = rootPostId?.asEventIdTag(marker = "root") + val replyEventTag = if (rootPostId != replyToPostId) { + replyToPostId?.asEventIdTag(marker = "reply") } else { null } - /* Article tag */ - val rootArticleTags = if (rootArticleId != null && rootArticleAuthorId != null && rootArticleEventId != null) { + val rootEventTags = if (rootHighlightId != null && rootHighlightAuthorId != null) { + listOf(rootHighlightId.asEventIdTag(marker = "root")) + } else if (rootArticleId != null && rootArticleAuthorId != null && rootArticleEventId != null) { val tagContent = "${NostrEventKind.LongFormContent.value}:$rootArticleAuthorId:$rootArticleId" listOf( rootArticleEventId.asEventIdTag(marker = "root"), tagContent.asReplaceableEventTag(marker = "root"), ) } else { - null + listOf(rootPostTag) } - /* Note tags */ - val mentionEventTags = content.parseEventTags(marker = "mention") - val rootPostTag = rootPostId?.asEventIdTag(marker = "root") - val replyEventTag = if (rootPostId != replyToPostId) { - replyToPostId?.asEventIdTag(marker = "reply") - } else { - null - } - val rootEventTags = rootHighlightTags ?: rootArticleTags ?: listOf(rootPostTag) val eventTags = setOfNotNull(*rootEventTags.toTypedArray(), replyEventTag) + mentionEventTags val relayHintsMap = withContext(dispatcherProvider.io()) { @@ -140,8 +135,7 @@ class NotePublishHandler @Inject constructor( mentionPubkeyTags, listOf( replyAuthorPubkeyTag, - rootArticleAuthorPubkeyTag, - replyHighlightAuthorPubkeyTag, + replyHighlightAuthorPubkeyTag ?: rootArticleAuthorPubkeyTag, ), ).flatten().filterNotNull().toSet() } diff --git a/app/src/main/kotlin/net/primal/android/navigation/PrimalAppNavigation.kt b/app/src/main/kotlin/net/primal/android/navigation/PrimalAppNavigation.kt index 62721a64..d070a07f 100644 --- a/app/src/main/kotlin/net/primal/android/navigation/PrimalAppNavigation.kt +++ b/app/src/main/kotlin/net/primal/android/navigation/PrimalAppNavigation.kt @@ -313,8 +313,13 @@ fun noteCallbacksHandler(navController: NavController) = noteId.hexToNoteHrp().toNostrUriInNoteEditorArgs(), ) }, - onHighlightReplyClick = { id -> - navController.navigateToNoteEditor(args = NoteEditorArgs(replyToHighlightId = id)) + onHighlightReplyClick = { highlightId, articleNaddr -> + navController.navigateToNoteEditor( + args = NoteEditorArgs( + replyToHighlightId = highlightId, + replyToArticleNaddr = articleNaddr, + ), + ) }, onHighlightQuoteClick = { nevent, naddr -> navController.navigateToNoteEditor(listOf(nevent, naddr).toNostrUriInNoteEditorArgs()) diff --git a/app/src/main/kotlin/net/primal/android/notes/feed/note/ui/events/NoteCallbacks.kt b/app/src/main/kotlin/net/primal/android/notes/feed/note/ui/events/NoteCallbacks.kt index 482e3a6d..9ac137fc 100644 --- a/app/src/main/kotlin/net/primal/android/notes/feed/note/ui/events/NoteCallbacks.kt +++ b/app/src/main/kotlin/net/primal/android/notes/feed/note/ui/events/NoteCallbacks.kt @@ -4,7 +4,7 @@ data class NoteCallbacks( val onNoteClick: ((noteId: String) -> Unit)? = null, val onNoteReplyClick: ((noteId: String) -> Unit)? = null, val onNoteQuoteClick: ((noteId: String) -> Unit)? = null, - val onHighlightReplyClick: ((highlightId: String) -> Unit)? = null, + val onHighlightReplyClick: ((highlightId: String, naddr: String) -> Unit)? = null, val onHighlightQuoteClick: ((nevent: String, naddr: String) -> Unit)? = null, val onArticleClick: ((naddr: String) -> Unit)? = null, val onArticleReplyClick: ((naddr: String) -> Unit)? = null, diff --git a/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/HighlightActivityBottomSheet.kt b/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/HighlightActivityBottomSheet.kt index ed9aae38..93a23284 100644 --- a/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/HighlightActivityBottomSheet.kt +++ b/app/src/main/kotlin/net/primal/android/thread/articles/details/ui/HighlightActivityBottomSheet.kt @@ -146,7 +146,14 @@ fun HighlightActivityBottomSheet( ) } }, - onCommentClick = { noteCallbacks.onHighlightReplyClick?.invoke(selectedHighlight.highlightId) }, + onCommentClick = { + articleNaddr?.let { + noteCallbacks.onHighlightReplyClick?.invoke( + selectedHighlight.highlightId, + it.toNaddrString(), + ) + } + }, onSaveHighlightClick = onSaveHighlightClick, onDeleteHighlightClick = onDeleteHighlightClick, isHighlighted = isHighlighted,