Adds support for Instagram feeds. https://github.com/nostr-protocol/nips/pull/1551
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
+117
-62
@@ -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) {
|
||||||
|
|||||||
+5
-21
@@ -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,
|
||||||
|
)*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-1
@@ -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?,
|
||||||
) {
|
) {
|
||||||
|
|||||||
+6
-4
@@ -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)) {
|
||||||
|
|||||||
+4
@@ -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) {
|
||||||
|
|||||||
+8
-2
@@ -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)
|
||||||
|
|||||||
+8
-7
@@ -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)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user