Moves to a fork model via interfaces to start creating specific screens for each event type

This commit is contained in:
Vitor Pamplona
2026-01-10 11:36:11 -05:00
parent 1c26ccecc0
commit 1ead2983ac
11 changed files with 169 additions and 59 deletions
@@ -805,7 +805,6 @@ object LocalCache : ILocalCache {
fun computeReplyTo(event: Event): List<Note> =
when (event) {
is PollNoteEvent -> event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) }
is WikiNoteEvent -> event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) }
is LongTextNoteEvent -> event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) }
is GitReplyEvent -> event.tagsWithoutCitations().filter { it != event.repository()?.toTag() }.mapNotNull { checkGetOrCreateNote(it) }
is TextNoteEvent -> event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) }
@@ -164,7 +164,7 @@ import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent
import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
import com.vitorpamplona.quartz.experimental.bounties.bountyBaseReward
import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent
import com.vitorpamplona.quartz.experimental.forks.isAFork
import com.vitorpamplona.quartz.experimental.forks.IForkableEvent
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryBaseEvent
import com.vitorpamplona.quartz.experimental.medical.FhirResourceEvent
import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent
@@ -173,7 +173,6 @@ import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent
import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent
import com.vitorpamplona.quartz.nip01Core.tags.geohash.geoHashOrScope
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip13Pow.strongPoWOrNull
import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent
@@ -1088,7 +1087,7 @@ fun SecondUserInfoRow(
modifier = UserNameMaxRowHeight,
) {
Column(modifier = remember(noteEvent) { Modifier.weight(1f) }) {
if (noteEvent is BaseThreadedEvent && noteEvent.isAFork()) {
if (noteEvent is IForkableEvent && noteEvent.isAFork()) {
ShowForkInformation(noteEvent, remember(noteEvent) { Modifier.weight(1f) }, accountViewModel, nav)
} else {
ObserveDisplayNip05Status(noteAuthor, accountViewModel, nav)
@@ -159,6 +159,7 @@ import com.vitorpamplona.amethyst.ui.theme.reactionBox
import com.vitorpamplona.amethyst.ui.theme.ripple24dp
import com.vitorpamplona.amethyst.ui.theme.selectedReactionBoxModifier
import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable
import com.vitorpamplona.quartz.nip30CustomEmoji.CustomEmoji
import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount
@@ -1394,16 +1395,20 @@ private fun BoostTypeChoicePopup(
Text(stringRes(R.string.quote), color = Color.White, textAlign = TextAlign.Center)
}
Button(
modifier = Modifier.padding(horizontal = 3.dp),
onClick = onFork,
shape = ButtonBorder,
colors =
ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary,
),
) {
Text(stringRes(R.string.fork), color = Color.White, textAlign = TextAlign.Center)
// removes the option to fork for now because we do not have screens for
// LongForm, Wiki and NIP posting.
if (baseNote.event is TextNoteEvent) {
Button(
modifier = Modifier.padding(horizontal = 3.dp),
onClick = onFork,
shape = ButtonBorder,
colors =
ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary,
),
) {
Text(stringRes(R.string.fork), color = Color.White, textAlign = TextAlign.Center)
}
}
}
}
@@ -47,13 +47,11 @@ import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.Font14SP
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.nip05
import com.vitorpamplona.quartz.experimental.forks.forkFromAddress
import com.vitorpamplona.quartz.experimental.forks.forkFromVersion
import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent
import com.vitorpamplona.quartz.experimental.forks.IForkableEvent
@Composable
fun ShowForkInformation(
noteEvent: BaseThreadedEvent,
noteEvent: IForkableEvent,
modifier: Modifier,
accountViewModel: AccountViewModel,
nav: INav,
@@ -67,7 +65,7 @@ fun ShowForkInformation(
}
}
} else if (forkedEvent != null) {
LoadNote(forkedEvent.eventId, accountViewModel) { event ->
LoadNote(forkedEvent, accountViewModel) { event ->
if (event != null) {
ForkInformationRowLightColor(event, modifier, accountViewModel, nav)
}
@@ -29,14 +29,13 @@ import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
import com.vitorpamplona.amethyst.ui.dal.FilterByListParams
import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
import com.vitorpamplona.quartz.experimental.forks.forkFromVersion
import com.vitorpamplona.quartz.experimental.forks.isForkFromAddressWithPubkey
import com.vitorpamplona.quartz.experimental.forks.IForkableEvent
import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent
import com.vitorpamplona.quartz.nip10Notes.BaseNoteEvent
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
@@ -187,7 +186,7 @@ class NotificationFeedFilter(
return true
}
if (event is BaseThreadedEvent) {
if (event is BaseNoteEvent) {
if (note.replyTo?.any { it.author?.pubkeyHex == authorHex } == true) {
return true
}
@@ -210,11 +209,23 @@ class NotificationFeedFilter(
}
val isAuthoredPostCited = event.findCitations().any { LocalCache.getNoteIfExists(it)?.author?.pubkeyHex == authorHex }
val isAuthorDirectlyCited = event.citedUsers().contains(authorHex)
val isAuthorOfAFork =
event.isForkFromAddressWithPubkey(authorHex) || (event.forkFromVersion()?.let { LocalCache.getNoteIfExists(it.eventId)?.author?.pubkeyHex == authorHex } == true)
return isAuthoredPostCited || isAuthorDirectlyCited || isAuthorOfAFork
if (isAuthoredPostCited) return true
val isAuthorDirectlyCited = event.citedUsers().contains(authorHex)
if (isAuthorDirectlyCited) return true
return if (event is IForkableEvent && event.isAFork()) {
val address = event.forkFromAddress()
val version = event.forkFromVersion()
// Displays notifications about forks
address?.pubKeyHex == authorHex ||
(version?.let { LocalCache.getNoteIfExists(it)?.author?.pubkeyHex == authorHex } == true)
} else {
false
}
}
if (event is ReactionEvent) {
@@ -1,4 +1,34 @@
/**
* Copyright (c) 2025 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.experimental.forks
class IForkableEvent {
}
import androidx.compose.runtime.Stable
import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip01Core.core.HexKey
@Stable
interface IForkableEvent {
fun isAFork(): Boolean
fun forkFromAddress(): Address?
fun forkFromVersion(): HexKey?
}
@@ -20,6 +20,7 @@
*/
package com.vitorpamplona.quartz.experimental.forks
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag
import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag.MARKER
@@ -37,3 +38,10 @@ fun MarkedETag.Companion.parseFork(tag: Array<String>): MarkedETag? {
),
)
}
fun MarkedETag.Companion.parseForkedEventId(tag: Array<String>): HexKey? {
if (tag.size < 4 || tag[0] != "e") return null
if (tag[ORDER_MARKER] != MARKER.FORK.code) return null
// ["e", id hex, relay hint, marker, pubkey]
return tag[ORDER_EVT_ID]
}
@@ -21,7 +21,8 @@
package com.vitorpamplona.quartz.experimental.nipsOnNostr
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.experimental.forks.parseFork
import com.vitorpamplona.quartz.experimental.forks.IForkableEvent
import com.vitorpamplona.quartz.experimental.forks.parseForkedEventId
import com.vitorpamplona.quartz.experimental.nipsOnNostr.tags.ForkTag
import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
@@ -64,6 +65,7 @@ class NipTextEvent(
RootScope,
EventHintProvider,
AddressHintProvider,
IForkableEvent,
SearchableEvent {
override fun indexableContent() = "title: " + title() + "\n" + content
@@ -105,11 +107,11 @@ class NipTextEvent(
return aHints + qHints + nip19Hints
}
fun isAFork() = tags.any { it.size > 3 && (it[0] == "a" || it[0] == "e") && it[3] == "fork" }
override fun isAFork() = tags.any { it.size > 3 && (it[0] == "a" || it[0] == "e") && it[3] == "fork" }
fun forkFromAddress() = tags.firstNotNullOfOrNull(ForkTag::parseAddress)
override fun forkFromAddress() = tags.firstNotNullOfOrNull(ForkTag::parseAddress)
fun forkFromVersion() = tags.firstNotNullOfOrNull(MarkedETag::parseFork)
override fun forkFromVersion() = tags.firstNotNullOfOrNull(MarkedETag::parseForkedEventId)
fun title() = tags.firstNotNullOfOrNull(TitleTag::parse)
@@ -21,6 +21,8 @@
package com.vitorpamplona.quartz.nip10Notes
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.experimental.forks.IForkableEvent
import com.vitorpamplona.quartz.experimental.forks.parseForkedEventId
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider
@@ -47,6 +49,7 @@ import com.vitorpamplona.quartz.nip19Bech32.pubKeys
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.nip50Search.SearchableEvent
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlinx.serialization.json.JsonNull.content
@Immutable
class TextNoteEvent(
@@ -60,6 +63,7 @@ class TextNoteEvent(
EventHintProvider,
AddressHintProvider,
PubKeyHintProvider,
IForkableEvent,
SearchableEvent {
override fun indexableContent() = "Subject: " + subject() + "\n" + content
@@ -109,6 +113,12 @@ class TextNoteEvent(
return pHints + nip19Hints
}
override fun isAFork() = tags.any { it.size > 3 && (it[0] == "a" || it[0] == "e") && it[3] == "fork" }
override fun forkFromAddress() = tags.firstNotNullOfOrNull(ATag::parseAddress)
override fun forkFromVersion() = tags.firstNotNullOfOrNull(MarkedETag::parseForkedEventId)
companion object {
const val KIND = 1
const val ALT = "A short note: "
@@ -1,13 +1,35 @@
/**
* Copyright (c) 2025 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.nip54Wiki
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
import com.vitorpamplona.quartz.nip23LongContent.tags.ImageTag
import com.vitorpamplona.quartz.nip23LongContent.tags.PublishedAtTag
import com.vitorpamplona.quartz.nip23LongContent.tags.SummaryTag
import com.vitorpamplona.quartz.nip23LongContent.tags.TitleTag
fun TagArrayBuilder<LongTextNoteEvent>.title(title: String) = addUnique(TitleTag.assemble(title))
fun TagArrayBuilder<WikiNoteEvent>.title(title: String) = addUnique(TitleTag.assemble(title))
fun TagArrayBuilder<LongTextNoteEvent>.summary(summary: String) = addUnique(SummaryTag.assemble(summary))
fun TagArrayBuilder<WikiNoteEvent>.summary(summary: String) = addUnique(SummaryTag.assemble(summary))
fun TagArrayBuilder<LongTextNoteEvent>.image(imageUrl: String) = addUnique(ImageTag.assemble(imageUrl))
fun TagArrayBuilder<WikiNoteEvent>.image(imageUrl: String) = addUnique(ImageTag.assemble(imageUrl))
fun TagArrayBuilder<LongTextNoteEvent>.publishedAt(publishedAt: Long) = addUnique(PublishedAtTag.assemble(publishedAt))
fun TagArrayBuilder<WikiNoteEvent>.publishedAt(publishedAt: Long) = addUnique(PublishedAtTag.assemble(publishedAt))
@@ -21,9 +21,13 @@
package com.vitorpamplona.quartz.nip54Wiki
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.experimental.forks.IForkableEvent
import com.vitorpamplona.quartz.experimental.forks.parseForkedEventId
import com.vitorpamplona.quartz.experimental.nipsOnNostr.tags.ForkTag
import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider
import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider
import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider
@@ -31,13 +35,14 @@ import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint
import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint
import com.vitorpamplona.quartz.nip01Core.hints.types.PubKeyHint
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
import com.vitorpamplona.quartz.nip01Core.tags.publishedAt.PublishedAtProvider
import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent
import com.vitorpamplona.quartz.nip10Notes.BaseNoteEvent
import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag
import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag
import com.vitorpamplona.quartz.nip19Bech32.addressHints
@@ -46,10 +51,16 @@ import com.vitorpamplona.quartz.nip19Bech32.eventHints
import com.vitorpamplona.quartz.nip19Bech32.eventIds
import com.vitorpamplona.quartz.nip19Bech32.pubKeyHints
import com.vitorpamplona.quartz.nip19Bech32.pubKeys
import com.vitorpamplona.quartz.nip22Comments.RootScope
import com.vitorpamplona.quartz.nip23LongContent.tags.ImageTag
import com.vitorpamplona.quartz.nip23LongContent.tags.PublishedAtTag
import com.vitorpamplona.quartz.nip31Alts.AltTag
import com.vitorpamplona.quartz.nip23LongContent.tags.SummaryTag
import com.vitorpamplona.quartz.nip23LongContent.tags.TitleTag
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.nip50Search.SearchableEvent
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
@Immutable
class WikiNoteEvent(
@@ -59,12 +70,14 @@ class WikiNoteEvent(
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : BaseThreadedEvent(id, pubKey, createdAt, KIND, tags, content, sig),
) : BaseNoteEvent(id, pubKey, createdAt, KIND, tags, content, sig),
AddressableEvent,
EventHintProvider,
AddressHintProvider,
PubKeyHintProvider,
PublishedAtProvider,
IForkableEvent,
RootScope,
SearchableEvent {
override fun indexableContent() = "title: " + title() + "\nsummary: " + summary() + "\n" + content
@@ -124,11 +137,17 @@ class WikiNoteEvent(
fun topics() = hashtags()
fun title() = tags.firstOrNull { it.size > 1 && it[0] == "title" }?.get(1)
fun title() = tags.firstNotNullOfOrNull(TitleTag::parse)
fun summary() = tags.firstOrNull { it.size > 1 && it[0] == "summary" }?.get(1)
fun image() = tags.firstNotNullOfOrNull(ImageTag::parse)
fun image() = tags.firstOrNull { it.size > 1 && it[0] == "image" }?.get(1)
fun summary() = tags.firstNotNullOfOrNull(SummaryTag::parse)
override fun isAFork() = tags.any { it.size > 3 && (it[0] == "a" || it[0] == "e") && it[3] == "fork" }
override fun forkFromAddress() = tags.firstNotNullOfOrNull(ForkTag::parseAddress)
override fun forkFromVersion() = tags.firstNotNullOfOrNull(MarkedETag::parseForkedEventId)
override fun publishedAt(): Long? {
val publishedAt = tags.firstNotNullOfOrNull(PublishedAtTag::parse) ?: return null
@@ -144,20 +163,27 @@ class WikiNoteEvent(
companion object {
const val KIND = 30818
suspend fun create(
msg: String,
title: String?,
replyTos: List<String>?,
mentions: List<String>?,
signer: NostrSigner,
@OptIn(ExperimentalUuidApi::class)
fun build(
description: String,
title: String,
summary: String? = null,
image: String? = null,
publishedAt: Long? = null,
dTag: String = Uuid.random().toString(),
createdAt: Long = TimeUtils.now(),
): WikiNoteEvent {
val tags = mutableListOf<Array<String>>()
replyTos?.forEach { tags.add(arrayOf("e", it)) }
mentions?.forEach { tags.add(arrayOf("p", it)) }
title?.let { tags.add(arrayOf("title", it)) }
tags.add(AltTag.assemble("Wiki Post: $title"))
return signer.sign(createdAt, KIND, tags.toTypedArray(), msg)
}
initializer: TagArrayBuilder<WikiNoteEvent>.() -> Unit = {},
): EventTemplate<WikiNoteEvent> =
eventTemplate(KIND, description, createdAt) {
dTag(dTag)
alt("Wiki entry: $title")
title(title)
summary?.let { summary(it) }
image?.let { image(it) }
publishedAt?.let { publishedAt(it) }
initializer()
}
}
}