Adds support for Interactive Stories

This commit is contained in:
Vitor Pamplona 2024-11-26 19:07:12 -05:00
parent 034ee46543
commit bdf012f641
24 changed files with 994 additions and 92 deletions

View File

@ -89,6 +89,10 @@ import com.vitorpamplona.quartz.events.GenericRepostEvent
import com.vitorpamplona.quartz.events.GiftWrapEvent
import com.vitorpamplona.quartz.events.GitReplyEvent
import com.vitorpamplona.quartz.events.HTTPAuthorizationEvent
import com.vitorpamplona.quartz.events.InteractiveStoryBaseEvent
import com.vitorpamplona.quartz.events.InteractiveStoryPrologueEvent
import com.vitorpamplona.quartz.events.InteractiveStoryReadingStateEvent
import com.vitorpamplona.quartz.events.InteractiveStorySceneEvent
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
import com.vitorpamplona.quartz.events.LnZapEvent
import com.vitorpamplona.quartz.events.LnZapPaymentRequestEvent
@ -114,6 +118,7 @@ import com.vitorpamplona.quartz.events.Response
import com.vitorpamplona.quartz.events.SealedGossipEvent
import com.vitorpamplona.quartz.events.SearchRelayListEvent
import com.vitorpamplona.quartz.events.StatusEvent
import com.vitorpamplona.quartz.events.StoryOption
import com.vitorpamplona.quartz.events.TextNoteEvent
import com.vitorpamplona.quartz.events.TextNoteModificationEvent
import com.vitorpamplona.quartz.events.TorrentCommentEvent
@ -2457,6 +2462,142 @@ class Account(
}
}
suspend fun createInteractiveStoryReadingState(
root: InteractiveStoryBaseEvent,
rootRelay: String?,
readingScene: InteractiveStoryBaseEvent,
readingSceneRelay: String?,
) {
if (!isWriteable()) return
val relayList = getPrivateOutBoxRelayList()
InteractiveStoryReadingStateEvent.create(
root = root,
rootRelay = rootRelay,
currentScene = readingScene,
currentSceneRelay = readingSceneRelay,
signer = signer,
) {
if (relayList.isNotEmpty()) {
Client.sendPrivately(it, relayList = relayList)
} else {
Client.send(it)
}
LocalCache.justConsume(it, null)
}
}
suspend fun updateInteractiveStoryReadingState(
readingState: InteractiveStoryReadingStateEvent,
readingScene: InteractiveStoryBaseEvent,
readingSceneRelay: String?,
) {
if (!isWriteable()) return
val relayList = getPrivateOutBoxRelayList()
InteractiveStoryReadingStateEvent.update(
base = readingState,
currentScene = readingScene,
currentSceneRelay = readingSceneRelay,
signer = signer,
) {
if (relayList.isNotEmpty()) {
Client.sendPrivately(it, relayList = relayList)
} else {
Client.send(it)
}
LocalCache.justConsume(it, null)
}
}
suspend fun sendInteractiveStoryPrologue(
baseId: String,
title: String,
content: String,
options: List<StoryOption>,
summary: String? = null,
image: String? = null,
zapReceiver: List<ZapSplitSetup>? = null,
wantsToMarkAsSensitive: Boolean = false,
zapRaiserAmount: Long? = null,
nip94attachments: List<FileHeaderEvent>? = null,
draftTag: String? = null,
relayList: List<RelaySetupInfo>,
) {
if (!isWriteable()) return
InteractiveStoryPrologueEvent.create(
baseId = baseId,
title = title,
content = content,
options = options,
summary = summary,
image = image,
zapReceiver = zapReceiver,
markAsSensitive = wantsToMarkAsSensitive,
zapRaiserAmount = zapRaiserAmount,
nip94attachments = nip94attachments,
signer = signer,
isDraft = draftTag != null,
) {
if (draftTag != null) {
if (content.isBlank()) {
deleteDraft(draftTag)
} else {
DraftEvent.create(draftTag, it, signer) { draftEvent ->
sendDraftEvent(draftEvent)
}
}
} else {
Client.send(it, relayList = relayList)
LocalCache.justConsume(it, null)
}
}
}
suspend fun sendInteractiveStoryScene(
baseId: String,
title: String,
content: String,
options: List<StoryOption>,
zapReceiver: List<ZapSplitSetup>? = null,
wantsToMarkAsSensitive: Boolean = false,
zapRaiserAmount: Long? = null,
nip94attachments: List<FileHeaderEvent>? = null,
draftTag: String? = null,
relayList: List<RelaySetupInfo>,
) {
if (!isWriteable()) return
InteractiveStorySceneEvent.create(
baseId = baseId,
title = title,
content = content,
options = options,
zapReceiver = zapReceiver,
markAsSensitive = wantsToMarkAsSensitive,
zapRaiserAmount = zapRaiserAmount,
nip94attachments = nip94attachments,
signer = signer,
isDraft = draftTag != null,
) {
if (draftTag != null) {
if (content.isBlank()) {
deleteDraft(draftTag)
} else {
DraftEvent.create(draftTag, it, signer) { draftEvent ->
sendDraftEvent(draftEvent)
}
}
} else {
Client.send(it, relayList = relayList)
LocalCache.justConsume(it, null)
}
}
}
suspend fun sendPost(
message: String,
replyTo: List<Note>?,
@ -2823,18 +2964,19 @@ class Account(
}
}
fun sendDraftEvent(draftEvent: DraftEvent) {
val relayList =
normalizedPrivateOutBoxRelaySet.value.map {
RelaySetupInfoToConnect(
it,
shouldUseTorForClean(it),
true,
true,
emptySet(),
)
}
fun getPrivateOutBoxRelayList(): List<RelaySetupInfoToConnect> =
normalizedPrivateOutBoxRelaySet.value.map {
RelaySetupInfoToConnect(
it,
shouldUseTorForClean(it),
true,
true,
emptySet(),
)
}
fun sendDraftEvent(draftEvent: DraftEvent) {
val relayList = getPrivateOutBoxRelayList()
if (relayList.isNotEmpty()) {
Client.sendPrivately(draftEvent, relayList)
} else {

View File

@ -86,6 +86,9 @@ 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.InteractiveStoryPrologueEvent
import com.vitorpamplona.quartz.events.InteractiveStoryReadingStateEvent
import com.vitorpamplona.quartz.events.InteractiveStorySceneEvent
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
import com.vitorpamplona.quartz.events.LiveActivitiesEvent
import com.vitorpamplona.quartz.events.LnZapEvent
@ -449,6 +452,21 @@ object LocalCache {
relay: Relay?,
) = consumeRegularEvent(event, relay)
fun consume(
event: InteractiveStoryPrologueEvent,
relay: Relay?,
) = consumeBaseReplaceable(event, relay)
fun consume(
event: InteractiveStorySceneEvent,
relay: Relay?,
) = consumeBaseReplaceable(event, relay)
fun consume(
event: InteractiveStoryReadingStateEvent,
relay: Relay?,
) = consumeBaseReplaceable(event, relay)
fun consumeRegularEvent(
event: Event,
relay: Relay?,
@ -2393,6 +2411,9 @@ object LocalCache {
is GitPatchEvent -> consume(event, relay)
is GitRepositoryEvent -> consume(event, relay)
is HighlightEvent -> consume(event, relay)
is InteractiveStoryPrologueEvent -> consume(event, relay)
is InteractiveStorySceneEvent -> consume(event, relay)
is InteractiveStoryReadingStateEvent -> consume(event, relay)
is LiveActivitiesEvent -> consume(event, relay)
is LiveActivitiesChatMessageEvent -> consume(event, relay)
is LnZapEvent -> {

View File

@ -56,6 +56,8 @@ import com.vitorpamplona.quartz.events.GitIssueEvent
import com.vitorpamplona.quartz.events.GitPatchEvent
import com.vitorpamplona.quartz.events.GitReplyEvent
import com.vitorpamplona.quartz.events.HighlightEvent
import com.vitorpamplona.quartz.events.InteractiveStoryPrologueEvent
import com.vitorpamplona.quartz.events.InteractiveStorySceneEvent
import com.vitorpamplona.quartz.events.LnZapEvent
import com.vitorpamplona.quartz.events.LnZapPaymentResponseEvent
import com.vitorpamplona.quartz.events.MetadataEvent
@ -245,6 +247,8 @@ object NostrAccountDataSource : AmethystNostrDataSource("AccountData") {
CalendarDateSlotEvent.KIND,
CalendarTimeSlotEvent.KIND,
CalendarRSVPEvent.KIND,
InteractiveStoryPrologueEvent.KIND,
InteractiveStorySceneEvent.KIND,
),
tags = mapOf("p" to listOf(account.userProfile().pubkeyHex)),
limit = 400,

View File

@ -29,6 +29,7 @@ import com.vitorpamplona.quartz.events.ChannelMessageEvent
import com.vitorpamplona.quartz.events.ClassifiedsEvent
import com.vitorpamplona.quartz.events.CommentEvent
import com.vitorpamplona.quartz.events.HighlightEvent
import com.vitorpamplona.quartz.events.InteractiveStorySceneEvent
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
import com.vitorpamplona.quartz.events.LongTextNoteEvent
import com.vitorpamplona.quartz.events.PollNoteEvent
@ -38,46 +39,59 @@ import com.vitorpamplona.quartz.events.WikiNoteEvent
object NostrHashtagDataSource : AmethystNostrDataSource("SingleHashtagFeed") {
private var hashtagToWatch: String? = null
fun createLoadHashtagFilter(): TypedFilter? {
val hashToLoad = hashtagToWatch ?: return null
fun createLoadHashtagFilter(): List<TypedFilter> {
val hashToLoad = hashtagToWatch ?: return emptyList()
return TypedFilter(
types = COMMON_FEED_TYPES,
filter =
SincePerRelayFilter(
tags =
mapOf(
"t" to
listOf(
hashToLoad,
hashToLoad.lowercase(),
hashToLoad.uppercase(),
hashToLoad.capitalize(),
),
),
kinds =
listOf(
TextNoteEvent.KIND,
ChannelMessageEvent.KIND,
LongTextNoteEvent.KIND,
PollNoteEvent.KIND,
LiveActivitiesChatMessageEvent.KIND,
ClassifiedsEvent.KIND,
HighlightEvent.KIND,
AudioTrackEvent.KIND,
AudioHeaderEvent.KIND,
WikiNoteEvent.KIND,
CommentEvent.KIND,
),
limit = 200,
),
val hashtagsToFollow =
listOf(
hashToLoad,
hashToLoad.lowercase(),
hashToLoad.uppercase(),
hashToLoad.capitalize(),
)
return listOf(
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
SincePerRelayFilter(
tags = mapOf("t" to hashtagsToFollow),
kinds =
listOf(
TextNoteEvent.KIND,
ChannelMessageEvent.KIND,
LongTextNoteEvent.KIND,
PollNoteEvent.KIND,
LiveActivitiesChatMessageEvent.KIND,
ClassifiedsEvent.KIND,
HighlightEvent.KIND,
AudioTrackEvent.KIND,
AudioHeaderEvent.KIND,
WikiNoteEvent.KIND,
CommentEvent.KIND,
),
limit = 200,
),
),
TypedFilter(
types = COMMON_FEED_TYPES,
filter =
SincePerRelayFilter(
tags = mapOf("t" to hashtagsToFollow),
kinds =
listOf(
InteractiveStorySceneEvent.KIND,
),
limit = 200,
),
),
)
}
val loadHashtagChannel = requestNewChannel()
override fun updateChannelFilters() {
loadHashtagChannel.typedFilters = listOfNotNull(createLoadHashtagFilter()).ifEmpty { null }
loadHashtagChannel.typedFilters = createLoadHashtagFilter().ifEmpty { null }
}
fun loadHashtag(tag: String?) {

View File

@ -35,6 +35,7 @@ import com.vitorpamplona.quartz.events.CommentEvent
import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
import com.vitorpamplona.quartz.events.GenericRepostEvent
import com.vitorpamplona.quartz.events.HighlightEvent
import com.vitorpamplona.quartz.events.InteractiveStoryPrologueEvent
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
import com.vitorpamplona.quartz.events.LiveActivitiesEvent
import com.vitorpamplona.quartz.events.LongTextNoteEvent
@ -86,38 +87,57 @@ object NostrHomeDataSource : AmethystNostrDataSource("HomeFeed") {
job2?.cancel()
}
fun createFollowAccountsFilter(): TypedFilter {
fun createFollowAccountsFilter(): List<TypedFilter> {
val follows =
account.liveHomeListAuthorsPerRelay.value
return TypedFilter(
types = setOf(if (follows == null) FeedType.GLOBAL else FeedType.FOLLOWS),
filter =
SinceAuthorPerRelayFilter(
kinds =
listOf(
TextNoteEvent.KIND,
RepostEvent.KIND,
GenericRepostEvent.KIND,
ClassifiedsEvent.KIND,
LongTextNoteEvent.KIND,
PollNoteEvent.KIND,
HighlightEvent.KIND,
AudioTrackEvent.KIND,
AudioHeaderEvent.KIND,
PinListEvent.KIND,
LiveActivitiesChatMessageEvent.KIND,
LiveActivitiesEvent.KIND,
WikiNoteEvent.KIND,
),
authors = follows,
limit = 400,
since =
latestEOSEs.users[account.userProfile()]
?.followList
?.get(account.settings.defaultHomeFollowList.value)
?.relayList,
),
return listOf(
TypedFilter(
types = setOf(if (follows == null) FeedType.GLOBAL else FeedType.FOLLOWS),
filter =
SinceAuthorPerRelayFilter(
kinds =
listOf(
TextNoteEvent.KIND,
RepostEvent.KIND,
GenericRepostEvent.KIND,
ClassifiedsEvent.KIND,
LongTextNoteEvent.KIND,
PollNoteEvent.KIND,
HighlightEvent.KIND,
AudioTrackEvent.KIND,
AudioHeaderEvent.KIND,
PinListEvent.KIND,
),
authors = follows,
limit = 400,
since =
latestEOSEs.users[account.userProfile()]
?.followList
?.get(account.settings.defaultHomeFollowList.value)
?.relayList,
),
),
TypedFilter(
types = setOf(if (follows == null) FeedType.GLOBAL else FeedType.FOLLOWS),
filter =
SinceAuthorPerRelayFilter(
kinds =
listOf(
InteractiveStoryPrologueEvent.KIND,
LiveActivitiesChatMessageEvent.KIND,
LiveActivitiesEvent.KIND,
WikiNoteEvent.KIND,
),
authors = follows,
limit = 400,
since =
latestEOSEs.users[account.userProfile()]
?.followList
?.get(account.settings.defaultHomeFollowList.value)
?.relayList,
),
),
)
}
@ -165,7 +185,7 @@ object NostrHomeDataSource : AmethystNostrDataSource("HomeFeed") {
ClassifiedsEvent.KIND,
HighlightEvent.KIND,
AudioHeaderEvent.KIND,
AudioTrackEvent.KIND,
InteractiveStoryPrologueEvent.KIND,
CommentEvent.KIND,
WikiNoteEvent.KIND,
),
@ -203,8 +223,7 @@ object NostrHomeDataSource : AmethystNostrDataSource("HomeFeed") {
LongTextNoteEvent.KIND,
ClassifiedsEvent.KIND,
HighlightEvent.KIND,
AudioHeaderEvent.KIND,
AudioTrackEvent.KIND,
InteractiveStoryPrologueEvent.KIND,
WikiNoteEvent.KIND,
CommentEvent.KIND,
),
@ -240,12 +259,10 @@ object NostrHomeDataSource : AmethystNostrDataSource("HomeFeed") {
LongTextNoteEvent.KIND,
ClassifiedsEvent.KIND,
HighlightEvent.KIND,
AudioHeaderEvent.KIND,
AudioTrackEvent.KIND,
PinListEvent.KIND,
WikiNoteEvent.KIND,
CommunityPostApprovalEvent.KIND,
CommentEvent.KIND,
InteractiveStoryPrologueEvent.KIND,
),
tags =
mapOf(
@ -273,12 +290,14 @@ object NostrHomeDataSource : AmethystNostrDataSource("HomeFeed") {
override fun updateChannelFilters() {
followAccountChannel.typedFilters =
listOfNotNull(
createFollowAccountsFilter(),
createFollowMetadataAndReleaseFilter(),
createFollowCommunitiesFilter(),
createFollowTagsFilter(),
createFollowGeohashesFilter(),
(
createFollowAccountsFilter() +
listOfNotNull(
createFollowMetadataAndReleaseFilter(),
createFollowCommunitiesFilter(),
createFollowTagsFilter(),
createFollowGeohashesFilter(),
)
).ifEmpty { null }
}
}

View File

@ -42,6 +42,8 @@ import com.vitorpamplona.quartz.events.CommentEvent
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
import com.vitorpamplona.quartz.events.EmojiPackEvent
import com.vitorpamplona.quartz.events.HighlightEvent
import com.vitorpamplona.quartz.events.InteractiveStoryPrologueEvent
import com.vitorpamplona.quartz.events.InteractiveStorySceneEvent
import com.vitorpamplona.quartz.events.LiveActivitiesEvent
import com.vitorpamplona.quartz.events.LongTextNoteEvent
import com.vitorpamplona.quartz.events.MetadataEvent
@ -184,6 +186,19 @@ object NostrSearchEventOrUserDataSource : AmethystNostrDataSource("SearchEventFe
limit = 100,
),
),
TypedFilter(
types = setOf(FeedType.SEARCH),
filter =
SincePerRelayFilter(
kinds =
listOf(
InteractiveStoryPrologueEvent.KIND,
InteractiveStorySceneEvent.KIND,
),
search = mySearchString,
limit = 100,
),
),
)
}

View File

@ -33,6 +33,7 @@ import com.vitorpamplona.quartz.events.BookmarkListEvent
import com.vitorpamplona.quartz.events.ContactListEvent
import com.vitorpamplona.quartz.events.GenericRepostEvent
import com.vitorpamplona.quartz.events.HighlightEvent
import com.vitorpamplona.quartz.events.InteractiveStoryPrologueEvent
import com.vitorpamplona.quartz.events.LnZapEvent
import com.vitorpamplona.quartz.events.LongTextNoteEvent
import com.vitorpamplona.quartz.events.MetadataEvent
@ -101,6 +102,7 @@ object NostrUserProfileDataSource : AmethystNostrDataSource("UserProfileFeed") {
listOf(
TorrentEvent.KIND,
TorrentCommentEvent.KIND,
InteractiveStoryPrologueEvent.KIND,
),
authors = listOf(it.pubkeyHex),
limit = 20,

View File

@ -29,6 +29,7 @@ import com.vitorpamplona.quartz.events.ClassifiedsEvent
import com.vitorpamplona.quartz.events.CommentEvent
import com.vitorpamplona.quartz.events.GenericRepostEvent
import com.vitorpamplona.quartz.events.HighlightEvent
import com.vitorpamplona.quartz.events.InteractiveStoryPrologueEvent
import com.vitorpamplona.quartz.events.LongTextNoteEvent
import com.vitorpamplona.quartz.events.MuteListEvent
import com.vitorpamplona.quartz.events.PeopleListEvent
@ -99,6 +100,7 @@ class HomeNewThreadFeedFilter(
(noteEvent is WikiNoteEvent && noteEvent.content.isNotEmpty()) ||
noteEvent is PollNoteEvent ||
noteEvent is HighlightEvent ||
noteEvent is InteractiveStoryPrologueEvent ||
noteEvent is CommentEvent ||
noteEvent is AudioTrackEvent ||
noteEvent is AudioHeaderEvent

View File

@ -31,6 +31,7 @@ import com.vitorpamplona.quartz.events.AudioTrackEvent
import com.vitorpamplona.quartz.events.ClassifiedsEvent
import com.vitorpamplona.quartz.events.GenericRepostEvent
import com.vitorpamplona.quartz.events.HighlightEvent
import com.vitorpamplona.quartz.events.InteractiveStoryPrologueEvent
import com.vitorpamplona.quartz.events.LongTextNoteEvent
import com.vitorpamplona.quartz.events.PollNoteEvent
import com.vitorpamplona.quartz.events.RepostEvent
@ -73,6 +74,7 @@ class UserProfileNewThreadFeedFilter(
it.event is WikiNoteEvent ||
it.event is PollNoteEvent ||
it.event is HighlightEvent ||
it.event is InteractiveStoryPrologueEvent ||
it.event is AudioTrackEvent ||
it.event is AudioHeaderEvent ||
it.event is TorrentEvent

View File

@ -29,6 +29,7 @@ import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.ui.navigation.INav
@ -48,6 +49,8 @@ fun FeedLoaded(
) {
val items by loaded.feed.collectAsStateWithLifecycle()
val scope = rememberCoroutineScope()
LazyColumn(
contentPadding = FeedPadding,
state = listState,

View File

@ -101,6 +101,7 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderGitIssueEvent
import com.vitorpamplona.amethyst.ui.note.types.RenderGitPatchEvent
import com.vitorpamplona.amethyst.ui.note.types.RenderGitRepositoryEvent
import com.vitorpamplona.amethyst.ui.note.types.RenderHighlight
import com.vitorpamplona.amethyst.ui.note.types.RenderInteractiveStory
import com.vitorpamplona.amethyst.ui.note.types.RenderLiveActivityChatMessage
import com.vitorpamplona.amethyst.ui.note.types.RenderLiveActivityEvent
import com.vitorpamplona.amethyst.ui.note.types.RenderLongFormContent
@ -171,6 +172,7 @@ import com.vitorpamplona.quartz.events.GitIssueEvent
import com.vitorpamplona.quartz.events.GitPatchEvent
import com.vitorpamplona.quartz.events.GitRepositoryEvent
import com.vitorpamplona.quartz.events.HighlightEvent
import com.vitorpamplona.quartz.events.InteractiveStoryBaseEvent
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
import com.vitorpamplona.quartz.events.LiveActivitiesEvent
import com.vitorpamplona.quartz.events.LongTextNoteEvent
@ -798,6 +800,17 @@ private fun RenderNoteRow(
nav,
)
is InteractiveStoryBaseEvent ->
RenderInteractiveStory(
baseNote,
makeItShort,
canPreview,
quotesLeft,
backgroundColor,
accountViewModel,
nav,
)
else -> {
RenderTextEvent(
baseNote,

View File

@ -0,0 +1,200 @@
/**
* 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.amethyst.ui.note.types
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.Stable
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.events.EmptyTagList
import com.vitorpamplona.quartz.events.InteractiveStoryBaseEvent
import com.vitorpamplona.quartz.events.InteractiveStoryReadingStateEvent
@Composable
fun RenderInteractiveStory(
baseNote: Note,
makeItShort: Boolean,
canPreview: Boolean,
quotesLeft: Int,
backgroundColor: MutableState<Color>,
accountViewModel: AccountViewModel,
nav: INav,
) {
val baseRootEvent = baseNote.event as? InteractiveStoryBaseEvent ?: return
val address = baseNote.address() ?: return
// keep updating the root event with new versions
val note = baseNote.live().metadata.observeAsState()
val rootEvent = note.value?.note?.event as? InteractiveStoryBaseEvent ?: return
// keep updating the reading state event with new versions
val readingStateNote = accountViewModel.getInteractiveStoryReadingState(address.toTag())
val latestReadingNoteState = readingStateNote.live().metadata.observeAsState()
val readingState = latestReadingNoteState.value?.note?.event as? InteractiveStoryReadingStateEvent
val currentScene = readingState?.currentScene()
if (currentScene != null && currentScene != rootEvent.address()) {
LoadAddressableNote(currentScene, accountViewModel) { currentSceneBaseNote ->
val currentScene = currentSceneBaseNote?.live()?.metadata?.observeAsState()
val currentSceneEvent = currentScene?.value?.note?.event as? InteractiveStoryBaseEvent
if (currentSceneEvent != null) {
RenderInteractiveStory(
section = currentSceneEvent,
onSelect = {
val event = it.event as? InteractiveStoryBaseEvent ?: return@RenderInteractiveStory
accountViewModel.updateInteractiveStoryReadingState(baseRootEvent, event)
},
onRestart = {
accountViewModel.updateInteractiveStoryReadingState(baseRootEvent, rootEvent)
},
makeItShort = makeItShort,
canPreview = canPreview,
quotesLeft = quotesLeft,
backgroundColor = backgroundColor,
accountViewModel = accountViewModel,
nav = nav,
)
}
}
} else {
RenderInteractiveStory(
section = rootEvent,
onSelect = {
val event = it.event as? InteractiveStoryBaseEvent ?: return@RenderInteractiveStory
accountViewModel.updateInteractiveStoryReadingState(baseRootEvent, event)
},
onRestart = {
accountViewModel.updateInteractiveStoryReadingState(baseRootEvent, rootEvent)
},
makeItShort = makeItShort,
canPreview = canPreview,
quotesLeft = quotesLeft,
backgroundColor = backgroundColor,
accountViewModel = accountViewModel,
nav = nav,
)
}
}
@Composable
fun RenderInteractiveStory(
section: InteractiveStoryBaseEvent,
onSelect: (AddressableNote) -> Unit,
onRestart: () -> Unit,
makeItShort: Boolean,
canPreview: Boolean,
quotesLeft: Int,
backgroundColor: MutableState<Color>,
accountViewModel: AccountViewModel,
nav: INav,
) {
section.title()?.let {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(top = 5.dp, bottom = 10.dp),
) {
Text(
text = it,
fontWeight = FontWeight.Bold,
maxLines = 3,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.fillMaxWidth(),
)
}
}
TranslatableRichTextViewer(
content = section.content,
canPreview = canPreview && !makeItShort,
quotesLeft = quotesLeft,
modifier = Modifier.fillMaxWidth(),
tags = EmptyTagList,
backgroundColor = backgroundColor,
id = section.id,
callbackUri = null,
accountViewModel = accountViewModel,
nav = nav,
)
val options = section.options()
if (options.isNotEmpty()) {
Column(Modifier.padding(top = 10.dp)) {
options.forEach { opt ->
LoadAddressableNote(opt.address, accountViewModel) { note ->
if (note != null) {
val optionState = note.live().metadata.observeAsState()
OutlinedButton(
onClick = { onSelect(note) },
) {
Text(opt.option)
}
}
}
}
}
} else {
Column(Modifier.padding(top = 10.dp)) {
OutlinedButton(
onClick = onRestart,
) {
Text("Restart")
}
}
}
}
@Stable
class StoryReadingState {
private var sectionList = mutableMapOf<HexKey, Note>()
private var sectionToShowId: HexKey? = null
val sectionToShow: MutableState<Note?> = mutableStateOf(null)
fun readSection(note: Note) {
sectionList[note.idHex] = note
sectionToShowId = note.idHex
sectionToShow.value = note
}
}

View File

@ -42,6 +42,7 @@ import com.vitorpamplona.quartz.events.ContactListEvent
import com.vitorpamplona.quartz.events.DeletionEvent
import com.vitorpamplona.quartz.events.GenericRepostEvent
import com.vitorpamplona.quartz.events.HighlightEvent
import com.vitorpamplona.quartz.events.InteractiveStoryPrologueEvent
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
import com.vitorpamplona.quartz.events.LiveActivitiesEvent
import com.vitorpamplona.quartz.events.LongTextNoteEvent
@ -354,6 +355,7 @@ val DEFAULT_FEED_KINDS =
LiveActivitiesChatMessageEvent.KIND,
LiveActivitiesEvent.KIND,
WikiNoteEvent.KIND,
InteractiveStoryPrologueEvent.KIND,
)
val DEFAULT_COMMUNITY_FEEDS =
@ -367,4 +369,5 @@ val DEFAULT_COMMUNITY_FEEDS =
PinListEvent.KIND,
WikiNoteEvent.KIND,
CommunityPostApprovalEvent.KIND,
InteractiveStoryPrologueEvent.KIND,
)

View File

@ -89,6 +89,8 @@ import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.EventInterface
import com.vitorpamplona.quartz.events.GenericRepostEvent
import com.vitorpamplona.quartz.events.GiftWrapEvent
import com.vitorpamplona.quartz.events.InteractiveStoryBaseEvent
import com.vitorpamplona.quartz.events.InteractiveStoryReadingStateEvent
import com.vitorpamplona.quartz.events.LnZapEvent
import com.vitorpamplona.quartz.events.LnZapRequestEvent
import com.vitorpamplona.quartz.events.NIP90ContentDiscoveryResponseEvent
@ -1102,7 +1104,7 @@ class AccountViewModel(
viewModelScope.launch(Dispatchers.IO) { onResult(checkGetOrCreateAddressableNote(key)) }
}
suspend fun getOrCreateAddressableNote(key: ATag): AddressableNote? = LocalCache.getOrCreateAddressableNote(key)
suspend fun getOrCreateAddressableNote(key: ATag): AddressableNote = LocalCache.getOrCreateAddressableNote(key)
fun getOrCreateAddressableNote(
key: ATag,
@ -1558,6 +1560,28 @@ class AccountViewModel(
AdvertisedRelayListEvent.createAddressTag(user.pubkeyHex),
)
fun getInteractiveStoryReadingState(dATag: String): AddressableNote = LocalCache.getOrCreateAddressableNote(InteractiveStoryReadingStateEvent.createAddressATag(account.signer.pubKey, dATag))
fun updateInteractiveStoryReadingState(
root: InteractiveStoryBaseEvent,
readingScene: InteractiveStoryBaseEvent,
) {
viewModelScope.launch(Dispatchers.IO) {
val sceneNoteRelayHint = LocalCache.getOrCreateAddressableNote(readingScene.address()).relayHintUrl()
val readingState = getInteractiveStoryReadingState(root.addressTag())
val readingStateEvent = readingState.event as? InteractiveStoryReadingStateEvent
if (readingStateEvent != null) {
account.updateInteractiveStoryReadingState(readingStateEvent, readingScene, sceneNoteRelayHint)
} else {
val rootNoteRelayHint = LocalCache.getOrCreateAddressableNote(root.address()).relayHintUrl()
account.createInteractiveStoryReadingState(root, rootNoteRelayHint, readingScene, sceneNoteRelayHint)
}
}
}
fun sendSats(
lnaddress: String,
milliSats: Long,

View File

@ -132,6 +132,7 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderGitIssueEvent
import com.vitorpamplona.amethyst.ui.note.types.RenderGitPatchEvent
import com.vitorpamplona.amethyst.ui.note.types.RenderGitRepositoryEvent
import com.vitorpamplona.amethyst.ui.note.types.RenderHighlight
import com.vitorpamplona.amethyst.ui.note.types.RenderInteractiveStory
import com.vitorpamplona.amethyst.ui.note.types.RenderLiveActivityChatMessage
import com.vitorpamplona.amethyst.ui.note.types.RenderPinListEvent
import com.vitorpamplona.amethyst.ui.note.types.RenderPoll
@ -184,6 +185,7 @@ import com.vitorpamplona.quartz.events.GitIssueEvent
import com.vitorpamplona.quartz.events.GitPatchEvent
import com.vitorpamplona.quartz.events.GitRepositoryEvent
import com.vitorpamplona.quartz.events.HighlightEvent
import com.vitorpamplona.quartz.events.InteractiveStoryBaseEvent
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
import com.vitorpamplona.quartz.events.LongTextNoteEvent
import com.vitorpamplona.quartz.events.PeopleListEvent
@ -562,6 +564,16 @@ private fun FullBleedNoteCompose(
RenderFhirResource(baseNote, accountViewModel, nav)
} else if (noteEvent is GitRepositoryEvent) {
RenderGitRepositoryEvent(baseNote, accountViewModel, nav)
} else if (noteEvent is InteractiveStoryBaseEvent) {
RenderInteractiveStory(
baseNote,
false,
true,
3,
backgroundColor,
accountViewModel,
nav,
)
} else if (noteEvent is GitPatchEvent) {
RenderGitPatchEvent(baseNote, makeItShort = false, canPreview = true, quotesLeft = 3, backgroundColor = backgroundColor, accountViewModel = accountViewModel, nav = nav)
} else if (noteEvent is GitIssueEvent) {

View File

@ -132,6 +132,17 @@ class DraftEvent(
create(dTag, originalNote, tagsWithMarkers, signer, createdAt, onReady)
}
fun create(
dTag: String,
originalNote: InteractiveStoryBaseEvent,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (DraftEvent) -> Unit,
) {
val tags = mutableListOf<Array<String>>()
create(dTag, originalNote, tags, signer, createdAt, onReady)
}
fun create(
dTag: String,
originalNote: LiveActivitiesChatMessageEvent,

View File

@ -556,7 +556,7 @@ class HostStub(
interface AddressableEvent {
fun dTag(): String
fun address(): ATag
fun address(relayHint: String? = null): ATag
fun addressTag(): String
}
@ -574,7 +574,7 @@ open class BaseAddressableEvent(
AddressableEvent {
override fun dTag() = tags.firstOrNull { it.size > 1 && it[0] == "d" }?.get(1) ?: ""
override fun address() = ATag(kind, pubKey, dTag(), null)
override fun address(relayHint: String?) = ATag(kind, pubKey, dTag(), relayHint)
/**
* Creates the tag in a memory effecient way (without creating the ATag class

View File

@ -26,7 +26,7 @@ import com.vitorpamplona.quartz.events.nip46.NostrConnectEvent
class EventFactory {
companion object {
val additionalFactories: MutableMap<Int, (HexKey, HexKey, Long, Array<Array<String>>, String, HexKey) -> Event> = mutableMapOf()
val factories: MutableMap<Int, (HexKey, HexKey, Long, Array<Array<String>>, String, HexKey) -> Event> = mutableMapOf()
fun create(
id: String,
@ -99,6 +99,9 @@ class EventFactory {
GoalEvent.KIND -> GoalEvent(id, pubKey, createdAt, tags, content, sig)
HighlightEvent.KIND -> HighlightEvent(id, pubKey, createdAt, tags, content, sig)
HTTPAuthorizationEvent.KIND -> HTTPAuthorizationEvent(id, pubKey, createdAt, tags, content, sig)
InteractiveStoryPrologueEvent.KIND -> InteractiveStoryPrologueEvent(id, pubKey, createdAt, tags, content, sig)
InteractiveStorySceneEvent.KIND -> InteractiveStorySceneEvent(id, pubKey, createdAt, tags, content, sig)
InteractiveStoryReadingStateEvent.KIND -> InteractiveStoryReadingStateEvent(id, pubKey, createdAt, tags, content, sig)
LiveActivitiesChatMessageEvent.KIND -> LiveActivitiesChatMessageEvent(id, pubKey, createdAt, tags, content, sig)
LiveActivitiesEvent.KIND -> LiveActivitiesEvent(id, pubKey, createdAt, tags, content, sig)
LnZapEvent.KIND -> LnZapEvent(id, pubKey, createdAt, tags, content, sig)
@ -141,7 +144,7 @@ class EventFactory {
VideoViewEvent.KIND -> VideoViewEvent(id, pubKey, createdAt, tags, content, sig)
WikiNoteEvent.KIND -> WikiNoteEvent(id, pubKey, createdAt, tags, content, sig)
else -> {
additionalFactories[kind]?.let {
factories[kind]?.let {
return it(id, pubKey, createdAt, tags, content, sig)
}

View File

@ -0,0 +1,115 @@
/**
* 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 com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.Nip92MediaAttachments
open class InteractiveStoryBaseEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
kind: Int,
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
fun title() = firstTag("title")
fun summary() = firstTag("summary")
fun image() = firstTag("image")
fun options() =
tags
.filter { it.size > 2 && it[0] == "option" }
.mapNotNull { ATag.parse(it[2], it.getOrNull(3))?.let { aTag -> StoryOption(it[1], aTag) } }
companion object {
fun generalTags(
content: String,
zapReceiver: List<ZapSplitSetup>? = null,
markAsSensitive: Boolean = false,
zapRaiserAmount: Long? = null,
geohash: String? = null,
nip94attachments: List<FileHeaderEvent>? = null,
): Array<Array<String>> {
val tags = mutableListOf<Array<String>>()
findHashtags(content).forEach {
val lowercaseTag = it.lowercase()
tags.add(arrayOf("t", it))
if (it != lowercaseTag) {
tags.add(arrayOf("t", it.lowercase()))
}
}
findURLs(content).forEach { tags.add(arrayOf("r", it)) }
zapReceiver?.forEach {
tags.add(arrayOf("zap", it.lnAddressOrPubKeyHex, it.relay ?: "", it.weight.toString()))
}
if (markAsSensitive) {
tags.add(arrayOf("content-warning", ""))
}
zapRaiserAmount?.let { tags.add(arrayOf("zapraiser", "$it")) }
geohash?.let { tags.addAll(geohashMipMap(it)) }
nip94attachments?.let {
it.forEach {
Nip92MediaAttachments().convertFromFileHeader(it)?.let {
tags.add(it)
}
}
}
return tags.toTypedArray()
}
fun makeTags(
baseId: String,
alt: String,
title: String,
summary: String? = null,
image: String? = null,
options: List<StoryOption> = emptyList(),
): Array<Array<String>> =
(
listOfNotNull(
arrayOf("d", baseId),
arrayOf("title", title),
summary?.let { arrayOf("summary", it) },
image?.let { arrayOf("image", it) },
arrayOf("alt", alt),
) +
options.map {
val relayUrl = it.address.relay
if (relayUrl != null) {
arrayOf("option", it.option, it.address.toTag(), relayUrl)
} else {
arrayOf("option", it.option, it.address.toTag())
}
}
).toTypedArray()
}
}
class StoryOption(
val option: String,
val address: ATag,
)

View File

@ -0,0 +1,78 @@
/**
* 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 com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.signers.NostrSigner
import com.vitorpamplona.quartz.utils.TimeUtils
class InteractiveStoryPrologueEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : InteractiveStoryBaseEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
companion object {
const val KIND = 30296
const val ALT = "The prologue of an interative story called "
fun createAddressATag(
pubKey: HexKey,
dtag: String,
): ATag = ATag(KIND, pubKey, dtag, null)
fun createAddressTag(
pubKey: HexKey,
dtag: String,
): String = ATag.assembleATag(KIND, pubKey, dtag)
fun create(
baseId: String,
title: String,
content: String,
options: List<StoryOption>,
summary: String? = null,
image: String? = null,
zapReceiver: List<ZapSplitSetup>? = null,
markAsSensitive: Boolean = false,
zapRaiserAmount: Long? = null,
geohash: String? = null,
nip94attachments: List<FileHeaderEvent>? = null,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
isDraft: Boolean,
onReady: (InteractiveStoryPrologueEvent) -> Unit,
) {
val tags =
makeTags(baseId, ALT + title, title, summary, image, options) +
generalTags(content, zapReceiver, markAsSensitive, zapRaiserAmount, geohash, nip94attachments)
if (isDraft) {
signer.assembleRumor(createdAt, KIND, tags, content, onReady)
} else {
signer.sign(createdAt, KIND, tags, content, onReady)
}
}
}
}

View File

@ -0,0 +1,143 @@
/**
* 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
import com.vitorpamplona.quartz.utils.removeTrailingNullsAndEmptyOthers
@Immutable
class InteractiveStoryReadingStateEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
fun title() = firstTag("title")
fun summary() = firstTag("summary")
fun image() = firstTag("image")
fun status() = firstTag("status")
fun root() =
tags.firstOrNull { it.size > 1 && it[0] == "A" }?.let {
ATag.parse(it[1], it.getOrNull(2))
}
fun currentScene() =
tags.firstOrNull { it.size > 1 && it[0] == "a" }?.let {
ATag.parse(it[1], it.getOrNull(2))
}
companion object {
const val KIND = 30298
const val ALT1 = "Interactive Story Reading state"
const val ALT2 = "The reading state of "
fun createAddressATag(
pubKey: HexKey,
dtag: String,
): ATag = ATag(KIND, pubKey, dtag, null)
fun createAddressTag(
pubKey: HexKey,
dtag: String,
): String = ATag.assembleATag(KIND, pubKey, dtag)
fun update(
base: InteractiveStoryReadingStateEvent,
currentScene: InteractiveStoryBaseEvent,
currentSceneRelay: String?,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (InteractiveStoryReadingStateEvent) -> Unit,
) {
val rootTag = base.dTag()
val sceneTag = currentScene.addressTag()
val status =
if (rootTag == sceneTag) {
"new"
} else if (currentScene.options().isEmpty()) {
"done"
} else {
"reading"
}
val tags =
base.tags.filter { it[0] != "a" && it[0] != "status" } +
listOf(
removeTrailingNullsAndEmptyOthers("a", sceneTag, currentSceneRelay),
arrayOf("status", status),
)
signer.sign(createdAt, KIND, tags.toTypedArray(), "", onReady)
}
fun create(
root: InteractiveStoryBaseEvent,
rootRelay: String?,
currentScene: InteractiveStoryBaseEvent,
currentSceneRelay: String?,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (InteractiveStoryReadingStateEvent) -> Unit,
) {
val rootTag = root.addressTag()
val sceneTag = currentScene.addressTag()
val status =
if (rootTag == sceneTag) {
"new"
} else if (currentScene.options().isEmpty()) {
"done"
} else {
"reading"
}
val tags =
listOfNotNull(
arrayOf("d", rootTag),
arrayOf("alt", root.title()?.let { ALT2 + it } ?: ALT1),
root.title()?.let { arrayOf("title", it) },
root.summary()?.let { arrayOf("summary", it) },
root.image()?.let { arrayOf("image", it) },
removeTrailingNullsAndEmptyOthers("A", rootTag, rootRelay),
removeTrailingNullsAndEmptyOthers("a", sceneTag, currentSceneRelay),
arrayOf("status", status),
).toTypedArray()
signer.sign(createdAt, KIND, tags, "", onReady)
}
}
enum class ReadingStatus {
NEW,
READING,
DONE,
}
}

View File

@ -0,0 +1,76 @@
/**
* 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 com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.signers.NostrSigner
import com.vitorpamplona.quartz.utils.TimeUtils
class InteractiveStorySceneEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : InteractiveStoryBaseEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
companion object {
const val KIND = 30297
const val ALT = "A scene of an interative story called "
fun createAddressATag(
pubKey: HexKey,
dtag: String,
): ATag = ATag(KIND, pubKey, dtag, null)
fun createAddressTag(
pubKey: HexKey,
dtag: String,
): String = ATag.assembleATag(KIND, pubKey, dtag)
fun create(
baseId: String,
title: String,
content: String,
options: List<StoryOption>,
zapReceiver: List<ZapSplitSetup>? = null,
markAsSensitive: Boolean = false,
zapRaiserAmount: Long? = null,
geohash: String? = null,
nip94attachments: List<FileHeaderEvent>? = null,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
isDraft: Boolean,
onReady: (InteractiveStorySceneEvent) -> Unit,
) {
val tags =
makeTags(baseId, ALT + title, title, options = options) +
generalTags(content, zapReceiver, markAsSensitive, zapRaiserAmount, geohash, nip94attachments)
if (isDraft) {
signer.assembleRumor(createdAt, KIND, tags, content, onReady)
} else {
signer.sign(createdAt, KIND, tags, content, onReady)
}
}
}
}

View File

@ -38,7 +38,7 @@ class LongTextNoteEvent(
AddressableEvent {
override fun dTag() = tags.firstOrNull { it.size > 1 && it[0] == "d" }?.get(1) ?: ""
override fun address() = ATag(kind, pubKey, dTag(), null)
override fun address(relayHint: String?) = ATag(kind, pubKey, dTag(), relayHint)
override fun addressTag() = ATag.assembleATag(kind, pubKey, dTag())

View File

@ -38,7 +38,7 @@ class WikiNoteEvent(
AddressableEvent {
override fun dTag() = tags.firstOrNull { it.size > 1 && it[0] == "d" }?.get(1) ?: ""
override fun address() = ATag(kind, pubKey, dTag(), null)
override fun address(relayHint: String?) = ATag(kind, pubKey, dTag(), relayHint)
override fun addressTag() = ATag.assembleATag(kind, pubKey, dTag())