Adds support for Custom Reaction selection on new posts and direct GIF url usage.

This commit is contained in:
Vitor Pamplona
2025-01-03 20:29:31 -05:00
parent c9a6b63f67
commit aa44560714
23 changed files with 443 additions and 17 deletions
@@ -31,6 +31,7 @@ import com.fasterxml.jackson.module.kotlin.readValue
import com.fonfon.kgeohash.GeoHash import com.fonfon.kgeohash.GeoHash
import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
import com.vitorpamplona.amethyst.service.LocationState import com.vitorpamplona.amethyst.service.LocationState
import com.vitorpamplona.amethyst.service.NostrLnZapPaymentResponseDataSource import com.vitorpamplona.amethyst.service.NostrLnZapPaymentResponseDataSource
@@ -1063,6 +1064,80 @@ class Account(
} }
} }
class EmojiMedia(
val code: String,
val url: MediaUrlImage,
)
fun getEmojiPackSelection(): EmojiPackSelectionEvent? = getEmojiPackSelectionNote().event as? EmojiPackSelectionEvent
fun getEmojiPackSelectionFlow(): StateFlow<NoteState> = getEmojiPackSelectionNote().flow().metadata.stateFlow
fun getEmojiPackSelectionNote(): AddressableNote = LocalCache.getOrCreateAddressableNote(EmojiPackSelectionEvent.createAddressATag(userProfile().pubkeyHex))
fun convertEmojiSelectionPack(selection: EmojiPackSelectionEvent?): List<StateFlow<NoteState>>? =
selection?.taggedAddresses()?.map {
LocalCache
.getOrCreateAddressableNote(it)
.flow()
.metadata.stateFlow
}
@OptIn(ExperimentalCoroutinesApi::class)
val liveEmojiSelectionPack: StateFlow<List<StateFlow<NoteState>>?> by lazy {
getEmojiPackSelectionFlow()
.transformLatest {
emit(convertEmojiSelectionPack(it.note.event as? EmojiPackSelectionEvent))
}.flowOn(Dispatchers.Default)
.stateIn(
scope,
SharingStarted.Eagerly,
runBlocking(Dispatchers.Default) {
convertEmojiSelectionPack(getEmojiPackSelection())
},
)
}
fun convertEmojiPack(pack: EmojiPackEvent): List<EmojiMedia> =
pack.taggedEmojis().map {
EmojiMedia(it.code, MediaUrlImage(it.url))
}
fun mergePack(list: Array<NoteState>): List<EmojiMedia> =
list
.mapNotNull {
val ev = it.note.event as? EmojiPackEvent
if (ev != null) {
convertEmojiPack(ev)
} else {
null
}
}.flatten()
.distinctBy { it.url }
@OptIn(ExperimentalCoroutinesApi::class)
val myEmojis by lazy {
liveEmojiSelectionPack
.transformLatest { emojiList ->
if (emojiList != null) {
emitAll(
combineTransform(emojiList) {
emit(mergePack(it))
},
)
} else {
emit(emptyList())
}
}.flowOn(Dispatchers.Default)
.stateIn(
scope,
SharingStarted.Eagerly,
runBlocking(Dispatchers.Default) {
mergePack(convertEmojiSelectionPack(getEmojiPackSelection())?.map { it.value }?.toTypedArray() ?: emptyArray())
},
)
}
fun addPaymentRequestIfNew(paymentRequest: PaymentRequest) { fun addPaymentRequestIfNew(paymentRequest: PaymentRequest) {
if ( if (
!this.transientPaymentRequests.value.contains(paymentRequest) && !this.transientPaymentRequests.value.contains(paymentRequest) &&
@@ -2110,6 +2185,7 @@ class Account(
relayList: List<RelaySetupInfo>, relayList: List<RelaySetupInfo>,
geohash: String? = null, geohash: String? = null,
imetas: List<IMetaTag>? = null, imetas: List<IMetaTag>? = null,
emojis: List<EmojiUrl>? = null,
draftTag: String?, draftTag: String?,
) { ) {
if (!isWriteable()) return if (!isWriteable()) return
@@ -2137,6 +2213,7 @@ class Account(
directMentions = directMentions, directMentions = directMentions,
geohash = geohash, geohash = geohash,
imetas = imetas, imetas = imetas,
emojis = emojis,
signer = signer, signer = signer,
isDraft = draftTag != null, isDraft = draftTag != null,
) { ) {
@@ -2177,6 +2254,7 @@ class Account(
relayList: List<RelaySetupInfo>, relayList: List<RelaySetupInfo>,
geohash: String? = null, geohash: String? = null,
imetas: List<IMetaTag>? = null, imetas: List<IMetaTag>? = null,
emojis: List<EmojiUrl>? = null,
draftTag: String?, draftTag: String?,
) { ) {
if (!isWriteable()) return if (!isWriteable()) return
@@ -2199,6 +2277,7 @@ class Account(
directMentions = directMentions, directMentions = directMentions,
geohash = geohash, geohash = geohash,
imetas = imetas, imetas = imetas,
emojis = emojis,
forkedFrom = forkedFrom, forkedFrom = forkedFrom,
signer = signer, signer = signer,
isDraft = draftTag != null, isDraft = draftTag != null,
@@ -2245,6 +2324,7 @@ class Account(
relayList: List<RelaySetupInfo>, relayList: List<RelaySetupInfo>,
geohash: String? = null, geohash: String? = null,
imetas: List<IMetaTag>? = null, imetas: List<IMetaTag>? = null,
emojis: List<EmojiUrl>? = null,
draftTag: String?, draftTag: String?,
) { ) {
if (!isWriteable()) return if (!isWriteable()) return
@@ -2265,6 +2345,7 @@ class Account(
directMentions = directMentions, directMentions = directMentions,
geohash = geohash, geohash = geohash,
imetas = imetas, imetas = imetas,
emojis = emojis,
forkedFrom = forkedFrom, forkedFrom = forkedFrom,
signer = signer, signer = signer,
isDraft = draftTag != null, isDraft = draftTag != null,
@@ -2328,6 +2409,7 @@ class Account(
directMentionsUsers: Set<User> = emptySet(), directMentionsUsers: Set<User> = emptySet(),
directMentionsNotes: Set<Note> = emptySet(), directMentionsNotes: Set<Note> = emptySet(),
imetas: List<IMetaTag>? = null, imetas: List<IMetaTag>? = null,
emojis: List<EmojiUrl>? = null,
geohash: String? = null, geohash: String? = null,
zapReceiver: List<ZapSplitSetup>? = null, zapReceiver: List<ZapSplitSetup>? = null,
wantsToMarkAsSensitive: Boolean = false, wantsToMarkAsSensitive: Boolean = false,
@@ -2371,6 +2453,7 @@ class Account(
addressesMentioned = addressesMentioned, addressesMentioned = addressesMentioned,
eventsMentioned = eventsMentioned, eventsMentioned = eventsMentioned,
imetas = imetas, imetas = imetas,
emojis = emojis,
geohash = geohash, geohash = geohash,
zapReceiver = zapReceiver, zapReceiver = zapReceiver,
markAsSensitive = wantsToMarkAsSensitive, markAsSensitive = wantsToMarkAsSensitive,
@@ -2403,6 +2486,7 @@ class Account(
addressesMentioned = addressesMentioned, addressesMentioned = addressesMentioned,
eventsMentioned = eventsMentioned, eventsMentioned = eventsMentioned,
imetas = imetas, imetas = imetas,
emojis = emojis,
geohash = geohash, geohash = geohash,
zapReceiver = zapReceiver, zapReceiver = zapReceiver,
markAsSensitive = wantsToMarkAsSensitive, markAsSensitive = wantsToMarkAsSensitive,
@@ -2437,6 +2521,7 @@ class Account(
directMentionsUsers: Set<User> = emptySet(), directMentionsUsers: Set<User> = emptySet(),
directMentionsNotes: Set<Note> = emptySet(), directMentionsNotes: Set<Note> = emptySet(),
imetas: List<IMetaTag>? = null, imetas: List<IMetaTag>? = null,
emojis: List<EmojiUrl>? = null,
zapReceiver: List<ZapSplitSetup>? = null, zapReceiver: List<ZapSplitSetup>? = null,
wantsToMarkAsSensitive: Boolean = false, wantsToMarkAsSensitive: Boolean = false,
zapRaiserAmount: Long? = null, zapRaiserAmount: Long? = null,
@@ -2479,6 +2564,7 @@ class Account(
addressesMentioned = addressesMentioned, addressesMentioned = addressesMentioned,
eventsMentioned = eventsMentioned, eventsMentioned = eventsMentioned,
imetas = imetas, imetas = imetas,
emojis = emojis,
zapReceiver = zapReceiver, zapReceiver = zapReceiver,
markAsSensitive = wantsToMarkAsSensitive, markAsSensitive = wantsToMarkAsSensitive,
zapRaiserAmount = zapRaiserAmount, zapRaiserAmount = zapRaiserAmount,
@@ -2683,6 +2769,7 @@ class Account(
relayList: List<RelaySetupInfo>, relayList: List<RelaySetupInfo>,
geohash: String? = null, geohash: String? = null,
imetas: List<IMetaTag>? = null, imetas: List<IMetaTag>? = null,
emojis: List<EmojiUrl>? = null,
draftTag: String?, draftTag: String?,
) { ) {
if (!isWriteable()) return if (!isWriteable()) return
@@ -2705,6 +2792,7 @@ class Account(
directMentions = directMentions, directMentions = directMentions,
geohash = geohash, geohash = geohash,
imetas = imetas, imetas = imetas,
emojis = emojis,
forkedFrom = forkedFrom, forkedFrom = forkedFrom,
signer = signer, signer = signer,
isDraft = draftTag != null, isDraft = draftTag != null,
@@ -2775,6 +2863,7 @@ class Account(
relayList: List<RelaySetupInfo>, relayList: List<RelaySetupInfo>,
geohash: String? = null, geohash: String? = null,
imetas: List<IMetaTag>? = null, imetas: List<IMetaTag>? = null,
emojis: List<EmojiUrl>? = null,
draftTag: String?, draftTag: String?,
) { ) {
if (!isWriteable()) return if (!isWriteable()) return
@@ -2835,6 +2924,7 @@ class Account(
directMentions: Set<HexKey>, directMentions: Set<HexKey>,
geohash: String? = null, geohash: String? = null,
imetas: List<IMetaTag>? = null, imetas: List<IMetaTag>? = null,
emojis: List<EmojiUrl>? = null,
draftTag: String?, draftTag: String?,
) { ) {
if (!isWriteable()) return if (!isWriteable()) return
@@ -2853,6 +2943,7 @@ class Account(
directMentions = directMentions, directMentions = directMentions,
geohash = geohash, geohash = geohash,
imetas = imetas, imetas = imetas,
emojis = emojis,
signer = signer, signer = signer,
isDraft = draftTag != null, isDraft = draftTag != null,
) { ) {
@@ -2881,6 +2972,7 @@ class Account(
zapRaiserAmount: Long? = null, zapRaiserAmount: Long? = null,
geohash: String? = null, geohash: String? = null,
imetas: List<IMetaTag>? = null, imetas: List<IMetaTag>? = null,
emojis: List<EmojiUrl>? = null,
draftTag: String?, draftTag: String?,
) { ) {
if (!isWriteable()) return if (!isWriteable()) return
@@ -2899,6 +2991,7 @@ class Account(
zapRaiserAmount = zapRaiserAmount, zapRaiserAmount = zapRaiserAmount,
geohash = geohash, geohash = geohash,
imetas = imetas, imetas = imetas,
emojis = emojis,
signer = signer, signer = signer,
isDraft = draftTag != null, isDraft = draftTag != null,
) { ) {
@@ -3043,6 +3136,7 @@ class Account(
zapRaiserAmount: Long? = null, zapRaiserAmount: Long? = null,
geohash: String? = null, geohash: String? = null,
imetas: List<IMetaTag>? = null, imetas: List<IMetaTag>? = null,
emojis: List<EmojiUrl>? = null,
draftTag: String? = null, draftTag: String? = null,
) { ) {
if (!isWriteable()) return if (!isWriteable()) return
@@ -3061,6 +3155,7 @@ class Account(
zapRaiserAmount = zapRaiserAmount, zapRaiserAmount = zapRaiserAmount,
geohash = geohash, geohash = geohash,
imetas = imetas, imetas = imetas,
emojis = emojis,
draftTag = draftTag, draftTag = draftTag,
signer = signer, signer = signer,
) { ) {
@@ -57,6 +57,7 @@ import com.vitorpamplona.quartz.encoders.Hex
import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.IMetaTag import com.vitorpamplona.quartz.encoders.IMetaTag
import com.vitorpamplona.quartz.encoders.IMetaTagBuilder import com.vitorpamplona.quartz.encoders.IMetaTagBuilder
import com.vitorpamplona.quartz.encoders.Nip30CustomEmoji
import com.vitorpamplona.quartz.encoders.toNpub import com.vitorpamplona.quartz.encoders.toNpub
import com.vitorpamplona.quartz.events.AddressableEvent import com.vitorpamplona.quartz.events.AddressableEvent
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
@@ -65,6 +66,7 @@ import com.vitorpamplona.quartz.events.ClassifiedsEvent
import com.vitorpamplona.quartz.events.CommentEvent 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.EmojiUrl
import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.FileStorageEvent import com.vitorpamplona.quartz.events.FileStorageEvent
import com.vitorpamplona.quartz.events.FileStorageHeaderEvent import com.vitorpamplona.quartz.events.FileStorageHeaderEvent
@@ -82,7 +84,12 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.util.UUID import java.util.UUID
@@ -120,6 +127,27 @@ open class NewPostViewModel : ViewModel() {
var userSuggestionAnchor: TextRange? = null var userSuggestionAnchor: TextRange? = null
var userSuggestionsMainMessage: UserSuggestionAnchor? = null var userSuggestionsMainMessage: UserSuggestionAnchor? = null
val emojiSearch: MutableStateFlow<String> = MutableStateFlow("")
val emojiSuggestions: StateFlow<List<Account.EmojiMedia>> by lazy {
account!!
.myEmojis
.combine(emojiSearch) { list, search ->
if (search.length == 1) {
list
} else if (search.isNotEmpty()) {
val code = search.removePrefix(":")
list.filter { it.code.startsWith(code) }
} else {
emptyList()
}
}.flowOn(Dispatchers.Default)
.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5000),
emptyList(),
)
}
// DMs // DMs
var wantsDirectMessage by mutableStateOf(false) var wantsDirectMessage by mutableStateOf(false)
var toUsers by mutableStateOf(TextFieldValue("")) var toUsers by mutableStateOf(TextFieldValue(""))
@@ -196,6 +224,7 @@ open class NewPostViewModel : ViewModel() {
) { ) {
this.accountViewModel = accountViewModel this.accountViewModel = accountViewModel
this.account = accountViewModel.account this.account = accountViewModel.account
this.emojiSuggestions.value
val noteEvent = draft?.event val noteEvent = draft?.event
val noteAuthor = draft?.author val noteAuthor = draft?.author
@@ -552,6 +581,7 @@ open class NewPostViewModel : ViewModel() {
} }
} }
val emojis = findEmoji(tagger.message, account?.myEmojis?.value)
val urls = findURLs(tagger.message) val urls = findURLs(tagger.message)
val usedAttachments = iMetaAttachments.filter { it.url in urls.toSet() } val usedAttachments = iMetaAttachments.filter { it.url in urls.toSet() }
@@ -569,6 +599,7 @@ open class NewPostViewModel : ViewModel() {
wantsToMarkAsSensitive = wantsToMarkAsSensitive, wantsToMarkAsSensitive = wantsToMarkAsSensitive,
zapRaiserAmount = localZapRaiserAmount, zapRaiserAmount = localZapRaiserAmount,
relayList = relayList, relayList = relayList,
emojis = emojis,
draftTag = localDraft, draftTag = localDraft,
) )
} else if (wantsExclusiveGeoPost && geoHash != null && (originalNote == null || originalNote?.event is CommentEvent)) { } else if (wantsExclusiveGeoPost && geoHash != null && (originalNote == null || originalNote?.event is CommentEvent)) {
@@ -583,6 +614,7 @@ open class NewPostViewModel : ViewModel() {
wantsToMarkAsSensitive = wantsToMarkAsSensitive, wantsToMarkAsSensitive = wantsToMarkAsSensitive,
zapRaiserAmount = localZapRaiserAmount, zapRaiserAmount = localZapRaiserAmount,
relayList = relayList, relayList = relayList,
emojis = emojis,
draftTag = localDraft, draftTag = localDraft,
) )
} else if (originalNote?.channelHex() != null) { } else if (originalNote?.channelHex() != null) {
@@ -597,6 +629,7 @@ open class NewPostViewModel : ViewModel() {
zapRaiserAmount = localZapRaiserAmount, zapRaiserAmount = localZapRaiserAmount,
geohash = geoHash, geohash = geoHash,
imetas = usedAttachments, imetas = usedAttachments,
emojis = emojis,
draftTag = localDraft, draftTag = localDraft,
) )
} else { } else {
@@ -611,6 +644,7 @@ open class NewPostViewModel : ViewModel() {
directMentions = tagger.directMentions, directMentions = tagger.directMentions,
geohash = geoHash, geohash = geoHash,
imetas = usedAttachments, imetas = usedAttachments,
emojis = emojis,
draftTag = localDraft, draftTag = localDraft,
) )
} }
@@ -639,6 +673,7 @@ open class NewPostViewModel : ViewModel() {
zapRaiserAmount = localZapRaiserAmount, zapRaiserAmount = localZapRaiserAmount,
geohash = geoHash, geohash = geoHash,
imetas = usedAttachments, imetas = usedAttachments,
emojis = emojis,
draftTag = localDraft, draftTag = localDraft,
) )
} else if (!dmUsers.isNullOrEmpty()) { } else if (!dmUsers.isNullOrEmpty()) {
@@ -654,6 +689,7 @@ open class NewPostViewModel : ViewModel() {
zapRaiserAmount = localZapRaiserAmount, zapRaiserAmount = localZapRaiserAmount,
geohash = geoHash, geohash = geoHash,
imetas = usedAttachments, imetas = usedAttachments,
emojis = emojis,
draftTag = localDraft, draftTag = localDraft,
) )
} else { } else {
@@ -708,6 +744,7 @@ open class NewPostViewModel : ViewModel() {
relayList = relayList, relayList = relayList,
geohash = geoHash, geohash = geoHash,
imetas = usedAttachments, imetas = usedAttachments,
emojis = emojis,
draftTag = localDraft, draftTag = localDraft,
) )
} else if (originalNote?.event is TorrentCommentEvent) { } else if (originalNote?.event is TorrentCommentEvent) {
@@ -747,6 +784,7 @@ open class NewPostViewModel : ViewModel() {
relayList = relayList, relayList = relayList,
geohash = geoHash, geohash = geoHash,
imetas = usedAttachments, imetas = usedAttachments,
emojis = emojis,
draftTag = localDraft, draftTag = localDraft,
) )
} }
@@ -776,6 +814,7 @@ open class NewPostViewModel : ViewModel() {
relayList = relayList, relayList = relayList,
geohash = geoHash, geohash = geoHash,
imetas = usedAttachments, imetas = usedAttachments,
emojis = emojis,
draftTag = localDraft, draftTag = localDraft,
) )
} else { } else {
@@ -795,6 +834,7 @@ open class NewPostViewModel : ViewModel() {
relayList = relayList, relayList = relayList,
geohash = geoHash, geohash = geoHash,
imetas = usedAttachments, imetas = usedAttachments,
emojis = emojis,
draftTag = localDraft, draftTag = localDraft,
) )
} else if (wantsProduct) { } else if (wantsProduct) {
@@ -814,6 +854,7 @@ open class NewPostViewModel : ViewModel() {
relayList = relayList, relayList = relayList,
geohash = geoHash, geohash = geoHash,
imetas = usedAttachments, imetas = usedAttachments,
emojis = emojis,
draftTag = localDraft, draftTag = localDraft,
) )
} else { } else {
@@ -851,12 +892,23 @@ open class NewPostViewModel : ViewModel() {
relayList = relayList, relayList = relayList,
geohash = geoHash, geohash = geoHash,
imetas = usedAttachments, imetas = usedAttachments,
emojis = emojis,
draftTag = localDraft, draftTag = localDraft,
) )
} }
} }
} }
fun findEmoji(
message: String,
myEmojiSet: List<Account.EmojiMedia>?,
): List<EmojiUrl> {
if (myEmojiSet == null) return emptyList()
return Nip30CustomEmoji.findAllEmojiCodes(message).mapNotNull { possibleEmoji ->
myEmojiSet.firstOrNull { it.code == possibleEmoji }?.let { EmojiUrl(it.code, it.url.url) }
}
}
fun upload( fun upload(
alt: String?, alt: String?,
sensitiveContent: Boolean, sensitiveContent: Boolean,
@@ -978,6 +1030,10 @@ open class NewPostViewModel : ViewModel() {
userSuggestionAnchor = null userSuggestionAnchor = null
userSuggestionsMainMessage = null userSuggestionsMainMessage = null
if (emojiSearch.value.isNotEmpty()) {
emojiSearch.tryEmit("")
}
draftTag = UUID.randomUUID().toString() draftTag = UUID.randomUUID().toString()
NostrSearchEventOrUserDataSource.clear() NostrSearchEventOrUserDataSource.clear()
@@ -1029,6 +1085,14 @@ open class NewPostViewModel : ViewModel() {
NostrSearchEventOrUserDataSource.clear() NostrSearchEventOrUserDataSource.clear()
userSuggestions = emptyList() userSuggestions = emptyList()
} }
if (lastWord.startsWith(":")) {
emojiSearch.tryEmit(lastWord)
} else {
if (emojiSearch.value.isNotBlank()) {
emojiSearch.tryEmit("")
}
}
} }
saveDraft() saveDraft()
@@ -1132,6 +1196,54 @@ open class NewPostViewModel : ViewModel() {
saveDraft() saveDraft()
} }
open fun autocompleteWithEmoji(item: Account.EmojiMedia) {
userSuggestionAnchor?.let {
val lastWord =
message.text
.substring(0, it.end)
.substringAfterLast("\n")
.substringAfterLast(" ")
val lastWordStart = it.end - lastWord.length
val wordToInsert = ":${item.code}:"
message =
TextFieldValue(
message.text.replaceRange(lastWordStart, it.end, wordToInsert),
TextRange(lastWordStart + wordToInsert.length, lastWordStart + wordToInsert.length),
)
userSuggestionAnchor = null
emojiSearch.tryEmit("")
}
saveDraft()
}
open fun autocompleteWithEmojiUrl(item: Account.EmojiMedia) {
userSuggestionAnchor?.let {
val lastWord =
message.text
.substring(0, it.end)
.substringAfterLast("\n")
.substringAfterLast(" ")
val lastWordStart = it.end - lastWord.length
val wordToInsert = item.url.url + " "
message =
TextFieldValue(
message.text.replaceRange(lastWordStart, it.end, wordToInsert),
TextRange(lastWordStart + wordToInsert.length, lastWordStart + wordToInsert.length),
)
userSuggestionAnchor = null
emojiSearch.tryEmit("")
}
urlPreview = findUrlInMessage()
saveDraft()
}
private fun newStateMapPollOptions(): SnapshotStateMap<Int, String> = mutableStateMapOf(Pair(0, ""), Pair(1, "")) private fun newStateMapPollOptions(): SnapshotStateMap<Int, String> = mutableStateMapOf(Pair(0, ""), Pair(1, ""))
fun canPost(): Boolean = fun canPost(): Boolean =
@@ -0,0 +1,136 @@
/**
* 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
import androidx.compose.foundation.clickable
import androidx.compose.foundation.content.MediaType.Companion.Text
import androidx.compose.foundation.layout.Arrangement.spacedBy
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.OpenInFull
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.gallery.UrlImageView
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.Size10dp
import com.vitorpamplona.quartz.events.EmojiPackSelectionEvent
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.Flow
@Composable
fun WatchAndLoadMyEmojiList(accountViewModel: AccountViewModel) {
LoadAddressableNote(
EmojiPackSelectionEvent.createAddressATag(accountViewModel.userProfile().pubkeyHex),
accountViewModel,
) { emptyNote ->
emptyNote?.let { usersEmojiList ->
val collections by usersEmojiList
.live()
.metadata
.map { (it.note.event as? EmojiPackSelectionEvent)?.taggedAddresses()?.toImmutableList() }
.distinctUntilChanged()
.observeAsState((usersEmojiList.event as? EmojiPackSelectionEvent)?.taggedAddresses()?.toImmutableList())
collections?.forEach {
LoadAddressableNote(aTag = it, accountViewModel) {
it?.live()?.metadata?.observeAsState()
}
}
}
}
}
@Composable
fun ShowEmojiSuggestionList(
emojiSuggestions: Flow<List<Account.EmojiMedia>>,
onSelect: (Account.EmojiMedia) -> Unit,
onFullSize: (Account.EmojiMedia) -> Unit,
accountViewModel: AccountViewModel,
modifier: Modifier = Modifier.heightIn(0.dp, 200.dp),
) {
val suggestions by emojiSuggestions.collectAsStateWithLifecycle(emptyList())
if (suggestions.isNotEmpty()) {
LazyColumn(
contentPadding = PaddingValues(top = 10.dp),
modifier = modifier,
) {
items(suggestions) {
Row(
modifier =
Modifier.clickable { onSelect(it) }.padding(
start = 12.dp,
end = 12.dp,
top = 10.dp,
bottom = 10.dp,
),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = spacedBy(Size10dp),
) {
Box(Modifier.size(40.dp)) {
UrlImageView(it.url, accountViewModel)
}
Text(it.code, fontWeight = FontWeight.Bold, modifier = Modifier.weight(1f))
Box(Modifier.size(40.dp), contentAlignment = Alignment.Center) {
IconButton(
onClick = {
onFullSize(it)
},
) {
Icon(
imageVector = Icons.Outlined.OpenInFull,
contentDescription = stringRes(R.string.use_direct_url),
modifier = Modifier.size(20.dp),
)
}
}
}
HorizontalDivider(
thickness = DividerThickness,
)
}
}
}
}
@@ -144,7 +144,6 @@ class UpdateReactionTypeViewModel : ViewModel() {
?.value ?.value
} }
@OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
fun UpdateReactionTypeDialog( fun UpdateReactionTypeDialog(
onClose: () -> Unit, onClose: () -> Unit,
@@ -157,8 +157,10 @@ import com.vitorpamplona.amethyst.ui.note.LoadCityName
import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.note.PollIcon import com.vitorpamplona.amethyst.ui.note.PollIcon
import com.vitorpamplona.amethyst.ui.note.RegularPostIcon import com.vitorpamplona.amethyst.ui.note.RegularPostIcon
import com.vitorpamplona.amethyst.ui.note.ShowEmojiSuggestionList
import com.vitorpamplona.amethyst.ui.note.ShowUserSuggestionList import com.vitorpamplona.amethyst.ui.note.ShowUserSuggestionList
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
import com.vitorpamplona.amethyst.ui.note.WatchAndLoadMyEmojiList
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.settings.SettingsRow import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsRow
@@ -209,6 +211,7 @@ fun NewPostScreen(
nav: Nav, nav: Nav,
) { ) {
val postViewModel: NewPostViewModel = viewModel() val postViewModel: NewPostViewModel = viewModel()
postViewModel.account = accountViewModel.account
postViewModel.wantsDirectMessage = enableMessageInterface postViewModel.wantsDirectMessage = enableMessageInterface
postViewModel.wantsToAddGeoHash = enableGeolocation postViewModel.wantsToAddGeoHash = enableGeolocation
@@ -278,6 +281,8 @@ fun NewPostScreen(
onDispose { activity.removeOnNewIntentListener(consumer) } onDispose { activity.removeOnNewIntentListener(consumer) }
} }
WatchAndLoadMyEmojiList(accountViewModel)
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(
@@ -575,6 +580,14 @@ fun NewPostScreen(
modifier = Modifier.heightIn(0.dp, 300.dp), modifier = Modifier.heightIn(0.dp, 300.dp),
) )
ShowEmojiSuggestionList(
postViewModel.emojiSuggestions,
postViewModel::autocompleteWithEmoji,
postViewModel::autocompleteWithEmojiUrl,
accountViewModel,
modifier = Modifier.heightIn(0.dp, 300.dp),
)
BottomRowActions(postViewModel) BottomRowActions(postViewModel)
} }
} }
@@ -130,6 +130,7 @@ import com.vitorpamplona.amethyst.ui.note.LikeReaction
import com.vitorpamplona.amethyst.ui.note.LoadChannel import com.vitorpamplona.amethyst.ui.note.LoadChannel
import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture
import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay
import com.vitorpamplona.amethyst.ui.note.ShowEmojiSuggestionList
import com.vitorpamplona.amethyst.ui.note.ShowUserSuggestionList import com.vitorpamplona.amethyst.ui.note.ShowUserSuggestionList
import com.vitorpamplona.amethyst.ui.note.UserPicture import com.vitorpamplona.amethyst.ui.note.UserPicture
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
@@ -397,6 +398,7 @@ private suspend fun innerSendPost(
val urls = findURLs(tagger.message) val urls = findURLs(tagger.message)
val usedAttachments = newPostModel.iMetaAttachments.filter { it.url in urls.toSet() } val usedAttachments = newPostModel.iMetaAttachments.filter { it.url in urls.toSet() }
val emojis = newPostModel.findEmoji(newPostModel.message.text, accountViewModel.account.myEmojis.value)
if (channel is PublicChatChannel) { if (channel is PublicChatChannel) {
accountViewModel.account.sendChannelMessage( accountViewModel.account.sendChannelMessage(
@@ -407,6 +409,7 @@ private suspend fun innerSendPost(
directMentions = tagger.directMentions, directMentions = tagger.directMentions,
wantsToMarkAsSensitive = false, wantsToMarkAsSensitive = false,
imetas = usedAttachments, imetas = usedAttachments,
emojis = emojis,
draftTag = draftTag, draftTag = draftTag,
) )
} else if (channel is LiveActivitiesChannel) { } else if (channel is LiveActivitiesChannel) {
@@ -417,6 +420,7 @@ private suspend fun innerSendPost(
mentions = tagger.pTags, mentions = tagger.pTags,
wantsToMarkAsSensitive = false, wantsToMarkAsSensitive = false,
imetas = usedAttachments, imetas = usedAttachments,
emojis = emojis,
draftTag = draftTag, draftTag = draftTag,
) )
} }
@@ -485,6 +489,13 @@ fun EditFieldRow(
accountViewModel, accountViewModel,
) )
ShowEmojiSuggestionList(
channelScreenModel.emojiSuggestions,
channelScreenModel::autocompleteWithEmoji,
channelScreenModel::autocompleteWithEmojiUrl,
accountViewModel,
)
MyTextField( MyTextField(
value = channelScreenModel.message, value = channelScreenModel.message,
onValueChange = { channelScreenModel.updateMessage(it) }, onValueChange = { channelScreenModel.updateMessage(it) },
@@ -95,6 +95,7 @@ import com.vitorpamplona.amethyst.ui.note.IncognitoIconOff
import com.vitorpamplona.amethyst.ui.note.IncognitoIconOn import com.vitorpamplona.amethyst.ui.note.IncognitoIconOn
import com.vitorpamplona.amethyst.ui.note.NonClickableUserPictures import com.vitorpamplona.amethyst.ui.note.NonClickableUserPictures
import com.vitorpamplona.amethyst.ui.note.QuickActionAlertDialog import com.vitorpamplona.amethyst.ui.note.QuickActionAlertDialog
import com.vitorpamplona.amethyst.ui.note.ShowEmojiSuggestionList
import com.vitorpamplona.amethyst.ui.note.ShowUserSuggestionList import com.vitorpamplona.amethyst.ui.note.ShowUserSuggestionList
import com.vitorpamplona.amethyst.ui.note.UserCompose import com.vitorpamplona.amethyst.ui.note.UserCompose
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
@@ -517,6 +518,7 @@ private fun innerSendPost(
) { ) {
val urls = findURLs(newPostModel.message.text) val urls = findURLs(newPostModel.message.text)
val usedAttachments = newPostModel.iMetaAttachments.filter { it.url !in urls.toSet() } val usedAttachments = newPostModel.iMetaAttachments.filter { it.url !in urls.toSet() }
val emojis = newPostModel.findEmoji(newPostModel.message.text, accountViewModel.account.myEmojis.value)
if (newPostModel.nip17 || room.users.size > 1 || replyTo.value?.event is NIP17Group) { if (newPostModel.nip17 || room.users.size > 1 || replyTo.value?.event is NIP17Group) {
accountViewModel.account.sendNIP17PrivateMessage( accountViewModel.account.sendNIP17PrivateMessage(
@@ -526,6 +528,7 @@ private fun innerSendPost(
mentions = null, mentions = null,
wantsToMarkAsSensitive = false, wantsToMarkAsSensitive = false,
imetas = usedAttachments, imetas = usedAttachments,
emojis = emojis,
draftTag = dTag, draftTag = dTag,
) )
} else { } else {
@@ -557,6 +560,13 @@ fun PrivateMessageEditFieldRow(
accountViewModel, accountViewModel,
) )
ShowEmojiSuggestionList(
channelScreenModel.emojiSuggestions,
channelScreenModel::autocompleteWithEmoji,
channelScreenModel::autocompleteWithEmojiUrl,
accountViewModel,
)
MyTextField( MyTextField(
value = channelScreenModel.message, value = channelScreenModel.message,
onValueChange = { channelScreenModel.updateMessage(it) }, onValueChange = { channelScreenModel.updateMessage(it) },
+2
View File
@@ -377,6 +377,8 @@
<string name="add_caption">Add a Caption</string> <string name="add_caption">Add a Caption</string>
<string name="add_caption_example">My lovely friend</string> <string name="add_caption_example">My lovely friend</string>
<string name="use_direct_url">Use direct URL</string>
<string name="content_description">Description of the contents</string> <string name="content_description">Description of the contents</string>
<string name="content_description_example">A blue boat in a white sandy beach at sunset</string> <string name="content_description_example">A blue boat in a white sandy beach at sunset</string>
@@ -50,6 +50,24 @@ class Nip30CustomEmoji {
fun createEmojiMap(tags: ImmutableListOfLists<String>): Map<String, String> = tags.lists.filter { it.size > 2 && it[0] == "emoji" }.associate { ":${it[1]}:" to it[2] } fun createEmojiMap(tags: ImmutableListOfLists<String>): Map<String, String> = tags.lists.filter { it.size > 2 && it[0] == "emoji" }.associate { ":${it[1]}:" to it[2] }
fun findAllEmojis(input: String): List<String> {
val matcher = customEmojiPattern.matcher(input)
val emojiNamesInOrder = mutableListOf<String>()
while (matcher.find()) {
emojiNamesInOrder.add(matcher.group())
}
return emojiNamesInOrder
}
fun findAllEmojiCodes(input: String): List<String> {
val matcher = customEmojiPattern.matcher(input)
val emojiNamesInOrder = mutableListOf<String>()
while (matcher.find()) {
matcher.group(1)?.let { emojiNamesInOrder.add(it) }
}
return emojiNamesInOrder
}
fun assembleAnnotatedList( fun assembleAnnotatedList(
input: String, input: String,
allTags: ImmutableListOfLists<String>?, allTags: ImmutableListOfLists<String>?,
@@ -65,12 +83,7 @@ class Nip30CustomEmoji {
input: String, input: String,
emojiPairs: Map<String, String>, emojiPairs: Map<String, String>,
): ImmutableList<Renderable>? { ): ImmutableList<Renderable>? {
val matcher = customEmojiPattern.matcher(input) val emojiNamesInOrder = findAllEmojis(input)
val emojiNamesInOrder = mutableListOf<String>()
while (matcher.find()) {
emojiNamesInOrder.add(matcher.group())
}
if (emojiNamesInOrder.isEmpty()) { if (emojiNamesInOrder.isEmpty()) {
return null return null
} }
@@ -62,6 +62,7 @@ class ChannelMessageEvent(
directMentions: Set<HexKey> = emptySet(), directMentions: Set<HexKey> = emptySet(),
geohash: String? = null, geohash: String? = null,
imetas: List<IMetaTag>? = null, imetas: List<IMetaTag>? = null,
emojis: List<EmojiUrl>? = null,
isDraft: Boolean, isDraft: Boolean,
onReady: (ChannelMessageEvent) -> Unit, onReady: (ChannelMessageEvent) -> Unit,
) { ) {
@@ -88,6 +89,7 @@ class ChannelMessageEvent(
imetas?.forEach { imetas?.forEach {
tags.add(Nip92MediaAttachments.createTag(it)) tags.add(Nip92MediaAttachments.createTag(it))
} }
emojis?.forEach { tags.add(it.toTagArray()) }
tags.add( tags.add(
arrayOf("alt", ALT), arrayOf("alt", ALT),
) )
@@ -84,6 +84,7 @@ class ChatMessageEvent(
signer: NostrSigner, signer: NostrSigner,
createdAt: Long = TimeUtils.now(), createdAt: Long = TimeUtils.now(),
imetas: List<IMetaTag>? = null, imetas: List<IMetaTag>? = null,
emojis: List<EmojiUrl>? = null,
isDraft: Boolean, isDraft: Boolean,
onReady: (ChatMessageEvent) -> Unit, onReady: (ChatMessageEvent) -> Unit,
) { ) {
@@ -103,6 +104,7 @@ class ChatMessageEvent(
imetas?.forEach { imetas?.forEach {
tags.add(Nip92MediaAttachments.createTag(it)) tags.add(Nip92MediaAttachments.createTag(it))
} }
emojis?.forEach { tags.add(it.toTagArray()) }
// tags.add(arrayOf("alt", alt)) // tags.add(arrayOf("alt", alt))
if (isDraft) { if (isDraft) {
@@ -115,6 +115,7 @@ class ClassifiedsEvent(
zapRaiserAmount: Long?, zapRaiserAmount: Long?,
geohash: String? = null, geohash: String? = null,
imetas: List<IMetaTag>? = null, imetas: List<IMetaTag>? = null,
emojis: List<EmojiUrl>? = null,
signer: NostrSigner, signer: NostrSigner,
createdAt: Long = TimeUtils.now(), createdAt: Long = TimeUtils.now(),
isDraft: Boolean, isDraft: Boolean,
@@ -190,9 +191,8 @@ class ClassifiedsEvent(
} }
zapRaiserAmount?.let { tags.add(arrayOf("zapraiser", "$it")) } zapRaiserAmount?.let { tags.add(arrayOf("zapraiser", "$it")) }
geohash?.let { tags.addAll(geohashMipMap(it)) } geohash?.let { tags.addAll(geohashMipMap(it)) }
imetas?.forEach { imetas?.forEach { tags.add(Nip92MediaAttachments.createTag(it)) }
tags.add(Nip92MediaAttachments.createTag(it)) emojis?.forEach { tags.add(it.toTagArray()) }
}
tags.add(arrayOf("alt", ALT)) tags.add(arrayOf("alt", ALT))
if (isDraft) { if (isDraft) {
@@ -98,6 +98,7 @@ class CommentEvent(
addressesMentioned: Set<ATag> = emptySet(), addressesMentioned: Set<ATag> = emptySet(),
eventsMentioned: Set<ETag> = emptySet(), eventsMentioned: Set<ETag> = emptySet(),
imetas: List<IMetaTag>? = null, imetas: List<IMetaTag>? = null,
emojis: List<EmojiUrl>? = null,
geohash: String? = null, geohash: String? = null,
zapReceiver: List<ZapSplitSetup>? = null, zapReceiver: List<ZapSplitSetup>? = null,
markAsSensitive: Boolean = false, markAsSensitive: Boolean = false,
@@ -120,7 +121,7 @@ class CommentEvent(
tags.add(removeTrailingNullsAndEmptyOthers("e", replyingTo.event.id, replyingTo.relay, replyingTo.event.pubKey)) tags.add(removeTrailingNullsAndEmptyOthers("e", replyingTo.event.id, replyingTo.relay, replyingTo.event.pubKey))
tags.add(arrayOf("k", "${replyingTo.event.kind}")) tags.add(arrayOf("k", "${replyingTo.event.kind}"))
create(msg, tags, usersMentioned, addressesMentioned, eventsMentioned, imetas, geohash, zapReceiver, markAsSensitive, zapRaiserAmount, isDraft, signer, createdAt, onReady) create(msg, tags, usersMentioned, addressesMentioned, eventsMentioned, imetas, emojis, geohash, zapReceiver, markAsSensitive, zapRaiserAmount, isDraft, signer, createdAt, onReady)
} }
fun replyComment( fun replyComment(
@@ -130,6 +131,7 @@ class CommentEvent(
addressesMentioned: Set<ATag> = emptySet(), addressesMentioned: Set<ATag> = emptySet(),
eventsMentioned: Set<ETag> = emptySet(), eventsMentioned: Set<ETag> = emptySet(),
imetas: List<IMetaTag>? = null, imetas: List<IMetaTag>? = null,
emojis: List<EmojiUrl>? = null,
geohash: String? = null, geohash: String? = null,
zapReceiver: List<ZapSplitSetup>? = null, zapReceiver: List<ZapSplitSetup>? = null,
markAsSensitive: Boolean = false, markAsSensitive: Boolean = false,
@@ -147,7 +149,7 @@ class CommentEvent(
tags.add(removeTrailingNullsAndEmptyOthers("e", replyingTo.event.id, replyingTo.relay, replyingTo.event.pubKey)) tags.add(removeTrailingNullsAndEmptyOthers("e", replyingTo.event.id, replyingTo.relay, replyingTo.event.pubKey))
tags.add(arrayOf("k", "${replyingTo.event.kind}")) tags.add(arrayOf("k", "${replyingTo.event.kind}"))
create(msg, tags, usersMentioned, addressesMentioned, eventsMentioned, imetas, geohash, zapReceiver, markAsSensitive, zapRaiserAmount, isDraft, signer, createdAt, onReady) create(msg, tags, usersMentioned, addressesMentioned, eventsMentioned, imetas, emojis, geohash, zapReceiver, markAsSensitive, zapRaiserAmount, isDraft, signer, createdAt, onReady)
} }
fun createGeoComment( fun createGeoComment(
@@ -157,6 +159,7 @@ class CommentEvent(
addressesMentioned: Set<ATag> = emptySet(), addressesMentioned: Set<ATag> = emptySet(),
eventsMentioned: Set<ETag> = emptySet(), eventsMentioned: Set<ETag> = emptySet(),
imetas: List<IMetaTag>? = null, imetas: List<IMetaTag>? = null,
emojis: List<EmojiUrl>? = null,
zapReceiver: List<ZapSplitSetup>? = null, zapReceiver: List<ZapSplitSetup>? = null,
markAsSensitive: Boolean = false, markAsSensitive: Boolean = false,
zapRaiserAmount: Long? = null, zapRaiserAmount: Long? = null,
@@ -169,7 +172,7 @@ class CommentEvent(
geohash?.let { tags.addAll(rootGeohashMipMap(it)) } geohash?.let { tags.addAll(rootGeohashMipMap(it)) }
tags.add(arrayOf("K", "geo")) tags.add(arrayOf("K", "geo"))
create(msg, tags, usersMentioned, addressesMentioned, eventsMentioned, imetas, null, zapReceiver, markAsSensitive, zapRaiserAmount, isDraft, signer, createdAt, onReady) create(msg, tags, usersMentioned, addressesMentioned, eventsMentioned, imetas, emojis, null, zapReceiver, markAsSensitive, zapRaiserAmount, isDraft, signer, createdAt, onReady)
} }
private fun create( private fun create(
@@ -179,6 +182,7 @@ class CommentEvent(
addressesMentioned: Set<ATag> = emptySet(), addressesMentioned: Set<ATag> = emptySet(),
eventsMentioned: Set<ETag> = emptySet(), eventsMentioned: Set<ETag> = emptySet(),
imetas: List<IMetaTag>? = null, imetas: List<IMetaTag>? = null,
emojis: List<EmojiUrl>? = null,
geohash: String? = null, geohash: String? = null,
zapReceiver: List<ZapSplitSetup>? = null, zapReceiver: List<ZapSplitSetup>? = null,
markAsSensitive: Boolean = false, markAsSensitive: Boolean = false,
@@ -202,6 +206,8 @@ class CommentEvent(
findURLs(msg).forEach { tags.add(arrayOf("r", it)) } findURLs(msg).forEach { tags.add(arrayOf("r", it)) }
emojis?.forEach { tags.add(it.toTagArray()) }
zapReceiver?.forEach { zapReceiver?.forEach {
tags.add(arrayOf("zap", it.lnAddressOrPubKeyHex, it.relay ?: "", it.weight.toString())) tags.add(arrayOf("zap", it.lnAddressOrPubKeyHex, it.relay ?: "", it.weight.toString()))
} }
@@ -62,6 +62,8 @@ data class EmojiUrl(
) { ) {
fun encode(): String = ":$code:$url" fun encode(): String = ":$code:$url"
fun toTagArray() = arrayOf("emoji", code, url)
companion object { companion object {
fun decode(encodedEmojiSetup: String): EmojiUrl? { fun decode(encodedEmojiSetup: String): EmojiUrl? {
val emojiParts = encodedEmojiSetup.split(":", limit = 3) val emojiParts = encodedEmojiSetup.split(":", limit = 3)
@@ -71,5 +73,12 @@ data class EmojiUrl(
null null
} }
} }
fun parse(tag: Array<String>): EmojiUrl? =
if (tag.size > 2 && tag[0] == "emoji") {
EmojiUrl(tag[1], tag[2])
} else {
null
}
} }
} }
@@ -42,6 +42,10 @@ class EmojiPackSelectionEvent(
const val FIXED_D_TAG = "" const val FIXED_D_TAG = ""
const val ALT = "Emoji selection" const val ALT = "Emoji selection"
fun createAddressATag(pubKey: HexKey): ATag = ATag(KIND, pubKey, AdvertisedRelayListEvent.FIXED_D_TAG, null)
fun createAddressTag(pubKey: HexKey): String = ATag.assembleATag(KIND, pubKey, AdvertisedRelayListEvent.FIXED_D_TAG)
fun create( fun create(
listOfEmojiPacks: List<ATag>?, listOfEmojiPacks: List<ATag>?,
signer: NostrSigner, signer: NostrSigner,
@@ -164,7 +164,7 @@ open class Event(
ATag.parse(aTagValue, relay) ATag.parse(aTagValue, relay)
} }
override fun taggedEmojis() = tags.filter { it.size > 2 && it[0] == "emoji" }.map { EmojiUrl(it[1], it[2]) } override fun taggedEmojis() = tags.filter { it.size > 2 && it[0] == "emoji" }.mapNotNull { EmojiUrl.parse(it) }
override fun isSensitive() = override fun isSensitive() =
tags.any { tags.any {
@@ -92,6 +92,7 @@ class GitReplyEvent(
directMentions: Set<HexKey> = emptySet(), directMentions: Set<HexKey> = emptySet(),
geohash: String? = null, geohash: String? = null,
imetas: List<IMetaTag>? = null, imetas: List<IMetaTag>? = null,
emojis: List<EmojiUrl>? = null,
forkedFrom: Event? = null, forkedFrom: Event? = null,
signer: NostrSigner, signer: NostrSigner,
createdAt: Long = TimeUtils.now(), createdAt: Long = TimeUtils.now(),
@@ -152,6 +153,7 @@ class GitReplyEvent(
imetas?.forEach { imetas?.forEach {
tags.add(Nip92MediaAttachments.createTag(it)) tags.add(Nip92MediaAttachments.createTag(it))
} }
emojis?.forEach { tags.add(it.toTagArray()) }
tags.add(arrayOf("alt", "a git issue reply")) tags.add(arrayOf("alt", "a git issue reply"))
if (isDraft) { if (isDraft) {
@@ -53,6 +53,7 @@ open class InteractiveStoryBaseEvent(
zapRaiserAmount: Long? = null, zapRaiserAmount: Long? = null,
geohash: String? = null, geohash: String? = null,
imetas: List<IMetaTag>? = null, imetas: List<IMetaTag>? = null,
emojis: List<EmojiUrl>? = null,
): Array<Array<String>> { ): Array<Array<String>> {
val tags = mutableListOf<Array<String>>() val tags = mutableListOf<Array<String>>()
findHashtags(content).forEach { findHashtags(content).forEach {
@@ -75,6 +76,7 @@ open class InteractiveStoryBaseEvent(
imetas?.forEach { imetas?.forEach {
tags.add(Nip92MediaAttachments.createTag(it)) tags.add(Nip92MediaAttachments.createTag(it))
} }
emojis?.forEach { tags.add(it.toTagArray()) }
return tags.toTypedArray() return tags.toTypedArray()
} }
@@ -75,6 +75,7 @@ class LiveActivitiesChatMessageEvent(
zapRaiserAmount: Long?, zapRaiserAmount: Long?,
geohash: String? = null, geohash: String? = null,
imetas: List<IMetaTag>? = null, imetas: List<IMetaTag>? = null,
emojis: List<EmojiUrl>? = null,
isDraft: Boolean, isDraft: Boolean,
onReady: (LiveActivitiesChatMessageEvent) -> Unit, onReady: (LiveActivitiesChatMessageEvent) -> Unit,
) { ) {
@@ -96,6 +97,7 @@ class LiveActivitiesChatMessageEvent(
imetas?.forEach { imetas?.forEach {
tags.add(Nip92MediaAttachments.createTag(it)) tags.add(Nip92MediaAttachments.createTag(it))
} }
emojis?.forEach { tags.add(it.toTagArray()) }
tags.add(arrayOf("alt", ALT)) tags.add(arrayOf("alt", ALT))
if (isDraft) { if (isDraft) {
@@ -82,6 +82,7 @@ class NIP17Factory {
zapRaiserAmount: Long? = null, zapRaiserAmount: Long? = null,
geohash: String? = null, geohash: String? = null,
imetas: List<IMetaTag>? = null, imetas: List<IMetaTag>? = null,
emojis: List<EmojiUrl>? = null,
draftTag: String? = null, draftTag: String? = null,
onReady: (Result) -> Unit, onReady: (Result) -> Unit,
) { ) {
@@ -100,6 +101,7 @@ class NIP17Factory {
geohash = geohash, geohash = geohash,
isDraft = draftTag != null, isDraft = draftTag != null,
imetas = imetas, imetas = imetas,
emojis = emojis,
) { senderMessage -> ) { senderMessage ->
if (draftTag != null) { if (draftTag != null) {
onReady( onReady(
@@ -81,6 +81,7 @@ class PollNoteEvent(
zapRaiserAmount: Long?, zapRaiserAmount: Long?,
geohash: String? = null, geohash: String? = null,
imetas: List<IMetaTag>? = null, imetas: List<IMetaTag>? = null,
emojis: List<EmojiUrl>? = null,
isDraft: Boolean, isDraft: Boolean,
onReady: (PollNoteEvent) -> Unit, onReady: (PollNoteEvent) -> Unit,
) { ) {
@@ -108,6 +109,7 @@ class PollNoteEvent(
imetas?.forEach { imetas?.forEach {
tags.add(Nip92MediaAttachments.createTag(it)) tags.add(Nip92MediaAttachments.createTag(it))
} }
emojis?.forEach { tags.add(it.toTagArray()) }
tags.add(arrayOf("alt", ALT)) tags.add(arrayOf("alt", ALT))
if (isDraft) { if (isDraft) {
@@ -58,6 +58,7 @@ class TextNoteEvent(
directMentions: Set<HexKey> = emptySet(), directMentions: Set<HexKey> = emptySet(),
geohash: String? = null, geohash: String? = null,
imetas: List<IMetaTag>? = null, imetas: List<IMetaTag>? = null,
emojis: List<EmojiUrl>? = null,
forkedFrom: Event? = null, forkedFrom: Event? = null,
signer: NostrSigner, signer: NostrSigner,
createdAt: Long = TimeUtils.now(), createdAt: Long = TimeUtils.now(),
@@ -123,9 +124,8 @@ class TextNoteEvent(
} }
zapRaiserAmount?.let { tags.add(arrayOf("zapraiser", "$it")) } zapRaiserAmount?.let { tags.add(arrayOf("zapraiser", "$it")) }
geohash?.let { tags.addAll(geohashMipMap(it)) } geohash?.let { tags.addAll(geohashMipMap(it)) }
imetas?.forEach { imetas?.forEach { tags.add(Nip92MediaAttachments.createTag(it)) }
tags.add(Nip92MediaAttachments.createTag(it)) emojis?.forEach { tags.add(it.toTagArray()) }
}
if (isDraft) { if (isDraft) {
signer.assembleRumor(createdAt, KIND, tags.toTypedArray(), msg, onReady) signer.assembleRumor(createdAt, KIND, tags.toTypedArray(), msg, onReady)
@@ -63,6 +63,7 @@ class TorrentCommentEvent(
zapRaiserAmount: Long?, zapRaiserAmount: Long?,
geohash: String? = null, geohash: String? = null,
imetas: List<IMetaTag>? = null, imetas: List<IMetaTag>? = null,
emojis: List<EmojiUrl>? = null,
forkedFrom: Event? = null, forkedFrom: Event? = null,
isDraft: Boolean, isDraft: Boolean,
onReady: (TorrentCommentEvent) -> Unit, onReady: (TorrentCommentEvent) -> Unit,
@@ -126,6 +127,7 @@ class TorrentCommentEvent(
imetas?.forEach { imetas?.forEach {
tags.add(Nip92MediaAttachments.createTag(it)) tags.add(Nip92MediaAttachments.createTag(it))
} }
emojis?.forEach { tags.add(it.toTagArray()) }
tags.add(arrayOf("alt", ALT)) tags.add(arrayOf("alt", ALT))
if (isDraft) { if (isDraft) {