add draft in the home feed
This commit is contained in:
@@ -56,6 +56,7 @@ import com.vitorpamplona.quartz.events.ClassifiedsEvent
|
||||
import com.vitorpamplona.quartz.events.Contact
|
||||
import com.vitorpamplona.quartz.events.ContactListEvent
|
||||
import com.vitorpamplona.quartz.events.DeletionEvent
|
||||
import com.vitorpamplona.quartz.events.DraftEvent
|
||||
import com.vitorpamplona.quartz.events.EmojiPackEvent
|
||||
import com.vitorpamplona.quartz.events.EmojiPackSelectionEvent
|
||||
import com.vitorpamplona.quartz.events.EmojiUrl
|
||||
@@ -1422,6 +1423,7 @@ class Account(
|
||||
relayList: List<Relay>? = null,
|
||||
geohash: String? = null,
|
||||
nip94attachments: List<FileHeaderEvent>? = null,
|
||||
draftTag: String?,
|
||||
) {
|
||||
if (!isWriteable()) return
|
||||
|
||||
@@ -1445,20 +1447,28 @@ class Account(
|
||||
nip94attachments = nip94attachments,
|
||||
forkedFrom = forkedFrom,
|
||||
signer = signer,
|
||||
isDraft = draftTag != null,
|
||||
) {
|
||||
Client.send(it, relayList = relayList)
|
||||
LocalCache.justConsume(it, null)
|
||||
|
||||
// broadcast replied notes
|
||||
replyingTo?.let {
|
||||
LocalCache.getNoteIfExists(replyingTo)?.event?.let {
|
||||
Client.send(it, relayList = relayList)
|
||||
if (draftTag != null) {
|
||||
DraftEvent.create(draftTag, it, signer) { draftEvent ->
|
||||
Client.send(draftEvent, relayList = relayList)
|
||||
LocalCache.justConsume(draftEvent, null)
|
||||
}
|
||||
}
|
||||
replyTo?.forEach { it.event?.let { Client.send(it, relayList = relayList) } }
|
||||
addresses?.forEach {
|
||||
LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let {
|
||||
Client.send(it, relayList = relayList)
|
||||
} else {
|
||||
Client.send(it, relayList = relayList)
|
||||
LocalCache.justConsume(it, null)
|
||||
|
||||
// broadcast replied notes
|
||||
replyingTo?.let {
|
||||
LocalCache.getNoteIfExists(replyingTo)?.event?.let {
|
||||
Client.send(it, relayList = relayList)
|
||||
}
|
||||
}
|
||||
replyTo?.forEach { it.event?.let { Client.send(it, relayList = relayList) } }
|
||||
addresses?.forEach {
|
||||
LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let {
|
||||
Client.send(it, relayList = relayList)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2210,6 +2220,7 @@ class Account(
|
||||
|
||||
fun cachedDecryptContent(note: Note): String? {
|
||||
val event = note.event
|
||||
|
||||
return if (event is PrivateDmEvent && isWriteable()) {
|
||||
event.cachedContentFor(signer)
|
||||
} else if (event is LnZapRequestEvent && event.isPrivateZap() && isWriteable()) {
|
||||
|
||||
@@ -61,6 +61,7 @@ import com.vitorpamplona.quartz.events.CommunityListEvent
|
||||
import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
|
||||
import com.vitorpamplona.quartz.events.ContactListEvent
|
||||
import com.vitorpamplona.quartz.events.DeletionEvent
|
||||
import com.vitorpamplona.quartz.events.DraftEvent
|
||||
import com.vitorpamplona.quartz.events.EmojiPackEvent
|
||||
import com.vitorpamplona.quartz.events.EmojiPackSelectionEvent
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
@@ -2042,6 +2043,13 @@ object LocalCache {
|
||||
}
|
||||
}
|
||||
|
||||
private fun consume(
|
||||
event: DraftEvent,
|
||||
relay: Relay?,
|
||||
) {
|
||||
consumeBaseReplaceable(event, relay)
|
||||
}
|
||||
|
||||
fun justConsume(
|
||||
event: Event,
|
||||
relay: Relay?,
|
||||
@@ -2079,6 +2087,7 @@ object LocalCache {
|
||||
}
|
||||
is ContactListEvent -> consume(event)
|
||||
is DeletionEvent -> consume(event)
|
||||
is DraftEvent -> consume(event, relay)
|
||||
is EmojiPackEvent -> consume(event, relay)
|
||||
is EmojiPackSelectionEvent -> consume(event, relay)
|
||||
is SealedGossipEvent -> consume(event, relay)
|
||||
|
||||
@@ -106,6 +106,7 @@ open class Note(val idHex: String) {
|
||||
var event: EventInterface? = null
|
||||
var author: User? = null
|
||||
var replyTo: List<Note>? = null
|
||||
var draft: String? = null
|
||||
|
||||
// These fields are updated every time an event related to this note is received.
|
||||
var replies = listOf<Note>()
|
||||
@@ -183,6 +184,17 @@ open class Note(val idHex: String) {
|
||||
|
||||
open fun createdAt() = event?.createdAt()
|
||||
|
||||
fun updateDraft(id: String) {
|
||||
draft = id
|
||||
}
|
||||
|
||||
fun isDraft(): Boolean {
|
||||
draft?.let {
|
||||
return it.isNotBlank()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun loadEvent(
|
||||
event: Event,
|
||||
author: User,
|
||||
|
||||
@@ -40,6 +40,7 @@ import com.vitorpamplona.quartz.events.CalendarRSVPEvent
|
||||
import com.vitorpamplona.quartz.events.CalendarTimeSlotEvent
|
||||
import com.vitorpamplona.quartz.events.ChannelMessageEvent
|
||||
import com.vitorpamplona.quartz.events.ContactListEvent
|
||||
import com.vitorpamplona.quartz.events.DraftEvent
|
||||
import com.vitorpamplona.quartz.events.EmojiPackSelectionEvent
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
import com.vitorpamplona.quartz.events.EventInterface
|
||||
@@ -229,6 +230,16 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
|
||||
)
|
||||
}
|
||||
|
||||
fun createDraftsFilter() =
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
JsonFilter(
|
||||
kinds = listOf(DraftEvent.KIND),
|
||||
authors = listOf(account.userProfile().pubkeyHex),
|
||||
),
|
||||
)
|
||||
|
||||
fun createGiftWrapsToMeFilter() =
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
@@ -262,22 +273,38 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
|
||||
checkNotInMainThread()
|
||||
|
||||
if (LocalCache.justVerify(event)) {
|
||||
if (event is GiftWrapEvent) {
|
||||
// Avoid decrypting over and over again if the event already exist.
|
||||
val note = LocalCache.getNoteIfExists(event.id)
|
||||
if (note != null && relay.brief in note.relays) return
|
||||
when (event) {
|
||||
is DraftEvent -> {
|
||||
// Avoid decrypting over and over again if the event already exist.
|
||||
val note = LocalCache.getNoteIfExists(event.id)
|
||||
if (note != null && relay.brief in note.relays) return
|
||||
|
||||
event.cachedGift(account.signer) { this.consume(it, relay) }
|
||||
}
|
||||
event.plainContent(account.signer) {
|
||||
LocalCache.justConsume(it, relay)
|
||||
val draftNote = LocalCache.getNoteIfExists(it.id)
|
||||
draftNote?.updateDraft(event.id)
|
||||
}
|
||||
}
|
||||
|
||||
if (event is SealedGossipEvent) {
|
||||
// Avoid decrypting over and over again if the event already exist.
|
||||
val note = LocalCache.getNoteIfExists(event.id)
|
||||
if (note != null && relay.brief in note.relays) return
|
||||
is GiftWrapEvent -> {
|
||||
// Avoid decrypting over and over again if the event already exist.
|
||||
val note = LocalCache.getNoteIfExists(event.id)
|
||||
if (note != null && relay.brief in note.relays) return
|
||||
|
||||
event.cachedGossip(account.signer) { LocalCache.justConsume(it, relay) }
|
||||
} else {
|
||||
LocalCache.justConsume(event, relay)
|
||||
event.cachedGift(account.signer) { this.consume(it, relay) }
|
||||
}
|
||||
|
||||
is SealedGossipEvent -> {
|
||||
// Avoid decrypting over and over again if the event already exist.
|
||||
val note = LocalCache.getNoteIfExists(event.id)
|
||||
if (note != null && relay.brief in note.relays) return
|
||||
|
||||
event.cachedGossip(account.signer) { LocalCache.justConsume(it, relay) }
|
||||
}
|
||||
|
||||
else -> {
|
||||
LocalCache.justConsume(event, relay)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -328,6 +355,7 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
|
||||
createAccountSettingsFilter(),
|
||||
createAccountLastPostsListFilter(),
|
||||
createOtherAccountsBaseFilter(),
|
||||
createDraftsFilter(),
|
||||
)
|
||||
.ifEmpty { null }
|
||||
} else {
|
||||
|
||||
@@ -263,7 +263,7 @@ class Relay(
|
||||
val subscriptionId = msgArray.get(1).asText()
|
||||
val event = Event.fromJson(msgArray.get(2))
|
||||
|
||||
// Log.w("Relay", "Relay onEVENT ${event.kind} $url, $subscriptionId ${msgArray.get(2)}")
|
||||
Log.w("Relay", "Relay onEVENT ${event.kind} $url, $subscriptionId ${msgArray.get(2)}")
|
||||
listeners.forEach {
|
||||
it.onEvent(
|
||||
this@Relay,
|
||||
|
||||
@@ -74,6 +74,7 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.UUID
|
||||
|
||||
enum class UserSuggestionAnchor {
|
||||
MAIN_MESSAGE,
|
||||
@@ -83,6 +84,7 @@ enum class UserSuggestionAnchor {
|
||||
|
||||
@Stable
|
||||
open class NewPostViewModel() : ViewModel() {
|
||||
var draftTag: String = UUID.randomUUID().toString()
|
||||
var accountViewModel: AccountViewModel? = null
|
||||
var account: Account? = null
|
||||
var requiresNIP24: Boolean = false
|
||||
@@ -306,11 +308,17 @@ open class NewPostViewModel() : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun sendPost(relayList: List<Relay>? = null) {
|
||||
viewModelScope.launch(Dispatchers.IO) { innerSendPost(relayList) }
|
||||
fun sendPost(
|
||||
relayList: List<Relay>? = null,
|
||||
localDraft: String? = null,
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) { innerSendPost(relayList, localDraft) }
|
||||
}
|
||||
|
||||
suspend fun innerSendPost(relayList: List<Relay>? = null) {
|
||||
private suspend fun innerSendPost(
|
||||
relayList: List<Relay>? = null,
|
||||
localDraft: String?,
|
||||
) {
|
||||
if (accountViewModel == null) {
|
||||
cancel()
|
||||
return
|
||||
@@ -555,11 +563,13 @@ open class NewPostViewModel() : ViewModel() {
|
||||
relayList = relayList,
|
||||
geohash = geoHash,
|
||||
nip94attachments = usedAttachments,
|
||||
draftTag = draftTag,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
cancel()
|
||||
if (localDraft == null) {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
fun upload(
|
||||
@@ -693,8 +703,8 @@ open class NewPostViewModel() : ViewModel() {
|
||||
pTags = pTags?.filter { it != userToRemove }
|
||||
}
|
||||
|
||||
open fun saveDraft(message: String) {
|
||||
account?.let { LocalPreferences.saveDraft(message, originalNote?.idHex, it) }
|
||||
open fun saveDraft() {
|
||||
sendPost(localDraft = draftTag)
|
||||
}
|
||||
|
||||
open fun loadDraft(): String? {
|
||||
@@ -708,9 +718,6 @@ open class NewPostViewModel() : ViewModel() {
|
||||
}
|
||||
|
||||
open fun updateMessage(it: TextFieldValue) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
saveDraft(it.text)
|
||||
}
|
||||
message = it
|
||||
urlPreview = findUrlInMessage()
|
||||
|
||||
@@ -732,6 +739,10 @@ open class NewPostViewModel() : ViewModel() {
|
||||
userSuggestions = emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
saveDraft()
|
||||
}
|
||||
}
|
||||
|
||||
open fun updateToUsers(it: TextFieldValue) {
|
||||
@@ -755,10 +766,16 @@ open class NewPostViewModel() : ViewModel() {
|
||||
userSuggestions = emptyList()
|
||||
}
|
||||
}
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
saveDraft()
|
||||
}
|
||||
}
|
||||
|
||||
open fun updateSubject(it: TextFieldValue) {
|
||||
subject = it
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
saveDraft()
|
||||
}
|
||||
}
|
||||
|
||||
open fun updateZapForwardTo(it: TextFieldValue) {
|
||||
@@ -785,6 +802,9 @@ open class NewPostViewModel() : ViewModel() {
|
||||
userSuggestions = emptyList()
|
||||
}
|
||||
}
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
saveDraft()
|
||||
}
|
||||
}
|
||||
|
||||
open fun autocompleteWithUser(item: User) {
|
||||
@@ -800,9 +820,6 @@ open class NewPostViewModel() : ViewModel() {
|
||||
message.text.replaceRange(lastWordStart, it.end, wordToInsert),
|
||||
TextRange(lastWordStart + wordToInsert.length, lastWordStart + wordToInsert.length),
|
||||
)
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
saveDraft(message.text)
|
||||
}
|
||||
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.FORWARD_ZAPS) {
|
||||
forwardZapTo.addItem(item)
|
||||
forwardZapToEditting = TextFieldValue("")
|
||||
@@ -833,6 +850,10 @@ open class NewPostViewModel() : ViewModel() {
|
||||
userSuggestionsMainMessage = null
|
||||
userSuggestions = emptyList()
|
||||
}
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
saveDraft()
|
||||
}
|
||||
}
|
||||
|
||||
private fun newStateMapPollOptions(): SnapshotStateMap<Int, String> {
|
||||
@@ -902,8 +923,8 @@ open class NewPostViewModel() : ViewModel() {
|
||||
nip94attachments = nip94attachments + event
|
||||
|
||||
message = message.insertUrlAtCursor(imageUrl)
|
||||
saveDraft(message.text)
|
||||
urlPreview = findUrlInMessage()
|
||||
saveDraft()
|
||||
}
|
||||
},
|
||||
onError = {
|
||||
@@ -945,10 +966,10 @@ open class NewPostViewModel() : ViewModel() {
|
||||
|
||||
note?.let {
|
||||
message = message.insertUrlAtCursor("nostr:" + it.toNEvent())
|
||||
saveDraft(message.text)
|
||||
}
|
||||
|
||||
urlPreview = findUrlInMessage()
|
||||
saveDraft()
|
||||
}
|
||||
},
|
||||
onError = {
|
||||
@@ -969,6 +990,7 @@ open class NewPostViewModel() : ViewModel() {
|
||||
locUtil?.let {
|
||||
location =
|
||||
it.locationStateFlow.mapLatest { it.toGeoHash(GeohashPrecision.KM_5_X_5.digits).toString() }
|
||||
saveDraft()
|
||||
}
|
||||
viewModelScope.launch(Dispatchers.IO) { locUtil?.start() }
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.quartz.events.ChannelMessageEvent
|
||||
import com.vitorpamplona.quartz.events.DraftEvent
|
||||
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
|
||||
import com.vitorpamplona.quartz.events.MuteListEvent
|
||||
import com.vitorpamplona.quartz.events.PeopleListEvent
|
||||
@@ -69,7 +70,8 @@ class HomeConversationsFeedFilter(val account: Account) : AdditiveFeedFilter<Not
|
||||
it.event is TextNoteEvent ||
|
||||
it.event is PollNoteEvent ||
|
||||
it.event is ChannelMessageEvent ||
|
||||
it.event is LiveActivitiesChatMessageEvent
|
||||
it.event is LiveActivitiesChatMessageEvent ||
|
||||
it.event is DraftEvent
|
||||
) &&
|
||||
(
|
||||
isGlobal ||
|
||||
|
||||
@@ -33,6 +33,7 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
@@ -51,6 +52,7 @@ import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.compositeOver
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.distinctUntilChanged
|
||||
@@ -113,6 +115,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.Font14SP
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfDoubleVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size25dp
|
||||
@@ -128,6 +131,7 @@ import com.vitorpamplona.amethyst.ui.theme.WidthAuthorPictureModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.boostedNoteModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.channelNotePictureModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.grayText
|
||||
import com.vitorpamplona.amethyst.ui.theme.lessImportantLink
|
||||
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
|
||||
import com.vitorpamplona.amethyst.ui.theme.normalNoteModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.normalWithTopMarginNoteModifier
|
||||
@@ -1109,10 +1113,26 @@ fun SecondUserInfoRow(
|
||||
DisplayPoW(pow)
|
||||
}
|
||||
|
||||
if (note.isDraft()) {
|
||||
Spacer(StdHorzSpacer)
|
||||
DisplayDraft()
|
||||
}
|
||||
|
||||
DisplayOts(note, accountViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DisplayDraft() {
|
||||
Text(
|
||||
"Draft",
|
||||
color = MaterialTheme.colorScheme.lessImportantLink,
|
||||
fontSize = Font14SP,
|
||||
fontWeight = FontWeight.Bold,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FirstUserInfoRow(
|
||||
baseNote: Note,
|
||||
|
||||
@@ -185,6 +185,7 @@ class AddBountyAmountViewModel : ViewModel() {
|
||||
root = null,
|
||||
directMentions = setOf(),
|
||||
forkedFrom = null,
|
||||
draftTag = null,
|
||||
)
|
||||
|
||||
nextAmount = TextFieldValue("")
|
||||
|
||||
@@ -92,6 +92,7 @@ import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
|
||||
import com.vitorpamplona.amethyst.ui.components.mockAccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routeToMessage
|
||||
import com.vitorpamplona.amethyst.ui.note.BlankNote
|
||||
import com.vitorpamplona.amethyst.ui.note.DisplayDraft
|
||||
import com.vitorpamplona.amethyst.ui.note.HiddenNote
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture
|
||||
@@ -468,6 +469,10 @@ fun NoteMaster(
|
||||
DisplayPoW(pow)
|
||||
}
|
||||
|
||||
if (note.isDraft()) {
|
||||
DisplayDraft()
|
||||
}
|
||||
|
||||
DisplayOts(note, accountViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user