mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-03-26 17:52:29 +01:00
Adds support for Interactive Stories
This commit is contained in:
parent
034ee46543
commit
bdf012f641
@ -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 {
|
||||
|
@ -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 -> {
|
||||
|
@ -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,
|
||||
|
@ -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?) {
|
||||
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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,
|
||||
)
|
||||
|
@ -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,
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
)
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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())
|
||||
|
||||
|
@ -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())
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user