From 49c2d4f4869ecc32f4b6b6c09bfff19e60043248 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 23 Feb 2024 22:09:06 -0500 Subject: [PATCH] Adding Basic Git Rendering support. --- .../amethyst/model/LocalCache.kt | 96 ++++++++ .../amethyst/ui/note/NoteCompose.kt | 221 ++++++++++++++++++ .../amethyst/ui/screen/ThreadFeedView.kt | 8 + app/src/main/res/values/strings.xml | 3 + .../quartz/events/EventFactory.kt | 4 + .../quartz/events/GitIssueEvent.kt | 79 +++++++ .../quartz/events/GitPatchEvent.kt | 95 ++++++++ .../quartz/events/GitReplyEvent.kt | 79 +++++++ .../quartz/events/GitRepositoryEvent.kt | 59 +++++ 9 files changed, 644 insertions(+) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/events/GitIssueEvent.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/events/GitPatchEvent.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/events/GitReplyEvent.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/events/GitRepositoryEvent.kt 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 3214a11b2..e23032590 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -71,6 +71,10 @@ import com.vitorpamplona.quartz.events.FileStorageEvent import com.vitorpamplona.quartz.events.FileStorageHeaderEvent import com.vitorpamplona.quartz.events.GenericRepostEvent import com.vitorpamplona.quartz.events.GiftWrapEvent +import com.vitorpamplona.quartz.events.GitIssueEvent +import com.vitorpamplona.quartz.events.GitPatchEvent +import com.vitorpamplona.quartz.events.GitReplyEvent +import com.vitorpamplona.quartz.events.GitRepositoryEvent import com.vitorpamplona.quartz.events.HighlightEvent import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.events.LiveActivitiesEvent @@ -361,6 +365,87 @@ object LocalCache { refreshObservers(note) } + fun consume( + event: GitPatchEvent, + relay: Relay? = null, + ) { + val note = getOrCreateNote(event.id) + val author = getOrCreateUser(event.pubKey) + + if (relay != null) { + author.addRelayBeingUsed(relay, event.createdAt) + note.addRelay(relay) + } + + // Already processed this event. + if (note.event != null) return + + if (antiSpam.isSpam(event, relay)) { + relay?.let { it.spamCounter++ } + return + } + + note.loadEvent(event, author, emptyList()) + + refreshObservers(note) + } + + fun consume( + event: GitIssueEvent, + relay: Relay? = null, + ) { + val note = getOrCreateNote(event.id) + val author = getOrCreateUser(event.pubKey) + + if (relay != null) { + author.addRelayBeingUsed(relay, event.createdAt) + note.addRelay(relay) + } + + // Already processed this event. + if (note.event != null) return + + if (antiSpam.isSpam(event, relay)) { + relay?.let { it.spamCounter++ } + return + } + + note.loadEvent(event, author, emptyList()) + + refreshObservers(note) + } + + fun consume( + event: GitReplyEvent, + relay: Relay? = null, + ) { + val note = getOrCreateNote(event.id) + val author = getOrCreateUser(event.pubKey) + + if (relay != null) { + author.addRelayBeingUsed(relay, event.createdAt) + note.addRelay(relay) + } + + // Already processed this event. + if (note.event != null) return + + if (antiSpam.isSpam(event, relay)) { + relay?.let { it.spamCounter++ } + return + } + + val replyTo = + event + .tagsWithoutCitations() + .filter { it != event.repository()?.toTag() } + .mapNotNull { checkGetOrCreateNote(it) } + + note.loadEvent(event, author, replyTo) + + refreshObservers(note) + } + fun consume( event: LongTextNoteEvent, relay: Relay?, @@ -508,6 +593,13 @@ object LocalCache { consumeBaseReplaceable(event, relay) } + fun consume( + event: GitRepositoryEvent, + relay: Relay?, + ) { + consumeBaseReplaceable(event, relay) + } + fun consume( event: ChannelListEvent, relay: Relay?, @@ -1847,6 +1939,10 @@ object LocalCache { is FileStorageEvent -> consume(event, relay) is FileStorageHeaderEvent -> consume(event, relay) is GiftWrapEvent -> consume(event, relay) + is GitIssueEvent -> consume(event, relay) + is GitReplyEvent -> consume(event, relay) + is GitPatchEvent -> consume(event, relay) + is GitRepositoryEvent -> consume(event, relay) is HighlightEvent -> consume(event, relay) is LiveActivitiesEvent -> consume(event, relay) is LiveActivitiesChatMessageEvent -> consume(event, relay) 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 e25618332..a895af644 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 @@ -48,6 +48,7 @@ import androidx.compose.foundation.shape.CutCornerShape import androidx.compose.foundation.text.ClickableText import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Divider +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.IconButton import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme @@ -204,6 +205,8 @@ import com.vitorpamplona.quartz.events.EmptyTagList import com.vitorpamplona.quartz.events.FileHeaderEvent import com.vitorpamplona.quartz.events.FileStorageHeaderEvent import com.vitorpamplona.quartz.events.GenericRepostEvent +import com.vitorpamplona.quartz.events.GitPatchEvent +import com.vitorpamplona.quartz.events.GitRepositoryEvent import com.vitorpamplona.quartz.events.HighlightEvent import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.events.LiveActivitiesEvent @@ -1182,6 +1185,19 @@ private fun RenderNoteRow( is LiveActivitiesEvent -> { RenderLiveActivityEvent(baseNote, accountViewModel, nav) } + is GitRepositoryEvent -> { + RenderGitRepositoryEvent(baseNote, accountViewModel, nav) + } + is GitPatchEvent -> { + RenderGitPatchEvent( + baseNote, + makeItShort, + canPreview, + backgroundColor, + accountViewModel, + nav, + ) + } is PrivateDmEvent -> { RenderPrivateMessage( baseNote, @@ -3464,6 +3480,211 @@ fun AudioHeader( } } +@Composable +fun RenderGitPatchEvent( + baseNote: Note, + makeItShort: Boolean, + canPreview: Boolean, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val event = baseNote.event as? GitPatchEvent ?: return + + RenderGitPatchEvent(event, baseNote, makeItShort, canPreview, backgroundColor, accountViewModel, nav) +} + +@Composable +private fun RenderShortRepositoryHeader( + baseNote: AddressableNote, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val noteState = baseNote.live().metadata.observeAsState() + val note = remember(noteState) { noteState.value?.note } ?: return + val noteEvent = note.event as? GitRepositoryEvent ?: return + + Column( + modifier = MaterialTheme.colorScheme.replyModifier.padding(10.dp), + ) { + val title = remember(noteEvent) { noteEvent.name() ?: noteEvent.dTag() } + Text( + text = stringResource(id = R.string.git_repository, title), + style = MaterialTheme.typography.titleLarge, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.fillMaxWidth(), + ) + + noteEvent.description()?.let { + Spacer(modifier = DoubleVertSpacer) + Text( + text = it, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } +} + +@Composable +private fun RenderGitPatchEvent( + noteEvent: GitPatchEvent, + note: Note, + makeItShort: Boolean, + canPreview: Boolean, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val repository = remember(noteEvent) { noteEvent.repository() } + + if (repository != null) { + LoadAddressableNote(aTag = repository, accountViewModel = accountViewModel) { + if (it != null) { + RenderShortRepositoryHeader(it, accountViewModel, nav) + Spacer(modifier = DoubleVertSpacer) + } + } + } + + LoadDecryptedContent(note, accountViewModel) { body -> + val eventContent by + remember(note.event) { + derivedStateOf { + val subject = (note.event as? TextNoteEvent)?.subject()?.ifEmpty { null } + + if (!subject.isNullOrBlank() && !body.split("\n")[0].contains(subject)) { + "### $subject\n$body" + } else { + body + } + } + } + + val isAuthorTheLoggedUser = remember(note.event) { accountViewModel.isLoggedUser(note.author) } + + if (makeItShort && isAuthorTheLoggedUser) { + Text( + text = eventContent, + color = MaterialTheme.colorScheme.placeholderText, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } else { + SensitivityWarning( + note = note, + accountViewModel = accountViewModel, + ) { + val modifier = remember(note) { Modifier.fillMaxWidth() } + val tags = remember(note) { note.event?.tags()?.toImmutableListOfLists() ?: EmptyTagList } + + TranslatableRichTextViewer( + content = eventContent, + canPreview = canPreview && !makeItShort, + modifier = modifier, + tags = tags, + backgroundColor = backgroundColor, + accountViewModel = accountViewModel, + nav = nav, + ) + } + + if (note.event?.hasHashtags() == true) { + val hashtags = + remember(note.event) { note.event?.hashtags()?.toImmutableList() ?: persistentListOf() } + DisplayUncitedHashtags(hashtags, eventContent, nav) + } + } + } +} + +@Composable +fun RenderGitRepositoryEvent( + baseNote: Note, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val event = baseNote.event as? GitRepositoryEvent ?: return + + RenderGitRepositoryEvent(event, baseNote, accountViewModel, nav) +} + +@Composable +private fun RenderGitRepositoryEvent( + noteEvent: GitRepositoryEvent, + note: Note, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val title = remember(noteEvent) { noteEvent.name() ?: noteEvent.dTag() } + val summary = remember(noteEvent) { noteEvent.description() } + val web = remember(noteEvent) { noteEvent.web() } + val clone = remember(noteEvent) { noteEvent.clone() } + + Row( + modifier = + Modifier + .clip(shape = QuoteBorder) + .border( + 1.dp, + MaterialTheme.colorScheme.subtleBorder, + QuoteBorder, + ).padding(Size10dp), + ) { + Column { + Text( + text = stringResource(id = R.string.git_repository, title), + style = MaterialTheme.typography.titleMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.fillMaxWidth(), + ) + + summary?.let { + Text( + text = it, + modifier = Modifier.fillMaxWidth().padding(vertical = Size5dp), + maxLines = 3, + overflow = TextOverflow.Ellipsis, + ) + } + + HorizontalDivider(thickness = DividerThickness) + + web?.let { + Row(Modifier.fillMaxWidth().padding(top = Size5dp)) { + Text( + text = stringResource(id = R.string.git_web_address), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Spacer(modifier = StdHorzSpacer) + ClickableUrl( + url = it, + urlText = it.removePrefix("https://").removePrefix("http://"), + ) + } + } + + clone?.let { + Row(Modifier.fillMaxWidth().padding(top = Size5dp)) { + Text( + text = stringResource(id = R.string.git_clone_address), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Spacer(modifier = StdHorzSpacer) + ClickableUrl( + url = it, + urlText = it.removePrefix("https://").removePrefix("http://"), + ) + } + } + } + } +} + @Composable fun RenderLiveActivityEvent( baseNote: Note, 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 a2e7ff250..abb9572d6 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 @@ -120,6 +120,8 @@ import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay import com.vitorpamplona.amethyst.ui.note.ReactionsRow import com.vitorpamplona.amethyst.ui.note.RenderAppDefinition import com.vitorpamplona.amethyst.ui.note.RenderEmojiPack +import com.vitorpamplona.amethyst.ui.note.RenderGitPatchEvent +import com.vitorpamplona.amethyst.ui.note.RenderGitRepositoryEvent import com.vitorpamplona.amethyst.ui.note.RenderPinListEvent import com.vitorpamplona.amethyst.ui.note.RenderPoll import com.vitorpamplona.amethyst.ui.note.RenderPostApproval @@ -157,6 +159,8 @@ import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.FileHeaderEvent import com.vitorpamplona.quartz.events.FileStorageHeaderEvent import com.vitorpamplona.quartz.events.GenericRepostEvent +import com.vitorpamplona.quartz.events.GitPatchEvent +import com.vitorpamplona.quartz.events.GitRepositoryEvent import com.vitorpamplona.quartz.events.HighlightEvent import com.vitorpamplona.quartz.events.LongTextNoteEvent import com.vitorpamplona.quartz.events.PeopleListEvent @@ -526,6 +530,10 @@ fun NoteMaster( accountViewModel, nav, ) + } else if (noteEvent is GitRepositoryEvent) { + RenderGitRepositoryEvent(baseNote, accountViewModel, nav) + } else if (noteEvent is GitPatchEvent) { + RenderGitPatchEvent(baseNote, false, true, backgroundColor, accountViewModel, nav) } else if (noteEvent is AppDefinitionEvent) { RenderAppDefinition(baseNote, accountViewModel, nav) } else if (noteEvent is HighlightEvent) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c48dd418b..d68c4f42e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -777,4 +777,7 @@ Max Limit Restricted Writes Forked from + Git Repository: %1$s + Web: + Clone: diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt index 0c7956752..05edf3688 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt @@ -89,6 +89,10 @@ class EventFactory { FileStorageHeaderEvent(id, pubKey, createdAt, tags, content, sig) GenericRepostEvent.KIND -> GenericRepostEvent(id, pubKey, createdAt, tags, content, sig) GiftWrapEvent.KIND -> GiftWrapEvent(id, pubKey, createdAt, tags, content, sig) + GitIssueEvent.KIND -> GitIssueEvent(id, pubKey, createdAt, tags, content, sig) + GitReplyEvent.KIND -> GitReplyEvent(id, pubKey, createdAt, tags, content, sig) + GitPatchEvent.KIND -> GitPatchEvent(id, pubKey, createdAt, tags, content, sig) + GitRepositoryEvent.KIND -> GitRepositoryEvent(id, pubKey, createdAt, tags, content, sig) GoalEvent.KIND -> GoalEvent(id, pubKey, createdAt, tags, content, sig) HighlightEvent.KIND -> HighlightEvent(id, pubKey, createdAt, tags, content, sig) HTTPAuthorizationEvent.KIND -> diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/GitIssueEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/GitIssueEvent.kt new file mode 100644 index 000000000..ed11d2015 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/GitIssueEvent.kt @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.events + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.encoders.ATag +import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner +import com.vitorpamplona.quartz.utils.TimeUtils + +@Immutable +class GitIssueEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : BaseTextNoteEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + private fun innerRepository() = + tags.firstOrNull { it.size > 3 && it[0] == "a" && it[3] == "root" } + ?: tags.firstOrNull { it.size > 1 && it[0] == "a" } + + private fun repositoryHex() = innerRepository()?.getOrNull(1) + + fun rootIssueOrPath() = tags.lastOrNull { it.size > 3 && it[0] == "e" && it[3] == "root" }?.get(1) + + fun repository() = + innerRepository()?.let { + if (it.size > 1) { + val aTagValue = it[1] + val relay = it.getOrNull(2) + + ATag.parse(aTagValue, relay) + } else { + null + } + } + + companion object { + const val KIND = 1621 + const val ALT = "A Git Issue" + + fun create( + patch: String, + createdAt: Long = TimeUtils.now(), + signer: NostrSigner, + onReady: (GitIssueEvent) -> Unit, + ) { + val content = patch + val tags = + mutableListOf( + arrayOf(), + ) + + tags.add(arrayOf("alt", ALT)) + + signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady) + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/GitPatchEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/GitPatchEvent.kt new file mode 100644 index 000000000..d9a6670e1 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/GitPatchEvent.kt @@ -0,0 +1,95 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.events + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.encoders.ATag +import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner +import com.vitorpamplona.quartz.utils.TimeUtils + +@Immutable +class GitPatchEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { + private fun innerRepository() = + tags.firstOrNull { it.size > 3 && it[0] == "a" && it[3] == "root" } + ?: tags.firstOrNull { it.size > 1 && it[0] == "a" } + + private fun repositoryHex() = innerRepository()?.getOrNull(1) + + fun repository() = + innerRepository()?.let { + if (it.size > 1) { + val aTagValue = it[1] + val relay = it.getOrNull(2) + + ATag.parse(aTagValue, relay) + } else { + null + } + } + + fun commit() = tags.firstOrNull { it.size > 1 && it[0] == "commit" }?.get(1) + + fun parentCommit() = tags.firstOrNull { it.size > 1 && it[0] == "parent-commit" }?.get(1) + + fun commitPGPSig() = tags.firstOrNull { it.size > 1 && it[0] == "commit-pgp-sig" }?.get(1) + + fun committer() = + tags.filter { it.size > 1 && it[0] == "committer" }?.mapNotNull { + Committer(it.getOrNull(1), it.getOrNull(2), it.getOrNull(3), it.getOrNull(4)) + } + + data class Committer( + val name: String?, + val email: String?, + val timestamp: String?, + val timezoneInMinutes: String?, + ) + + companion object { + const val KIND = 1617 + const val ALT = "A Git Patch" + + fun create( + patch: String, + createdAt: Long = TimeUtils.now(), + signer: NostrSigner, + onReady: (GitPatchEvent) -> Unit, + ) { + val content = patch + val tags = + mutableListOf( + arrayOf(), + ) + + tags.add(arrayOf("alt", ALT)) + + signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady) + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/GitReplyEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/GitReplyEvent.kt new file mode 100644 index 000000000..a6b1a5658 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/GitReplyEvent.kt @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.events + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.encoders.ATag +import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner +import com.vitorpamplona.quartz.utils.TimeUtils + +@Immutable +class GitReplyEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : BaseTextNoteEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + private fun innerRepository() = + tags.firstOrNull { it.size > 3 && it[0] == "a" && it[3] == "root" } + ?: tags.firstOrNull { it.size > 1 && it[0] == "a" } + + private fun repositoryHex() = innerRepository()?.getOrNull(1) + + fun repository() = + innerRepository()?.let { + if (it.size > 1) { + val aTagValue = it[1] + val relay = it.getOrNull(2) + + ATag.parse(aTagValue, relay) + } else { + null + } + } + + fun rootIssueOrPath() = tags.lastOrNull { it.size > 3 && it[0] == "e" && it[3] == "root" }?.get(1) + + companion object { + const val KIND = 1622 + const val ALT = "A Git Issue" + + fun create( + patch: String, + createdAt: Long = TimeUtils.now(), + signer: NostrSigner, + onReady: (GitReplyEvent) -> Unit, + ) { + val content = patch + val tags = + mutableListOf( + arrayOf(), + ) + + tags.add(arrayOf("alt", ALT)) + + signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady) + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/GitRepositoryEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/GitRepositoryEvent.kt new file mode 100644 index 000000000..f610dc7e2 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/GitRepositoryEvent.kt @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.events + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner +import com.vitorpamplona.quartz.utils.TimeUtils + +@Immutable +class GitRepositoryEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + fun name() = tags.firstOrNull { it.size > 1 && it[0] == "name" }?.get(1) + + fun description() = tags.firstOrNull { it.size > 1 && it[0] == "description" }?.get(1) + + fun web() = tags.firstOrNull { it.size > 1 && it[0] == "web" }?.get(1) + + fun clone() = tags.firstOrNull { it.size > 1 && it[0] == "clone" }?.get(1) + + companion object { + const val KIND = 30617 + const val ALT = "Git Repository" + + fun create( + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (GitRepositoryEvent) -> Unit, + ) { + val tags = mutableListOf>() + tags.add(arrayOf("alt", ALT)) + signer.sign(createdAt, KIND, tags.toTypedArray(), "", onReady) + } + } +}