Mention other users.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
package ac.cord.auxiliary.compose.database.dao
|
||||
|
||||
import ac.cord.auxiliary.compose.database.GENESIS_AT
|
||||
import ac.cord.auxiliary.compose.database.model.Profile
|
||||
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalProfile
|
||||
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalProfileWithFollowers
|
||||
@@ -10,11 +11,12 @@ import androidx.room3.OnConflictStrategy.Companion.IGNORE
|
||||
import androidx.room3.Query
|
||||
import androidx.room3.Upsert
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlin.time.Instant
|
||||
|
||||
@Dao
|
||||
interface ProfileDao {
|
||||
@Query("SELECT * FROM Profile LIMIT :limit")
|
||||
fun getAllProfiles(limit: Int = 21): List<Profile>
|
||||
@Query("SELECT * FROM Profile WHERE createdAt > :createdAt LIMIT :limit")
|
||||
fun getAllProfiles(limit: Int = 21, createdAt: Instant = GENESIS_AT): List<Profile>
|
||||
|
||||
@Query("SELECT * FROM NostrEvent WHERE kind = 0 AND content LIKE '%' || :searchTerm || '%' LIMIT :limit")
|
||||
fun filterProfiles(searchTerm: String, limit: Int = 21): List<LocalProfile>
|
||||
|
||||
@@ -237,6 +237,7 @@ class DatabaseNostrRepository(
|
||||
|
||||
override suspend fun createNewTextNote(
|
||||
publicKey: HexKey,
|
||||
mentionedPublicKeys: List<HexKey>,
|
||||
textInput: String,
|
||||
inReplyToNostrEvent: LocalNostrEvent?,
|
||||
quotedNostrEvent: LocalNostrEvent?,
|
||||
@@ -271,12 +272,16 @@ class DatabaseNostrRepository(
|
||||
)
|
||||
|
||||
it.profile?.let { profile ->
|
||||
val ptags = arrayOf(
|
||||
profile,
|
||||
// Other profiles tagged in the event...
|
||||
).map { notifyProfile ->
|
||||
val publicKeys = mutableSetOf<HexKey>()
|
||||
publicKeys.addAll(
|
||||
mentionedPublicKeys.toSet()
|
||||
)
|
||||
|
||||
publicKeys.add(profile.publicKey)
|
||||
|
||||
val ptags = publicKeys.map { publicKey ->
|
||||
PTag(
|
||||
notifyProfile.publicKey,
|
||||
publicKey,
|
||||
// TODO: Add relayHints...
|
||||
)
|
||||
}
|
||||
@@ -285,6 +290,16 @@ class DatabaseNostrRepository(
|
||||
}
|
||||
}
|
||||
|
||||
if (inReplyToNostrEvent == null || inReplyToNostrEvent.profile == null) {
|
||||
val pTags = mentionedPublicKeys.toSet().map {
|
||||
PTag(
|
||||
pubKey = it
|
||||
)
|
||||
}
|
||||
|
||||
notify(pTags)
|
||||
}
|
||||
|
||||
quotedNostrEvent?.let {
|
||||
it.profile?.let { profile ->
|
||||
val ptags = listOf(
|
||||
|
||||
@@ -18,6 +18,7 @@ import ac.cord.auxiliary.compose.database.model.typealiases.SynchronizationFilte
|
||||
import ac.cord.auxiliary.compose.database.model.types.SynchronizationFilter
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Kind
|
||||
import fr.acinq.secp256k1.Hex
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface NostrRepository {
|
||||
@@ -46,6 +47,7 @@ interface NostrRepository {
|
||||
|
||||
suspend fun createNewTextNote(
|
||||
publicKey: HexKey,
|
||||
mentionedPublicKeys: List<HexKey>,
|
||||
textInput: String,
|
||||
inReplyToNostrEvent: LocalNostrEvent?,
|
||||
quotedNostrEvent: LocalNostrEvent?,
|
||||
@@ -145,6 +147,7 @@ interface NostrRepository {
|
||||
|
||||
override suspend fun createNewTextNote(
|
||||
publicKey: HexKey,
|
||||
mentionedPublicKeys: List<HexKey>,
|
||||
textInput: String,
|
||||
inReplyToNostrEvent: LocalNostrEvent?,
|
||||
quotedNostrEvent: LocalNostrEvent?,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package ac.cord.auxiliary.compose.ui.composable
|
||||
|
||||
import ac.cord.auxiliary.compose.database.model.Mention
|
||||
import ac.cord.auxiliary.compose.database.model.NostrEvent
|
||||
import ac.cord.auxiliary.compose.database.model.Profile
|
||||
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalMention
|
||||
@@ -10,11 +11,13 @@ import ac.cord.auxiliary.compose.repository.NostrRepository
|
||||
import ac.cord.auxiliary.compose.ui.composable.widgets.LoadingDataIndicator
|
||||
import ac.cord.auxiliary.compose.ui.composable.widgets.content.ContentParser
|
||||
import ac.cord.auxiliary.compose.ui.composable.widgets.content.ContentSegment
|
||||
import ac.cord.auxiliary.compose.ui.composable.widgets.content.RichContent
|
||||
import ac.cord.auxiliary.compose.ui.composable.widgets.profile.ProfileAvatar
|
||||
import ac.cord.auxiliary.compose.ui.composable.widgets.profile.ProfileColor
|
||||
import ac.cord.auxiliary.compose.ui.theme.AuxTheme
|
||||
import ac.cord.auxiliary.compose.ui.view.model.WriteNewNoteViewModel
|
||||
import ac.cord.auxiliary.compose.ui.view.state.WriteNewNoteUIState
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -25,11 +28,14 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.text.input.InputTransformation
|
||||
import androidx.compose.foundation.text.input.OutputTransformation
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.NavigateNext
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -41,7 +47,11 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
@@ -50,11 +60,12 @@ import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||
import kotlin.time.Clock
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun WriteNewNoteScreen(
|
||||
initialWriteNewNoteUIState: WriteNewNoteUIState = WriteNewNoteUIState.Loading,
|
||||
@@ -125,7 +136,7 @@ fun WriteNewNoteScreen(
|
||||
)
|
||||
}
|
||||
|
||||
val mentionedProfiles: List<Profile> = emptyList()
|
||||
|
||||
|
||||
TextField(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@@ -168,20 +179,30 @@ fun WriteNewNoteScreen(
|
||||
size = 35.dp
|
||||
)
|
||||
},
|
||||
inputTransformation = InputTransformation {
|
||||
if (changes.changeCount > 0) {
|
||||
val range = changes.getRange(changes.changeCount-1)
|
||||
val newInput = asCharSequence().substring(range.start, range.end)
|
||||
|
||||
if (newInput == "@") {
|
||||
writeNewNoteViewModel.atSymbolRange.value = range
|
||||
}
|
||||
}
|
||||
},
|
||||
outputTransformation = OutputTransformation {
|
||||
val content = originalText.toString()
|
||||
val nostrProfiles = ContentParser.parse(content.trimEnd('\n', '\r'), emptyMap(), emptyMap(), trimBlankLines = true)
|
||||
.filterIsInstance<ContentSegment.NostrProfileSegment>()
|
||||
// TODO: We might want to order these profiles by the startIndex so that we can replace the better
|
||||
|
||||
if (mentionedProfiles.isNotEmpty()) {
|
||||
if (writeNewNoteViewModel.mentionedProfiles.isNotEmpty()) {
|
||||
for (nostrProfile in nostrProfiles) {
|
||||
mentionedProfiles.find { mentionedProfile -> mentionedProfile.publicKey == nostrProfile.pubkey }?.let { mentionedProfile ->
|
||||
writeNewNoteViewModel.mentionedProfiles.find { mentionedProfile -> mentionedProfile.publicKey == nostrProfile.pubkey }?.let { mentionedProfile ->
|
||||
val profileMention = "@${mentionedProfile.humanReadableNameOrPubkey()}"
|
||||
|
||||
replace(
|
||||
start = nostrProfile.startIndex,
|
||||
end = nostrProfile.endIndex,
|
||||
end = nostrProfile.endIndex + 1,
|
||||
text = profileMention
|
||||
)
|
||||
|
||||
@@ -190,7 +211,7 @@ fun WriteNewNoteScreen(
|
||||
color = ProfileColor.fromPublicKey(mentionedProfile.publicKey)
|
||||
),
|
||||
nostrProfile.startIndex,
|
||||
profileMention.length
|
||||
nostrProfile.startIndex + profileMention.length
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -198,6 +219,33 @@ fun WriteNewNoteScreen(
|
||||
}
|
||||
)
|
||||
|
||||
// TODO: Dropdown for the mentioned users
|
||||
|
||||
DropdownMenu(
|
||||
expanded = writeNewNoteViewModel.atSymbolRange.value != null,
|
||||
onDismissRequest = {
|
||||
writeNewNoteViewModel.atSymbolRange.value = null
|
||||
}
|
||||
) {
|
||||
writeNewNoteViewModel.mentionableProfiles.forEach { profile ->
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
text = profile.humanReadableNameOrPubkey()
|
||||
)
|
||||
},
|
||||
leadingIcon = {
|
||||
ProfileAvatar(
|
||||
profile = profile
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
writeNewNoteViewModel.addProfileToMentionedProfiles(profile)
|
||||
writeNewNoteViewModel.atSymbolRange.value = null
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
if (writeNewNoteUIState.quotedNostrEvent?.nostrEvent?.kind == TextNoteEvent.KIND) {
|
||||
Card(
|
||||
modifier = Modifier.padding(
|
||||
@@ -298,12 +346,37 @@ fun WriteNewNoteScreen(
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = writeNewNoteUIState.text,
|
||||
RichContent(
|
||||
// Hack to have the thing rendered for a preview...
|
||||
nostrEvent = NostrEvent(
|
||||
id = "",
|
||||
content = writeNewNoteUIState.text,
|
||||
sig = "",
|
||||
tags = emptyArray(),
|
||||
kind = 1,
|
||||
createdAt = Clock.System.now(),
|
||||
pubKey = ""
|
||||
),
|
||||
localQuotedNostrEvent = null,
|
||||
mentionedProfiles = writeNewNoteViewModel.mentionableProfiles.map {
|
||||
LocalMention(
|
||||
mention = Mention(
|
||||
mentionedPublicKey = it.publicKey,
|
||||
mentioningNostrEventId = ""
|
||||
),
|
||||
profile = it
|
||||
)
|
||||
},
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
textAlign = TextAlign.Center
|
||||
// TODO: Add TextAlign.Center
|
||||
)
|
||||
|
||||
// Text(
|
||||
// text = writeNewNoteUIState.text,
|
||||
// style = MaterialTheme.typography.bodySmall,
|
||||
// textAlign = TextAlign.Center
|
||||
// )
|
||||
|
||||
if (writeNewNoteUIState.quotedNostrEvent?.nostrEvent?.kind == TextNoteEvent.KIND) {
|
||||
Card {
|
||||
writeNewNoteUIState.quotedNostrEvent.RenderNotePreview()
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package ac.cord.auxiliary.compose.ui.view.model
|
||||
|
||||
import ac.cord.auxiliary.compose.database.model.Profile
|
||||
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalNostrEvent
|
||||
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalProfileWithFollowing
|
||||
import ac.cord.auxiliary.compose.managers.SeedManager
|
||||
import ac.cord.auxiliary.compose.repository.NostrRepository
|
||||
import ac.cord.auxiliary.compose.ui.composable.widgets.content.ContentParser
|
||||
import ac.cord.auxiliary.compose.ui.composable.widgets.content.ContentSegment
|
||||
import ac.cord.auxiliary.compose.ui.view.state.WriteNewNoteUIState
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -13,11 +16,14 @@ import androidx.lifecycle.viewmodel.initializer
|
||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||
import ac.cord.auxiliary.compose.ui.view.state.form.WriteNewNoteFormState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.IO
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
@@ -58,8 +64,45 @@ class WriteNewNoteViewModel(
|
||||
var writeNewNoteUIState: WriteNewNoteUIState by mutableStateOf(initialWriteNewNoteUIState)
|
||||
private set
|
||||
|
||||
var mentionableProfiles = mutableStateListOf<Profile>()
|
||||
private set
|
||||
|
||||
var mentionedProfiles = mutableStateListOf<Profile>()
|
||||
private set
|
||||
|
||||
var atSymbolRange = mutableStateOf<TextRange?>(null)
|
||||
|
||||
fun initiate() {
|
||||
observeActiveUserProfileWithFollowing()
|
||||
loadSearchableProfiles()
|
||||
}
|
||||
|
||||
fun loadSearchableProfiles() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
mentionableProfiles.addAll(
|
||||
nostrRepository.searchableProfiles()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun addProfileToMentionedProfiles(profile: Profile) {
|
||||
atSymbolRange.value?.let { range ->
|
||||
mentionedProfiles.add(profile)
|
||||
writeNewNoteFormState.textField.textFieldState.edit {
|
||||
val mentionProfile = "nostr:${
|
||||
NProfile.create(
|
||||
profile.publicKey,
|
||||
null // TODO: Get the relay this profile was seen from...
|
||||
)
|
||||
}"
|
||||
|
||||
replace(
|
||||
range.start,
|
||||
range.end,
|
||||
mentionProfile
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun observeActiveUserProfileWithFollowing() {
|
||||
@@ -80,7 +123,6 @@ class WriteNewNoteViewModel(
|
||||
val activePublicKey = SeedManager.activePublicKey().toHexKey()
|
||||
|
||||
// TODO: Get note being replied too...
|
||||
// TODO: Get note being quoted...
|
||||
// TODO: Get users being mentioned...
|
||||
nostrRepository.observeProfileWithFollowing(
|
||||
activePublicKey
|
||||
@@ -136,8 +178,13 @@ class WriteNewNoteViewModel(
|
||||
isActionPending.value = true
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val mentionedPublicKeys = ContentParser.parse(
|
||||
writeNewNoteFormState.textField.textFieldState.text.toString(),
|
||||
).filterIsInstance<ContentSegment.NostrProfileSegment>().map { it.pubkey }
|
||||
|
||||
nostrRepository.createNewTextNote(
|
||||
publicKey = SeedManager.activePublicKey().toHexKey(),
|
||||
mentionedPublicKeys = mentionedPublicKeys,
|
||||
textInput = writeNewNoteFormState.textField.textFieldState.text.toString(),
|
||||
inReplyToNostrEvent = inReplyToNostrEvent,
|
||||
quotedNostrEvent = quotedNostrEvent,
|
||||
|
||||
Reference in New Issue
Block a user