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

View File

@ -31,6 +31,7 @@ import com.fasterxml.jackson.module.kotlin.readValue
import com.fonfon.kgeohash.GeoHash
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
import com.vitorpamplona.amethyst.service.LocationState
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) {
if (
!this.transientPaymentRequests.value.contains(paymentRequest) &&
@ -2110,6 +2185,7 @@ class Account(
relayList: List<RelaySetupInfo>,
geohash: String? = null,
imetas: List<IMetaTag>? = null,
emojis: List<EmojiUrl>? = null,
draftTag: String?,
) {
if (!isWriteable()) return
@ -2137,6 +2213,7 @@ class Account(
directMentions = directMentions,
geohash = geohash,
imetas = imetas,
emojis = emojis,
signer = signer,
isDraft = draftTag != null,
) {
@ -2177,6 +2254,7 @@ class Account(
relayList: List<RelaySetupInfo>,
geohash: String? = null,
imetas: List<IMetaTag>? = null,
emojis: List<EmojiUrl>? = null,
draftTag: String?,
) {
if (!isWriteable()) return
@ -2199,6 +2277,7 @@ class Account(
directMentions = directMentions,
geohash = geohash,
imetas = imetas,
emojis = emojis,
forkedFrom = forkedFrom,
signer = signer,
isDraft = draftTag != null,
@ -2245,6 +2324,7 @@ class Account(
relayList: List<RelaySetupInfo>,
geohash: String? = null,
imetas: List<IMetaTag>? = null,
emojis: List<EmojiUrl>? = null,
draftTag: String?,
) {
if (!isWriteable()) return
@ -2265,6 +2345,7 @@ class Account(
directMentions = directMentions,
geohash = geohash,
imetas = imetas,
emojis = emojis,
forkedFrom = forkedFrom,
signer = signer,
isDraft = draftTag != null,
@ -2328,6 +2409,7 @@ class Account(
directMentionsUsers: Set<User> = emptySet(),
directMentionsNotes: Set<Note> = emptySet(),
imetas: List<IMetaTag>? = null,
emojis: List<EmojiUrl>? = null,
geohash: String? = null,
zapReceiver: List<ZapSplitSetup>? = null,
wantsToMarkAsSensitive: Boolean = false,
@ -2371,6 +2453,7 @@ class Account(
addressesMentioned = addressesMentioned,
eventsMentioned = eventsMentioned,
imetas = imetas,
emojis = emojis,
geohash = geohash,
zapReceiver = zapReceiver,
markAsSensitive = wantsToMarkAsSensitive,
@ -2403,6 +2486,7 @@ class Account(
addressesMentioned = addressesMentioned,
eventsMentioned = eventsMentioned,
imetas = imetas,
emojis = emojis,
geohash = geohash,
zapReceiver = zapReceiver,
markAsSensitive = wantsToMarkAsSensitive,
@ -2437,6 +2521,7 @@ class Account(
directMentionsUsers: Set<User> = emptySet(),
directMentionsNotes: Set<Note> = emptySet(),
imetas: List<IMetaTag>? = null,
emojis: List<EmojiUrl>? = null,
zapReceiver: List<ZapSplitSetup>? = null,
wantsToMarkAsSensitive: Boolean = false,
zapRaiserAmount: Long? = null,
@ -2479,6 +2564,7 @@ class Account(
addressesMentioned = addressesMentioned,
eventsMentioned = eventsMentioned,
imetas = imetas,
emojis = emojis,
zapReceiver = zapReceiver,
markAsSensitive = wantsToMarkAsSensitive,
zapRaiserAmount = zapRaiserAmount,
@ -2683,6 +2769,7 @@ class Account(
relayList: List<RelaySetupInfo>,
geohash: String? = null,
imetas: List<IMetaTag>? = null,
emojis: List<EmojiUrl>? = null,
draftTag: String?,
) {
if (!isWriteable()) return
@ -2705,6 +2792,7 @@ class Account(
directMentions = directMentions,
geohash = geohash,
imetas = imetas,
emojis = emojis,
forkedFrom = forkedFrom,
signer = signer,
isDraft = draftTag != null,
@ -2775,6 +2863,7 @@ class Account(
relayList: List<RelaySetupInfo>,
geohash: String? = null,
imetas: List<IMetaTag>? = null,
emojis: List<EmojiUrl>? = null,
draftTag: String?,
) {
if (!isWriteable()) return
@ -2835,6 +2924,7 @@ class Account(
directMentions: Set<HexKey>,
geohash: String? = null,
imetas: List<IMetaTag>? = null,
emojis: List<EmojiUrl>? = null,
draftTag: String?,
) {
if (!isWriteable()) return
@ -2853,6 +2943,7 @@ class Account(
directMentions = directMentions,
geohash = geohash,
imetas = imetas,
emojis = emojis,
signer = signer,
isDraft = draftTag != null,
) {
@ -2881,6 +2972,7 @@ class Account(
zapRaiserAmount: Long? = null,
geohash: String? = null,
imetas: List<IMetaTag>? = null,
emojis: List<EmojiUrl>? = null,
draftTag: String?,
) {
if (!isWriteable()) return
@ -2899,6 +2991,7 @@ class Account(
zapRaiserAmount = zapRaiserAmount,
geohash = geohash,
imetas = imetas,
emojis = emojis,
signer = signer,
isDraft = draftTag != null,
) {
@ -3043,6 +3136,7 @@ class Account(
zapRaiserAmount: Long? = null,
geohash: String? = null,
imetas: List<IMetaTag>? = null,
emojis: List<EmojiUrl>? = null,
draftTag: String? = null,
) {
if (!isWriteable()) return
@ -3061,6 +3155,7 @@ class Account(
zapRaiserAmount = zapRaiserAmount,
geohash = geohash,
imetas = imetas,
emojis = emojis,
draftTag = draftTag,
signer = signer,
) {

View File

@ -57,6 +57,7 @@ import com.vitorpamplona.quartz.encoders.Hex
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.IMetaTag
import com.vitorpamplona.quartz.encoders.IMetaTagBuilder
import com.vitorpamplona.quartz.encoders.Nip30CustomEmoji
import com.vitorpamplona.quartz.encoders.toNpub
import com.vitorpamplona.quartz.events.AddressableEvent
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.CommunityDefinitionEvent
import com.vitorpamplona.quartz.events.DraftEvent
import com.vitorpamplona.quartz.events.EmojiUrl
import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.FileStorageEvent
import com.vitorpamplona.quartz.events.FileStorageHeaderEvent
@ -82,7 +84,12 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
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.withContext
import java.util.UUID
@ -120,6 +127,27 @@ open class NewPostViewModel : ViewModel() {
var userSuggestionAnchor: TextRange? = 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
var wantsDirectMessage by mutableStateOf(false)
var toUsers by mutableStateOf(TextFieldValue(""))
@ -196,6 +224,7 @@ open class NewPostViewModel : ViewModel() {
) {
this.accountViewModel = accountViewModel
this.account = accountViewModel.account
this.emojiSuggestions.value
val noteEvent = draft?.event
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 usedAttachments = iMetaAttachments.filter { it.url in urls.toSet() }
@ -569,6 +599,7 @@ open class NewPostViewModel : ViewModel() {
wantsToMarkAsSensitive = wantsToMarkAsSensitive,
zapRaiserAmount = localZapRaiserAmount,
relayList = relayList,
emojis = emojis,
draftTag = localDraft,
)
} else if (wantsExclusiveGeoPost && geoHash != null && (originalNote == null || originalNote?.event is CommentEvent)) {
@ -583,6 +614,7 @@ open class NewPostViewModel : ViewModel() {
wantsToMarkAsSensitive = wantsToMarkAsSensitive,
zapRaiserAmount = localZapRaiserAmount,
relayList = relayList,
emojis = emojis,
draftTag = localDraft,
)
} else if (originalNote?.channelHex() != null) {
@ -597,6 +629,7 @@ open class NewPostViewModel : ViewModel() {
zapRaiserAmount = localZapRaiserAmount,
geohash = geoHash,
imetas = usedAttachments,
emojis = emojis,
draftTag = localDraft,
)
} else {
@ -611,6 +644,7 @@ open class NewPostViewModel : ViewModel() {
directMentions = tagger.directMentions,
geohash = geoHash,
imetas = usedAttachments,
emojis = emojis,
draftTag = localDraft,
)
}
@ -639,6 +673,7 @@ open class NewPostViewModel : ViewModel() {
zapRaiserAmount = localZapRaiserAmount,
geohash = geoHash,
imetas = usedAttachments,
emojis = emojis,
draftTag = localDraft,
)
} else if (!dmUsers.isNullOrEmpty()) {
@ -654,6 +689,7 @@ open class NewPostViewModel : ViewModel() {
zapRaiserAmount = localZapRaiserAmount,
geohash = geoHash,
imetas = usedAttachments,
emojis = emojis,
draftTag = localDraft,
)
} else {
@ -708,6 +744,7 @@ open class NewPostViewModel : ViewModel() {
relayList = relayList,
geohash = geoHash,
imetas = usedAttachments,
emojis = emojis,
draftTag = localDraft,
)
} else if (originalNote?.event is TorrentCommentEvent) {
@ -747,6 +784,7 @@ open class NewPostViewModel : ViewModel() {
relayList = relayList,
geohash = geoHash,
imetas = usedAttachments,
emojis = emojis,
draftTag = localDraft,
)
}
@ -776,6 +814,7 @@ open class NewPostViewModel : ViewModel() {
relayList = relayList,
geohash = geoHash,
imetas = usedAttachments,
emojis = emojis,
draftTag = localDraft,
)
} else {
@ -795,6 +834,7 @@ open class NewPostViewModel : ViewModel() {
relayList = relayList,
geohash = geoHash,
imetas = usedAttachments,
emojis = emojis,
draftTag = localDraft,
)
} else if (wantsProduct) {
@ -814,6 +854,7 @@ open class NewPostViewModel : ViewModel() {
relayList = relayList,
geohash = geoHash,
imetas = usedAttachments,
emojis = emojis,
draftTag = localDraft,
)
} else {
@ -851,12 +892,23 @@ open class NewPostViewModel : ViewModel() {
relayList = relayList,
geohash = geoHash,
imetas = usedAttachments,
emojis = emojis,
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(
alt: String?,
sensitiveContent: Boolean,
@ -978,6 +1030,10 @@ open class NewPostViewModel : ViewModel() {
userSuggestionAnchor = null
userSuggestionsMainMessage = null
if (emojiSearch.value.isNotEmpty()) {
emojiSearch.tryEmit("")
}
draftTag = UUID.randomUUID().toString()
NostrSearchEventOrUserDataSource.clear()
@ -1029,6 +1085,14 @@ open class NewPostViewModel : ViewModel() {
NostrSearchEventOrUserDataSource.clear()
userSuggestions = emptyList()
}
if (lastWord.startsWith(":")) {
emojiSearch.tryEmit(lastWord)
} else {
if (emojiSearch.value.isNotBlank()) {
emojiSearch.tryEmit("")
}
}
}
saveDraft()
@ -1132,6 +1196,54 @@ open class NewPostViewModel : ViewModel() {
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, ""))
fun canPost(): Boolean =

View File

@ -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,
)
}
}
}
}

View File

@ -144,7 +144,6 @@ class UpdateReactionTypeViewModel : ViewModel() {
?.value
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun UpdateReactionTypeDialog(
onClose: () -> Unit,

View File

@ -157,8 +157,10 @@ import com.vitorpamplona.amethyst.ui.note.LoadCityName
import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.note.PollIcon
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.UsernameDisplay
import com.vitorpamplona.amethyst.ui.note.WatchAndLoadMyEmojiList
import com.vitorpamplona.amethyst.ui.note.ZapSplitIcon
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.MyTextField
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsRow
@ -209,6 +211,7 @@ fun NewPostScreen(
nav: Nav,
) {
val postViewModel: NewPostViewModel = viewModel()
postViewModel.account = accountViewModel.account
postViewModel.wantsDirectMessage = enableMessageInterface
postViewModel.wantsToAddGeoHash = enableGeolocation
@ -278,6 +281,8 @@ fun NewPostScreen(
onDispose { activity.removeOnNewIntentListener(consumer) }
}
WatchAndLoadMyEmojiList(accountViewModel)
Scaffold(
topBar = {
TopAppBar(
@ -575,6 +580,14 @@ fun NewPostScreen(
modifier = Modifier.heightIn(0.dp, 300.dp),
)
ShowEmojiSuggestionList(
postViewModel.emojiSuggestions,
postViewModel::autocompleteWithEmoji,
postViewModel::autocompleteWithEmojiUrl,
accountViewModel,
modifier = Modifier.heightIn(0.dp, 300.dp),
)
BottomRowActions(postViewModel)
}
}

View File

@ -130,6 +130,7 @@ import com.vitorpamplona.amethyst.ui.note.LikeReaction
import com.vitorpamplona.amethyst.ui.note.LoadChannel
import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture
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.UserPicture
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
@ -397,6 +398,7 @@ private suspend fun innerSendPost(
val urls = findURLs(tagger.message)
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) {
accountViewModel.account.sendChannelMessage(
@ -407,6 +409,7 @@ private suspend fun innerSendPost(
directMentions = tagger.directMentions,
wantsToMarkAsSensitive = false,
imetas = usedAttachments,
emojis = emojis,
draftTag = draftTag,
)
} else if (channel is LiveActivitiesChannel) {
@ -417,6 +420,7 @@ private suspend fun innerSendPost(
mentions = tagger.pTags,
wantsToMarkAsSensitive = false,
imetas = usedAttachments,
emojis = emojis,
draftTag = draftTag,
)
}
@ -485,6 +489,13 @@ fun EditFieldRow(
accountViewModel,
)
ShowEmojiSuggestionList(
channelScreenModel.emojiSuggestions,
channelScreenModel::autocompleteWithEmoji,
channelScreenModel::autocompleteWithEmojiUrl,
accountViewModel,
)
MyTextField(
value = channelScreenModel.message,
onValueChange = { channelScreenModel.updateMessage(it) },

View File

@ -95,6 +95,7 @@ import com.vitorpamplona.amethyst.ui.note.IncognitoIconOff
import com.vitorpamplona.amethyst.ui.note.IncognitoIconOn
import com.vitorpamplona.amethyst.ui.note.NonClickableUserPictures
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.UserCompose
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
@ -517,6 +518,7 @@ private fun innerSendPost(
) {
val urls = findURLs(newPostModel.message.text)
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) {
accountViewModel.account.sendNIP17PrivateMessage(
@ -526,6 +528,7 @@ private fun innerSendPost(
mentions = null,
wantsToMarkAsSensitive = false,
imetas = usedAttachments,
emojis = emojis,
draftTag = dTag,
)
} else {
@ -557,6 +560,13 @@ fun PrivateMessageEditFieldRow(
accountViewModel,
)
ShowEmojiSuggestionList(
channelScreenModel.emojiSuggestions,
channelScreenModel::autocompleteWithEmoji,
channelScreenModel::autocompleteWithEmojiUrl,
accountViewModel,
)
MyTextField(
value = channelScreenModel.message,
onValueChange = { channelScreenModel.updateMessage(it) },

View File

@ -377,6 +377,8 @@
<string name="add_caption">Add a Caption</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_example">A blue boat in a white sandy beach at sunset</string>

View File

@ -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 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(
input: String,
allTags: ImmutableListOfLists<String>?,
@ -65,12 +83,7 @@ class Nip30CustomEmoji {
input: String,
emojiPairs: Map<String, String>,
): ImmutableList<Renderable>? {
val matcher = customEmojiPattern.matcher(input)
val emojiNamesInOrder = mutableListOf<String>()
while (matcher.find()) {
emojiNamesInOrder.add(matcher.group())
}
val emojiNamesInOrder = findAllEmojis(input)
if (emojiNamesInOrder.isEmpty()) {
return null
}

View File

@ -62,6 +62,7 @@ class ChannelMessageEvent(
directMentions: Set<HexKey> = emptySet(),
geohash: String? = null,
imetas: List<IMetaTag>? = null,
emojis: List<EmojiUrl>? = null,
isDraft: Boolean,
onReady: (ChannelMessageEvent) -> Unit,
) {
@ -88,6 +89,7 @@ class ChannelMessageEvent(
imetas?.forEach {
tags.add(Nip92MediaAttachments.createTag(it))
}
emojis?.forEach { tags.add(it.toTagArray()) }
tags.add(
arrayOf("alt", ALT),
)

View File

@ -84,6 +84,7 @@ class ChatMessageEvent(
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
imetas: List<IMetaTag>? = null,
emojis: List<EmojiUrl>? = null,
isDraft: Boolean,
onReady: (ChatMessageEvent) -> Unit,
) {
@ -103,6 +104,7 @@ class ChatMessageEvent(
imetas?.forEach {
tags.add(Nip92MediaAttachments.createTag(it))
}
emojis?.forEach { tags.add(it.toTagArray()) }
// tags.add(arrayOf("alt", alt))
if (isDraft) {

View File

@ -115,6 +115,7 @@ class ClassifiedsEvent(
zapRaiserAmount: Long?,
geohash: String? = null,
imetas: List<IMetaTag>? = null,
emojis: List<EmojiUrl>? = null,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
isDraft: Boolean,
@ -190,9 +191,8 @@ class ClassifiedsEvent(
}
zapRaiserAmount?.let { tags.add(arrayOf("zapraiser", "$it")) }
geohash?.let { tags.addAll(geohashMipMap(it)) }
imetas?.forEach {
tags.add(Nip92MediaAttachments.createTag(it))
}
imetas?.forEach { tags.add(Nip92MediaAttachments.createTag(it)) }
emojis?.forEach { tags.add(it.toTagArray()) }
tags.add(arrayOf("alt", ALT))
if (isDraft) {

View File

@ -98,6 +98,7 @@ class CommentEvent(
addressesMentioned: Set<ATag> = emptySet(),
eventsMentioned: Set<ETag> = emptySet(),
imetas: List<IMetaTag>? = null,
emojis: List<EmojiUrl>? = null,
geohash: String? = null,
zapReceiver: List<ZapSplitSetup>? = null,
markAsSensitive: Boolean = false,
@ -120,7 +121,7 @@ class CommentEvent(
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, 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(
@ -130,6 +131,7 @@ class CommentEvent(
addressesMentioned: Set<ATag> = emptySet(),
eventsMentioned: Set<ETag> = emptySet(),
imetas: List<IMetaTag>? = null,
emojis: List<EmojiUrl>? = null,
geohash: String? = null,
zapReceiver: List<ZapSplitSetup>? = null,
markAsSensitive: Boolean = false,
@ -147,7 +149,7 @@ class CommentEvent(
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, 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(
@ -157,6 +159,7 @@ class CommentEvent(
addressesMentioned: Set<ATag> = emptySet(),
eventsMentioned: Set<ETag> = emptySet(),
imetas: List<IMetaTag>? = null,
emojis: List<EmojiUrl>? = null,
zapReceiver: List<ZapSplitSetup>? = null,
markAsSensitive: Boolean = false,
zapRaiserAmount: Long? = null,
@ -169,7 +172,7 @@ class CommentEvent(
geohash?.let { tags.addAll(rootGeohashMipMap(it)) }
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(
@ -179,6 +182,7 @@ class CommentEvent(
addressesMentioned: Set<ATag> = emptySet(),
eventsMentioned: Set<ETag> = emptySet(),
imetas: List<IMetaTag>? = null,
emojis: List<EmojiUrl>? = null,
geohash: String? = null,
zapReceiver: List<ZapSplitSetup>? = null,
markAsSensitive: Boolean = false,
@ -202,6 +206,8 @@ class CommentEvent(
findURLs(msg).forEach { tags.add(arrayOf("r", it)) }
emojis?.forEach { tags.add(it.toTagArray()) }
zapReceiver?.forEach {
tags.add(arrayOf("zap", it.lnAddressOrPubKeyHex, it.relay ?: "", it.weight.toString()))
}

View File

@ -62,6 +62,8 @@ data class EmojiUrl(
) {
fun encode(): String = ":$code:$url"
fun toTagArray() = arrayOf("emoji", code, url)
companion object {
fun decode(encodedEmojiSetup: String): EmojiUrl? {
val emojiParts = encodedEmojiSetup.split(":", limit = 3)
@ -71,5 +73,12 @@ data class EmojiUrl(
null
}
}
fun parse(tag: Array<String>): EmojiUrl? =
if (tag.size > 2 && tag[0] == "emoji") {
EmojiUrl(tag[1], tag[2])
} else {
null
}
}
}

View File

@ -42,6 +42,10 @@ class EmojiPackSelectionEvent(
const val FIXED_D_TAG = ""
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(
listOfEmojiPacks: List<ATag>?,
signer: NostrSigner,

View File

@ -164,7 +164,7 @@ open class Event(
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() =
tags.any {

View File

@ -92,6 +92,7 @@ class GitReplyEvent(
directMentions: Set<HexKey> = emptySet(),
geohash: String? = null,
imetas: List<IMetaTag>? = null,
emojis: List<EmojiUrl>? = null,
forkedFrom: Event? = null,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
@ -152,6 +153,7 @@ class GitReplyEvent(
imetas?.forEach {
tags.add(Nip92MediaAttachments.createTag(it))
}
emojis?.forEach { tags.add(it.toTagArray()) }
tags.add(arrayOf("alt", "a git issue reply"))
if (isDraft) {

View File

@ -53,6 +53,7 @@ open class InteractiveStoryBaseEvent(
zapRaiserAmount: Long? = null,
geohash: String? = null,
imetas: List<IMetaTag>? = null,
emojis: List<EmojiUrl>? = null,
): Array<Array<String>> {
val tags = mutableListOf<Array<String>>()
findHashtags(content).forEach {
@ -75,6 +76,7 @@ open class InteractiveStoryBaseEvent(
imetas?.forEach {
tags.add(Nip92MediaAttachments.createTag(it))
}
emojis?.forEach { tags.add(it.toTagArray()) }
return tags.toTypedArray()
}

View File

@ -75,6 +75,7 @@ class LiveActivitiesChatMessageEvent(
zapRaiserAmount: Long?,
geohash: String? = null,
imetas: List<IMetaTag>? = null,
emojis: List<EmojiUrl>? = null,
isDraft: Boolean,
onReady: (LiveActivitiesChatMessageEvent) -> Unit,
) {
@ -96,6 +97,7 @@ class LiveActivitiesChatMessageEvent(
imetas?.forEach {
tags.add(Nip92MediaAttachments.createTag(it))
}
emojis?.forEach { tags.add(it.toTagArray()) }
tags.add(arrayOf("alt", ALT))
if (isDraft) {

View File

@ -82,6 +82,7 @@ class NIP17Factory {
zapRaiserAmount: Long? = null,
geohash: String? = null,
imetas: List<IMetaTag>? = null,
emojis: List<EmojiUrl>? = null,
draftTag: String? = null,
onReady: (Result) -> Unit,
) {
@ -100,6 +101,7 @@ class NIP17Factory {
geohash = geohash,
isDraft = draftTag != null,
imetas = imetas,
emojis = emojis,
) { senderMessage ->
if (draftTag != null) {
onReady(

View File

@ -81,6 +81,7 @@ class PollNoteEvent(
zapRaiserAmount: Long?,
geohash: String? = null,
imetas: List<IMetaTag>? = null,
emojis: List<EmojiUrl>? = null,
isDraft: Boolean,
onReady: (PollNoteEvent) -> Unit,
) {
@ -108,6 +109,7 @@ class PollNoteEvent(
imetas?.forEach {
tags.add(Nip92MediaAttachments.createTag(it))
}
emojis?.forEach { tags.add(it.toTagArray()) }
tags.add(arrayOf("alt", ALT))
if (isDraft) {

View File

@ -58,6 +58,7 @@ class TextNoteEvent(
directMentions: Set<HexKey> = emptySet(),
geohash: String? = null,
imetas: List<IMetaTag>? = null,
emojis: List<EmojiUrl>? = null,
forkedFrom: Event? = null,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
@ -123,9 +124,8 @@ class TextNoteEvent(
}
zapRaiserAmount?.let { tags.add(arrayOf("zapraiser", "$it")) }
geohash?.let { tags.addAll(geohashMipMap(it)) }
imetas?.forEach {
tags.add(Nip92MediaAttachments.createTag(it))
}
imetas?.forEach { tags.add(Nip92MediaAttachments.createTag(it)) }
emojis?.forEach { tags.add(it.toTagArray()) }
if (isDraft) {
signer.assembleRumor(createdAt, KIND, tags.toTypedArray(), msg, onReady)

View File

@ -63,6 +63,7 @@ class TorrentCommentEvent(
zapRaiserAmount: Long?,
geohash: String? = null,
imetas: List<IMetaTag>? = null,
emojis: List<EmojiUrl>? = null,
forkedFrom: Event? = null,
isDraft: Boolean,
onReady: (TorrentCommentEvent) -> Unit,
@ -126,6 +127,7 @@ class TorrentCommentEvent(
imetas?.forEach {
tags.add(Nip92MediaAttachments.createTag(it))
}
emojis?.forEach { tags.add(it.toTagArray()) }
tags.add(arrayOf("alt", ALT))
if (isDraft) {