Adds a New Message Tagger to Chats (tags Notes and Users by default) and Private Message (doesn't tag Users and Notes)

This commit is contained in:
Vitor Pamplona
2023-04-06 11:58:49 -04:00
parent 8676752f19
commit 6cf1960df4
5 changed files with 88 additions and 152 deletions

View File

@@ -402,10 +402,11 @@ class Account(
LocalCache.consume(signedEvent) LocalCache.consume(signedEvent)
} }
fun sendChannelMessage(message: String, toChannel: String, replyingTo: Note? = null, mentions: List<User>?) { fun sendChannelMessage(message: String, toChannel: String, replyTo: List<Note>?, mentions: List<User>?) {
if (!isWriteable()) return if (!isWriteable()) return
val repliesToHex = listOfNotNull(replyingTo?.idHex).ifEmpty { null } // val repliesToHex = listOfNotNull(replyingTo?.idHex).ifEmpty { null }
val repliesToHex = replyTo?.map { it.idHex }
val mentionsHex = mentions?.map { it.pubkeyHex } val mentionsHex = mentions?.map { it.pubkeyHex }
val signedEvent = ChannelMessageEvent.create( val signedEvent = ChannelMessageEvent.create(

View File

@@ -1,80 +1,81 @@
package com.vitorpamplona.amethyst.ui.actions package com.vitorpamplona.amethyst.ui.actions
import com.vitorpamplona.amethyst.model.Channel
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.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.model.parseDirtyWordForKey import com.vitorpamplona.amethyst.model.parseDirtyWordForKey
import com.vitorpamplona.amethyst.service.nip19.Nip19 import com.vitorpamplona.amethyst.service.nip19.Nip19
class NewMessageProcessor(var originalNote: Note?, var mentions: List<User>?, var replyTos: List<Note>?, var message: String) { class NewMessageTagger(var channel: Channel?, var mentions: List<User>?, var replyTos: List<Note>?, var message: String) {
open fun addUserToMentions(user: User) { open fun addUserToMentions(user: User) {
mentions = if (mentions?.contains(user) == true) mentions else mentions?.plus(user) ?: listOf(user) mentions = if (mentions?.contains(user) == true) mentions else mentions?.plus(user) ?: listOf(user)
}
open fun addNoteToReplyTos(note: Note) {
note.author?.let { addUserToMentions(it) }
replyTos = if (replyTos?.contains(note) == true) replyTos else replyTos?.plus(note) ?: listOf(note)
}
open fun tagIndex(user: User): Int {
// Postr Events assembles replies before mentions in the tag order
return (if (originalNote?.channel() != null) 1 else 0) + (replyTos?.size ?: 0) + (mentions?.indexOf(user) ?: 0)
}
open fun tagIndex(note: Note): Int {
// Postr Events assembles replies before mentions in the tag order
return (if (originalNote?.channel() != null) 1 else 0) + (replyTos?.indexOf(note) ?: 0)
}
fun run() {
// adds all references to mentions and reply tos
message.split('\n').forEach { paragraph: String ->
paragraph.split(' ').forEach { word: String ->
val results = parseDirtyWordForKey(word)
if (results?.key?.type == Nip19.Type.USER) {
addUserToMentions(LocalCache.getOrCreateUser(results.key.hex))
} else if (results?.key?.type == Nip19.Type.NOTE) {
addNoteToReplyTos(LocalCache.getOrCreateNote(results.key.hex))
} else if (results?.key?.type == Nip19.Type.EVENT) {
addNoteToReplyTos(LocalCache.getOrCreateNote(results.key.hex))
} else if (results?.key?.type == Nip19.Type.ADDRESS) {
val note = LocalCache.checkGetOrCreateAddressableNote(results.key.hex)
if (note != null) {
addNoteToReplyTos(note)
}
}
}
} }
// Tags the text in the correct order. open fun addNoteToReplyTos(note: Note) {
message = message.split('\n').map { paragraph: String -> note.author?.let { addUserToMentions(it) }
paragraph.split(' ').map { word: String -> replyTos = if (replyTos?.contains(note) == true) replyTos else replyTos?.plus(note) ?: listOf(note)
val results = parseDirtyWordForKey(word) }
if (results?.key?.type == Nip19.Type.USER) {
val user = LocalCache.getOrCreateUser(results.key.hex)
"#[${tagIndex(user)}]${results.restOfWord}" open fun tagIndex(user: User): Int {
} else if (results?.key?.type == Nip19.Type.NOTE) { // Postr Events assembles replies before mentions in the tag order
val note = LocalCache.getOrCreateNote(results.key.hex) return (if (channel != null) 1 else 0) + (replyTos?.size ?: 0) + (mentions?.indexOf(user) ?: 0)
}
"#[${tagIndex(note)}]${results.restOfWord}" open fun tagIndex(note: Note): Int {
} else if (results?.key?.type == Nip19.Type.EVENT) { // Postr Events assembles replies before mentions in the tag order
val note = LocalCache.getOrCreateNote(results.key.hex) return (if (channel != null) 1 else 0) + (replyTos?.indexOf(note) ?: 0)
}
"#[${tagIndex(note)}]${results.restOfWord}" fun run() {
} else if (results?.key?.type == Nip19.Type.ADDRESS) { // adds all references to mentions and reply tos
val note = LocalCache.checkGetOrCreateAddressableNote(results.key.hex) message.split('\n').forEach { paragraph: String ->
if (note != null) { paragraph.split(' ').forEach { word: String ->
"#[${tagIndex(note)}]${results.restOfWord}" val results = parseDirtyWordForKey(word)
} else {
word if (results?.key?.type == Nip19.Type.USER) {
} addUserToMentions(LocalCache.getOrCreateUser(results.key.hex))
} else { } else if (results?.key?.type == Nip19.Type.NOTE) {
word addNoteToReplyTos(LocalCache.getOrCreateNote(results.key.hex))
} else if (results?.key?.type == Nip19.Type.EVENT) {
addNoteToReplyTos(LocalCache.getOrCreateNote(results.key.hex))
} else if (results?.key?.type == Nip19.Type.ADDRESS) {
val note = LocalCache.checkGetOrCreateAddressableNote(results.key.hex)
if (note != null) {
addNoteToReplyTos(note)
}
}
}
} }
}.joinToString(" ")
}.joinToString("\n") // Tags the text in the correct order.
} message = message.split('\n').map { paragraph: String ->
paragraph.split(' ').map { word: String ->
val results = parseDirtyWordForKey(word)
if (results?.key?.type == Nip19.Type.USER) {
val user = LocalCache.getOrCreateUser(results.key.hex)
"#[${tagIndex(user)}]${results.restOfWord}"
} else if (results?.key?.type == Nip19.Type.NOTE) {
val note = LocalCache.getOrCreateNote(results.key.hex)
"#[${tagIndex(note)}]${results.restOfWord}"
} else if (results?.key?.type == Nip19.Type.EVENT) {
val note = LocalCache.getOrCreateNote(results.key.hex)
"#[${tagIndex(note)}]${results.restOfWord}"
} else if (results?.key?.type == Nip19.Type.ADDRESS) {
val note = LocalCache.checkGetOrCreateAddressableNote(results.key.hex)
if (note != null) {
"#[${tagIndex(note)}]${results.restOfWord}"
} else {
word
}
} else {
word
}
}.joinToString(" ")
}.joinToString("\n")
}
} }

View File

@@ -15,7 +15,6 @@ import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.model.* import com.vitorpamplona.amethyst.model.*
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
import com.vitorpamplona.amethyst.service.model.TextNoteEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent
import com.vitorpamplona.amethyst.service.nip19.Nip19
import com.vitorpamplona.amethyst.ui.components.isValidURL import com.vitorpamplona.amethyst.ui.components.isValidURL
import com.vitorpamplona.amethyst.ui.components.noProtocolUrlValidator import com.vitorpamplona.amethyst.ui.components.noProtocolUrlValidator
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -56,7 +55,6 @@ open class NewPostViewModel : ViewModel() {
// Invoices // Invoices
var wantsInvoice by mutableStateOf(false) var wantsInvoice by mutableStateOf(false)
open fun load(account: Account, replyingTo: Note?, quote: Note?) { open fun load(account: Account, replyingTo: Note?, quote: Note?) {
originalNote = replyingTo originalNote = replyingTo
replyingTo?.let { replyNote -> replyingTo?.let { replyNote ->
@@ -84,83 +82,18 @@ open class NewPostViewModel : ViewModel() {
this.account = account this.account = account
} }
open fun addUserToMentions(user: User) {
mentions = if (mentions?.contains(user) == true) mentions else mentions?.plus(user) ?: listOf(user)
}
open fun addNoteToReplyTos(note: Note) {
note.author?.let { addUserToMentions(it) }
replyTos = if (replyTos?.contains(note) == true) replyTos else replyTos?.plus(note) ?: listOf(note)
}
open fun tagIndex(user: User): Int {
// Postr Events assembles replies before mentions in the tag order
return (if (originalNote?.channel() != null) 1 else 0) + (replyTos?.size ?: 0) + (mentions?.indexOf(user) ?: 0)
}
open fun tagIndex(note: Note): Int {
// Postr Events assembles replies before mentions in the tag order
return (if (originalNote?.channel() != null) 1 else 0) + (replyTos?.indexOf(note) ?: 0)
}
fun sendPost() { fun sendPost() {
// adds all references to mentions and reply tos val tagger = NewMessageTagger(originalNote?.channel(), mentions, replyTos, message.text)
message.text.split('\n').forEach { paragraph: String -> tagger.run()
paragraph.split(' ').forEach { word: String ->
val results = parseDirtyWordForKey(word)
if (results?.key?.type == Nip19.Type.USER) {
addUserToMentions(LocalCache.getOrCreateUser(results.key.hex))
} else if (results?.key?.type == Nip19.Type.NOTE) {
addNoteToReplyTos(LocalCache.getOrCreateNote(results.key.hex))
} else if (results?.key?.type == Nip19.Type.EVENT) {
addNoteToReplyTos(LocalCache.getOrCreateNote(results.key.hex))
} else if (results?.key?.type == Nip19.Type.ADDRESS) {
val note = LocalCache.checkGetOrCreateAddressableNote(results.key.hex)
if (note != null) {
addNoteToReplyTos(note)
}
}
}
}
// Tags the text in the correct order.
val newMessage = message.text.split('\n').map { paragraph: String ->
paragraph.split(' ').map { word: String ->
val results = parseDirtyWordForKey(word)
if (results?.key?.type == Nip19.Type.USER) {
val user = LocalCache.getOrCreateUser(results.key.hex)
"#[${tagIndex(user)}]${results.restOfWord}"
} else if (results?.key?.type == Nip19.Type.NOTE) {
val note = LocalCache.getOrCreateNote(results.key.hex)
"#[${tagIndex(note)}]${results.restOfWord}"
} else if (results?.key?.type == Nip19.Type.EVENT) {
val note = LocalCache.getOrCreateNote(results.key.hex)
"#[${tagIndex(note)}]${results.restOfWord}"
} else if (results?.key?.type == Nip19.Type.ADDRESS) {
val note = LocalCache.checkGetOrCreateAddressableNote(results.key.hex)
if (note != null) {
"#[${tagIndex(note)}]${results.restOfWord}"
} else {
word
}
} else {
word
}
}.joinToString(" ")
}.joinToString("\n")
if (wantsPoll) { if (wantsPoll) {
account?.sendPoll(newMessage, replyTos, mentions, pollOptions, valueMaximum, valueMinimum, consensusThreshold, closedAt) account?.sendPoll(tagger.message, tagger.replyTos, tagger.mentions, pollOptions, valueMaximum, valueMinimum, consensusThreshold, closedAt)
} else if (originalNote?.channel() != null) { } else if (originalNote?.channel() != null) {
account?.sendChannelMessage(newMessage, originalNote!!.channel()!!.idHex, originalNote!!, mentions) account?.sendChannelMessage(tagger.message, tagger.channel!!.idHex, tagger.replyTos, tagger.mentions)
} else if (originalNote?.event is PrivateDmEvent) { } else if (originalNote?.event is PrivateDmEvent) {
account?.sendPrivateMessage(newMessage, originalNote!!.author!!.pubkeyHex, originalNote!!, mentions) account?.sendPrivateMessage(tagger.message, originalNote!!.author!!.pubkeyHex, originalNote!!, tagger.mentions)
} else { } else {
account?.sendPost(newMessage, replyTos, mentions) account?.sendPost(tagger.message, tagger.replyTos, tagger.mentions)
} }
cancel() cancel()

View File

@@ -230,17 +230,15 @@ fun ChatroomMessageCompose(
if (!innerQuote && !replyTo.isNullOrEmpty()) { if (!innerQuote && !replyTo.isNullOrEmpty()) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
replyTo.toSet().mapIndexed { _, note -> replyTo.toSet().mapIndexed { _, note ->
if (note.event != null) { ChatroomMessageCompose(
ChatroomMessageCompose( note,
note, null,
null, innerQuote = true,
innerQuote = true, parentBackgroundColor = backgroundBubbleColor,
parentBackgroundColor = backgroundBubbleColor, accountViewModel = accountViewModel,
accountViewModel = accountViewModel, navController = navController,
navController = navController, onWantsToReply = onWantsToReply
onWantsToReply = onWantsToReply )
)
}
} }
} }
} }

View File

@@ -66,6 +66,7 @@ import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.NostrChannelDataSource import com.vitorpamplona.amethyst.service.NostrChannelDataSource
import com.vitorpamplona.amethyst.ui.actions.NewChannelView import com.vitorpamplona.amethyst.ui.actions.NewChannelView
import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger
import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel
import com.vitorpamplona.amethyst.ui.actions.PostButton import com.vitorpamplona.amethyst.ui.actions.PostButton
import com.vitorpamplona.amethyst.ui.actions.UploadFromGallery import com.vitorpamplona.amethyst.ui.actions.UploadFromGallery
@@ -213,7 +214,9 @@ fun ChannelScreen(
trailingIcon = { trailingIcon = {
PostButton( PostButton(
onPost = { onPost = {
account.sendChannelMessage(channelScreenModel.message.text, channel.idHex, replyTo.value, null) val tagger = NewMessageTagger(channel, listOfNotNull(replyTo.value?.author), listOfNotNull(replyTo.value), channelScreenModel.message.text)
tagger.run()
account.sendChannelMessage(tagger.message, channel.idHex, tagger.replyTos, tagger.mentions)
channelScreenModel.message = TextFieldValue("") channelScreenModel.message = TextFieldValue("")
replyTo.value = null replyTo.value = null
feedViewModel.invalidateData() // Don't wait a full second before updating feedViewModel.invalidateData() // Don't wait a full second before updating