This commit is contained in:
Vitor Pamplona
2024-11-19 15:36:30 -05:00
parent 248848799c
commit 4db6f5531c
23 changed files with 791 additions and 573 deletions
@@ -67,6 +67,7 @@ import com.vitorpamplona.quartz.events.CommentEvent
import com.vitorpamplona.quartz.events.Contact import com.vitorpamplona.quartz.events.Contact
import com.vitorpamplona.quartz.events.ContactListEvent import com.vitorpamplona.quartz.events.ContactListEvent
import com.vitorpamplona.quartz.events.DeletionEvent import com.vitorpamplona.quartz.events.DeletionEvent
import com.vitorpamplona.quartz.events.Dimension
import com.vitorpamplona.quartz.events.DraftEvent import com.vitorpamplona.quartz.events.DraftEvent
import com.vitorpamplona.quartz.events.EmojiPackEvent import com.vitorpamplona.quartz.events.EmojiPackEvent
import com.vitorpamplona.quartz.events.EmojiPackSelectionEvent import com.vitorpamplona.quartz.events.EmojiPackSelectionEvent
@@ -2190,34 +2191,68 @@ class Account(
} }
} }
CommentEvent.replyComment( if (replyingTo.event is CommentEvent) {
msg = message, CommentEvent.replyComment(
replyingTo = EventHint(replyingTo.event as CommentEvent, replyingTo.relayHintUrl()), msg = message,
usersMentioned = usersMentioned, replyingTo = EventHint(replyingTo.event as CommentEvent, replyingTo.relayHintUrl()),
addressesMentioned = addressesMentioned, usersMentioned = usersMentioned,
eventsMentioned = eventsMentioned, addressesMentioned = addressesMentioned,
nip94attachments = nip94attachments, eventsMentioned = eventsMentioned,
geohash = geohash, nip94attachments = nip94attachments,
zapReceiver = zapReceiver, geohash = geohash,
markAsSensitive = wantsToMarkAsSensitive, zapReceiver = zapReceiver,
zapRaiserAmount = zapRaiserAmount, markAsSensitive = wantsToMarkAsSensitive,
isDraft = draftTag != null, zapRaiserAmount = zapRaiserAmount,
signer = signer, isDraft = draftTag != null,
) { signer = signer,
if (draftTag != null) { ) {
if (message.isBlank()) { if (draftTag != null) {
deleteDraft(draftTag) if (message.isBlank()) {
deleteDraft(draftTag)
} else {
DraftEvent.create(draftTag, it, signer) { draftEvent ->
sendDraftEvent(draftEvent)
}
}
} else { } else {
DraftEvent.create(draftTag, it, signer) { draftEvent -> Client.send(it, relayList = relayList)
sendDraftEvent(draftEvent) LocalCache.justConsume(it, null)
replyingTo.event?.let {
Client.send(it, relayList = relayList)
} }
} }
} else { }
Client.send(it, relayList = relayList) } else {
LocalCache.justConsume(it, null) CommentEvent.firstReplyToEvent(
msg = message,
replyingTo.event?.let { replyingTo = EventHint(replyingTo.event as Event, replyingTo.relayHintUrl()),
usersMentioned = usersMentioned,
addressesMentioned = addressesMentioned,
eventsMentioned = eventsMentioned,
nip94attachments = nip94attachments,
geohash = geohash,
zapReceiver = zapReceiver,
markAsSensitive = wantsToMarkAsSensitive,
zapRaiserAmount = zapRaiserAmount,
isDraft = draftTag != null,
signer = signer,
) {
if (draftTag != null) {
if (message.isBlank()) {
deleteDraft(draftTag)
} else {
DraftEvent.create(draftTag, it, signer) { draftEvent ->
sendDraftEvent(draftEvent)
}
}
} else {
Client.send(it, relayList = relayList) Client.send(it, relayList = relayList)
LocalCache.justConsume(it, null)
replyingTo.event?.let {
Client.send(it, relayList = relayList)
}
} }
} }
} }
@@ -2882,7 +2917,7 @@ class Account(
url: String, url: String,
relay: String?, relay: String?,
blurhash: String?, blurhash: String?,
dim: String?, dim: Dimension?,
hash: String?, hash: String?,
mimeType: String?, mimeType: String?,
) { ) {
@@ -47,6 +47,7 @@ import com.vitorpamplona.quartz.events.BadgeAwardEvent
import com.vitorpamplona.quartz.events.BadgeDefinitionEvent import com.vitorpamplona.quartz.events.BadgeDefinitionEvent
import com.vitorpamplona.quartz.events.BadgeProfilesEvent import com.vitorpamplona.quartz.events.BadgeProfilesEvent
import com.vitorpamplona.quartz.events.BaseAddressableEvent import com.vitorpamplona.quartz.events.BaseAddressableEvent
import com.vitorpamplona.quartz.events.BaseTextNoteEvent
import com.vitorpamplona.quartz.events.BookmarkListEvent import com.vitorpamplona.quartz.events.BookmarkListEvent
import com.vitorpamplona.quartz.events.CalendarDateSlotEvent import com.vitorpamplona.quartz.events.CalendarDateSlotEvent
import com.vitorpamplona.quartz.events.CalendarEvent import com.vitorpamplona.quartz.events.CalendarEvent
@@ -101,6 +102,7 @@ import com.vitorpamplona.quartz.events.NIP90UserDiscoveryResponseEvent
import com.vitorpamplona.quartz.events.NNSEvent import com.vitorpamplona.quartz.events.NNSEvent
import com.vitorpamplona.quartz.events.OtsEvent import com.vitorpamplona.quartz.events.OtsEvent
import com.vitorpamplona.quartz.events.PeopleListEvent import com.vitorpamplona.quartz.events.PeopleListEvent
import com.vitorpamplona.quartz.events.PictureEvent
import com.vitorpamplona.quartz.events.PinListEvent import com.vitorpamplona.quartz.events.PinListEvent
import com.vitorpamplona.quartz.events.PollNoteEvent import com.vitorpamplona.quartz.events.PollNoteEvent
import com.vitorpamplona.quartz.events.PrivateDmEvent import com.vitorpamplona.quartz.events.PrivateDmEvent
@@ -439,61 +441,15 @@ object LocalCache {
fun consume( fun consume(
event: TextNoteEvent, event: TextNoteEvent,
relay: Relay? = null, relay: Relay? = null,
) { ) = consumeRegularEvent(event, relay)
val note = getOrCreateNote(event.id)
val author = getOrCreateUser(event.pubKey)
if (relay != null) {
author.addRelayBeingUsed(relay, event.createdAt)
note.addRelay(relay)
}
// Already processed this event.
if (note.event != null) return
if (antiSpam.isSpam(event, relay)) {
return
}
val replyTo = computeReplyTo(event)
note.loadEvent(event, author, replyTo)
// Log.d("TN", "New Note (${notes.size},${users.size}) ${note.author?.toBestDisplayName()}
// ${note.event?.content()?.split("\n")?.take(100)} ${formattedDateTime(event.createdAt)}")
// Counts the replies
replyTo.forEach { it.addReply(note) }
refreshObservers(note)
}
fun consume( fun consume(
event: TorrentEvent, event: TorrentEvent,
relay: Relay?, relay: Relay?,
) { ) = consumeRegularEvent(event, relay)
val note = getOrCreateNote(event.id)
val author = getOrCreateUser(event.pubKey)
if (relay != null) { fun consumeRegularEvent(
author.addRelayBeingUsed(relay, event.createdAt) event: Event,
note.addRelay(relay)
}
// Already processed this event.
if (note.event != null) return
if (antiSpam.isSpam(event, relay)) {
return
}
note.loadEvent(event, author, emptyList())
refreshObservers(note)
}
fun consume(
event: TorrentCommentEvent,
relay: Relay?, relay: Relay?,
) { ) {
val note = getOrCreateNote(event.id) val note = getOrCreateNote(event.id)
@@ -507,242 +463,69 @@ object LocalCache {
// Already processed this event. // Already processed this event.
if (note.event != null) return if (note.event != null) return
if (antiSpam.isSpam(event, relay)) { val replyTo = computeReplyTo(event)
if (event is BaseTextNoteEvent && antiSpam.isSpam(event, relay)) {
return return
} }
val replyTo = computeReplyTo(event)
note.loadEvent(event, author, replyTo) note.loadEvent(event, author, replyTo)
// Counts the replies // Counts the replies
replyTo.forEach { replyTo.forEach { it.addReply(note) }
it.addReply(note)
}
refreshObservers(note) refreshObservers(note)
} }
fun consume(
event: PictureEvent,
relay: Relay?,
) = consumeRegularEvent(event, relay)
fun consume(
event: TorrentCommentEvent,
relay: Relay?,
) = consumeRegularEvent(event, relay)
fun consume( fun consume(
event: NIP90ContentDiscoveryResponseEvent, event: NIP90ContentDiscoveryResponseEvent,
relay: Relay? = null, relay: Relay?,
) { ) = consumeRegularEvent(event, relay)
val note = getOrCreateNote(event.id)
val author = getOrCreateUser(event.pubKey)
// Log.d("TN", "New Response ${event.taggedEvents().joinToString(", ") { it }}}")
if (relay != null) {
author.addRelayBeingUsed(relay, event.createdAt)
note.addRelay(relay)
}
// Already processed this event.
if (note.event != null) return
val replyTo = computeReplyTo(event)
note.loadEvent(event, author, replyTo)
// Log.d("TN", "New Note (${notes.size},${users.size}) ${note.author?.toBestDisplayName()}
// ${note.event?.content()?.split("\n")?.take(100)} ${formattedDateTime(event.createdAt)}")
// Counts the replies
replyTo.forEach { it.addReply(note) }
refreshObservers(note)
}
fun consume( fun consume(
event: NIP90ContentDiscoveryRequestEvent, event: NIP90ContentDiscoveryRequestEvent,
relay: Relay? = null, relay: Relay?,
) { ) = consumeRegularEvent(event, relay)
val note = getOrCreateNote(event.id)
val author = getOrCreateUser(event.pubKey)
if (relay != null) {
author.addRelayBeingUsed(relay, event.createdAt)
note.addRelay(relay)
}
// Already processed this event.
if (note.event != null) return
val replyTo = computeReplyTo(event)
note.loadEvent(event, author, replyTo)
// Log.d("TN", "New Note (${notes.size},${users.size}) ${note.author?.toBestDisplayName()}
// ${note.event?.content()?.split("\n")?.take(100)} ${formattedDateTime(event.createdAt)}")
// Counts the replies
replyTo.forEach { it.addReply(note) }
refreshObservers(note)
}
fun consume( fun consume(
event: NIP90StatusEvent, event: NIP90StatusEvent,
relay: Relay? = null, relay: Relay?,
) { ) = consumeRegularEvent(event, relay)
val note = getOrCreateNote(event.id)
val author = getOrCreateUser(event.pubKey)
if (relay != null) {
author.addRelayBeingUsed(relay, event.createdAt)
note.addRelay(relay)
}
// Already processed this event.
if (note.event != null) return
val replyTo = computeReplyTo(event)
note.loadEvent(event, author, replyTo)
// Log.d("TN", "New Note (${notes.size},${users.size}) ${note.author?.toBestDisplayName()}
// ${note.event?.content()?.split("\n")?.take(100)} ${formattedDateTime(event.createdAt)}")
// Counts the replies
replyTo.forEach { it.addReply(note) }
refreshObservers(note)
}
fun consume( fun consume(
event: NIP90UserDiscoveryResponseEvent, event: NIP90UserDiscoveryResponseEvent,
relay: Relay? = null, relay: Relay?,
) { ) = consumeRegularEvent(event, relay)
val note = getOrCreateNote(event.id)
val author = getOrCreateUser(event.pubKey)
if (relay != null) {
author.addRelayBeingUsed(relay, event.createdAt)
note.addRelay(relay)
}
// Already processed this event.
if (note.event != null) return
val replyTo = computeReplyTo(event)
note.loadEvent(event, author, replyTo)
// Log.d("TN", "New Note (${notes.size},${users.size}) ${note.author?.toBestDisplayName()}
// ${note.event?.content()?.split("\n")?.take(100)} ${formattedDateTime(event.createdAt)}")
// Counts the replies
replyTo.forEach { it.addReply(note) }
refreshObservers(note)
}
fun consume( fun consume(
event: NIP90UserDiscoveryRequestEvent, event: NIP90UserDiscoveryRequestEvent,
relay: Relay? = null, relay: Relay?,
) { ) = consumeRegularEvent(event, relay)
val note = getOrCreateNote(event.id)
val author = getOrCreateUser(event.pubKey)
if (relay != null) {
author.addRelayBeingUsed(relay, event.createdAt)
note.addRelay(relay)
}
// Already processed this event.
if (note.event != null) return
val replyTo = computeReplyTo(event)
note.loadEvent(event, author, replyTo)
// Log.d("TN", "New Note (${notes.size},${users.size}) ${note.author?.toBestDisplayName()}
// ${note.event?.content()?.split("\n")?.take(100)} ${formattedDateTime(event.createdAt)}")
// Counts the replies
replyTo.forEach { it.addReply(note) }
refreshObservers(note)
}
fun consume( fun consume(
event: GitPatchEvent, event: GitPatchEvent,
relay: Relay? = null, relay: Relay?,
) { ) = consumeRegularEvent(event, relay)
val note = getOrCreateNote(event.id)
val author = getOrCreateUser(event.pubKey)
if (relay != null) {
author.addRelayBeingUsed(relay, event.createdAt)
note.addRelay(relay)
}
// Already processed this event.
if (note.event != null) return
if (antiSpam.isSpam(event, relay)) {
return
}
note.loadEvent(event, author, emptyList())
refreshObservers(note)
}
fun consume( fun consume(
event: GitIssueEvent, event: GitIssueEvent,
relay: Relay? = null, relay: Relay?,
) { ) = consumeRegularEvent(event, relay)
val note = getOrCreateNote(event.id)
val author = getOrCreateUser(event.pubKey)
if (relay != null) {
author.addRelayBeingUsed(relay, event.createdAt)
note.addRelay(relay)
}
// Already processed this event.
if (note.event != null) return
if (antiSpam.isSpam(event, relay)) {
return
}
note.loadEvent(event, author, emptyList())
refreshObservers(note)
}
fun consume( fun consume(
event: GitReplyEvent, event: GitReplyEvent,
relay: Relay? = null, relay: Relay?,
) { ) = consumeRegularEvent(event, relay)
val note = getOrCreateNote(event.id)
val author = getOrCreateUser(event.pubKey)
if (relay != null) {
author.addRelayBeingUsed(relay, event.createdAt)
note.addRelay(relay)
}
// Already processed this event.
if (note.event != null) return
if (antiSpam.isSpam(event, relay)) {
return
}
val replyTo = computeReplyTo(event)
// println("New GitReply ${event.id} for ${replyTo.firstOrNull()?.event?.id()} ${event.tagsWithoutCitations().filter { it != event.repository()?.toTag() }.firstOrNull()}")
note.loadEvent(event, author, replyTo)
// Counts the replies
replyTo.forEach { it.addReply(note) }
refreshObservers(note)
}
fun consume( fun consume(
event: LongTextNoteEvent, event: LongTextNoteEvent,
@@ -869,35 +652,8 @@ object LocalCache {
fun consume( fun consume(
event: PollNoteEvent, event: PollNoteEvent,
relay: Relay? = null, relay: Relay?,
) { ) = consumeRegularEvent(event, relay)
val note = getOrCreateNote(event.id)
val author = getOrCreateUser(event.pubKey)
if (relay != null) {
author.addRelayBeingUsed(relay, event.createdAt)
note.addRelay(relay)
}
// Already processed this event.
if (note.event != null) return
if (antiSpam.isSpam(event, relay)) {
return
}
val replyTo = computeReplyTo(event)
note.loadEvent(event, author, replyTo)
// Log.d("TN", "New Note (${notes.size},${users.size}) ${note.author?.toBestDisplayName()}
// ${note.event?.content()?.split("\n")?.take(100)} ${formattedDateTime(event.createdAt)}")
// Counts the replies
replyTo.forEach { it.addReply(note) }
refreshObservers(note)
}
private fun consume( private fun consume(
event: LiveActivitiesEvent, event: LiveActivitiesEvent,
@@ -1137,25 +893,10 @@ object LocalCache {
} }
} }
fun consume(event: BadgeAwardEvent) { fun consume(
val note = getOrCreateNote(event.id) event: BadgeAwardEvent,
relay: Relay?,
// Already processed this event. ) = consumeRegularEvent(event, relay)
if (note.event != null) return
// Log.d("TN", "New Boost (${notes.size},${users.size}) ${note.author?.toBestDisplayName()}
// ${formattedDateTime(event.createdAt)}")
val author = getOrCreateUser(event.pubKey)
val awardDefinition = computeReplyTo(event)
note.loadEvent(event, author, awardDefinition)
// Replies of an Badge Definition are Award Events
awardDefinition.forEach { it.addReply(note) }
refreshObservers(note)
}
private fun comsume( private fun comsume(
event: NNSEvent, event: NNSEvent,
@@ -1638,31 +1379,7 @@ object LocalCache {
fun consume( fun consume(
event: CommentEvent, event: CommentEvent,
relay: Relay?, relay: Relay?,
) { ) = consumeRegularEvent(event, relay)
val note = getOrCreateNote(event.id)
val author = getOrCreateUser(event.pubKey)
if (relay != null) {
author.addRelayBeingUsed(relay, event.createdAt)
note.addRelay(relay)
}
// Already processed this event.
if (note.event != null) return
if (antiSpam.isSpam(event, relay)) {
return
}
val replyTo = computeReplyTo(event)
note.loadEvent(event, author, replyTo)
// Counts the replies
replyTo.forEach { it.addReply(note) }
refreshObservers(note)
}
fun consume( fun consume(
event: LiveActivitiesChatMessageEvent, event: LiveActivitiesChatMessageEvent,
@@ -1759,102 +1476,27 @@ object LocalCache {
fun consume( fun consume(
event: AudioHeaderEvent, event: AudioHeaderEvent,
relay: Relay?, relay: Relay?,
) { ) = consumeRegularEvent(event, relay)
val note = getOrCreateNote(event.id)
val author = getOrCreateUser(event.pubKey)
if (relay != null) {
author.addRelayBeingUsed(relay, event.createdAt)
note.addRelay(relay)
}
// Already processed this event.
if (note.event != null) return
note.loadEvent(event, author, emptyList())
refreshObservers(note)
}
fun consume( fun consume(
event: FileHeaderEvent, event: FileHeaderEvent,
relay: Relay?, relay: Relay?,
) { ) = consumeRegularEvent(event, relay)
val note = getOrCreateNote(event.id)
val author = getOrCreateUser(event.pubKey)
if (relay != null) {
author.addRelayBeingUsed(relay, event.createdAt)
note.addRelay(relay)
}
// Already processed this event.
if (note.event != null) return
note.loadEvent(event, author, emptyList())
refreshObservers(note)
}
fun consume( fun consume(
event: ProfileGalleryEntryEvent, event: ProfileGalleryEntryEvent,
relay: Relay?, relay: Relay?,
) { ) = consumeRegularEvent(event, relay)
val note = getOrCreateNote(event.id)
val author = getOrCreateUser(event.pubKey)
if (relay != null) {
author.addRelayBeingUsed(relay, event.createdAt)
note.addRelay(relay)
}
// Already processed this event.
if (note.event != null) return
note.loadEvent(event, author, emptyList())
refreshObservers(note)
}
fun consume( fun consume(
event: FileStorageHeaderEvent, event: FileStorageHeaderEvent,
relay: Relay?, relay: Relay?,
) { ) = consumeRegularEvent(event, relay)
val note = getOrCreateNote(event.id)
val author = getOrCreateUser(event.pubKey)
if (relay != null) {
author.addRelayBeingUsed(relay, event.createdAt)
note.addRelay(relay)
}
// Already processed this event.
if (note.event != null) return
note.loadEvent(event, author, emptyList())
refreshObservers(note)
}
fun consume( fun consume(
event: FhirResourceEvent, event: FhirResourceEvent,
relay: Relay?, relay: Relay?,
) { ) = consumeRegularEvent(event, relay)
val note = getOrCreateNote(event.id)
val author = getOrCreateUser(event.pubKey)
if (relay != null) {
author.addRelayBeingUsed(relay, event.createdAt)
note.addRelay(relay)
}
// Already processed this event.
if (note.event != null) return
note.loadEvent(event, author, emptyList())
refreshObservers(note)
}
fun consume( fun consume(
event: TextNoteModificationEvent, event: TextNoteModificationEvent,
@@ -1887,22 +1529,7 @@ object LocalCache {
fun consume( fun consume(
event: HighlightEvent, event: HighlightEvent,
relay: Relay?, relay: Relay?,
) { ) = consumeRegularEvent(event, relay)
val note = getOrCreateNote(event.id)
val author = getOrCreateUser(event.pubKey)
if (relay != null) {
author.addRelayBeingUsed(relay, event.createdAt)
note.addRelay(relay)
}
// Already processed this event.
if (note.event != null) return
note.loadEvent(event, author, emptyList())
refreshObservers(note)
}
fun consume( fun consume(
event: FileStorageEvent, event: FileStorageEvent,
@@ -2127,8 +1754,7 @@ object LocalCache {
} }
if (note.event?.matchTag1With(text) == true || if (note.event?.matchTag1With(text) == true ||
note.idHex.startsWith(text, true) || note.idHex.startsWith(text, true)
note.idNote().startsWith(text, true)
) { ) {
if (!note.isHiddenFor(forAccount.flowHiddenUsers.value)) { if (!note.isHiddenFor(forAccount.flowHiddenUsers.value)) {
return@filter true return@filter true
@@ -2713,7 +2339,7 @@ object LocalCache {
is AppSpecificDataEvent -> consume(event, relay) is AppSpecificDataEvent -> consume(event, relay)
is AudioHeaderEvent -> consume(event, relay) is AudioHeaderEvent -> consume(event, relay)
is AudioTrackEvent -> consume(event, relay) is AudioTrackEvent -> consume(event, relay)
is BadgeAwardEvent -> consume(event) is BadgeAwardEvent -> consume(event, relay)
is BadgeDefinitionEvent -> consume(event, relay) is BadgeDefinitionEvent -> consume(event, relay)
is BadgeProfilesEvent -> consume(event) is BadgeProfilesEvent -> consume(event)
is BookmarkListEvent -> consume(event) is BookmarkListEvent -> consume(event)
@@ -2780,6 +2406,7 @@ object LocalCache {
is MuteListEvent -> consume(event, relay) is MuteListEvent -> consume(event, relay)
is NNSEvent -> comsume(event, relay) is NNSEvent -> comsume(event, relay)
is OtsEvent -> consume(event, relay) is OtsEvent -> consume(event, relay)
is PictureEvent -> consume(event, relay)
is PrivateDmEvent -> consume(event, relay) is PrivateDmEvent -> consume(event, relay)
is PrivateOutboxRelayListEvent -> consume(event, relay) is PrivateOutboxRelayListEvent -> consume(event, relay)
is PinListEvent -> consume(event, relay) is PinListEvent -> consume(event, relay)
@@ -29,6 +29,7 @@ import com.vitorpamplona.ammolite.relays.filters.SinceAuthorPerRelayFilter
import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
import com.vitorpamplona.quartz.events.FileHeaderEvent import com.vitorpamplona.quartz.events.FileHeaderEvent
import com.vitorpamplona.quartz.events.FileStorageHeaderEvent import com.vitorpamplona.quartz.events.FileStorageHeaderEvent
import com.vitorpamplona.quartz.events.PictureEvent
import com.vitorpamplona.quartz.events.VideoHorizontalEvent import com.vitorpamplona.quartz.events.VideoHorizontalEvent
import com.vitorpamplona.quartz.events.VideoVerticalEvent import com.vitorpamplona.quartz.events.VideoVerticalEvent
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -64,85 +65,139 @@ object NostrVideoDataSource : AmethystNostrDataSource("VideoFeed") {
job?.cancel() job?.cancel()
} }
fun createContextualFilter(): TypedFilter { fun createContextualFilter(): List<TypedFilter> {
val follows = account.liveStoriesListAuthorsPerRelay.value val follows = account.liveStoriesListAuthorsPerRelay.value
return TypedFilter( val types = if (follows == null) setOf(FeedType.GLOBAL) else setOf(FeedType.FOLLOWS)
types = if (follows == null) setOf(FeedType.GLOBAL) else setOf(FeedType.FOLLOWS),
filter = return listOf(
SinceAuthorPerRelayFilter( TypedFilter(
authors = follows, types = types,
kinds = listOf(FileHeaderEvent.KIND, FileStorageHeaderEvent.KIND, VideoHorizontalEvent.KIND, VideoVerticalEvent.KIND), filter =
limit = 200, SinceAuthorPerRelayFilter(
tags = mapOf("m" to SUPPORTED_VIDEO_FEED_MIME_TYPES), authors = follows,
since = kinds = listOf(PictureEvent.KIND, VideoHorizontalEvent.KIND, VideoVerticalEvent.KIND),
latestEOSEs.users[account.userProfile()] limit = 200,
?.followList since =
?.get(account.settings.defaultStoriesFollowList.value) latestEOSEs.users[account.userProfile()]
?.relayList, ?.followList
), ?.get(account.settings.defaultStoriesFollowList.value)
?.relayList,
),
),
TypedFilter(
types = types,
filter =
SinceAuthorPerRelayFilter(
authors = follows,
kinds = listOf(FileHeaderEvent.KIND, FileStorageHeaderEvent.KIND),
limit = 200,
tags = mapOf("m" to SUPPORTED_VIDEO_FEED_MIME_TYPES),
since =
latestEOSEs.users[account.userProfile()]
?.followList
?.get(account.settings.defaultStoriesFollowList.value)
?.relayList,
),
),
) )
} }
fun createFollowTagsFilter(): TypedFilter? { fun createFollowTagsFilter(): List<TypedFilter> {
val hashToLoad = val hashToLoad =
account.liveStoriesFollowLists.value account.liveStoriesFollowLists.value
?.hashtags ?.hashtags
?.toList() ?: return null ?.toList() ?: return emptyList()
if (hashToLoad.isEmpty()) return null if (hashToLoad.isEmpty()) return emptyList()
return TypedFilter( val hashtags =
types = setOf(FeedType.GLOBAL), hashToLoad
filter = .map { listOf(it, it.lowercase(), it.uppercase(), it.capitalize()) }
SincePerRelayFilter( .flatten()
kinds = listOf(FileHeaderEvent.KIND, FileStorageHeaderEvent.KIND, VideoHorizontalEvent.KIND, VideoVerticalEvent.KIND),
tags = return listOf(
mapOf( TypedFilter(
"t" to types = setOf(FeedType.GLOBAL),
hashToLoad filter =
.map { listOf(it, it.lowercase(), it.uppercase(), it.capitalize()) } SincePerRelayFilter(
.flatten(), kinds = listOf(PictureEvent.KIND, VideoHorizontalEvent.KIND, VideoVerticalEvent.KIND),
"m" to SUPPORTED_VIDEO_FEED_MIME_TYPES, tags = mapOf("t" to hashtags),
), limit = 100,
limit = 100, since =
since = latestEOSEs.users[account.userProfile()]
latestEOSEs.users[account.userProfile()] ?.followList
?.followList ?.get(account.settings.defaultStoriesFollowList.value)
?.get(account.settings.defaultStoriesFollowList.value) ?.relayList,
?.relayList, ),
), ),
TypedFilter(
types = setOf(FeedType.GLOBAL),
filter =
SincePerRelayFilter(
kinds = listOf(FileHeaderEvent.KIND, FileStorageHeaderEvent.KIND),
tags =
mapOf(
"t" to hashtags,
"m" to SUPPORTED_VIDEO_FEED_MIME_TYPES,
),
limit = 100,
since =
latestEOSEs.users[account.userProfile()]
?.followList
?.get(account.settings.defaultStoriesFollowList.value)
?.relayList,
),
),
) )
} }
fun createFollowGeohashesFilter(): TypedFilter? { fun createFollowGeohashesFilter(): List<TypedFilter> {
val hashToLoad = val hashToLoad =
account.liveStoriesFollowLists.value account.liveStoriesFollowLists.value
?.geotags ?.geotags
?.toList() ?: return null ?.toList() ?: return emptyList()
if (hashToLoad.isEmpty()) return null if (hashToLoad.isEmpty()) return emptyList()
return TypedFilter( val geoHashes =
types = setOf(FeedType.GLOBAL), hashToLoad
filter = .map { listOf(it, it.lowercase(), it.uppercase(), it.capitalize()) }
SincePerRelayFilter( .flatten()
kinds = listOf(FileHeaderEvent.KIND, FileStorageHeaderEvent.KIND, VideoHorizontalEvent.KIND, VideoVerticalEvent.KIND),
tags = return listOf(
mapOf( TypedFilter(
"g" to types = setOf(FeedType.GLOBAL),
hashToLoad filter =
.map { listOf(it, it.lowercase(), it.uppercase(), it.capitalize()) } SincePerRelayFilter(
.flatten(), kinds = listOf(PictureEvent.KIND, VideoHorizontalEvent.KIND, VideoVerticalEvent.KIND),
"m" to SUPPORTED_VIDEO_FEED_MIME_TYPES, tags = mapOf("g" to geoHashes),
), limit = 100,
limit = 100, since =
since = latestEOSEs.users[account.userProfile()]
latestEOSEs.users[account.userProfile()] ?.followList
?.followList ?.get(account.settings.defaultStoriesFollowList.value)
?.get(account.settings.defaultStoriesFollowList.value) ?.relayList,
?.relayList, ),
), ),
TypedFilter(
types = setOf(FeedType.GLOBAL),
filter =
SincePerRelayFilter(
kinds = listOf(FileHeaderEvent.KIND, FileStorageHeaderEvent.KIND),
tags =
mapOf(
"g" to geoHashes,
"m" to SUPPORTED_VIDEO_FEED_MIME_TYPES,
),
limit = 100,
since =
latestEOSEs.users[account.userProfile()]
?.followList
?.get(account.settings.defaultStoriesFollowList.value)
?.relayList,
),
),
) )
} }
@@ -162,6 +217,6 @@ object NostrVideoDataSource : AmethystNostrDataSource("VideoFeed") {
createContextualFilter(), createContextualFilter(),
createFollowTagsFilter(), createFollowTagsFilter(),
createFollowGeohashesFilter(), createFollowGeohashesFilter(),
).ifEmpty { null } ).flatten().ifEmpty { null }
} }
} }
@@ -554,7 +554,7 @@ open class NewPostViewModel : ViewModel() {
val replyingTo = originalNote val replyingTo = originalNote
if (replyingTo?.event is CommentEvent) { if (replyingTo?.event is CommentEvent || replyingTo?.event is Event) {
account?.sendReplyComment( account?.sendReplyComment(
message = tagger.message, message = tagger.message,
replyingTo = replyingTo, replyingTo = replyingTo,
@@ -120,6 +120,7 @@ import com.vitorpamplona.amethyst.ui.theme.VolumeBottomIconSize
import com.vitorpamplona.amethyst.ui.theme.imageModifier import com.vitorpamplona.amethyst.ui.theme.imageModifier
import com.vitorpamplona.amethyst.ui.theme.videoGalleryModifier import com.vitorpamplona.amethyst.ui.theme.videoGalleryModifier
import com.vitorpamplona.ammolite.service.HttpClientManager import com.vitorpamplona.ammolite.service.HttpClientManager
import com.vitorpamplona.quartz.events.Dimension
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -211,7 +212,7 @@ fun VideoView(
waveform: ImmutableList<Int>? = null, waveform: ImmutableList<Int>? = null,
artworkUri: String? = null, artworkUri: String? = null,
authorName: String? = null, authorName: String? = null,
dimensions: String? = null, dimensions: Dimension? = null,
blurhash: String? = null, blurhash: String? = null,
nostrUriCallback: String? = null, nostrUriCallback: String? = null,
onDialog: ((Boolean) -> Unit)? = null, onDialog: ((Boolean) -> Unit)? = null,
@@ -242,7 +243,7 @@ fun VideoView(
waveform: ImmutableList<Int>? = null, waveform: ImmutableList<Int>? = null,
artworkUri: String? = null, artworkUri: String? = null,
authorName: String? = null, authorName: String? = null,
dimensions: String? = null, dimensions: Dimension? = null,
blurhash: String? = null, blurhash: String? = null,
nostrUriCallback: String? = null, nostrUriCallback: String? = null,
onDialog: ((Boolean) -> Unit)? = null, onDialog: ((Boolean) -> Unit)? = null,
@@ -260,7 +261,7 @@ fun VideoView(
} }
if (blurhash == null) { if (blurhash == null) {
val ratio = aspectRatio(dimensions) val ratio = dimensions?.aspectRatio()
val modifier = val modifier =
if (ratio != null && automaticallyStartPlayback.value) { if (ratio != null && automaticallyStartPlayback.value) {
Modifier.aspectRatio(ratio) Modifier.aspectRatio(ratio)
@@ -294,7 +295,7 @@ fun VideoView(
} }
} }
} else { } else {
val ratio = aspectRatio(dimensions) val ratio = dimensions?.aspectRatio()
val modifier = val modifier =
if (ratio != null) { if (ratio != null) {
@@ -98,9 +98,9 @@ import com.vitorpamplona.amethyst.ui.theme.videoGalleryModifier
import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.Nip19Bech32 import com.vitorpamplona.quartz.encoders.Nip19Bech32
import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.events.Dimension
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -389,7 +389,7 @@ fun UrlImageView(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
alwayShowImage: Boolean = false, alwayShowImage: Boolean = false,
) { ) {
val ratio = remember(content) { aspectRatio(content.dim) } val ratio = content.dim?.aspectRatio()
val showImage = val showImage =
remember { remember {
@@ -552,26 +552,10 @@ fun ShowHash(content: MediaUrlContent) {
verifiedHash?.let { HashVerificationSymbol(it) } verifiedHash?.let { HashVerificationSymbol(it) }
} }
fun aspectRatio(dim: String?): Float? { fun aspectRatio(dim: Dimension?): Float? {
if (dim == null) return null if (dim == null) return null
if (dim == "0x0") return null
val parts = dim.split("x") return dim.width.toFloat() / dim.height.toFloat()
if (parts.size != 2) return null
return try {
val width = parts[0].toFloat()
val height = parts[1].toFloat()
if (width < 0.1 || height < 0.1) {
null
} else {
width / height
}
} catch (e: Exception) {
if (e is CancellationException) throw e
null
}
} }
@Composable @Composable
@@ -719,7 +703,7 @@ fun ShareImageAction(
videoUri: String?, videoUri: String?,
postNostrUri: String?, postNostrUri: String?,
blurhash: String?, blurhash: String?,
dim: String?, dim: Dimension?,
hash: String?, hash: String?,
mimeType: String?, mimeType: String?,
onDismiss: () -> Unit, onDismiss: () -> Unit,
@@ -20,6 +20,7 @@
*/ */
package com.vitorpamplona.amethyst.ui.dal package com.vitorpamplona.amethyst.ui.dal
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser.Companion.isImageOrVideoUrl
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
@@ -28,6 +29,7 @@ import com.vitorpamplona.quartz.events.FileHeaderEvent
import com.vitorpamplona.quartz.events.FileStorageHeaderEvent import com.vitorpamplona.quartz.events.FileStorageHeaderEvent
import com.vitorpamplona.quartz.events.MuteListEvent import com.vitorpamplona.quartz.events.MuteListEvent
import com.vitorpamplona.quartz.events.PeopleListEvent import com.vitorpamplona.quartz.events.PeopleListEvent
import com.vitorpamplona.quartz.events.PictureEvent
import com.vitorpamplona.quartz.events.VideoHorizontalEvent import com.vitorpamplona.quartz.events.VideoHorizontalEvent
import com.vitorpamplona.quartz.events.VideoVerticalEvent import com.vitorpamplona.quartz.events.VideoVerticalEvent
@@ -66,10 +68,11 @@ class VideoFeedFilter(
val noteEvent = it.event val noteEvent = it.event
return ( return (
(noteEvent is FileHeaderEvent && noteEvent.hasUrl() && noteEvent.isOneOf(SUPPORTED_VIDEO_FEED_MIME_TYPES_SET)) || (noteEvent is FileHeaderEvent && noteEvent.hasUrl() && (noteEvent.urls().any { isImageOrVideoUrl(it) } || noteEvent.isOneOf(SUPPORTED_VIDEO_FEED_MIME_TYPES_SET))) ||
(noteEvent is VideoVerticalEvent && noteEvent.hasUrl() && noteEvent.isOneOf(SUPPORTED_VIDEO_FEED_MIME_TYPES_SET)) || (noteEvent is VideoVerticalEvent && noteEvent.hasUrl() && (noteEvent.urls().any { isImageOrVideoUrl(it) } || noteEvent.isOneOf(SUPPORTED_VIDEO_FEED_MIME_TYPES_SET))) ||
(noteEvent is VideoHorizontalEvent && noteEvent.hasUrl() && noteEvent.isOneOf(SUPPORTED_VIDEO_FEED_MIME_TYPES_SET)) || (noteEvent is VideoHorizontalEvent && noteEvent.hasUrl() && (noteEvent.urls().any { isImageOrVideoUrl(it) } || noteEvent.isOneOf(SUPPORTED_VIDEO_FEED_MIME_TYPES_SET))) ||
(noteEvent is FileStorageHeaderEvent && noteEvent.isOneOf(SUPPORTED_VIDEO_FEED_MIME_TYPES_SET)) (noteEvent is FileStorageHeaderEvent && noteEvent.isOneOf(SUPPORTED_VIDEO_FEED_MIME_TYPES_SET)) ||
noteEvent is PictureEvent
) && ) &&
params.match(noteEvent) && params.match(noteEvent) &&
(params.isHiddenList || account.isAcceptable(it)) (params.isHiddenList || account.isAcceptable(it))
@@ -86,6 +86,7 @@ import com.vitorpamplona.amethyst.ui.note.types.EmptyState
import com.vitorpamplona.amethyst.ui.note.types.FileHeaderDisplay import com.vitorpamplona.amethyst.ui.note.types.FileHeaderDisplay
import com.vitorpamplona.amethyst.ui.note.types.FileStorageHeaderDisplay import com.vitorpamplona.amethyst.ui.note.types.FileStorageHeaderDisplay
import com.vitorpamplona.amethyst.ui.note.types.JustVideoDisplay import com.vitorpamplona.amethyst.ui.note.types.JustVideoDisplay
import com.vitorpamplona.amethyst.ui.note.types.PictureDisplay
import com.vitorpamplona.amethyst.ui.note.types.RenderAppDefinition import com.vitorpamplona.amethyst.ui.note.types.RenderAppDefinition
import com.vitorpamplona.amethyst.ui.note.types.RenderAudioHeader import com.vitorpamplona.amethyst.ui.note.types.RenderAudioHeader
import com.vitorpamplona.amethyst.ui.note.types.RenderAudioTrack import com.vitorpamplona.amethyst.ui.note.types.RenderAudioTrack
@@ -176,6 +177,7 @@ import com.vitorpamplona.quartz.events.LongTextNoteEvent
import com.vitorpamplona.quartz.events.NIP90ContentDiscoveryResponseEvent import com.vitorpamplona.quartz.events.NIP90ContentDiscoveryResponseEvent
import com.vitorpamplona.quartz.events.NIP90StatusEvent import com.vitorpamplona.quartz.events.NIP90StatusEvent
import com.vitorpamplona.quartz.events.PeopleListEvent import com.vitorpamplona.quartz.events.PeopleListEvent
import com.vitorpamplona.quartz.events.PictureEvent
import com.vitorpamplona.quartz.events.PinListEvent import com.vitorpamplona.quartz.events.PinListEvent
import com.vitorpamplona.quartz.events.PollNoteEvent import com.vitorpamplona.quartz.events.PollNoteEvent
import com.vitorpamplona.quartz.events.PrivateDmEvent import com.vitorpamplona.quartz.events.PrivateDmEvent
@@ -730,6 +732,8 @@ private fun RenderNoteRow(
is FileHeaderEvent -> FileHeaderDisplay(baseNote, true, false, accountViewModel) is FileHeaderEvent -> FileHeaderDisplay(baseNote, true, false, accountViewModel)
is VideoHorizontalEvent -> VideoDisplay(baseNote, makeItShort, canPreview, backgroundColor, false, accountViewModel, nav) is VideoHorizontalEvent -> VideoDisplay(baseNote, makeItShort, canPreview, backgroundColor, false, accountViewModel, nav)
is VideoVerticalEvent -> VideoDisplay(baseNote, makeItShort, canPreview, backgroundColor, false, accountViewModel, nav) is VideoVerticalEvent -> VideoDisplay(baseNote, makeItShort, canPreview, backgroundColor, false, accountViewModel, nav)
is PictureEvent -> PictureDisplay(baseNote, true, false, backgroundColor, accountViewModel, nav)
is FileStorageHeaderEvent -> FileStorageHeaderDisplay(baseNote, true, false, accountViewModel) is FileStorageHeaderEvent -> FileStorageHeaderDisplay(baseNote, true, false, accountViewModel)
is CommunityPostApprovalEvent -> { is CommunityPostApprovalEvent -> {
RenderPostApproval( RenderPostApproval(
@@ -0,0 +1,135 @@
/**
* 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.Spacer
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextOverflow
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.components.ZoomableContentView
import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.HalfPadding
import com.vitorpamplona.amethyst.ui.theme.HalfVertPadding
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.quartz.events.EmptyTagList
import com.vitorpamplona.quartz.events.PictureEvent
import kotlinx.collections.immutable.toImmutableList
@Composable
fun PictureDisplay(
note: Note,
roundedCorner: Boolean,
isFiniteHeight: Boolean,
backgroundColor: MutableState<Color>,
accountViewModel: AccountViewModel,
nav: INav,
) {
val event = (note.event as? PictureEvent) ?: return
val uri = note.toNostrUri()
val images by
remember(note) {
mutableStateOf(
event
.imetaTags()
.map {
MediaUrlImage(
url = it.url,
description = it.alt,
hash = it.hash,
blurhash = it.blurhash,
dim = it.dimension,
uri = uri,
mimeType = it.mimeType,
)
}.toImmutableList(),
)
}
val first = images.firstOrNull()
if (first != null) {
val title = event.title()
SensitivityWarning(note = note, accountViewModel = accountViewModel) {
Column {
if (title != null) {
Text(
modifier = if (isFiniteHeight) HalfPadding else HalfVertPadding,
text = title,
style = MaterialTheme.typography.bodyLarge,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
} else {
Spacer(StdVertSpacer)
}
ZoomableContentView(
content = first,
images = images,
roundedCorner = roundedCorner,
isFiniteHeight = isFiniteHeight,
accountViewModel = accountViewModel,
)
TranslatableRichTextViewer(
content = event.content,
canPreview = false,
quotesLeft = 0,
modifier = if (isFiniteHeight) HalfPadding else HalfVertPadding,
tags = EmptyTagList,
backgroundColor = backgroundColor,
id = note.idHex,
callbackUri = uri,
accountViewModel = accountViewModel,
nav = nav,
)
/*
TranslatableRichTextViewer(
content = ,
modifier = if (isFiniteHeight) HalfPadding else HalfVertPadding,
text = ,
style = MaterialTheme.typography.bodySmall,
color = Color.Gray,
maxLines = 3,
overflow = TextOverflow.Ellipsis,
)*/
}
}
}
}
@@ -83,6 +83,7 @@ import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.events.ChatMessageRelayListEvent import com.vitorpamplona.quartz.events.ChatMessageRelayListEvent
import com.vitorpamplona.quartz.events.ChatroomKey import com.vitorpamplona.quartz.events.ChatroomKey
import com.vitorpamplona.quartz.events.ChatroomKeyable import com.vitorpamplona.quartz.events.ChatroomKeyable
import com.vitorpamplona.quartz.events.Dimension
import com.vitorpamplona.quartz.events.DraftEvent import com.vitorpamplona.quartz.events.DraftEvent
import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.EventInterface import com.vitorpamplona.quartz.events.EventInterface
@@ -754,7 +755,7 @@ class AccountViewModel(
url: String, url: String,
relay: String?, relay: String?,
blurhash: String?, blurhash: String?,
dim: String?, dim: Dimension?,
hash: String?, hash: String?,
mimeType: String?, mimeType: String?,
) { ) {
@@ -338,6 +338,8 @@ fun InnerRenderGalleryThumb(
note: Note, note: Note,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
) { ) {
val noteEvent = note.event as? ProfileGalleryEntryEvent ?: return
Box( Box(
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -345,11 +347,11 @@ fun InnerRenderGalleryThumb(
contentAlignment = BottomStart, contentAlignment = BottomStart,
) { ) {
card.image?.let { card.image?.let {
var blurHash = (note.event as ProfileGalleryEntryEvent).blurhash() val blurHash = noteEvent.blurhash()
var description = (note.event as ProfileGalleryEntryEvent).content val description = noteEvent.content
// var hash = (note.event as ProfileGalleryEntryEvent).hash() // var hash = (note.event as ProfileGalleryEntryEvent).hash()
var dimensions = (note.event as ProfileGalleryEntryEvent).dimensions() val dimensions = noteEvent.dimensions()
var mimeType = (note.event as ProfileGalleryEntryEvent).mimeType() val mimeType = noteEvent.mimeType()
var content: BaseMediaContent? = null var content: BaseMediaContent? = null
if (isVideoUrl(it)) { if (isVideoUrl(it)) {
@@ -123,6 +123,7 @@ import com.vitorpamplona.amethyst.ui.note.types.DisplaySearchRelayList
import com.vitorpamplona.amethyst.ui.note.types.EditState import com.vitorpamplona.amethyst.ui.note.types.EditState
import com.vitorpamplona.amethyst.ui.note.types.FileHeaderDisplay import com.vitorpamplona.amethyst.ui.note.types.FileHeaderDisplay
import com.vitorpamplona.amethyst.ui.note.types.FileStorageHeaderDisplay import com.vitorpamplona.amethyst.ui.note.types.FileStorageHeaderDisplay
import com.vitorpamplona.amethyst.ui.note.types.PictureDisplay
import com.vitorpamplona.amethyst.ui.note.types.RenderAppDefinition import com.vitorpamplona.amethyst.ui.note.types.RenderAppDefinition
import com.vitorpamplona.amethyst.ui.note.types.RenderChannelMessage import com.vitorpamplona.amethyst.ui.note.types.RenderChannelMessage
import com.vitorpamplona.amethyst.ui.note.types.RenderEmojiPack import com.vitorpamplona.amethyst.ui.note.types.RenderEmojiPack
@@ -186,6 +187,7 @@ import com.vitorpamplona.quartz.events.HighlightEvent
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
import com.vitorpamplona.quartz.events.LongTextNoteEvent import com.vitorpamplona.quartz.events.LongTextNoteEvent
import com.vitorpamplona.quartz.events.PeopleListEvent import com.vitorpamplona.quartz.events.PeopleListEvent
import com.vitorpamplona.quartz.events.PictureEvent
import com.vitorpamplona.quartz.events.PinListEvent import com.vitorpamplona.quartz.events.PinListEvent
import com.vitorpamplona.quartz.events.PollNoteEvent import com.vitorpamplona.quartz.events.PollNoteEvent
import com.vitorpamplona.quartz.events.PrivateDmEvent import com.vitorpamplona.quartz.events.PrivateDmEvent
@@ -509,6 +511,8 @@ private fun FullBleedNoteCompose(
) )
} else if (noteEvent is VideoEvent) { } else if (noteEvent is VideoEvent) {
VideoDisplay(baseNote, makeItShort = false, canPreview = true, backgroundColor = backgroundColor, isFiniteHeight = false, accountViewModel = accountViewModel, nav = nav) VideoDisplay(baseNote, makeItShort = false, canPreview = true, backgroundColor = backgroundColor, isFiniteHeight = false, accountViewModel = accountViewModel, nav = nav)
} else if (noteEvent is PictureEvent) {
PictureDisplay(baseNote, roundedCorner = true, isFiniteHeight = false, backgroundColor, accountViewModel = accountViewModel, nav)
} else if (noteEvent is FileHeaderEvent) { } else if (noteEvent is FileHeaderEvent) {
FileHeaderDisplay(baseNote, roundedCorner = true, isFiniteHeight = false, accountViewModel = accountViewModel) FileHeaderDisplay(baseNote, roundedCorner = true, isFiniteHeight = false, accountViewModel = accountViewModel)
} else if (noteEvent is FileStorageHeaderEvent) { } else if (noteEvent is FileStorageHeaderEvent) {
@@ -43,9 +43,9 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
@@ -85,6 +85,7 @@ import com.vitorpamplona.amethyst.ui.note.elements.NoteDropDownMenu
import com.vitorpamplona.amethyst.ui.note.types.FileHeaderDisplay import com.vitorpamplona.amethyst.ui.note.types.FileHeaderDisplay
import com.vitorpamplona.amethyst.ui.note.types.FileStorageHeaderDisplay import com.vitorpamplona.amethyst.ui.note.types.FileStorageHeaderDisplay
import com.vitorpamplona.amethyst.ui.note.types.JustVideoDisplay import com.vitorpamplona.amethyst.ui.note.types.JustVideoDisplay
import com.vitorpamplona.amethyst.ui.note.types.PictureDisplay
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.DisappearingScaffold import com.vitorpamplona.amethyst.ui.screen.loggedIn.DisappearingScaffold
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.stringRes
@@ -101,6 +102,7 @@ import com.vitorpamplona.amethyst.ui.theme.VideoReactionColumnPadding
import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.quartz.events.FileHeaderEvent import com.vitorpamplona.quartz.events.FileHeaderEvent
import com.vitorpamplona.quartz.events.FileStorageHeaderEvent import com.vitorpamplona.quartz.events.FileStorageHeaderEvent
import com.vitorpamplona.quartz.events.PictureEvent
import com.vitorpamplona.quartz.events.VideoEvent import com.vitorpamplona.quartz.events.VideoEvent
@Composable @Composable
@@ -290,7 +292,11 @@ private fun RenderVideoOrPictureNote(
Column(Modifier.fillMaxSize(1f), verticalArrangement = Arrangement.Center) { Column(Modifier.fillMaxSize(1f), verticalArrangement = Arrangement.Center) {
Row(Modifier.weight(1f), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center) { Row(Modifier.weight(1f), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center) {
val noteEvent = remember { note.event } val noteEvent = remember { note.event }
if (noteEvent is FileHeaderEvent) { if (noteEvent is PictureEvent) {
val backgroundColor = remember { mutableStateOf(Color.Transparent) }
PictureDisplay(note, false, true, backgroundColor, accountViewModel, nav)
} else if (noteEvent is FileHeaderEvent) {
FileHeaderDisplay(note, false, true, accountViewModel) FileHeaderDisplay(note, false, true, accountViewModel)
} else if (noteEvent is FileStorageHeaderEvent) { } else if (noteEvent is FileStorageHeaderEvent) {
FileStorageHeaderDisplay(note, false, true, accountViewModel) FileStorageHeaderDisplay(note, false, true, accountViewModel)
@@ -21,12 +21,13 @@
package com.vitorpamplona.amethyst.commons.richtext package com.vitorpamplona.amethyst.commons.richtext
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.events.Dimension
import java.io.File import java.io.File
@Immutable @Immutable
abstract class BaseMediaContent( abstract class BaseMediaContent(
val description: String? = null, val description: String? = null,
val dim: String? = null, val dim: Dimension? = null,
val blurhash: String? = null, val blurhash: String? = null,
) )
@@ -35,7 +36,7 @@ abstract class MediaUrlContent(
val url: String, val url: String,
description: String? = null, description: String? = null,
val hash: String? = null, val hash: String? = null,
dim: String? = null, dim: Dimension? = null,
blurhash: String? = null, blurhash: String? = null,
val uri: String? = null, val uri: String? = null,
val mimeType: String? = null, val mimeType: String? = null,
@@ -47,7 +48,7 @@ class MediaUrlImage(
description: String? = null, description: String? = null,
hash: String? = null, hash: String? = null,
blurhash: String? = null, blurhash: String? = null,
dim: String? = null, dim: Dimension? = null,
uri: String? = null, uri: String? = null,
val contentWarning: String? = null, val contentWarning: String? = null,
mimeType: String? = null, mimeType: String? = null,
@@ -58,7 +59,7 @@ class MediaUrlVideo(
url: String, url: String,
description: String? = null, description: String? = null,
hash: String? = null, hash: String? = null,
dim: String? = null, dim: Dimension? = null,
uri: String? = null, uri: String? = null,
val artworkUri: String? = null, val artworkUri: String? = null,
val authorName: String? = null, val authorName: String? = null,
@@ -73,7 +74,7 @@ abstract class MediaPreloadedContent(
description: String? = null, description: String? = null,
val mimeType: String? = null, val mimeType: String? = null,
val isVerified: Boolean? = null, val isVerified: Boolean? = null,
dim: String? = null, dim: Dimension? = null,
blurhash: String? = null, blurhash: String? = null,
val uri: String, val uri: String,
val id: String? = null, val id: String? = null,
@@ -86,7 +87,7 @@ class MediaLocalImage(
localFile: File?, localFile: File?,
mimeType: String? = null, mimeType: String? = null,
description: String? = null, description: String? = null,
dim: String? = null, dim: Dimension? = null,
blurhash: String? = null, blurhash: String? = null,
isVerified: Boolean? = null, isVerified: Boolean? = null,
uri: String, uri: String,
@@ -97,7 +98,7 @@ class MediaLocalVideo(
localFile: File?, localFile: File?,
mimeType: String? = null, mimeType: String? = null,
description: String? = null, description: String? = null,
dim: String? = null, dim: Dimension? = null,
blurhash: String? = null, blurhash: String? = null,
isVerified: Boolean? = null, isVerified: Boolean? = null,
uri: String, uri: String,
@@ -27,6 +27,7 @@ import com.linkedin.urls.detection.UrlDetectorOptions
import com.vitorpamplona.quartz.encoders.Nip30CustomEmoji import com.vitorpamplona.quartz.encoders.Nip30CustomEmoji
import com.vitorpamplona.quartz.encoders.Nip54InlineMetadata import com.vitorpamplona.quartz.encoders.Nip54InlineMetadata
import com.vitorpamplona.quartz.encoders.Nip92MediaAttachments import com.vitorpamplona.quartz.encoders.Nip92MediaAttachments
import com.vitorpamplona.quartz.events.Dimension
import com.vitorpamplona.quartz.events.FileHeaderEvent import com.vitorpamplona.quartz.events.FileHeaderEvent
import com.vitorpamplona.quartz.events.ImmutableListOfLists import com.vitorpamplona.quartz.events.ImmutableListOfLists
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@@ -56,7 +57,7 @@ class RichTextParser {
description = description ?: frags[FileHeaderEvent.ALT] ?: tags[FileHeaderEvent.ALT], description = description ?: frags[FileHeaderEvent.ALT] ?: tags[FileHeaderEvent.ALT],
hash = frags[FileHeaderEvent.HASH] ?: tags[FileHeaderEvent.HASH], hash = frags[FileHeaderEvent.HASH] ?: tags[FileHeaderEvent.HASH],
blurhash = frags[FileHeaderEvent.BLUR_HASH] ?: tags[FileHeaderEvent.BLUR_HASH], blurhash = frags[FileHeaderEvent.BLUR_HASH] ?: tags[FileHeaderEvent.BLUR_HASH],
dim = frags[FileHeaderEvent.DIMENSION] ?: tags[FileHeaderEvent.DIMENSION], dim = frags[FileHeaderEvent.DIMENSION]?.let { Dimension.parse(it) } ?: tags[FileHeaderEvent.DIMENSION]?.let { Dimension.parse(it) },
contentWarning = frags["content-warning"] ?: tags["content-warning"], contentWarning = frags["content-warning"] ?: tags["content-warning"],
uri = callbackUri, uri = callbackUri,
mimeType = frags[FileHeaderEvent.MIME_TYPE] ?: tags[FileHeaderEvent.MIME_TYPE], mimeType = frags[FileHeaderEvent.MIME_TYPE] ?: tags[FileHeaderEvent.MIME_TYPE],
@@ -76,7 +77,7 @@ class RichTextParser {
description = description ?: frags[FileHeaderEvent.ALT] ?: tags[FileHeaderEvent.ALT], description = description ?: frags[FileHeaderEvent.ALT] ?: tags[FileHeaderEvent.ALT],
hash = frags[FileHeaderEvent.HASH] ?: tags[FileHeaderEvent.HASH], hash = frags[FileHeaderEvent.HASH] ?: tags[FileHeaderEvent.HASH],
blurhash = frags[FileHeaderEvent.BLUR_HASH] ?: tags[FileHeaderEvent.BLUR_HASH], blurhash = frags[FileHeaderEvent.BLUR_HASH] ?: tags[FileHeaderEvent.BLUR_HASH],
dim = frags[FileHeaderEvent.DIMENSION] ?: tags[FileHeaderEvent.DIMENSION], dim = frags[FileHeaderEvent.DIMENSION]?.let { Dimension.parse(it) } ?: tags[FileHeaderEvent.DIMENSION]?.let { Dimension.parse(it) },
contentWarning = frags["content-warning"] ?: tags["content-warning"], contentWarning = frags["content-warning"] ?: tags["content-warning"],
uri = callbackUri, uri = callbackUri,
mimeType = frags[FileHeaderEvent.MIME_TYPE] ?: tags[FileHeaderEvent.MIME_TYPE], mimeType = frags[FileHeaderEvent.MIME_TYPE] ?: tags[FileHeaderEvent.MIME_TYPE],
@@ -24,7 +24,7 @@ import com.vitorpamplona.quartz.events.FileHeaderEvent
class Nip92MediaAttachments { class Nip92MediaAttachments {
companion object { companion object {
private const val IMETA = "imeta" const val IMETA = "imeta"
} }
fun convertFromFileHeader(header: FileHeaderEvent): Array<String>? { fun convertFromFileHeader(header: FileHeaderEvent): Array<String>? {
@@ -63,13 +63,22 @@ class Nip92MediaAttachments {
.firstOrNull { .firstOrNull {
it.size > 1 && it[0] == IMETA && it[1] == "url $imageUrl" it.size > 1 && it[0] == IMETA && it[1] == "url $imageUrl"
}?.let { tagList -> }?.let { tagList ->
tagList.associate { tag -> parseIMeta(tagList)
val parts = tag.split(" ", limit = 2)
when (parts.size) {
2 -> parts[0] to parts[1]
1 -> parts[0] to ""
else -> "" to ""
}
}
} ?: emptyMap() } ?: emptyMap()
fun parse(tags: Array<Array<String>>): Map<String, Map<String, String>> =
tags.filter { it.size > 1 && it[0] == IMETA }.associate {
val allTags = parseIMeta(it)
(allTags.get("url") ?: "") to allTags
}
fun parseIMeta(tags: Array<String>): Map<String, String> =
tags.associate { tag ->
val parts = tag.split(" ", limit = 2)
when (parts.size) {
2 -> parts[0] to parts[1]
1 -> parts[0] to ""
else -> "" to ""
}
}
} }
@@ -87,6 +87,33 @@ class CommentEvent(
.reversed() .reversed()
.toTypedArray() .toTypedArray()
fun firstReplyToEvent(
msg: String,
replyingTo: EventHint<Event>,
usersMentioned: Set<PTag> = emptySet(),
addressesMentioned: Set<ATag> = emptySet(),
eventsMentioned: Set<ETag> = emptySet(),
nip94attachments: List<FileHeaderEvent>? = null,
geohash: String? = null,
zapReceiver: List<ZapSplitSetup>? = null,
markAsSensitive: Boolean = false,
zapRaiserAmount: Long? = null,
isDraft: Boolean,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (CommentEvent) -> Unit,
) {
val tags = mutableListOf<Array<String>>()
tags.add(removeTrailingNullsAndEmptyOthers("E", replyingTo.event.id, replyingTo.relay, replyingTo.event.pubKey))
tags.add(arrayOf("K", "${replyingTo.event.kind}"))
tags.add(removeTrailingNullsAndEmptyOthers("e", replyingTo.event.id, replyingTo.relay, replyingTo.event.pubKey))
tags.add(arrayOf("k", "${replyingTo.event.kind}"))
create(msg, tags, usersMentioned, addressesMentioned, eventsMentioned, nip94attachments, geohash, zapReceiver, markAsSensitive, zapRaiserAmount, isDraft, signer, createdAt, onReady)
}
fun replyComment( fun replyComment(
msg: String, msg: String,
replyingTo: EventHint<CommentEvent>, replyingTo: EventHint<CommentEvent>,
@@ -118,6 +118,7 @@ class EventFactory {
NIP90UserDiscoveryResponseEvent.KIND -> NIP90UserDiscoveryResponseEvent(id, pubKey, createdAt, tags, content, sig) NIP90UserDiscoveryResponseEvent.KIND -> NIP90UserDiscoveryResponseEvent(id, pubKey, createdAt, tags, content, sig)
OtsEvent.KIND -> OtsEvent(id, pubKey, createdAt, tags, content, sig) OtsEvent.KIND -> OtsEvent(id, pubKey, createdAt, tags, content, sig)
PeopleListEvent.KIND -> PeopleListEvent(id, pubKey, createdAt, tags, content, sig) PeopleListEvent.KIND -> PeopleListEvent(id, pubKey, createdAt, tags, content, sig)
PictureEvent.KIND -> PictureEvent(id, pubKey, createdAt, tags, content, sig)
PinListEvent.KIND -> PinListEvent(id, pubKey, createdAt, tags, content, sig) PinListEvent.KIND -> PinListEvent(id, pubKey, createdAt, tags, content, sig)
PollNoteEvent.KIND -> PollNoteEvent(id, pubKey, createdAt, tags, content, sig) PollNoteEvent.KIND -> PollNoteEvent(id, pubKey, createdAt, tags, content, sig)
PrivateDmEvent.KIND -> PrivateDmEvent(id, pubKey, createdAt, tags, content, sig) PrivateDmEvent.KIND -> PrivateDmEvent(id, pubKey, createdAt, tags, content, sig)
@@ -46,7 +46,7 @@ class FileHeaderEvent(
fun alt() = tags.firstOrNull { it.size > 1 && it[0] == ALT }?.get(1) fun alt() = tags.firstOrNull { it.size > 1 && it[0] == ALT }?.get(1)
fun dimensions() = tags.firstOrNull { it.size > 1 && it[0] == DIMENSION }?.get(1) fun dimensions() = tags.firstOrNull { it.size > 1 && it[0] == DIMENSION }?.get(1)?.let { Dimension.parse(it) }
fun magnetURI() = tags.firstOrNull { it.size > 1 && it[0] == MAGNET_URI }?.get(1) fun magnetURI() = tags.firstOrNull { it.size > 1 && it[0] == MAGNET_URI }?.get(1)
@@ -44,7 +44,7 @@ class FileStorageHeaderEvent(
fun alt() = tags.firstOrNull { it.size > 1 && it[0] == ALT }?.get(1) fun alt() = tags.firstOrNull { it.size > 1 && it[0] == ALT }?.get(1)
fun dimensions() = tags.firstOrNull { it.size > 1 && it[0] == DIMENSION }?.get(1) fun dimensions() = tags.firstOrNull { it.size > 1 && it[0] == DIMENSION }?.get(1)?.let { Dimension.parse(it) }
fun magnetURI() = tags.firstOrNull { it.size > 1 && it[0] == MAGNET_URI }?.get(1) fun magnetURI() = tags.firstOrNull { it.size > 1 && it[0] == MAGNET_URI }?.get(1)
@@ -0,0 +1,322 @@
/**
* 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.ETag
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.Nip92MediaAttachments
import com.vitorpamplona.quartz.encoders.Nip92MediaAttachments.Companion.IMETA
import com.vitorpamplona.quartz.encoders.PTag
import com.vitorpamplona.quartz.events.FileHeaderEvent.Companion.ALT
import com.vitorpamplona.quartz.events.FileHeaderEvent.Companion.BLUR_HASH
import com.vitorpamplona.quartz.events.FileHeaderEvent.Companion.DIMENSION
import com.vitorpamplona.quartz.events.FileHeaderEvent.Companion.FILE_SIZE
import com.vitorpamplona.quartz.events.FileHeaderEvent.Companion.MAGNET_URI
import com.vitorpamplona.quartz.events.FileHeaderEvent.Companion.TORRENT_INFOHASH
import com.vitorpamplona.quartz.events.FileHeaderEvent.Companion.URL
import com.vitorpamplona.quartz.signers.NostrSigner
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlin.coroutines.cancellation.CancellationException
@Immutable
class PictureEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
fun mimeTypes() = tags.filter { it.size > 1 && it[0] == MIME_TYPE }
fun hashes() = tags.filter { it.size > 1 && it[0] == HASH }
fun title() = tags.firstOrNull { it.size > 1 && it[0] == TITLE }?.get(1)
fun url() = tags.firstOrNull { it.size > 1 && it[0] == URL }?.get(1)
fun urls() = tags.filter { it.size > 1 && it[0] == URL }.map { it[1] }
fun mimeType() = tags.firstOrNull { it.size > 1 && it[0] == FileHeaderEvent.MIME_TYPE }?.get(1)
fun hash() = tags.firstOrNull { it.size > 1 && it[0] == FileHeaderEvent.HASH }?.get(1)
fun size() = tags.firstOrNull { it.size > 1 && it[0] == FILE_SIZE }?.get(1)
fun alt() = tags.firstOrNull { it.size > 1 && it[0] == ALT }?.get(1)
fun dimensions() = tags.firstOrNull { it.size > 1 && it[0] == DIMENSION }?.get(1)?.let { Dimension.parse(it) }
fun magnetURI() = tags.firstOrNull { it.size > 1 && it[0] == MAGNET_URI }?.get(1)
fun torrentInfoHash() = tags.firstOrNull { it.size > 1 && it[0] == TORRENT_INFOHASH }?.get(1)
fun blurhash() = tags.firstOrNull { it.size > 1 && it[0] == BLUR_HASH }?.get(1)
fun hasUrl() = tags.any { it.size > 1 && it[0] == URL }
// hack to fix pablo's bug
fun rootImage() =
url()?.let {
PictureMeta(
url = it,
mimeType = mimeType(),
blurhash = blurhash(),
alt = alt(),
hash = hash(),
dimension = dimensions(),
size = size()?.toLongOrNull(),
fallback = emptyList(),
annotations = emptyList(),
)
}
fun imetaTags() =
tags
.map { tagArray ->
if (tagArray.size > 1 && tagArray[0] == IMETA) {
PictureMeta.parse(tagArray)
} else {
null
}
}.plus(rootImage())
.filterNotNull()
companion object {
const val KIND = 20
const val ALT_DESCRIPTION = "Picture"
private const val MIME_TYPE = "m"
private const val HASH = "x"
private const val TITLE = "title"
private fun create(
msg: String,
tags: MutableList<Array<String>>,
usersMentioned: Set<PTag> = emptySet(),
addressesMentioned: Set<ATag> = emptySet(),
eventsMentioned: Set<ETag> = emptySet(),
nip94attachments: List<FileHeaderEvent>? = null,
geohash: String? = null,
zapReceiver: List<ZapSplitSetup>? = null,
markAsSensitive: Boolean = false,
zapRaiserAmount: Long? = null,
isDraft: Boolean,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (PictureEvent) -> Unit,
) {
usersMentioned.forEach { tags.add(it.toPTagArray()) }
addressesMentioned.forEach { tags.add(it.toQTagArray()) }
eventsMentioned.forEach { tags.add(it.toQTagArray()) }
findHashtags(msg).forEach {
val lowercaseTag = it.lowercase()
tags.add(arrayOf("t", it))
if (it != lowercaseTag) {
tags.add(arrayOf("t", it.lowercase()))
}
}
findURLs(msg).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)
}
}
}
if (isDraft) {
signer.assembleRumor(createdAt, KIND, tags.toTypedArray(), msg, onReady)
} else {
signer.sign(createdAt, KIND, tags.toTypedArray(), msg, onReady)
}
}
}
}
class PictureMeta(
val url: String,
val mimeType: String?,
val blurhash: String?,
val dimension: Dimension?,
val alt: String?,
val hash: String?,
val size: Long?,
val fallback: List<String>,
val annotations: List<UserAnnotation>,
) {
companion object {
fun parse(tagArray: Array<String>): PictureMeta? {
var url: String? = null
var mimeType: String? = null
var blurhash: String? = null
var dim: Dimension? = null
var alt: String? = null
var hash: String? = null
var size: Long? = null
val fallback = mutableListOf<String>()
val annotations = mutableListOf<UserAnnotation>()
if (tagArray.size == 2 && tagArray[1].contains("url") && (tagArray[1].contains("blurhash") || tagArray[1].contains("size"))) {
// hack to fix pablo's bug
val keys = setOf("url", "m", "blurhash", "dim", "alt", "x", "size", "fallback", "annotate-user")
var keyNextValue: String? = null
val values = mutableListOf<String>()
tagArray[1].split(" ").forEach {
if (it in keys) {
if (keyNextValue != null && values.isNotEmpty()) {
when (keyNextValue) {
"url" -> url = values.joinToString(" ")
"m" -> mimeType = values.joinToString(" ")
"blurhash" -> blurhash = values.joinToString(" ")
"dim" -> dim = Dimension.parse(values.joinToString(" "))
"alt" -> alt = values.joinToString(" ")
"x" -> hash = values.joinToString(" ")
"size" -> size = values.joinToString(" ").toLongOrNull()
"fallback" -> fallback.add(values.joinToString(" "))
"annotate-user" -> {
UserAnnotation.parse(values.joinToString(" "))?.let {
annotations.add(it)
}
}
}
values.clear()
}
keyNextValue = it
} else {
values.add(it)
}
}
if (keyNextValue != null && values.isNotEmpty()) {
when (keyNextValue) {
"url" -> url = values.joinToString(" ")
"m" -> mimeType = values.joinToString(" ")
"blurhash" -> blurhash = values.joinToString(" ")
"dim" -> dim = Dimension.parse(values.joinToString(" "))
"alt" -> alt = values.joinToString(" ")
"x" -> hash = values.joinToString(" ")
"size" -> size = values.joinToString(" ").toLongOrNull()
"fallback" -> fallback.add(values.joinToString(" "))
"annotate-user" -> {
UserAnnotation.parse(values.joinToString(" "))?.let {
annotations.add(it)
}
}
}
values.clear()
keyNextValue = null
}
} else {
tagArray.forEach {
val parts = it.split(" ", limit = 2)
val key = parts[0]
val value = if (parts.size == 2) parts[1] else ""
if (value.isNotBlank()) {
when (key) {
"url" -> url = value
"m" -> mimeType = value
"blurhash" -> blurhash = value
"dim" -> dim = Dimension.parse(value)
"alt" -> alt = value
"x" -> hash = value
"size" -> size = value.toLongOrNull()
"fallback" -> fallback.add(value)
"annotate-user" -> {
UserAnnotation.parse(value)?.let {
annotations.add(it)
}
}
}
}
}
}
return url?.let {
PictureMeta(it, mimeType, blurhash, dim, alt, hash, size, fallback, annotations)
}
}
}
}
class Dimension(
val width: Int,
val height: Int,
) {
fun aspectRatio() = width.toFloat() / height.toFloat()
override fun toString() = "${width}x$height"
companion object {
fun parse(dim: String): Dimension? {
if (dim == "0x0") return null
val parts = dim.split("x")
if (parts.size != 2) return null
return try {
val width = parts[0].toInt()
val height = parts[1].toInt()
Dimension(width, height)
} catch (e: Exception) {
if (e is CancellationException) throw e
null
}
}
}
}
class UserAnnotation(
val pubkey: HexKey,
val x: Int,
val y: Int,
) {
companion object {
fun parse(value: String): UserAnnotation? {
val ann = value.split(":")
if (ann.size == 3) {
val x = ann[1].toIntOrNull()
val y = ann[2].toIntOrNull()
if (x != null && y != null) {
return UserAnnotation(ann[0], x, y)
}
}
return null
}
}
}
@@ -46,7 +46,7 @@ class ProfileGalleryEntryEvent(
fun alt() = tags.firstOrNull { it.size > 1 && it[0] == ALT }?.get(1) fun alt() = tags.firstOrNull { it.size > 1 && it[0] == ALT }?.get(1)
fun dimensions() = tags.firstOrNull { it.size > 1 && it[0] == DIMENSION }?.get(1) fun dimensions() = tags.firstOrNull { it.size > 1 && it[0] == DIMENSION }?.get(1)?.let { Dimension.parse(it) }
fun magnetURI() = tags.firstOrNull { it.size > 1 && it[0] == MAGNET_URI }?.get(1) fun magnetURI() = tags.firstOrNull { it.size > 1 && it[0] == MAGNET_URI }?.get(1)
@@ -87,7 +87,7 @@ class ProfileGalleryEntryEvent(
alt: String? = null, alt: String? = null,
hash: String? = null, hash: String? = null,
size: String? = null, size: String? = null,
dimensions: String? = null, dimensions: Dimension? = null,
blurhash: String? = null, blurhash: String? = null,
originalHash: String? = null, originalHash: String? = null,
magnetURI: String? = null, magnetURI: String? = null,
@@ -109,7 +109,7 @@ class ProfileGalleryEntryEvent(
alt?.ifBlank { null }?.let { arrayOf(ALT, it) } ?: arrayOf("alt", ALT_DESCRIPTION), alt?.ifBlank { null }?.let { arrayOf(ALT, it) } ?: arrayOf("alt", ALT_DESCRIPTION),
hash?.let { arrayOf(HASH, it) }, hash?.let { arrayOf(HASH, it) },
size?.let { arrayOf(FILE_SIZE, it) }, size?.let { arrayOf(FILE_SIZE, it) },
dimensions?.let { arrayOf(DIMENSION, it) }, dimensions?.let { arrayOf(DIMENSION, it.toString()) },
blurhash?.let { arrayOf(BLUR_HASH, it) }, blurhash?.let { arrayOf(BLUR_HASH, it) },
originalHash?.let { arrayOf(ORIGINAL_HASH, it) }, originalHash?.let { arrayOf(ORIGINAL_HASH, it) },
magnetURI?.let { arrayOf(MAGNET_URI, it) }, magnetURI?.let { arrayOf(MAGNET_URI, it) },
@@ -47,7 +47,7 @@ abstract class VideoEvent(
fun alt() = tags.firstOrNull { it.size > 1 && it[0] == ALT }?.get(1) fun alt() = tags.firstOrNull { it.size > 1 && it[0] == ALT }?.get(1)
fun dimensions() = tags.firstOrNull { it.size > 1 && it[0] == DIMENSION }?.get(1) fun dimensions() = tags.firstOrNull { it.size > 1 && it[0] == DIMENSION }?.get(1)?.let { Dimension.parse(it) }
fun magnetURI() = tags.firstOrNull { it.size > 1 && it[0] == MAGNET_URI }?.get(1) fun magnetURI() = tags.firstOrNull { it.size > 1 && it[0] == MAGNET_URI }?.get(1)