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.ContactListEvent
import com.vitorpamplona.quartz.events.DeletionEvent
import com.vitorpamplona.quartz.events.Dimension
import com.vitorpamplona.quartz.events.DraftEvent
import com.vitorpamplona.quartz.events.EmojiPackEvent
import com.vitorpamplona.quartz.events.EmojiPackSelectionEvent
@@ -2190,34 +2191,68 @@ class Account(
}
}
CommentEvent.replyComment(
msg = message,
replyingTo = EventHint(replyingTo.event as CommentEvent, 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)
if (replyingTo.event is CommentEvent) {
CommentEvent.replyComment(
msg = message,
replyingTo = EventHint(replyingTo.event as CommentEvent, 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 {
DraftEvent.create(draftTag, it, signer) { draftEvent ->
sendDraftEvent(draftEvent)
Client.send(it, relayList = relayList)
LocalCache.justConsume(it, null)
replyingTo.event?.let {
Client.send(it, relayList = relayList)
}
}
} else {
Client.send(it, relayList = relayList)
LocalCache.justConsume(it, null)
replyingTo.event?.let {
}
} else {
CommentEvent.firstReplyToEvent(
msg = message,
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)
LocalCache.justConsume(it, null)
replyingTo.event?.let {
Client.send(it, relayList = relayList)
}
}
}
}
@@ -2882,7 +2917,7 @@ class Account(
url: String,
relay: String?,
blurhash: String?,
dim: String?,
dim: Dimension?,
hash: String?,
mimeType: String?,
) {
@@ -47,6 +47,7 @@ import com.vitorpamplona.quartz.events.BadgeAwardEvent
import com.vitorpamplona.quartz.events.BadgeDefinitionEvent
import com.vitorpamplona.quartz.events.BadgeProfilesEvent
import com.vitorpamplona.quartz.events.BaseAddressableEvent
import com.vitorpamplona.quartz.events.BaseTextNoteEvent
import com.vitorpamplona.quartz.events.BookmarkListEvent
import com.vitorpamplona.quartz.events.CalendarDateSlotEvent
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.OtsEvent
import com.vitorpamplona.quartz.events.PeopleListEvent
import com.vitorpamplona.quartz.events.PictureEvent
import com.vitorpamplona.quartz.events.PinListEvent
import com.vitorpamplona.quartz.events.PollNoteEvent
import com.vitorpamplona.quartz.events.PrivateDmEvent
@@ -439,61 +441,15 @@ object LocalCache {
fun consume(
event: TextNoteEvent,
relay: Relay? = null,
) {
val note = getOrCreateNote(event.id)
val author = getOrCreateUser(event.pubKey)
if (relay != null) {
author.addRelayBeingUsed(relay, event.createdAt)
note.addRelay(relay)
}
// Already processed this event.
if (note.event != null) return
if (antiSpam.isSpam(event, relay)) {
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)
}
) = consumeRegularEvent(event, relay)
fun consume(
event: TorrentEvent,
relay: Relay?,
) {
val note = getOrCreateNote(event.id)
val author = getOrCreateUser(event.pubKey)
) = consumeRegularEvent(event, relay)
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(
event: TorrentCommentEvent,
fun consumeRegularEvent(
event: Event,
relay: Relay?,
) {
val note = getOrCreateNote(event.id)
@@ -507,242 +463,69 @@ object LocalCache {
// Already processed this event.
if (note.event != null) return
if (antiSpam.isSpam(event, relay)) {
val replyTo = computeReplyTo(event)
if (event is BaseTextNoteEvent && antiSpam.isSpam(event, relay)) {
return
}
val replyTo = computeReplyTo(event)
note.loadEvent(event, author, replyTo)
// Counts the replies
replyTo.forEach {
it.addReply(note)
}
replyTo.forEach { it.addReply(note) }
refreshObservers(note)
}
fun consume(
event: PictureEvent,
relay: Relay?,
) = consumeRegularEvent(event, relay)
fun consume(
event: TorrentCommentEvent,
relay: Relay?,
) = consumeRegularEvent(event, relay)
fun consume(
event: NIP90ContentDiscoveryResponseEvent,
relay: Relay? = null,
) {
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)
}
relay: Relay?,
) = consumeRegularEvent(event, relay)
fun consume(
event: NIP90ContentDiscoveryRequestEvent,
relay: Relay? = null,
) {
val note = getOrCreateNote(event.id)
val author = getOrCreateUser(event.pubKey)
if (relay != null) {
author.addRelayBeingUsed(relay, event.createdAt)
note.addRelay(relay)
}
// Already processed this event.
if (note.event != null) return
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)
}
relay: Relay?,
) = consumeRegularEvent(event, relay)
fun consume(
event: NIP90StatusEvent,
relay: Relay? = null,
) {
val note = getOrCreateNote(event.id)
val author = getOrCreateUser(event.pubKey)
if (relay != null) {
author.addRelayBeingUsed(relay, event.createdAt)
note.addRelay(relay)
}
// Already processed this event.
if (note.event != null) return
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)
}
relay: Relay?,
) = consumeRegularEvent(event, relay)
fun consume(
event: NIP90UserDiscoveryResponseEvent,
relay: Relay? = null,
) {
val note = getOrCreateNote(event.id)
val author = getOrCreateUser(event.pubKey)
if (relay != null) {
author.addRelayBeingUsed(relay, event.createdAt)
note.addRelay(relay)
}
// Already processed this event.
if (note.event != null) return
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)
}
relay: Relay?,
) = consumeRegularEvent(event, relay)
fun consume(
event: NIP90UserDiscoveryRequestEvent,
relay: Relay? = null,
) {
val note = getOrCreateNote(event.id)
val author = getOrCreateUser(event.pubKey)
if (relay != null) {
author.addRelayBeingUsed(relay, event.createdAt)
note.addRelay(relay)
}
// Already processed this event.
if (note.event != null) return
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)
}
relay: Relay?,
) = consumeRegularEvent(event, relay)
fun consume(
event: GitPatchEvent,
relay: Relay? = null,
) {
val note = getOrCreateNote(event.id)
val author = getOrCreateUser(event.pubKey)
if (relay != null) {
author.addRelayBeingUsed(relay, event.createdAt)
note.addRelay(relay)
}
// Already processed this event.
if (note.event != null) return
if (antiSpam.isSpam(event, relay)) {
return
}
note.loadEvent(event, author, emptyList())
refreshObservers(note)
}
relay: Relay?,
) = consumeRegularEvent(event, relay)
fun consume(
event: GitIssueEvent,
relay: Relay? = null,
) {
val note = getOrCreateNote(event.id)
val author = getOrCreateUser(event.pubKey)
if (relay != null) {
author.addRelayBeingUsed(relay, event.createdAt)
note.addRelay(relay)
}
// Already processed this event.
if (note.event != null) return
if (antiSpam.isSpam(event, relay)) {
return
}
note.loadEvent(event, author, emptyList())
refreshObservers(note)
}
relay: Relay?,
) = consumeRegularEvent(event, relay)
fun consume(
event: GitReplyEvent,
relay: Relay? = null,
) {
val note = getOrCreateNote(event.id)
val author = getOrCreateUser(event.pubKey)
if (relay != null) {
author.addRelayBeingUsed(relay, event.createdAt)
note.addRelay(relay)
}
// Already processed this event.
if (note.event != null) return
if (antiSpam.isSpam(event, relay)) {
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)
}
relay: Relay?,
) = consumeRegularEvent(event, relay)
fun consume(
event: LongTextNoteEvent,
@@ -869,35 +652,8 @@ object LocalCache {
fun consume(
event: PollNoteEvent,
relay: Relay? = null,
) {
val note = getOrCreateNote(event.id)
val author = getOrCreateUser(event.pubKey)
if (relay != null) {
author.addRelayBeingUsed(relay, event.createdAt)
note.addRelay(relay)
}
// Already processed this event.
if (note.event != null) return
if (antiSpam.isSpam(event, relay)) {
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)
}
relay: Relay?,
) = consumeRegularEvent(event, relay)
private fun consume(
event: LiveActivitiesEvent,
@@ -1137,25 +893,10 @@ object LocalCache {
}
}
fun consume(event: BadgeAwardEvent) {
val note = getOrCreateNote(event.id)
// Already processed this event.
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)
}
fun consume(
event: BadgeAwardEvent,
relay: Relay?,
) = consumeRegularEvent(event, relay)
private fun comsume(
event: NNSEvent,
@@ -1638,31 +1379,7 @@ object LocalCache {
fun consume(
event: CommentEvent,
relay: 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)
}
) = consumeRegularEvent(event, relay)
fun consume(
event: LiveActivitiesChatMessageEvent,
@@ -1759,102 +1476,27 @@ object LocalCache {
fun consume(
event: AudioHeaderEvent,
relay: 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)
}
) = consumeRegularEvent(event, relay)
fun consume(
event: FileHeaderEvent,
relay: 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)
}
) = consumeRegularEvent(event, relay)
fun consume(
event: ProfileGalleryEntryEvent,
relay: 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)
}
) = consumeRegularEvent(event, relay)
fun consume(
event: FileStorageHeaderEvent,
relay: 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)
}
) = consumeRegularEvent(event, relay)
fun consume(
event: FhirResourceEvent,
relay: 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)
}
) = consumeRegularEvent(event, relay)
fun consume(
event: TextNoteModificationEvent,
@@ -1887,22 +1529,7 @@ object LocalCache {
fun consume(
event: HighlightEvent,
relay: 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)
}
) = consumeRegularEvent(event, relay)
fun consume(
event: FileStorageEvent,
@@ -2127,8 +1754,7 @@ object LocalCache {
}
if (note.event?.matchTag1With(text) == true ||
note.idHex.startsWith(text, true) ||
note.idNote().startsWith(text, true)
note.idHex.startsWith(text, true)
) {
if (!note.isHiddenFor(forAccount.flowHiddenUsers.value)) {
return@filter true
@@ -2713,7 +2339,7 @@ object LocalCache {
is AppSpecificDataEvent -> consume(event, relay)
is AudioHeaderEvent -> consume(event, relay)
is AudioTrackEvent -> consume(event, relay)
is BadgeAwardEvent -> consume(event)
is BadgeAwardEvent -> consume(event, relay)
is BadgeDefinitionEvent -> consume(event, relay)
is BadgeProfilesEvent -> consume(event)
is BookmarkListEvent -> consume(event)
@@ -2780,6 +2406,7 @@ object LocalCache {
is MuteListEvent -> consume(event, relay)
is NNSEvent -> comsume(event, relay)
is OtsEvent -> consume(event, relay)
is PictureEvent -> consume(event, relay)
is PrivateDmEvent -> consume(event, relay)
is PrivateOutboxRelayListEvent -> 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.quartz.events.FileHeaderEvent
import com.vitorpamplona.quartz.events.FileStorageHeaderEvent
import com.vitorpamplona.quartz.events.PictureEvent
import com.vitorpamplona.quartz.events.VideoHorizontalEvent
import com.vitorpamplona.quartz.events.VideoVerticalEvent
import kotlinx.coroutines.Dispatchers
@@ -64,85 +65,139 @@ object NostrVideoDataSource : AmethystNostrDataSource("VideoFeed") {
job?.cancel()
}
fun createContextualFilter(): TypedFilter {
fun createContextualFilter(): List<TypedFilter> {
val follows = account.liveStoriesListAuthorsPerRelay.value
return TypedFilter(
types = if (follows == null) setOf(FeedType.GLOBAL) else setOf(FeedType.FOLLOWS),
filter =
SinceAuthorPerRelayFilter(
authors = follows,
kinds = listOf(FileHeaderEvent.KIND, FileStorageHeaderEvent.KIND, VideoHorizontalEvent.KIND, VideoVerticalEvent.KIND),
limit = 200,
tags = mapOf("m" to SUPPORTED_VIDEO_FEED_MIME_TYPES),
since =
latestEOSEs.users[account.userProfile()]
?.followList
?.get(account.settings.defaultStoriesFollowList.value)
?.relayList,
),
val types = if (follows == null) setOf(FeedType.GLOBAL) else setOf(FeedType.FOLLOWS)
return listOf(
TypedFilter(
types = types,
filter =
SinceAuthorPerRelayFilter(
authors = follows,
kinds = listOf(PictureEvent.KIND, VideoHorizontalEvent.KIND, VideoVerticalEvent.KIND),
limit = 200,
since =
latestEOSEs.users[account.userProfile()]
?.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 =
account.liveStoriesFollowLists.value
?.hashtags
?.toList() ?: return null
?.toList() ?: return emptyList()
if (hashToLoad.isEmpty()) return null
if (hashToLoad.isEmpty()) return emptyList()
return TypedFilter(
types = setOf(FeedType.GLOBAL),
filter =
SincePerRelayFilter(
kinds = listOf(FileHeaderEvent.KIND, FileStorageHeaderEvent.KIND, VideoHorizontalEvent.KIND, VideoVerticalEvent.KIND),
tags =
mapOf(
"t" to
hashToLoad
.map { listOf(it, it.lowercase(), it.uppercase(), it.capitalize()) }
.flatten(),
"m" to SUPPORTED_VIDEO_FEED_MIME_TYPES,
),
limit = 100,
since =
latestEOSEs.users[account.userProfile()]
?.followList
?.get(account.settings.defaultStoriesFollowList.value)
?.relayList,
),
val hashtags =
hashToLoad
.map { listOf(it, it.lowercase(), it.uppercase(), it.capitalize()) }
.flatten()
return listOf(
TypedFilter(
types = setOf(FeedType.GLOBAL),
filter =
SincePerRelayFilter(
kinds = listOf(PictureEvent.KIND, VideoHorizontalEvent.KIND, VideoVerticalEvent.KIND),
tags = mapOf("t" to hashtags),
limit = 100,
since =
latestEOSEs.users[account.userProfile()]
?.followList
?.get(account.settings.defaultStoriesFollowList.value)
?.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 =
account.liveStoriesFollowLists.value
?.geotags
?.toList() ?: return null
?.toList() ?: return emptyList()
if (hashToLoad.isEmpty()) return null
if (hashToLoad.isEmpty()) return emptyList()
return TypedFilter(
types = setOf(FeedType.GLOBAL),
filter =
SincePerRelayFilter(
kinds = listOf(FileHeaderEvent.KIND, FileStorageHeaderEvent.KIND, VideoHorizontalEvent.KIND, VideoVerticalEvent.KIND),
tags =
mapOf(
"g" to
hashToLoad
.map { listOf(it, it.lowercase(), it.uppercase(), it.capitalize()) }
.flatten(),
"m" to SUPPORTED_VIDEO_FEED_MIME_TYPES,
),
limit = 100,
since =
latestEOSEs.users[account.userProfile()]
?.followList
?.get(account.settings.defaultStoriesFollowList.value)
?.relayList,
),
val geoHashes =
hashToLoad
.map { listOf(it, it.lowercase(), it.uppercase(), it.capitalize()) }
.flatten()
return listOf(
TypedFilter(
types = setOf(FeedType.GLOBAL),
filter =
SincePerRelayFilter(
kinds = listOf(PictureEvent.KIND, VideoHorizontalEvent.KIND, VideoVerticalEvent.KIND),
tags = mapOf("g" to geoHashes),
limit = 100,
since =
latestEOSEs.users[account.userProfile()]
?.followList
?.get(account.settings.defaultStoriesFollowList.value)
?.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(),
createFollowTagsFilter(),
createFollowGeohashesFilter(),
).ifEmpty { null }
).flatten().ifEmpty { null }
}
}
@@ -554,7 +554,7 @@ open class NewPostViewModel : ViewModel() {
val replyingTo = originalNote
if (replyingTo?.event is CommentEvent) {
if (replyingTo?.event is CommentEvent || replyingTo?.event is Event) {
account?.sendReplyComment(
message = tagger.message,
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.videoGalleryModifier
import com.vitorpamplona.ammolite.service.HttpClientManager
import com.vitorpamplona.quartz.events.Dimension
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
@@ -211,7 +212,7 @@ fun VideoView(
waveform: ImmutableList<Int>? = null,
artworkUri: String? = null,
authorName: String? = null,
dimensions: String? = null,
dimensions: Dimension? = null,
blurhash: String? = null,
nostrUriCallback: String? = null,
onDialog: ((Boolean) -> Unit)? = null,
@@ -242,7 +243,7 @@ fun VideoView(
waveform: ImmutableList<Int>? = null,
artworkUri: String? = null,
authorName: String? = null,
dimensions: String? = null,
dimensions: Dimension? = null,
blurhash: String? = null,
nostrUriCallback: String? = null,
onDialog: ((Boolean) -> Unit)? = null,
@@ -260,7 +261,7 @@ fun VideoView(
}
if (blurhash == null) {
val ratio = aspectRatio(dimensions)
val ratio = dimensions?.aspectRatio()
val modifier =
if (ratio != null && automaticallyStartPlayback.value) {
Modifier.aspectRatio(ratio)
@@ -294,7 +295,7 @@ fun VideoView(
}
}
} else {
val ratio = aspectRatio(dimensions)
val ratio = dimensions?.aspectRatio()
val modifier =
if (ratio != null) {
@@ -98,9 +98,9 @@ import com.vitorpamplona.amethyst.ui.theme.videoGalleryModifier
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.Nip19Bech32
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.events.Dimension
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@@ -389,7 +389,7 @@ fun UrlImageView(
accountViewModel: AccountViewModel,
alwayShowImage: Boolean = false,
) {
val ratio = remember(content) { aspectRatio(content.dim) }
val ratio = content.dim?.aspectRatio()
val showImage =
remember {
@@ -552,26 +552,10 @@ fun ShowHash(content: MediaUrlContent) {
verifiedHash?.let { HashVerificationSymbol(it) }
}
fun aspectRatio(dim: String?): Float? {
fun aspectRatio(dim: Dimension?): Float? {
if (dim == null) return null
if (dim == "0x0") return null
val parts = dim.split("x")
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
}
return dim.width.toFloat() / dim.height.toFloat()
}
@Composable
@@ -719,7 +703,7 @@ fun ShareImageAction(
videoUri: String?,
postNostrUri: String?,
blurhash: String?,
dim: String?,
dim: Dimension?,
hash: String?,
mimeType: String?,
onDismiss: () -> Unit,
@@ -20,6 +20,7 @@
*/
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.LocalCache
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.MuteListEvent
import com.vitorpamplona.quartz.events.PeopleListEvent
import com.vitorpamplona.quartz.events.PictureEvent
import com.vitorpamplona.quartz.events.VideoHorizontalEvent
import com.vitorpamplona.quartz.events.VideoVerticalEvent
@@ -66,10 +68,11 @@ class VideoFeedFilter(
val noteEvent = it.event
return (
(noteEvent is FileHeaderEvent && noteEvent.hasUrl() && noteEvent.isOneOf(SUPPORTED_VIDEO_FEED_MIME_TYPES_SET)) ||
(noteEvent is VideoVerticalEvent && noteEvent.hasUrl() && noteEvent.isOneOf(SUPPORTED_VIDEO_FEED_MIME_TYPES_SET)) ||
(noteEvent is VideoHorizontalEvent && noteEvent.hasUrl() && noteEvent.isOneOf(SUPPORTED_VIDEO_FEED_MIME_TYPES_SET)) ||
(noteEvent is FileStorageHeaderEvent && 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.urls().any { isImageOrVideoUrl(it) } || 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 PictureEvent
) &&
params.match(noteEvent) &&
(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.FileStorageHeaderDisplay
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.RenderAudioHeader
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.NIP90StatusEvent
import com.vitorpamplona.quartz.events.PeopleListEvent
import com.vitorpamplona.quartz.events.PictureEvent
import com.vitorpamplona.quartz.events.PinListEvent
import com.vitorpamplona.quartz.events.PollNoteEvent
import com.vitorpamplona.quartz.events.PrivateDmEvent
@@ -730,6 +732,8 @@ private fun RenderNoteRow(
is FileHeaderEvent -> FileHeaderDisplay(baseNote, true, false, accountViewModel)
is VideoHorizontalEvent -> 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 CommunityPostApprovalEvent -> {
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.ChatroomKey
import com.vitorpamplona.quartz.events.ChatroomKeyable
import com.vitorpamplona.quartz.events.Dimension
import com.vitorpamplona.quartz.events.DraftEvent
import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.EventInterface
@@ -754,7 +755,7 @@ class AccountViewModel(
url: String,
relay: String?,
blurhash: String?,
dim: String?,
dim: Dimension?,
hash: String?,
mimeType: String?,
) {
@@ -338,6 +338,8 @@ fun InnerRenderGalleryThumb(
note: Note,
accountViewModel: AccountViewModel,
) {
val noteEvent = note.event as? ProfileGalleryEntryEvent ?: return
Box(
Modifier
.fillMaxWidth()
@@ -345,11 +347,11 @@ fun InnerRenderGalleryThumb(
contentAlignment = BottomStart,
) {
card.image?.let {
var blurHash = (note.event as ProfileGalleryEntryEvent).blurhash()
var description = (note.event as ProfileGalleryEntryEvent).content
val blurHash = noteEvent.blurhash()
val description = noteEvent.content
// var hash = (note.event as ProfileGalleryEntryEvent).hash()
var dimensions = (note.event as ProfileGalleryEntryEvent).dimensions()
var mimeType = (note.event as ProfileGalleryEntryEvent).mimeType()
val dimensions = noteEvent.dimensions()
val mimeType = noteEvent.mimeType()
var content: BaseMediaContent? = null
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.FileHeaderDisplay
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.RenderChannelMessage
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.LongTextNoteEvent
import com.vitorpamplona.quartz.events.PeopleListEvent
import com.vitorpamplona.quartz.events.PictureEvent
import com.vitorpamplona.quartz.events.PinListEvent
import com.vitorpamplona.quartz.events.PollNoteEvent
import com.vitorpamplona.quartz.events.PrivateDmEvent
@@ -509,6 +511,8 @@ private fun FullBleedNoteCompose(
)
} else if (noteEvent is VideoEvent) {
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) {
FileHeaderDisplay(baseNote, roundedCorner = true, isFiniteHeight = false, accountViewModel = accountViewModel)
} else if (noteEvent is FileStorageHeaderEvent) {
@@ -43,9 +43,9 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
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.FileStorageHeaderDisplay
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.DisappearingScaffold
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.quartz.events.FileHeaderEvent
import com.vitorpamplona.quartz.events.FileStorageHeaderEvent
import com.vitorpamplona.quartz.events.PictureEvent
import com.vitorpamplona.quartz.events.VideoEvent
@Composable
@@ -290,7 +292,11 @@ private fun RenderVideoOrPictureNote(
Column(Modifier.fillMaxSize(1f), verticalArrangement = Arrangement.Center) {
Row(Modifier.weight(1f), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center) {
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)
} else if (noteEvent is FileStorageHeaderEvent) {
FileStorageHeaderDisplay(note, false, true, accountViewModel)