Adds support for NIP-22 exclusive geo posts
This commit is contained in:
@@ -47,8 +47,11 @@ import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter
|
|||||||
import com.vitorpamplona.ammolite.service.HttpClientManager
|
import com.vitorpamplona.ammolite.service.HttpClientManager
|
||||||
import com.vitorpamplona.quartz.crypto.KeyPair
|
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||||
import com.vitorpamplona.quartz.encoders.ATag
|
import com.vitorpamplona.quartz.encoders.ATag
|
||||||
|
import com.vitorpamplona.quartz.encoders.ETag
|
||||||
|
import com.vitorpamplona.quartz.encoders.EventHint
|
||||||
import com.vitorpamplona.quartz.encoders.HexKey
|
import com.vitorpamplona.quartz.encoders.HexKey
|
||||||
import com.vitorpamplona.quartz.encoders.Nip47WalletConnect
|
import com.vitorpamplona.quartz.encoders.Nip47WalletConnect
|
||||||
|
import com.vitorpamplona.quartz.encoders.PTag
|
||||||
import com.vitorpamplona.quartz.encoders.RelayUrlFormatter
|
import com.vitorpamplona.quartz.encoders.RelayUrlFormatter
|
||||||
import com.vitorpamplona.quartz.encoders.hexToByteArray
|
import com.vitorpamplona.quartz.encoders.hexToByteArray
|
||||||
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
|
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
|
||||||
@@ -60,6 +63,7 @@ import com.vitorpamplona.quartz.events.ChannelMetadataEvent
|
|||||||
import com.vitorpamplona.quartz.events.ChatMessageEvent
|
import com.vitorpamplona.quartz.events.ChatMessageEvent
|
||||||
import com.vitorpamplona.quartz.events.ChatMessageRelayListEvent
|
import com.vitorpamplona.quartz.events.ChatMessageRelayListEvent
|
||||||
import com.vitorpamplona.quartz.events.ClassifiedsEvent
|
import com.vitorpamplona.quartz.events.ClassifiedsEvent
|
||||||
|
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
|
||||||
@@ -2145,6 +2149,182 @@ class Account(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun sendReplyComment(
|
||||||
|
message: String,
|
||||||
|
replyingTo: Note,
|
||||||
|
directMentionsUsers: Set<User> = emptySet(),
|
||||||
|
directMentionsNotes: Set<Note> = emptySet(),
|
||||||
|
nip94attachments: List<FileHeaderEvent>? = null,
|
||||||
|
geohash: String? = null,
|
||||||
|
zapReceiver: List<ZapSplitSetup>? = null,
|
||||||
|
wantsToMarkAsSensitive: Boolean = false,
|
||||||
|
zapRaiserAmount: Long? = null,
|
||||||
|
relayList: List<RelaySetupInfo>,
|
||||||
|
draftTag: String? = null,
|
||||||
|
) {
|
||||||
|
if (!isWriteable()) return
|
||||||
|
|
||||||
|
val usersMentioned =
|
||||||
|
directMentionsUsers
|
||||||
|
.mapTo(HashSet(directMentionsUsers.size)) {
|
||||||
|
PTag(it.pubkeyHex, it.latestMetadataRelay)
|
||||||
|
}
|
||||||
|
|
||||||
|
val addressesMentioned =
|
||||||
|
directMentionsNotes
|
||||||
|
.mapNotNullTo(HashSet(directMentionsNotes.size)) { note ->
|
||||||
|
if (note is AddressableNote) {
|
||||||
|
note.address
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val eventsMentioned =
|
||||||
|
directMentionsNotes
|
||||||
|
.mapNotNullTo(HashSet(directMentionsNotes.size)) { note ->
|
||||||
|
if (note !is AddressableNote) {
|
||||||
|
ETag(note.idHex, note.author?.pubkeyHex, note.relayHintUrl())
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
Client.send(it, relayList = relayList)
|
||||||
|
LocalCache.justConsume(it, null)
|
||||||
|
|
||||||
|
replyingTo.event?.let {
|
||||||
|
Client.send(it, relayList = relayList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun sendGeoComment(
|
||||||
|
message: String,
|
||||||
|
geohash: String,
|
||||||
|
replyingTo: Note? = null,
|
||||||
|
directMentionsUsers: Set<User> = emptySet(),
|
||||||
|
directMentionsNotes: Set<Note> = emptySet(),
|
||||||
|
nip94attachments: List<FileHeaderEvent>? = null,
|
||||||
|
zapReceiver: List<ZapSplitSetup>? = null,
|
||||||
|
wantsToMarkAsSensitive: Boolean = false,
|
||||||
|
zapRaiserAmount: Long? = null,
|
||||||
|
relayList: List<RelaySetupInfo>,
|
||||||
|
draftTag: String? = null,
|
||||||
|
) {
|
||||||
|
if (!isWriteable()) return
|
||||||
|
|
||||||
|
val usersMentioned =
|
||||||
|
directMentionsUsers
|
||||||
|
.mapTo(HashSet(directMentionsUsers.size)) {
|
||||||
|
PTag(it.pubkeyHex, it.latestMetadataRelay)
|
||||||
|
}
|
||||||
|
|
||||||
|
val addressesMentioned =
|
||||||
|
directMentionsNotes
|
||||||
|
.mapNotNullTo(HashSet(directMentionsNotes.size)) { note ->
|
||||||
|
if (note is AddressableNote) {
|
||||||
|
note.address
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val eventsMentioned =
|
||||||
|
directMentionsNotes
|
||||||
|
.mapNotNullTo(HashSet(directMentionsNotes.size)) { note ->
|
||||||
|
if (note !is AddressableNote) {
|
||||||
|
ETag(note.idHex, note.author?.pubkeyHex, note.relayHintUrl())
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (replyingTo != null) {
|
||||||
|
CommentEvent.replyComment(
|
||||||
|
msg = message,
|
||||||
|
replyingTo = EventHint<CommentEvent>(replyingTo.event as CommentEvent, replyingTo.relayHintUrl()),
|
||||||
|
usersMentioned = usersMentioned,
|
||||||
|
addressesMentioned = addressesMentioned,
|
||||||
|
eventsMentioned = eventsMentioned,
|
||||||
|
nip94attachments = nip94attachments,
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
CommentEvent.createGeoComment(
|
||||||
|
msg = message,
|
||||||
|
geohash = geohash,
|
||||||
|
usersMentioned = usersMentioned,
|
||||||
|
addressesMentioned = addressesMentioned,
|
||||||
|
eventsMentioned = eventsMentioned,
|
||||||
|
nip94attachments = nip94attachments,
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun sendPost(
|
suspend fun sendPost(
|
||||||
message: String,
|
message: String,
|
||||||
replyTo: List<Note>?,
|
replyTo: List<Note>?,
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ import com.vitorpamplona.quartz.events.ChatMessageEvent
|
|||||||
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.ClassifiedsEvent
|
import com.vitorpamplona.quartz.events.ClassifiedsEvent
|
||||||
|
import com.vitorpamplona.quartz.events.CommentEvent
|
||||||
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
|
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
|
||||||
import com.vitorpamplona.quartz.events.CommunityListEvent
|
import com.vitorpamplona.quartz.events.CommunityListEvent
|
||||||
import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
|
import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
|
||||||
@@ -818,6 +819,8 @@ object LocalCache {
|
|||||||
is LongTextNoteEvent -> event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) }
|
is LongTextNoteEvent -> event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) }
|
||||||
is GitReplyEvent -> event.tagsWithoutCitations().filter { it != event.repository()?.toTag() }.mapNotNull { checkGetOrCreateNote(it) }
|
is GitReplyEvent -> event.tagsWithoutCitations().filter { it != event.repository()?.toTag() }.mapNotNull { checkGetOrCreateNote(it) }
|
||||||
is TextNoteEvent -> event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) }
|
is TextNoteEvent -> event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) }
|
||||||
|
is CommentEvent -> event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) }
|
||||||
|
|
||||||
is ChatMessageEvent -> event.taggedEvents().mapNotNull { checkGetOrCreateNote(it) }
|
is ChatMessageEvent -> event.taggedEvents().mapNotNull { checkGetOrCreateNote(it) }
|
||||||
is LnZapEvent ->
|
is LnZapEvent ->
|
||||||
event.zappedPost().mapNotNull { checkGetOrCreateNote(it) } +
|
event.zappedPost().mapNotNull { checkGetOrCreateNote(it) } +
|
||||||
@@ -1632,6 +1635,35 @@ object LocalCache {
|
|||||||
refreshObservers(note)
|
refreshObservers(note)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
fun consume(
|
fun consume(
|
||||||
event: LiveActivitiesChatMessageEvent,
|
event: LiveActivitiesChatMessageEvent,
|
||||||
relay: Relay?,
|
relay: Relay?,
|
||||||
@@ -2698,6 +2730,7 @@ object LocalCache {
|
|||||||
is ChatMessageEvent -> consume(event, relay)
|
is ChatMessageEvent -> consume(event, relay)
|
||||||
is ChatMessageRelayListEvent -> consume(event, relay)
|
is ChatMessageRelayListEvent -> consume(event, relay)
|
||||||
is ClassifiedsEvent -> consume(event, relay)
|
is ClassifiedsEvent -> consume(event, relay)
|
||||||
|
is CommentEvent -> consume(event, relay)
|
||||||
is CommunityDefinitionEvent -> consume(event, relay)
|
is CommunityDefinitionEvent -> consume(event, relay)
|
||||||
is CommunityListEvent -> consume(event, relay)
|
is CommunityListEvent -> consume(event, relay)
|
||||||
is CommunityPostApprovalEvent -> {
|
is CommunityPostApprovalEvent -> {
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ class AddressableNote(
|
|||||||
return minOf(publishedAt, lastCreatedAt)
|
return minOf(publishedAt, lastCreatedAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun dTag(): String? = (event as? AddressableEvent)?.dTag()
|
fun dTag(): String = address.dTag
|
||||||
|
|
||||||
override fun wasOrShouldBeDeletedBy(
|
override fun wasOrShouldBeDeletedBy(
|
||||||
deletionEvents: Set<HexKey>,
|
deletionEvents: Set<HexKey>,
|
||||||
@@ -157,13 +157,27 @@ open class Note(
|
|||||||
host.id,
|
host.id,
|
||||||
host.pubKey,
|
host.pubKey,
|
||||||
host.kind,
|
host.kind,
|
||||||
relays.firstOrNull()?.url,
|
relayHintUrl(),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Nip19Bech32.createNEvent(idHex, author?.pubkeyHex, event?.kind(), relays.firstOrNull()?.url)
|
Nip19Bech32.createNEvent(idHex, author?.pubkeyHex, event?.kind(), relayHintUrl())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Nip19Bech32.createNEvent(idHex, author?.pubkeyHex, event?.kind(), relays.firstOrNull()?.url)
|
Nip19Bech32.createNEvent(idHex, author?.pubkeyHex, event?.kind(), relayHintUrl())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun relayHintUrl(): String? {
|
||||||
|
val authorRelay = author?.latestMetadataRelay
|
||||||
|
|
||||||
|
return if (relays.isNotEmpty()) {
|
||||||
|
if (authorRelay != null && relays.any { it.url == authorRelay }) {
|
||||||
|
authorRelay
|
||||||
|
} else {
|
||||||
|
relays.firstOrNull()?.url
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import com.vitorpamplona.quartz.events.CalendarRSVPEvent
|
|||||||
import com.vitorpamplona.quartz.events.CalendarTimeSlotEvent
|
import com.vitorpamplona.quartz.events.CalendarTimeSlotEvent
|
||||||
import com.vitorpamplona.quartz.events.ChannelMessageEvent
|
import com.vitorpamplona.quartz.events.ChannelMessageEvent
|
||||||
import com.vitorpamplona.quartz.events.ChatMessageRelayListEvent
|
import com.vitorpamplona.quartz.events.ChatMessageRelayListEvent
|
||||||
|
import com.vitorpamplona.quartz.events.CommentEvent
|
||||||
import com.vitorpamplona.quartz.events.ContactListEvent
|
import com.vitorpamplona.quartz.events.ContactListEvent
|
||||||
import com.vitorpamplona.quartz.events.DraftEvent
|
import com.vitorpamplona.quartz.events.DraftEvent
|
||||||
import com.vitorpamplona.quartz.events.EmojiPackSelectionEvent
|
import com.vitorpamplona.quartz.events.EmojiPackSelectionEvent
|
||||||
@@ -237,6 +238,7 @@ object NostrAccountDataSource : AmethystNostrDataSource("AccountData") {
|
|||||||
GitIssueEvent.KIND,
|
GitIssueEvent.KIND,
|
||||||
GitPatchEvent.KIND,
|
GitPatchEvent.KIND,
|
||||||
HighlightEvent.KIND,
|
HighlightEvent.KIND,
|
||||||
|
CommentEvent.KIND,
|
||||||
CalendarDateSlotEvent.KIND,
|
CalendarDateSlotEvent.KIND,
|
||||||
CalendarTimeSlotEvent.KIND,
|
CalendarTimeSlotEvent.KIND,
|
||||||
CalendarRSVPEvent.KIND,
|
CalendarRSVPEvent.KIND,
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ import com.vitorpamplona.quartz.events.AudioHeaderEvent
|
|||||||
import com.vitorpamplona.quartz.events.AudioTrackEvent
|
import com.vitorpamplona.quartz.events.AudioTrackEvent
|
||||||
import com.vitorpamplona.quartz.events.ChannelMessageEvent
|
import com.vitorpamplona.quartz.events.ChannelMessageEvent
|
||||||
import com.vitorpamplona.quartz.events.ClassifiedsEvent
|
import com.vitorpamplona.quartz.events.ClassifiedsEvent
|
||||||
|
import com.vitorpamplona.quartz.events.CommentEvent
|
||||||
import com.vitorpamplona.quartz.events.HighlightEvent
|
import com.vitorpamplona.quartz.events.HighlightEvent
|
||||||
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
|
|
||||||
import com.vitorpamplona.quartz.events.LongTextNoteEvent
|
import com.vitorpamplona.quartz.events.LongTextNoteEvent
|
||||||
import com.vitorpamplona.quartz.events.PollNoteEvent
|
import com.vitorpamplona.quartz.events.PollNoteEvent
|
||||||
import com.vitorpamplona.quartz.events.TextNoteEvent
|
import com.vitorpamplona.quartz.events.TextNoteEvent
|
||||||
@@ -57,12 +57,12 @@ object NostrGeohashDataSource : AmethystNostrDataSource("SingleGeoHashFeed") {
|
|||||||
ChannelMessageEvent.KIND,
|
ChannelMessageEvent.KIND,
|
||||||
LongTextNoteEvent.KIND,
|
LongTextNoteEvent.KIND,
|
||||||
PollNoteEvent.KIND,
|
PollNoteEvent.KIND,
|
||||||
LiveActivitiesChatMessageEvent.KIND,
|
|
||||||
ClassifiedsEvent.KIND,
|
ClassifiedsEvent.KIND,
|
||||||
HighlightEvent.KIND,
|
HighlightEvent.KIND,
|
||||||
AudioTrackEvent.KIND,
|
AudioTrackEvent.KIND,
|
||||||
AudioHeaderEvent.KIND,
|
AudioHeaderEvent.KIND,
|
||||||
WikiNoteEvent.KIND,
|
WikiNoteEvent.KIND,
|
||||||
|
CommentEvent.KIND,
|
||||||
),
|
),
|
||||||
limit = 200,
|
limit = 200,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import com.vitorpamplona.quartz.events.AudioHeaderEvent
|
|||||||
import com.vitorpamplona.quartz.events.AudioTrackEvent
|
import com.vitorpamplona.quartz.events.AudioTrackEvent
|
||||||
import com.vitorpamplona.quartz.events.ChannelMessageEvent
|
import com.vitorpamplona.quartz.events.ChannelMessageEvent
|
||||||
import com.vitorpamplona.quartz.events.ClassifiedsEvent
|
import com.vitorpamplona.quartz.events.ClassifiedsEvent
|
||||||
|
import com.vitorpamplona.quartz.events.CommentEvent
|
||||||
import com.vitorpamplona.quartz.events.HighlightEvent
|
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
|
||||||
@@ -66,6 +67,7 @@ object NostrHashtagDataSource : AmethystNostrDataSource("SingleHashtagFeed") {
|
|||||||
AudioTrackEvent.KIND,
|
AudioTrackEvent.KIND,
|
||||||
AudioHeaderEvent.KIND,
|
AudioHeaderEvent.KIND,
|
||||||
WikiNoteEvent.KIND,
|
WikiNoteEvent.KIND,
|
||||||
|
CommentEvent.KIND,
|
||||||
),
|
),
|
||||||
limit = 200,
|
limit = 200,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
|
|||||||
import com.vitorpamplona.quartz.events.AudioHeaderEvent
|
import com.vitorpamplona.quartz.events.AudioHeaderEvent
|
||||||
import com.vitorpamplona.quartz.events.AudioTrackEvent
|
import com.vitorpamplona.quartz.events.AudioTrackEvent
|
||||||
import com.vitorpamplona.quartz.events.ClassifiedsEvent
|
import com.vitorpamplona.quartz.events.ClassifiedsEvent
|
||||||
|
import com.vitorpamplona.quartz.events.CommentEvent
|
||||||
import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
|
import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
|
||||||
import com.vitorpamplona.quartz.events.GenericRepostEvent
|
import com.vitorpamplona.quartz.events.GenericRepostEvent
|
||||||
import com.vitorpamplona.quartz.events.HighlightEvent
|
import com.vitorpamplona.quartz.events.HighlightEvent
|
||||||
@@ -165,7 +166,7 @@ object NostrHomeDataSource : AmethystNostrDataSource("HomeFeed") {
|
|||||||
HighlightEvent.KIND,
|
HighlightEvent.KIND,
|
||||||
AudioHeaderEvent.KIND,
|
AudioHeaderEvent.KIND,
|
||||||
AudioTrackEvent.KIND,
|
AudioTrackEvent.KIND,
|
||||||
PinListEvent.KIND,
|
CommentEvent.KIND,
|
||||||
WikiNoteEvent.KIND,
|
WikiNoteEvent.KIND,
|
||||||
),
|
),
|
||||||
tags =
|
tags =
|
||||||
@@ -204,8 +205,8 @@ object NostrHomeDataSource : AmethystNostrDataSource("HomeFeed") {
|
|||||||
HighlightEvent.KIND,
|
HighlightEvent.KIND,
|
||||||
AudioHeaderEvent.KIND,
|
AudioHeaderEvent.KIND,
|
||||||
AudioTrackEvent.KIND,
|
AudioTrackEvent.KIND,
|
||||||
PinListEvent.KIND,
|
|
||||||
WikiNoteEvent.KIND,
|
WikiNoteEvent.KIND,
|
||||||
|
CommentEvent.KIND,
|
||||||
),
|
),
|
||||||
tags =
|
tags =
|
||||||
mapOf(
|
mapOf(
|
||||||
@@ -244,6 +245,7 @@ object NostrHomeDataSource : AmethystNostrDataSource("HomeFeed") {
|
|||||||
PinListEvent.KIND,
|
PinListEvent.KIND,
|
||||||
WikiNoteEvent.KIND,
|
WikiNoteEvent.KIND,
|
||||||
CommunityPostApprovalEvent.KIND,
|
CommunityPostApprovalEvent.KIND,
|
||||||
|
CommentEvent.KIND,
|
||||||
),
|
),
|
||||||
tags =
|
tags =
|
||||||
mapOf(
|
mapOf(
|
||||||
|
|||||||
+2
@@ -38,6 +38,7 @@ import com.vitorpamplona.quartz.events.BookmarkListEvent
|
|||||||
import com.vitorpamplona.quartz.events.ChannelCreateEvent
|
import com.vitorpamplona.quartz.events.ChannelCreateEvent
|
||||||
import com.vitorpamplona.quartz.events.ChannelMetadataEvent
|
import com.vitorpamplona.quartz.events.ChannelMetadataEvent
|
||||||
import com.vitorpamplona.quartz.events.ClassifiedsEvent
|
import com.vitorpamplona.quartz.events.ClassifiedsEvent
|
||||||
|
import com.vitorpamplona.quartz.events.CommentEvent
|
||||||
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
|
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
|
||||||
import com.vitorpamplona.quartz.events.EmojiPackEvent
|
import com.vitorpamplona.quartz.events.EmojiPackEvent
|
||||||
import com.vitorpamplona.quartz.events.HighlightEvent
|
import com.vitorpamplona.quartz.events.HighlightEvent
|
||||||
@@ -177,6 +178,7 @@ object NostrSearchEventOrUserDataSource : AmethystNostrDataSource("SearchEventFe
|
|||||||
PollNoteEvent.KIND,
|
PollNoteEvent.KIND,
|
||||||
NNSEvent.KIND,
|
NNSEvent.KIND,
|
||||||
WikiNoteEvent.KIND,
|
WikiNoteEvent.KIND,
|
||||||
|
CommentEvent.KIND,
|
||||||
),
|
),
|
||||||
search = mySearchString,
|
search = mySearchString,
|
||||||
limit = 100,
|
limit = 100,
|
||||||
|
|||||||
@@ -39,29 +39,23 @@ class NewMessageTagger(
|
|||||||
var dao: Dao,
|
var dao: Dao,
|
||||||
) {
|
) {
|
||||||
val directMentions = mutableSetOf<HexKey>()
|
val directMentions = mutableSetOf<HexKey>()
|
||||||
|
val directMentionsNotes = mutableSetOf<Note>()
|
||||||
|
val directMentionsUsers = mutableSetOf<User>()
|
||||||
|
|
||||||
fun addUserToMentions(user: User) {
|
fun addUserToMentions(user: User) {
|
||||||
|
directMentionsUsers.add(user)
|
||||||
directMentions.add(user.pubkeyHex)
|
directMentions.add(user.pubkeyHex)
|
||||||
pTags = if (pTags?.contains(user) == true) pTags else pTags?.plus(user) ?: listOf(user)
|
pTags = if (pTags?.contains(user) == true) pTags else pTags?.plus(user) ?: listOf(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addNoteToReplyTos(note: Note) {
|
fun addNoteToReplyTos(note: Note) {
|
||||||
|
directMentionsNotes.add(note)
|
||||||
directMentions.add(note.idHex)
|
directMentions.add(note.idHex)
|
||||||
|
|
||||||
note.author?.let { addUserToMentions(it) }
|
note.author?.let { addUserToMentions(it) }
|
||||||
eTags = if (eTags?.contains(note) == true) eTags else eTags?.plus(note) ?: listOf(note)
|
eTags = if (eTags?.contains(note) == true) eTags else eTags?.plus(note) ?: listOf(note)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun tagIndex(user: User): Int {
|
|
||||||
// Postr Events assembles replies before mentions in the tag order
|
|
||||||
return (if (channelHex != null) 1 else 0) + (eTags?.size ?: 0) + (pTags?.indexOf(user) ?: 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun tagIndex(note: Note): Int {
|
|
||||||
// Postr Events assembles replies before mentions in the tag order
|
|
||||||
return (if (channelHex != null) 1 else 0) + (eTags?.indexOf(note) ?: 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun run() {
|
suspend fun run() {
|
||||||
// adds all references to mentions and reply tos
|
// adds all references to mentions and reply tos
|
||||||
message.split('\n').forEach { paragraph: String ->
|
message.split('\n').forEach { paragraph: String ->
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
|
|||||||
import com.vitorpamplona.quartz.events.BaseTextNoteEvent
|
import com.vitorpamplona.quartz.events.BaseTextNoteEvent
|
||||||
import com.vitorpamplona.quartz.events.ChatMessageEvent
|
import com.vitorpamplona.quartz.events.ChatMessageEvent
|
||||||
import com.vitorpamplona.quartz.events.ClassifiedsEvent
|
import com.vitorpamplona.quartz.events.ClassifiedsEvent
|
||||||
|
import com.vitorpamplona.quartz.events.CommentEvent
|
||||||
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
|
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
|
||||||
import com.vitorpamplona.quartz.events.DraftEvent
|
import com.vitorpamplona.quartz.events.DraftEvent
|
||||||
import com.vitorpamplona.quartz.events.Event
|
import com.vitorpamplona.quartz.events.Event
|
||||||
@@ -162,6 +163,7 @@ open class NewPostViewModel : ViewModel() {
|
|||||||
// GeoHash
|
// GeoHash
|
||||||
var wantsToAddGeoHash by mutableStateOf(false)
|
var wantsToAddGeoHash by mutableStateOf(false)
|
||||||
var location: StateFlow<LocationState.LocationResult>? = null
|
var location: StateFlow<LocationState.LocationResult>? = null
|
||||||
|
var wantsExclusiveGeoPost by mutableStateOf(false)
|
||||||
|
|
||||||
// ZapRaiser
|
// ZapRaiser
|
||||||
var canAddZapRaiser by mutableStateOf(false)
|
var canAddZapRaiser by mutableStateOf(false)
|
||||||
@@ -336,7 +338,13 @@ open class NewPostViewModel : ViewModel() {
|
|||||||
wantsForwardZapTo = localfowardZapTo.isNotEmpty()
|
wantsForwardZapTo = localfowardZapTo.isNotEmpty()
|
||||||
|
|
||||||
wantsToMarkAsSensitive = draftEvent.tags().any { it.size > 1 && it[0] == "content-warning" }
|
wantsToMarkAsSensitive = draftEvent.tags().any { it.size > 1 && it[0] == "content-warning" }
|
||||||
wantsToAddGeoHash = draftEvent.tags().any { it.size > 1 && it[0] == "g" }
|
|
||||||
|
val geohash = draftEvent.getGeoHash()
|
||||||
|
wantsToAddGeoHash = geohash != null
|
||||||
|
if (geohash != null) {
|
||||||
|
wantsExclusiveGeoPost = draftEvent.kind() == CommentEvent.KIND
|
||||||
|
}
|
||||||
|
|
||||||
val zapraiser = draftEvent.tags().filter { it.size > 1 && it[0] == "zapraiser" }
|
val zapraiser = draftEvent.tags().filter { it.size > 1 && it[0] == "zapraiser" }
|
||||||
wantsZapraiser = zapraiser.isNotEmpty()
|
wantsZapraiser = zapraiser.isNotEmpty()
|
||||||
zapRaiserAmount = null
|
zapRaiserAmount = null
|
||||||
@@ -544,7 +552,37 @@ open class NewPostViewModel : ViewModel() {
|
|||||||
// Doesn't send as nip94 yet because we don't know if it makes sense.
|
// Doesn't send as nip94 yet because we don't know if it makes sense.
|
||||||
// usedAttachments.forEach { account?.sendHeader(it, relayList, {}) }
|
// usedAttachments.forEach { account?.sendHeader(it, relayList, {}) }
|
||||||
|
|
||||||
if (originalNote?.channelHex() != null) {
|
val replyingTo = originalNote
|
||||||
|
|
||||||
|
if (replyingTo?.event is CommentEvent) {
|
||||||
|
account?.sendReplyComment(
|
||||||
|
message = tagger.message,
|
||||||
|
replyingTo = replyingTo,
|
||||||
|
directMentionsUsers = tagger.directMentionsUsers,
|
||||||
|
directMentionsNotes = tagger.directMentionsNotes,
|
||||||
|
nip94attachments = usedAttachments,
|
||||||
|
geohash = geoHash,
|
||||||
|
zapReceiver = zapReceiver,
|
||||||
|
wantsToMarkAsSensitive = wantsToMarkAsSensitive,
|
||||||
|
zapRaiserAmount = localZapRaiserAmount,
|
||||||
|
relayList = relayList,
|
||||||
|
draftTag = localDraft,
|
||||||
|
)
|
||||||
|
} else if (wantsExclusiveGeoPost && geoHash != null && (originalNote == null || originalNote?.event is CommentEvent)) {
|
||||||
|
account?.sendGeoComment(
|
||||||
|
message = tagger.message,
|
||||||
|
geohash = geoHash,
|
||||||
|
replyingTo = originalNote,
|
||||||
|
directMentionsUsers = tagger.directMentionsUsers,
|
||||||
|
directMentionsNotes = tagger.directMentionsNotes,
|
||||||
|
nip94attachments = usedAttachments,
|
||||||
|
zapReceiver = zapReceiver,
|
||||||
|
wantsToMarkAsSensitive = wantsToMarkAsSensitive,
|
||||||
|
zapRaiserAmount = localZapRaiserAmount,
|
||||||
|
relayList = relayList,
|
||||||
|
draftTag = localDraft,
|
||||||
|
)
|
||||||
|
} else if (originalNote?.channelHex() != null) {
|
||||||
if (originalNote is AddressableEvent && originalNote?.address() != null) {
|
if (originalNote is AddressableEvent && originalNote?.address() != null) {
|
||||||
account?.sendLiveMessage(
|
account?.sendLiveMessage(
|
||||||
message = tagger.message,
|
message = tagger.message,
|
||||||
@@ -946,6 +984,7 @@ open class NewPostViewModel : ViewModel() {
|
|||||||
wantsForwardZapTo = false
|
wantsForwardZapTo = false
|
||||||
wantsToMarkAsSensitive = false
|
wantsToMarkAsSensitive = false
|
||||||
wantsToAddGeoHash = false
|
wantsToAddGeoHash = false
|
||||||
|
wantsExclusiveGeoPost = false
|
||||||
forwardZapTo = Split()
|
forwardZapTo = Split()
|
||||||
forwardZapToEditting = TextFieldValue("")
|
forwardZapToEditting = TextFieldValue("")
|
||||||
|
|
||||||
|
|||||||
+2
@@ -24,6 +24,7 @@ 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
|
||||||
import com.vitorpamplona.quartz.events.ChannelMessageEvent
|
import com.vitorpamplona.quartz.events.ChannelMessageEvent
|
||||||
|
import com.vitorpamplona.quartz.events.CommentEvent
|
||||||
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
|
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
|
||||||
import com.vitorpamplona.quartz.events.MuteListEvent
|
import com.vitorpamplona.quartz.events.MuteListEvent
|
||||||
import com.vitorpamplona.quartz.events.PeopleListEvent
|
import com.vitorpamplona.quartz.events.PeopleListEvent
|
||||||
@@ -75,6 +76,7 @@ class HomeConversationsFeedFilter(
|
|||||||
it.event is TextNoteEvent ||
|
it.event is TextNoteEvent ||
|
||||||
it.event is PollNoteEvent ||
|
it.event is PollNoteEvent ||
|
||||||
it.event is ChannelMessageEvent ||
|
it.event is ChannelMessageEvent ||
|
||||||
|
it.event is CommentEvent ||
|
||||||
it.event is LiveActivitiesChatMessageEvent
|
it.event is LiveActivitiesChatMessageEvent
|
||||||
) &&
|
) &&
|
||||||
filterParams.match(it.event) &&
|
filterParams.match(it.event) &&
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import com.vitorpamplona.amethyst.model.Note
|
|||||||
import com.vitorpamplona.quartz.events.AudioHeaderEvent
|
import com.vitorpamplona.quartz.events.AudioHeaderEvent
|
||||||
import com.vitorpamplona.quartz.events.AudioTrackEvent
|
import com.vitorpamplona.quartz.events.AudioTrackEvent
|
||||||
import com.vitorpamplona.quartz.events.ClassifiedsEvent
|
import com.vitorpamplona.quartz.events.ClassifiedsEvent
|
||||||
|
import com.vitorpamplona.quartz.events.CommentEvent
|
||||||
import com.vitorpamplona.quartz.events.GenericRepostEvent
|
import com.vitorpamplona.quartz.events.GenericRepostEvent
|
||||||
import com.vitorpamplona.quartz.events.HighlightEvent
|
import com.vitorpamplona.quartz.events.HighlightEvent
|
||||||
import com.vitorpamplona.quartz.events.LongTextNoteEvent
|
import com.vitorpamplona.quartz.events.LongTextNoteEvent
|
||||||
@@ -98,6 +99,7 @@ class HomeNewThreadFeedFilter(
|
|||||||
(noteEvent is WikiNoteEvent && noteEvent.content.isNotEmpty()) ||
|
(noteEvent is WikiNoteEvent && noteEvent.content.isNotEmpty()) ||
|
||||||
noteEvent is PollNoteEvent ||
|
noteEvent is PollNoteEvent ||
|
||||||
noteEvent is HighlightEvent ||
|
noteEvent is HighlightEvent ||
|
||||||
|
noteEvent is CommentEvent ||
|
||||||
noteEvent is AudioTrackEvent ||
|
noteEvent is AudioTrackEvent ||
|
||||||
noteEvent is AudioHeaderEvent
|
noteEvent is AudioHeaderEvent
|
||||||
) &&
|
) &&
|
||||||
|
|||||||
@@ -304,6 +304,7 @@ fun AppNavigation(
|
|||||||
val version = it.arguments?.getString("version")
|
val version = it.arguments?.getString("version")
|
||||||
val draft = it.arguments?.getString("draft")
|
val draft = it.arguments?.getString("draft")
|
||||||
val enableMessageInterface = it.arguments?.getBoolean("enableMessageInterface") ?: false
|
val enableMessageInterface = it.arguments?.getBoolean("enableMessageInterface") ?: false
|
||||||
|
val enableGeolocation = it.arguments?.getBoolean("enableGeolocation") ?: false
|
||||||
|
|
||||||
NewPostScreen(
|
NewPostScreen(
|
||||||
message = draftMessage,
|
message = draftMessage,
|
||||||
@@ -314,6 +315,7 @@ fun AppNavigation(
|
|||||||
version = version?.let { hex -> accountViewModel.getNoteIfExists(hex) },
|
version = version?.let { hex -> accountViewModel.getNoteIfExists(hex) },
|
||||||
draft = draft?.let { hex -> accountViewModel.getNoteIfExists(hex) },
|
draft = draft?.let { hex -> accountViewModel.getNoteIfExists(hex) },
|
||||||
enableMessageInterface = enableMessageInterface,
|
enableMessageInterface = enableMessageInterface,
|
||||||
|
enableGeolocation = enableGeolocation,
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
nav = nav,
|
nav = nav,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ sealed class Route(
|
|||||||
|
|
||||||
object NewPost :
|
object NewPost :
|
||||||
Route(
|
Route(
|
||||||
route = "NewPost?message={message}&attachment={attachment}&baseReplyTo={baseReplyTo}"e={quote}&fork={fork}&version={version}&draft={draft}&enableMessageInterface={enableMessageInterface}",
|
route = "NewPost?message={message}&attachment={attachment}&baseReplyTo={baseReplyTo}"e={quote}&fork={fork}&version={version}&draft={draft}&enableGeolocation={enableGeolocation}&enableMessageInterface={enableMessageInterface}",
|
||||||
icon = R.drawable.ic_moments,
|
icon = R.drawable.ic_moments,
|
||||||
arguments =
|
arguments =
|
||||||
listOf(
|
listOf(
|
||||||
@@ -236,6 +236,7 @@ sealed class Route(
|
|||||||
navArgument("fork") { type = NavType.StringType },
|
navArgument("fork") { type = NavType.StringType },
|
||||||
navArgument("version") { type = NavType.StringType },
|
navArgument("version") { type = NavType.StringType },
|
||||||
navArgument("draft") { type = NavType.StringType },
|
navArgument("draft") { type = NavType.StringType },
|
||||||
|
navArgument("enableGeolocation") { type = NavType.BoolType },
|
||||||
navArgument("enableMessageInterface") { type = NavType.BoolType },
|
navArgument("enableMessageInterface") { type = NavType.BoolType },
|
||||||
).toImmutableList(),
|
).toImmutableList(),
|
||||||
)
|
)
|
||||||
@@ -307,6 +308,7 @@ fun buildNewPostRoute(
|
|||||||
fork: String? = null,
|
fork: String? = null,
|
||||||
version: String? = null,
|
version: String? = null,
|
||||||
draft: String? = null,
|
draft: String? = null,
|
||||||
|
enableGeolocation: Boolean = false,
|
||||||
enableMessageInterface: Boolean = false,
|
enableMessageInterface: Boolean = false,
|
||||||
): String =
|
): String =
|
||||||
"NewPost?" +
|
"NewPost?" +
|
||||||
@@ -317,4 +319,5 @@ fun buildNewPostRoute(
|
|||||||
"fork=${fork ?: ""}&" +
|
"fork=${fork ?: ""}&" +
|
||||||
"version=${version ?: ""}&" +
|
"version=${version ?: ""}&" +
|
||||||
"draft=${draft ?: ""}&" +
|
"draft=${draft ?: ""}&" +
|
||||||
|
"enableGeolocation=$enableGeolocation&" +
|
||||||
"enableMessageInterface=$enableMessageInterface"
|
"enableMessageInterface=$enableMessageInterface"
|
||||||
|
|||||||
@@ -157,6 +157,7 @@ import com.vitorpamplona.quartz.events.ChannelMetadataEvent
|
|||||||
import com.vitorpamplona.quartz.events.ChatMessageEvent
|
import com.vitorpamplona.quartz.events.ChatMessageEvent
|
||||||
import com.vitorpamplona.quartz.events.ChatMessageRelayListEvent
|
import com.vitorpamplona.quartz.events.ChatMessageRelayListEvent
|
||||||
import com.vitorpamplona.quartz.events.ClassifiedsEvent
|
import com.vitorpamplona.quartz.events.ClassifiedsEvent
|
||||||
|
import com.vitorpamplona.quartz.events.CommentEvent
|
||||||
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
|
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
|
||||||
import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
|
import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
|
||||||
import com.vitorpamplona.quartz.events.DraftEvent
|
import com.vitorpamplona.quartz.events.DraftEvent
|
||||||
@@ -683,6 +684,19 @@ private fun RenderNoteRow(
|
|||||||
nav,
|
nav,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
is CommentEvent -> {
|
||||||
|
RenderTextEvent(
|
||||||
|
baseNote,
|
||||||
|
makeItShort,
|
||||||
|
canPreview,
|
||||||
|
quotesLeft,
|
||||||
|
unPackReply,
|
||||||
|
backgroundColor,
|
||||||
|
editState,
|
||||||
|
accountViewModel,
|
||||||
|
nav,
|
||||||
|
)
|
||||||
|
}
|
||||||
is NIP90ContentDiscoveryResponseEvent ->
|
is NIP90ContentDiscoveryResponseEvent ->
|
||||||
RenderNIP90ContentDiscoveryResponse(
|
RenderNIP90ContentDiscoveryResponse(
|
||||||
baseNote,
|
baseNote,
|
||||||
|
|||||||
+27
-20
@@ -164,6 +164,7 @@ import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
|||||||
import com.vitorpamplona.amethyst.ui.note.ZapSplitIcon
|
import com.vitorpamplona.amethyst.ui.note.ZapSplitIcon
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.MyTextField
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.MyTextField
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.ShowUserSuggestionList
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.ShowUserSuggestionList
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsRow
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||||
@@ -206,12 +207,14 @@ fun NewPostScreen(
|
|||||||
fork: Note? = null,
|
fork: Note? = null,
|
||||||
version: Note? = null,
|
version: Note? = null,
|
||||||
draft: Note? = null,
|
draft: Note? = null,
|
||||||
|
enableGeolocation: Boolean = false,
|
||||||
enableMessageInterface: Boolean = false,
|
enableMessageInterface: Boolean = false,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: Nav,
|
nav: Nav,
|
||||||
) {
|
) {
|
||||||
val postViewModel: NewPostViewModel = viewModel()
|
val postViewModel: NewPostViewModel = viewModel()
|
||||||
postViewModel.wantsDirectMessage = enableMessageInterface
|
postViewModel.wantsDirectMessage = enableMessageInterface
|
||||||
|
postViewModel.wantsToAddGeoHash = enableGeolocation
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val activity = context.getActivity()
|
val activity = context.getActivity()
|
||||||
@@ -693,27 +696,24 @@ fun TakePictureButton(onPictureTaken: (Uri) -> Unit) {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
Box {
|
IconButton(
|
||||||
IconButton(
|
onClick = {
|
||||||
modifier = Modifier.align(Alignment.Center),
|
if (cameraPermissionState.status.isGranted) {
|
||||||
onClick = {
|
scope.launch(Dispatchers.IO) {
|
||||||
if (cameraPermissionState.status.isGranted) {
|
imageUri = getPhotoUri(context)
|
||||||
scope.launch(Dispatchers.IO) {
|
imageUri?.let { uri -> launcher.launch(uri) }
|
||||||
imageUri = getPhotoUri(context)
|
|
||||||
imageUri?.let { uri -> launcher.launch(uri) }
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
cameraPermissionState.launchPermissionRequest()
|
|
||||||
}
|
}
|
||||||
},
|
} else {
|
||||||
) {
|
cameraPermissionState.launchPermissionRequest()
|
||||||
Icon(
|
}
|
||||||
imageVector = Icons.Default.CameraAlt,
|
},
|
||||||
contentDescription = stringRes(id = R.string.upload_image),
|
) {
|
||||||
modifier = Modifier.height(25.dp),
|
Icon(
|
||||||
tint = MaterialTheme.colorScheme.onBackground,
|
imageVector = Icons.Default.CameraAlt,
|
||||||
)
|
contentDescription = stringRes(id = R.string.upload_image),
|
||||||
}
|
modifier = Modifier.height(25.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.onBackground,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1332,6 +1332,13 @@ fun LocationAsHash(postViewModel: NewPostViewModel) {
|
|||||||
color = MaterialTheme.colorScheme.placeholderText,
|
color = MaterialTheme.colorScheme.placeholderText,
|
||||||
modifier = Modifier.padding(vertical = 10.dp),
|
modifier = Modifier.padding(vertical = 10.dp),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
SettingsRow(
|
||||||
|
R.string.geohash_exclusive,
|
||||||
|
R.string.geohash_exclusive_explainer,
|
||||||
|
) {
|
||||||
|
Switch(postViewModel.wantsExclusiveGeoPost, onCheckedChange = { postViewModel.wantsExclusiveGeoPost = it })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
LaunchedEffect(locationPermissionState) { locationPermissionState.launchPermissionRequest() }
|
LaunchedEffect(locationPermissionState) { locationPermissionState.launchPermissionRequest() }
|
||||||
|
|||||||
+6
-1
@@ -50,6 +50,7 @@ import androidx.lifecycle.Lifecycle
|
|||||||
import androidx.lifecycle.LifecycleEventObserver
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.model.AROUND_ME
|
||||||
import com.vitorpamplona.amethyst.service.NostrHomeDataSource
|
import com.vitorpamplona.amethyst.service.NostrHomeDataSource
|
||||||
import com.vitorpamplona.amethyst.service.OnlineChecker
|
import com.vitorpamplona.amethyst.service.OnlineChecker
|
||||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||||
@@ -191,7 +192,11 @@ private fun HomePages(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
floatingButton = {
|
floatingButton = {
|
||||||
NewNoteButton(accountViewModel, nav)
|
val list =
|
||||||
|
accountViewModel.account.settings.defaultHomeFollowList
|
||||||
|
.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
NewNoteButton(nav, list.value == AROUND_ME)
|
||||||
},
|
},
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
) {
|
) {
|
||||||
|
|||||||
+2
-3
@@ -35,18 +35,17 @@ import androidx.compose.ui.unit.dp
|
|||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.buildNewPostRoute
|
import com.vitorpamplona.amethyst.ui.navigation.buildNewPostRoute
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
|
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NewNoteButton(
|
fun NewNoteButton(
|
||||||
accountViewModel: AccountViewModel,
|
|
||||||
nav: INav,
|
nav: INav,
|
||||||
|
enableGeolocation: Boolean = false,
|
||||||
) {
|
) {
|
||||||
FloatingActionButton(
|
FloatingActionButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
val route = buildNewPostRoute()
|
val route = buildNewPostRoute(enableGeolocation = enableGeolocation)
|
||||||
nav.nav(route)
|
nav.nav(route)
|
||||||
},
|
},
|
||||||
modifier = Size55Modifier,
|
modifier = Size55Modifier,
|
||||||
|
|||||||
+13
@@ -169,6 +169,7 @@ import com.vitorpamplona.quartz.events.ChannelMessageEvent
|
|||||||
import com.vitorpamplona.quartz.events.ChannelMetadataEvent
|
import com.vitorpamplona.quartz.events.ChannelMetadataEvent
|
||||||
import com.vitorpamplona.quartz.events.ChatMessageRelayListEvent
|
import com.vitorpamplona.quartz.events.ChatMessageRelayListEvent
|
||||||
import com.vitorpamplona.quartz.events.ClassifiedsEvent
|
import com.vitorpamplona.quartz.events.ClassifiedsEvent
|
||||||
|
import com.vitorpamplona.quartz.events.CommentEvent
|
||||||
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
|
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
|
||||||
import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
|
import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
|
||||||
import com.vitorpamplona.quartz.events.DraftEvent
|
import com.vitorpamplona.quartz.events.DraftEvent
|
||||||
@@ -572,6 +573,18 @@ private fun FullBleedNoteCompose(
|
|||||||
RenderDraft(baseNote, 3, true, backgroundColor, accountViewModel, nav)
|
RenderDraft(baseNote, 3, true, backgroundColor, accountViewModel, nav)
|
||||||
} else if (noteEvent is HighlightEvent) {
|
} else if (noteEvent is HighlightEvent) {
|
||||||
RenderHighlight(baseNote, false, canPreview, quotesLeft = 3, backgroundColor, accountViewModel, nav)
|
RenderHighlight(baseNote, false, canPreview, quotesLeft = 3, backgroundColor, accountViewModel, nav)
|
||||||
|
} else if (noteEvent is CommentEvent) {
|
||||||
|
RenderTextEvent(
|
||||||
|
baseNote,
|
||||||
|
false,
|
||||||
|
canPreview,
|
||||||
|
quotesLeft = 3,
|
||||||
|
unPackReply = false,
|
||||||
|
backgroundColor,
|
||||||
|
editState,
|
||||||
|
accountViewModel,
|
||||||
|
nav,
|
||||||
|
)
|
||||||
} else if (noteEvent is RepostEvent || noteEvent is GenericRepostEvent) {
|
} else if (noteEvent is RepostEvent || noteEvent is GenericRepostEvent) {
|
||||||
RenderRepost(baseNote, quotesLeft = 3, backgroundColor, accountViewModel, nav)
|
RenderRepost(baseNote, quotesLeft = 3, backgroundColor, accountViewModel, nav)
|
||||||
} else if (noteEvent is TextNoteModificationEvent) {
|
} else if (noteEvent is TextNoteModificationEvent) {
|
||||||
|
|||||||
@@ -645,6 +645,9 @@
|
|||||||
<string name="geohash_title">Expose Location as </string>
|
<string name="geohash_title">Expose Location as </string>
|
||||||
<string name="geohash_explainer">Adds a Geohash of your location to the post. The public will know you are within 5km (3mi) of the current location</string>
|
<string name="geohash_explainer">Adds a Geohash of your location to the post. The public will know you are within 5km (3mi) of the current location</string>
|
||||||
|
|
||||||
|
<string name="geohash_exclusive">Location-exclusive Post</string>
|
||||||
|
<string name="geohash_exclusive_explainer">Only followers of the location will see it. Your general followers won\'t see it.</string>
|
||||||
|
|
||||||
<string name="loading_location">Loading location</string>
|
<string name="loading_location">Loading location</string>
|
||||||
<string name="lack_location_permissions">No Location Permissions</string>
|
<string name="lack_location_permissions">No Location Permissions</string>
|
||||||
|
|
||||||
|
|||||||
@@ -24,14 +24,25 @@ import android.util.Log
|
|||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import com.vitorpamplona.quartz.utils.bytesUsedInMemory
|
import com.vitorpamplona.quartz.utils.bytesUsedInMemory
|
||||||
import com.vitorpamplona.quartz.utils.pointerSizeInBytes
|
import com.vitorpamplona.quartz.utils.pointerSizeInBytes
|
||||||
|
import com.vitorpamplona.quartz.utils.removeTrailingNullsAndEmptyOthers
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class ATag(
|
data class ATag(
|
||||||
val kind: Int,
|
val kind: Int,
|
||||||
val pubKeyHex: String,
|
val pubKeyHex: String,
|
||||||
val dTag: String,
|
val dTag: String,
|
||||||
val relay: String?,
|
|
||||||
) {
|
) {
|
||||||
|
var relay: String? = null
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
kind: Int,
|
||||||
|
pubKeyHex: String,
|
||||||
|
dTag: String,
|
||||||
|
relayHint: String?,
|
||||||
|
) : this(kind, pubKeyHex, dTag) {
|
||||||
|
this.relay = relayHint
|
||||||
|
}
|
||||||
|
|
||||||
fun countMemory(): Long =
|
fun countMemory(): Long =
|
||||||
5 * pointerSizeInBytes + // 7 fields, 4 bytes each reference (32bit)
|
5 * pointerSizeInBytes + // 7 fields, 4 bytes each reference (32bit)
|
||||||
8L + // kind
|
8L + // kind
|
||||||
@@ -41,6 +52,10 @@ data class ATag(
|
|||||||
|
|
||||||
fun toTag() = assembleATag(kind, pubKeyHex, dTag)
|
fun toTag() = assembleATag(kind, pubKeyHex, dTag)
|
||||||
|
|
||||||
|
fun toATagArray() = removeTrailingNullsAndEmptyOthers("a", toTag(), relay)
|
||||||
|
|
||||||
|
fun toQTagArray() = removeTrailingNullsAndEmptyOthers("q", toTag(), relay)
|
||||||
|
|
||||||
fun toNAddr(): String =
|
fun toNAddr(): String =
|
||||||
TlvBuilder()
|
TlvBuilder()
|
||||||
.apply {
|
.apply {
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
/**
|
||||||
|
* 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.encoders
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import com.vitorpamplona.quartz.utils.bytesUsedInMemory
|
||||||
|
import com.vitorpamplona.quartz.utils.pointerSizeInBytes
|
||||||
|
import com.vitorpamplona.quartz.utils.removeTrailingNullsAndEmptyOthers
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class ETag(
|
||||||
|
val eventId: HexKey,
|
||||||
|
) {
|
||||||
|
var relay: String? = null
|
||||||
|
var authorPubKeyHex: HexKey? = null
|
||||||
|
|
||||||
|
constructor(eventId: HexKey, relayHint: String? = null, authorPubKeyHex: HexKey? = null) : this(eventId) {
|
||||||
|
this.relay = relayHint
|
||||||
|
this.authorPubKeyHex = authorPubKeyHex
|
||||||
|
}
|
||||||
|
|
||||||
|
fun countMemory(): Long =
|
||||||
|
2 * pointerSizeInBytes + // 2 fields, 4 bytes each reference (32bit)
|
||||||
|
eventId.bytesUsedInMemory() +
|
||||||
|
(relay?.bytesUsedInMemory() ?: 0)
|
||||||
|
|
||||||
|
fun toNEvent(): String = Nip19Bech32.createNEvent(eventId, authorPubKeyHex, null, relay)
|
||||||
|
|
||||||
|
fun toNote(): String = Nip19Bech32.createNote(eventId)
|
||||||
|
|
||||||
|
fun toETagArray() = removeTrailingNullsAndEmptyOthers("e", eventId, relay, authorPubKeyHex)
|
||||||
|
|
||||||
|
fun toQTagArray() = removeTrailingNullsAndEmptyOthers("q", eventId, relay, authorPubKeyHex)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun parseNIP19(nevent: String): ETag? {
|
||||||
|
try {
|
||||||
|
val parsed = Nip19Bech32.uriToRoute(nevent)?.entity
|
||||||
|
|
||||||
|
return when (parsed) {
|
||||||
|
is Nip19Bech32.Note -> ETag(parsed.hex)
|
||||||
|
is Nip19Bech32.NEvent -> ETag(parsed.hex, parsed.author, parsed.relay.firstOrNull())
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Log.w("PTag", "Issue trying to Decode NIP19 $this: ${e.message}")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
/**
|
||||||
|
* 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.encoders
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import com.vitorpamplona.quartz.events.Event
|
||||||
|
import com.vitorpamplona.quartz.utils.bytesUsedInMemory
|
||||||
|
import com.vitorpamplona.quartz.utils.pointerSizeInBytes
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class EventHint<T : Event>(
|
||||||
|
val event: T,
|
||||||
|
) {
|
||||||
|
var relay: String? = null
|
||||||
|
|
||||||
|
constructor(event: T, relayHint: String? = null) : this(event) {
|
||||||
|
this.relay = relayHint
|
||||||
|
}
|
||||||
|
|
||||||
|
fun countMemory(): Long =
|
||||||
|
2 * pointerSizeInBytes + // 2 fields, 4 bytes each reference (32bit)
|
||||||
|
event.countMemory() +
|
||||||
|
(relay?.bytesUsedInMemory() ?: 0)
|
||||||
|
|
||||||
|
fun toNEvent(): String = Nip19Bech32.createNEvent(event.id, event.pubKey, event.kind, relay)
|
||||||
|
|
||||||
|
fun toNPub(): String = Nip19Bech32.createNPub(event.id)
|
||||||
|
|
||||||
|
fun toTagArray(tag: String) = listOfNotNull(tag, event.id, relay, event.pubKey).toTypedArray()
|
||||||
|
|
||||||
|
fun toETagArray() = toTagArray("e")
|
||||||
|
|
||||||
|
fun toQTagArray() = toTagArray("q")
|
||||||
|
}
|
||||||
@@ -270,6 +270,10 @@ object Nip19Bech32 {
|
|||||||
}.build()
|
}.build()
|
||||||
.toNEvent()
|
.toNEvent()
|
||||||
|
|
||||||
|
fun createNote(eventId: HexKey): String = eventId.hexToByteArray().toNote()
|
||||||
|
|
||||||
|
fun createNPub(authorPubKeyHex: HexKey): String = authorPubKeyHex.hexToByteArray().toNpub()
|
||||||
|
|
||||||
fun createNProfile(
|
fun createNProfile(
|
||||||
authorPubKeyHex: String,
|
authorPubKeyHex: String,
|
||||||
relay: List<String>,
|
relay: List<String>,
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
/**
|
||||||
|
* 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.encoders
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import com.vitorpamplona.quartz.utils.bytesUsedInMemory
|
||||||
|
import com.vitorpamplona.quartz.utils.pointerSizeInBytes
|
||||||
|
import com.vitorpamplona.quartz.utils.removeTrailingNullsAndEmptyOthers
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class PTag(
|
||||||
|
val pubKeyHex: HexKey,
|
||||||
|
) {
|
||||||
|
var relay: String? = null
|
||||||
|
|
||||||
|
constructor(pubKeyHex: HexKey, relayHint: String?) : this(pubKeyHex) {
|
||||||
|
this.relay = relayHint?.ifBlank { null }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun countMemory(): Long =
|
||||||
|
2 * pointerSizeInBytes + // 2 fields, 4 bytes each reference (32bit)
|
||||||
|
pubKeyHex.bytesUsedInMemory() +
|
||||||
|
(relay?.bytesUsedInMemory() ?: 0)
|
||||||
|
|
||||||
|
fun toNProfile(): String = Nip19Bech32.createNProfile(pubKeyHex, relay?.let { listOf(it) } ?: emptyList())
|
||||||
|
|
||||||
|
fun toNPub(): String = Nip19Bech32.createNPub(pubKeyHex)
|
||||||
|
|
||||||
|
fun toPTagArray() = removeTrailingNullsAndEmptyOthers("p", pubKeyHex, relay)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun parseNAddr(nprofile: String): PTag? {
|
||||||
|
try {
|
||||||
|
val parsed = Nip19Bech32.uriToRoute(nprofile)?.entity
|
||||||
|
|
||||||
|
return when (parsed) {
|
||||||
|
is Nip19Bech32.NPub -> PTag(parsed.hex)
|
||||||
|
is Nip19Bech32.NProfile -> PTag(parsed.hex, parsed.relay.firstOrNull())
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Log.w("PTag", "Issue trying to Decode NIP19 $this: ${e.message}")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -71,7 +71,7 @@ open class BaseTextNoteEvent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun replyingTo(): HexKey? {
|
open fun replyingTo(): HexKey? {
|
||||||
val oldStylePositional = tags.lastOrNull { it.size > 1 && it.size <= 3 && it[0] == "e" }?.get(1)
|
val oldStylePositional = tags.lastOrNull { it.size > 1 && it.size <= 3 && it[0] == "e" }?.get(1)
|
||||||
val newStyleReply = tags.lastOrNull { it.size > 3 && it[0] == "e" && it[3] == "reply" }?.get(1)
|
val newStyleReply = tags.lastOrNull { it.size > 3 && it[0] == "e" && it[3] == "reply" }?.get(1)
|
||||||
val newStyleRoot = tags.lastOrNull { it.size > 3 && it[0] == "e" && it[3] == "root" }?.get(1)
|
val newStyleRoot = tags.lastOrNull { it.size > 3 && it[0] == "e" && it[3] == "root" }?.get(1)
|
||||||
@@ -79,7 +79,7 @@ open class BaseTextNoteEvent(
|
|||||||
return newStyleReply ?: newStyleRoot ?: oldStylePositional
|
return newStyleReply ?: newStyleRoot ?: oldStylePositional
|
||||||
}
|
}
|
||||||
|
|
||||||
fun replyingToAddress(): ATag? {
|
open fun replyingToAddress(): ATag? {
|
||||||
val oldStylePositional = tags.lastOrNull { it.size > 1 && it.size <= 3 && it[0] == "a" }?.let { ATag.parseAtag(it[1], it[2]) }
|
val oldStylePositional = tags.lastOrNull { it.size > 1 && it.size <= 3 && it[0] == "a" }?.let { ATag.parseAtag(it[1], it[2]) }
|
||||||
val newStyleReply = tags.lastOrNull { it.size > 3 && it[0] == "a" && it[3] == "reply" }?.let { ATag.parseAtag(it[1], it[2]) }
|
val newStyleReply = tags.lastOrNull { it.size > 3 && it[0] == "a" && it[3] == "reply" }?.let { ATag.parseAtag(it[1], it[2]) }
|
||||||
val newStyleRoot = tags.lastOrNull { it.size > 3 && it[0] == "a" && it[3] == "root" }?.let { ATag.parseAtag(it[1], it[2]) }
|
val newStyleRoot = tags.lastOrNull { it.size > 3 && it[0] == "a" && it[3] == "root" }?.let { ATag.parseAtag(it[1], it[2]) }
|
||||||
@@ -87,7 +87,7 @@ open class BaseTextNoteEvent(
|
|||||||
return newStyleReply ?: newStyleRoot ?: oldStylePositional
|
return newStyleReply ?: newStyleRoot ?: oldStylePositional
|
||||||
}
|
}
|
||||||
|
|
||||||
fun replyingToAddressOrEvent(): String? {
|
open fun replyingToAddressOrEvent(): String? {
|
||||||
val oldStylePositional = tags.lastOrNull { it.size > 1 && it.size <= 3 && (it[0] == "e" || it[0] == "a") }?.get(1)
|
val oldStylePositional = tags.lastOrNull { it.size > 1 && it.size <= 3 && (it[0] == "e" || it[0] == "a") }?.get(1)
|
||||||
val newStyleReply = tags.lastOrNull { it.size > 3 && (it[0] == "e" || it[0] == "a") && it[3] == "reply" }?.get(1)
|
val newStyleReply = tags.lastOrNull { it.size > 3 && (it[0] == "e" || it[0] == "a") && it[3] == "reply" }?.get(1)
|
||||||
val newStyleRoot = tags.lastOrNull { it.size > 3 && (it[0] == "e" || it[0] == "a") && it[3] == "root" }?.get(1)
|
val newStyleRoot = tags.lastOrNull { it.size > 3 && (it[0] == "e" || it[0] == "a") && it[3] == "root" }?.get(1)
|
||||||
|
|||||||
@@ -0,0 +1,192 @@
|
|||||||
|
/**
|
||||||
|
* 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.EventHint
|
||||||
|
import com.vitorpamplona.quartz.encoders.HexKey
|
||||||
|
import com.vitorpamplona.quartz.encoders.Nip92MediaAttachments
|
||||||
|
import com.vitorpamplona.quartz.encoders.PTag
|
||||||
|
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||||
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
|
import com.vitorpamplona.quartz.utils.removeTrailingNullsAndEmptyOthers
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
class CommentEvent(
|
||||||
|
id: HexKey,
|
||||||
|
pubKey: HexKey,
|
||||||
|
createdAt: Long,
|
||||||
|
tags: Array<Array<String>>,
|
||||||
|
content: String,
|
||||||
|
sig: HexKey,
|
||||||
|
) : BaseTextNoteEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||||
|
fun root() = tags.firstOrNull { it.size > 3 && it[3] == "root" }?.get(1)
|
||||||
|
|
||||||
|
fun getRootScopes() = tags.filter { it.size > 1 && it[0] == "I" || it[0] == "A" || it[0] == "E" }
|
||||||
|
|
||||||
|
fun getRootKinds() = tags.filter { it.size > 1 && it[0] == "K" }
|
||||||
|
|
||||||
|
fun getDirectReplies() = tags.filter { it.size > 1 && it[0] == "i" || it[0] == "a" || it[0] == "e" }
|
||||||
|
|
||||||
|
fun getDirectKinds() = tags.filter { it.size > 1 && it[0] == "k" }
|
||||||
|
|
||||||
|
fun isGeohashTag(tag: Array<String>) = tag.size > 1 && (tag[0] == "i" || tag[0] == "I") && tag[1].startsWith("geo:")
|
||||||
|
|
||||||
|
private fun getGeoHashList() = tags.filter { isGeohashTag(it) }
|
||||||
|
|
||||||
|
override fun hasGeohashes() = tags.any { isGeohashTag(it) }
|
||||||
|
|
||||||
|
override fun geohashes() = getGeoHashList().map { it[1].drop(4).lowercase() }
|
||||||
|
|
||||||
|
override fun getGeoHash(): String? = geohashes().maxByOrNull { it.length }
|
||||||
|
|
||||||
|
override fun isTaggedGeoHash(hashtag: String) = tags.any { isGeohashTag(it) && it[1].endsWith(hashtag, true) }
|
||||||
|
|
||||||
|
override fun isTaggedGeoHashes(hashtags: Set<String>) = geohashes().any { it in hashtags }
|
||||||
|
|
||||||
|
override fun replyTos(): List<HexKey> = tags.filter { it.size > 1 && it[0] == "e" }.map { it[1] } + tags.filter { it.size > 1 && it[0] == "E" }.map { it[1] }
|
||||||
|
|
||||||
|
override fun replyingTo(): HexKey? =
|
||||||
|
tags.lastOrNull { it.size > 1 && it[0] == "e" }?.get(1)
|
||||||
|
?: tags.lastOrNull { it.size > 1 && it[0] == "E" }?.get(1)
|
||||||
|
|
||||||
|
override fun replyingToAddress(): ATag? =
|
||||||
|
tags.lastOrNull { it.size > 1 && it[0] == "a" }?.let { ATag.parseAtag(it[1], it.getOrNull(2)) }
|
||||||
|
?: tags.lastOrNull { it.size > 1 && it[0] == "A" }?.let { ATag.parseAtag(it[1], it.getOrNull(2)) }
|
||||||
|
|
||||||
|
override fun replyingToAddressOrEvent(): HexKey? = replyingToAddress()?.toTag() ?: replyingTo()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val KIND = 1111
|
||||||
|
|
||||||
|
fun rootGeohashMipMap(geohash: String): Array<Array<String>> =
|
||||||
|
geohash.indices
|
||||||
|
.asSequence()
|
||||||
|
.map { arrayOf("I", "geo:" + geohash.substring(0, it + 1)) }
|
||||||
|
.toList()
|
||||||
|
.reversed()
|
||||||
|
.toTypedArray()
|
||||||
|
|
||||||
|
fun replyComment(
|
||||||
|
msg: String,
|
||||||
|
replyingTo: EventHint<CommentEvent>,
|
||||||
|
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.addAll(replyingTo.event.getRootScopes())
|
||||||
|
tags.addAll(replyingTo.event.getRootKinds())
|
||||||
|
|
||||||
|
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 createGeoComment(
|
||||||
|
msg: String,
|
||||||
|
geohash: String? = null,
|
||||||
|
usersMentioned: Set<PTag> = emptySet(),
|
||||||
|
addressesMentioned: Set<ATag> = emptySet(),
|
||||||
|
eventsMentioned: Set<ETag> = emptySet(),
|
||||||
|
nip94attachments: List<FileHeaderEvent>? = 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>>()
|
||||||
|
geohash?.let { tags.addAll(rootGeohashMipMap(it)) }
|
||||||
|
tags.add(arrayOf("K", "geo"))
|
||||||
|
|
||||||
|
create(msg, tags, usersMentioned, addressesMentioned, eventsMentioned, nip94attachments, null, zapReceiver, markAsSensitive, zapRaiserAmount, isDraft, signer, createdAt, onReady)
|
||||||
|
}
|
||||||
|
|
||||||
|
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: (CommentEvent) -> 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -141,21 +141,11 @@ class ContactListEvent(
|
|||||||
val tags =
|
val tags =
|
||||||
listOf(arrayOf("alt", ALT)) +
|
listOf(arrayOf("alt", ALT)) +
|
||||||
followUsers.map {
|
followUsers.map {
|
||||||
if (it.relayUri != null) {
|
listOfNotNull("a", it.pubKeyHex, it.relayUri).toTypedArray()
|
||||||
arrayOf("p", it.pubKeyHex, it.relayUri)
|
|
||||||
} else {
|
|
||||||
arrayOf("p", it.pubKeyHex)
|
|
||||||
}
|
|
||||||
} +
|
} +
|
||||||
followTags.map { arrayOf("t", it) } +
|
followTags.map { arrayOf("t", it) } +
|
||||||
followEvents.map { arrayOf("e", it) } +
|
followEvents.map { arrayOf("e", it) } +
|
||||||
followCommunities.map {
|
followCommunities.map { it.toATagArray() } +
|
||||||
if (it.relay != null) {
|
|
||||||
arrayOf("a", it.toTag(), it.relay)
|
|
||||||
} else {
|
|
||||||
arrayOf("a", it.toTag())
|
|
||||||
}
|
|
||||||
} +
|
|
||||||
followGeohashes.map { arrayOf("g", it) }
|
followGeohashes.map { arrayOf("g", it) }
|
||||||
|
|
||||||
return signer.sign(createdAt, KIND, tags.toTypedArray(), content)
|
return signer.sign(createdAt, KIND, tags.toTypedArray(), content)
|
||||||
@@ -189,13 +179,7 @@ class ContactListEvent(
|
|||||||
} +
|
} +
|
||||||
followTags.map { arrayOf("t", it) } +
|
followTags.map { arrayOf("t", it) } +
|
||||||
followEvents.map { arrayOf("e", it) } +
|
followEvents.map { arrayOf("e", it) } +
|
||||||
followCommunities.map {
|
followCommunities.map { it.toATagArray() } +
|
||||||
if (it.relay != null) {
|
|
||||||
arrayOf("a", it.toTag(), it.relay)
|
|
||||||
} else {
|
|
||||||
arrayOf("a", it.toTag())
|
|
||||||
}
|
|
||||||
} +
|
|
||||||
followGeohashes.map { arrayOf("g", it) }
|
followGeohashes.map { arrayOf("g", it) }
|
||||||
|
|
||||||
return create(
|
return create(
|
||||||
|
|||||||
@@ -188,6 +188,18 @@ class DraftEvent(
|
|||||||
create(dTag, originalNote, tagsWithMarkers, signer, createdAt, onReady)
|
create(dTag, originalNote, tagsWithMarkers, signer, createdAt, onReady)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun create(
|
||||||
|
dTag: String,
|
||||||
|
originalNote: CommentEvent,
|
||||||
|
signer: NostrSigner,
|
||||||
|
createdAt: Long = TimeUtils.now(),
|
||||||
|
onReady: (DraftEvent) -> Unit,
|
||||||
|
) {
|
||||||
|
val tagsWithMarkers = originalNote.getRootScopes() + originalNote.getDirectReplies()
|
||||||
|
|
||||||
|
create(dTag, originalNote, tagsWithMarkers, signer, createdAt, onReady)
|
||||||
|
}
|
||||||
|
|
||||||
fun create(
|
fun create(
|
||||||
dTag: String,
|
dTag: String,
|
||||||
originalNote: TextNoteEvent,
|
originalNote: TextNoteEvent,
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ class EventFactory {
|
|||||||
}
|
}
|
||||||
ChatMessageRelayListEvent.KIND -> ChatMessageRelayListEvent(id, pubKey, createdAt, tags, content, sig)
|
ChatMessageRelayListEvent.KIND -> ChatMessageRelayListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
ClassifiedsEvent.KIND -> ClassifiedsEvent(id, pubKey, createdAt, tags, content, sig)
|
ClassifiedsEvent.KIND -> ClassifiedsEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
|
CommentEvent.KIND -> CommentEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
CommunityDefinitionEvent.KIND -> CommunityDefinitionEvent(id, pubKey, createdAt, tags, content, sig)
|
CommunityDefinitionEvent.KIND -> CommunityDefinitionEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
CommunityListEvent.KIND -> CommunityListEvent(id, pubKey, createdAt, tags, content, sig)
|
CommunityListEvent.KIND -> CommunityListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
CommunityPostApprovalEvent.KIND -> CommunityPostApprovalEvent(id, pubKey, createdAt, tags, content, sig)
|
CommunityPostApprovalEvent.KIND -> CommunityPostApprovalEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* 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.utils
|
||||||
|
|
||||||
|
public fun removeTrailingNullsAndEmptyOthers(vararg elements: String?): Array<String> {
|
||||||
|
val lastNonNullIndex = elements.indexOfLast { it != null }
|
||||||
|
|
||||||
|
if (lastNonNullIndex < 0) return Array(0) { "" }
|
||||||
|
|
||||||
|
return Array(lastNonNullIndex + 1) { index ->
|
||||||
|
elements[index] ?: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user