Merge pull request #749 from greenart7c3/main

save a draft while you are typing the post
This commit is contained in:
Vitor Pamplona
2024-03-26 15:24:59 -04:00
committed by GitHub
41 changed files with 1166 additions and 243 deletions
@@ -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
@@ -845,7 +846,14 @@ class Account(
}
suspend fun delete(note: Note) {
return delete(listOf(note))
if (note.isDraft()) {
note.event?.let {
val drafts = LocalCache.getDrafts(it.id())
return delete(drafts)
}
} else {
return delete(listOf(note))
}
}
suspend fun delete(notes: List<Note>) {
@@ -897,10 +905,17 @@ class Account(
fun broadcast(note: Note) {
note.event?.let {
if (it is WrappedEvent && it.host != null) {
it.host?.let { hostEvent -> Client.send(hostEvent) }
if (note.isDraft()) {
val drafts = LocalCache.getDrafts(it.id())
drafts.forEach { draftNote ->
broadcast(draftNote)
}
} else {
Client.send(it)
if (it is WrappedEvent && it.host != null) {
it.host?.let { hostEvent -> Client.send(hostEvent) }
} else {
Client.send(it)
}
}
}
}
@@ -929,6 +944,7 @@ class Account(
fun timestamp(note: Note) {
if (!isWriteable()) return
if (note.isDraft()) return
val id = note.event?.id() ?: note.idHex
@@ -1318,6 +1334,7 @@ class Account(
relayList: List<Relay>? = null,
geohash: String? = null,
nip94attachments: List<Event>? = null,
draftTag: String?,
) {
if (!isWriteable()) return
@@ -1345,14 +1362,24 @@ class Account(
geohash = geohash,
nip94attachments = nip94attachments,
signer = signer,
isDraft = draftTag != null,
) {
Client.send(it, relayList = relayList)
LocalCache.justConsume(it, null)
if (draftTag != null) {
DraftEvent.create(draftTag, it, signer) { draftEvent ->
Client.send(draftEvent, relayList = relayList)
LocalCache.justConsume(draftEvent, null)
LocalCache.justConsume(it, null)
LocalCache.addDraft(draftTag, draftEvent.id(), it.id())
}
} else {
Client.send(it, relayList = relayList)
LocalCache.justConsume(it, null)
replyTo?.forEach { it.event?.let { Client.send(it, relayList = relayList) } }
addresses?.forEach {
LocalCache.getAddressableNoteIfExists(it.toTag())?.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)
}
}
}
}
@@ -1373,6 +1400,7 @@ class Account(
relayList: List<Relay>? = null,
geohash: String? = null,
nip94attachments: List<FileHeaderEvent>? = null,
draftTag: String?,
) {
if (!isWriteable()) return
@@ -1396,20 +1424,30 @@ 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)
LocalCache.justConsume(it, null)
LocalCache.addDraft(draftTag, draftEvent.id(), it.id())
}
}
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)
}
}
}
}
@@ -1430,6 +1468,7 @@ class Account(
relayList: List<Relay>? = null,
geohash: String? = null,
nip94attachments: List<FileHeaderEvent>? = null,
draftTag: String?,
) {
if (!isWriteable()) return
@@ -1453,20 +1492,30 @@ 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)
LocalCache.justConsume(it, null)
LocalCache.addDraft(draftTag, draftEvent.id(), it.id())
}
}
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)
}
}
}
}
@@ -1510,6 +1559,7 @@ class Account(
relayList: List<Relay>? = null,
geohash: String? = null,
nip94attachments: List<FileHeaderEvent>? = null,
draftTag: String?,
) {
if (!isWriteable()) return
@@ -1533,15 +1583,25 @@ class Account(
zapRaiserAmount = zapRaiserAmount,
geohash = geohash,
nip94attachments = nip94attachments,
isDraft = draftTag != null,
) {
Client.send(it, relayList = relayList)
LocalCache.justConsume(it, null)
if (draftTag != null) {
DraftEvent.create(draftTag, it, signer) { draftEvent ->
Client.send(draftEvent, relayList = relayList)
LocalCache.justConsume(draftEvent, null)
LocalCache.justConsume(it, null)
LocalCache.addDraft(draftTag, draftEvent.id(), it.id())
}
} else {
Client.send(it, relayList = relayList)
LocalCache.justConsume(it, null)
// Rebroadcast replies and tags to the current relay set
replyTo?.forEach { it.event?.let { Client.send(it, relayList = relayList) } }
addresses?.forEach {
LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let {
Client.send(it, relayList = relayList)
// Rebroadcast replies and tags to the current relay set
replyTo?.forEach { it.event?.let { Client.send(it, relayList = relayList) } }
addresses?.forEach {
LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let {
Client.send(it, relayList = relayList)
}
}
}
}
@@ -1557,6 +1617,7 @@ class Account(
zapRaiserAmount: Long? = null,
geohash: String? = null,
nip94attachments: List<FileHeaderEvent>? = null,
draftTag: String?,
) {
if (!isWriteable()) return
@@ -1574,9 +1635,19 @@ class Account(
geohash = geohash,
nip94attachments = nip94attachments,
signer = signer,
isDraft = draftTag != null,
) {
Client.send(it)
LocalCache.justConsume(it, null)
if (draftTag != null) {
DraftEvent.create(draftTag, it, signer) { draftEvent ->
Client.send(draftEvent)
LocalCache.justConsume(draftEvent, null)
LocalCache.justConsume(it, null)
LocalCache.addDraft(draftTag, draftEvent.id(), it.id())
}
} else {
Client.send(it)
LocalCache.justConsume(it, null)
}
}
}
@@ -1590,6 +1661,7 @@ class Account(
zapRaiserAmount: Long? = null,
geohash: String? = null,
nip94attachments: List<FileHeaderEvent>? = null,
draftTag: String?,
) {
if (!isWriteable()) return
@@ -1608,9 +1680,19 @@ class Account(
geohash = geohash,
nip94attachments = nip94attachments,
signer = signer,
isDraft = draftTag != null,
) {
Client.send(it)
LocalCache.justConsume(it, null)
if (draftTag != null) {
DraftEvent.create(draftTag, it, signer) { draftEvent ->
Client.send(draftEvent)
LocalCache.justConsume(draftEvent, null)
LocalCache.justConsume(it, null)
LocalCache.addDraft(draftTag, draftEvent.id(), it.id())
}
} else {
Client.send(it)
LocalCache.justConsume(it, null)
}
}
}
@@ -1624,6 +1706,7 @@ class Account(
zapRaiserAmount: Long? = null,
geohash: String? = null,
nip94attachments: List<FileHeaderEvent>? = null,
draftTag: String?,
) {
sendPrivateMessage(
message,
@@ -1635,6 +1718,7 @@ class Account(
zapRaiserAmount,
geohash,
nip94attachments,
draftTag,
)
}
@@ -1648,6 +1732,7 @@ class Account(
zapRaiserAmount: Long? = null,
geohash: String? = null,
nip94attachments: List<FileHeaderEvent>? = null,
draftTag: String?,
) {
if (!isWriteable()) return
@@ -1667,9 +1752,19 @@ class Account(
nip94attachments = nip94attachments,
signer = signer,
advertiseNip18 = false,
isDraft = draftTag != null,
) {
Client.send(it)
LocalCache.consume(it, null)
if (draftTag != null) {
DraftEvent.create(draftTag, it, signer) { draftEvent ->
Client.send(draftEvent)
LocalCache.justConsume(draftEvent, null)
LocalCache.justConsume(it, null)
LocalCache.addDraft(draftTag, draftEvent.id(), it.id())
}
} else {
Client.send(it)
LocalCache.consume(it, null)
}
}
}
@@ -1684,6 +1779,7 @@ class Account(
zapRaiserAmount: Long? = null,
geohash: String? = null,
nip94attachments: List<FileHeaderEvent>? = null,
draftTag: String? = null,
) {
if (!isWriteable()) return
@@ -1701,9 +1797,19 @@ class Account(
zapRaiserAmount = zapRaiserAmount,
geohash = geohash,
nip94attachments = nip94attachments,
draftTag = draftTag,
signer = signer,
) {
broadcastPrivately(it)
if (draftTag != null) {
DraftEvent.create(draftTag, it.msg, signer) { draftEvent ->
Client.send(draftEvent)
LocalCache.justConsume(draftEvent, null)
LocalCache.justConsume(it.msg, null)
LocalCache.addDraft(draftTag, draftEvent.id(), it.msg.id())
}
} else {
broadcastPrivately(it)
}
}
}
@@ -1851,6 +1957,7 @@ class Account(
isPrivate: Boolean,
) {
if (!isWriteable()) return
if (note.isDraft()) return
if (note is AddressableNote) {
BookmarkListEvent.addReplaceable(
@@ -2218,6 +2325,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()) {
@@ -0,0 +1,23 @@
/**
* 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.model
data class Drafts(val mainId: String, val eventId: String)
@@ -62,6 +62,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
@@ -128,7 +129,7 @@ object LocalCache {
val users = LargeCache<HexKey, User>()
val notes = LargeCache<HexKey, Note>()
val addressables = LargeCache<String, AddressableNote>()
val drafts = ConcurrentHashMap<String, MutableList<Drafts>>()
val channels = LargeCache<HexKey, Channel>()
val awaitingPaymentRequests = ConcurrentHashMap<HexKey, Pair<Note?, (LnZapPaymentResponseEvent) -> Unit>>(10)
@@ -141,6 +142,34 @@ object LocalCache {
return null
}
fun draftNotes(draftTag: String): List<Note> {
return drafts[draftTag]?.mapNotNull {
getNoteIfExists(it.mainId)
} ?: listOf()
}
fun getDrafts(eventId: String): List<Note> {
return drafts.filter {
it.value.any { it.eventId == eventId }
}.values.map {
it.mapNotNull {
checkGetOrCreateNote(it.mainId)
}
}.flatten()
}
fun addDraft(
key: String,
mainId: String,
draftId: String,
) {
val data = drafts[key] ?: mutableListOf()
if (data.none { it.mainId == mainId }) {
data.add(Drafts(mainId, draftId))
drafts[key] = data
}
}
fun getOrCreateUser(key: HexKey): User {
// checkNotInMainThread()
require(isValidHex(key = key)) { "$key is not a valid hex" }
@@ -2013,6 +2042,13 @@ object LocalCache {
}
}
private fun consume(
event: DraftEvent,
relay: Relay?,
) {
consumeBaseReplaceable(event, relay)
}
fun justConsume(
event: Event,
relay: Relay?,
@@ -2050,6 +2086,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)
@@ -184,6 +184,13 @@ open class Note(val idHex: String) {
open fun createdAt() = event?.createdAt()
fun isDraft(): Boolean {
event?.let {
return it.sig().isBlank()
}
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,46 @@ 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.
event.cachedGift(account.signer) { this.consume(it, relay) }
}
val note = LocalCache.getNoteIfExists(event.id)
if (note != null && relay.brief in note.relays) return
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
LocalCache.justConsume(event, relay)
event.plainContent(account.signer) {
val tag =
event.tags().filter { it.size > 1 && it[0] == "d" }.map {
it[1]
}.firstOrNull()
event.cachedGossip(account.signer) { LocalCache.justConsume(it, relay) }
} else {
LocalCache.justConsume(event, relay)
LocalCache.justConsume(it, relay)
tag?.let { lTag ->
LocalCache.addDraft(lTag, event.id(), it.id())
}
}
}
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.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 +363,7 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
createAccountSettingsFilter(),
createAccountLastPostsListFilter(),
createOtherAccountsBaseFilter(),
createDraftsFilter(),
)
.ifEmpty { null }
} else {
@@ -34,8 +34,11 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.tooling.preview.Preview
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Composable
fun NewPollOption(
@@ -45,7 +48,12 @@ fun NewPollOption(
Row {
val deleteIcon: @Composable (() -> Unit) = {
IconButton(
onClick = { pollViewModel.pollOptions.remove(optionIndex) },
onClick = {
pollViewModel.pollOptions.remove(optionIndex)
pollViewModel.viewModelScope.launch(Dispatchers.IO) {
pollViewModel.saveDraft()
}
},
) {
Icon(
imageVector = Icons.Default.Delete,
@@ -57,7 +65,12 @@ fun NewPollOption(
OutlinedTextField(
modifier = Modifier.weight(1F),
value = pollViewModel.pollOptions[optionIndex] ?: "",
onValueChange = { pollViewModel.pollOptions[optionIndex] = it },
onValueChange = {
pollViewModel.pollOptions[optionIndex] = it
pollViewModel.viewModelScope.launch(Dispatchers.IO) {
pollViewModel.saveDraft()
}
},
label = {
Text(
text = stringResource(R.string.poll_option_index).format(optionIndex + 1),
@@ -119,6 +119,7 @@ import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import coil.compose.AsyncImage
import com.google.accompanist.permissions.ExperimentalPermissionsApi
@@ -171,13 +172,18 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.lang.Math.round
@OptIn(ExperimentalMaterial3Api::class)
@OptIn(ExperimentalMaterial3Api::class, FlowPreview::class)
@Composable
fun NewPostView(
onClose: () -> Unit,
@@ -185,6 +191,7 @@ fun NewPostView(
quote: Note? = null,
fork: Note? = null,
version: Note? = null,
draft: Note? = null,
enableMessageInterface: Boolean = false,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
@@ -200,9 +207,17 @@ fun NewPostView(
var relayList = remember { accountViewModel.account.activeWriteRelays().toImmutableList() }
LaunchedEffect(Unit) {
postViewModel.load(accountViewModel, baseReplyTo, quote, fork, version)
launch(Dispatchers.IO) {
postViewModel.draftTextChanges
.receiveAsFlow()
.debounce(1000)
.collectLatest {
postViewModel.sendPost(relayList = relayList, localDraft = postViewModel.draftTag)
}
}
launch(Dispatchers.IO) {
postViewModel.load(accountViewModel, baseReplyTo, quote, fork, version, draft)
postViewModel.imageUploadingError.collect { error ->
withContext(Dispatchers.Main) { Toast.makeText(context, error, Toast.LENGTH_SHORT).show() }
}
@@ -582,6 +597,9 @@ private fun BottomRowActions(postViewModel: NewPostViewModel) {
MarkAsSensitive(postViewModel) {
postViewModel.wantsToMarkAsSensitive = !postViewModel.wantsToMarkAsSensitive
postViewModel.viewModelScope.launch(Dispatchers.IO) {
postViewModel.saveDraft()
}
}
AddGeoHash(postViewModel) {
@@ -827,7 +845,12 @@ fun SellProduct(postViewModel: NewPostViewModel) {
MyTextField(
value = postViewModel.title,
onValueChange = { postViewModel.title = it },
onValueChange = {
postViewModel.title = it
postViewModel.viewModelScope.launch(Dispatchers.IO) {
postViewModel.saveDraft()
}
},
modifier = Modifier.fillMaxWidth(),
placeholder = {
Text(
@@ -870,6 +893,9 @@ fun SellProduct(postViewModel: NewPostViewModel) {
postViewModel.price = it
}
}
postViewModel.viewModelScope.launch(Dispatchers.IO) {
postViewModel.saveDraft()
}
},
placeholder = {
Text(
@@ -934,7 +960,12 @@ fun SellProduct(postViewModel: NewPostViewModel) {
TextSpinner(
placeholder = conditionTypes.filter { it.first == postViewModel.condition }.first().second,
options = conditionOptions,
onSelect = { postViewModel.condition = conditionTypes[it].first },
onSelect = {
postViewModel.condition = conditionTypes[it].first
postViewModel.viewModelScope.launch(Dispatchers.IO) {
postViewModel.saveDraft()
}
},
modifier =
Modifier
.weight(1f)
@@ -998,7 +1029,12 @@ fun SellProduct(postViewModel: NewPostViewModel) {
categoryTypes.filter { it.second == postViewModel.category.text }.firstOrNull()?.second
?: "",
options = categoryOptions,
onSelect = { postViewModel.category = TextFieldValue(categoryTypes[it].second) },
onSelect = {
postViewModel.category = TextFieldValue(categoryTypes[it].second)
postViewModel.viewModelScope.launch(Dispatchers.IO) {
postViewModel.saveDraft()
}
},
modifier =
Modifier
.weight(1f)
@@ -1033,7 +1069,12 @@ fun SellProduct(postViewModel: NewPostViewModel) {
MyTextField(
value = postViewModel.locationText,
onValueChange = { postViewModel.locationText = it },
onValueChange = {
postViewModel.locationText = it
postViewModel.viewModelScope.launch(Dispatchers.IO) {
postViewModel.saveDraft()
}
},
modifier = Modifier.fillMaxWidth(),
placeholder = {
Text(
@@ -69,10 +69,12 @@ import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
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,
@@ -82,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
@@ -164,6 +167,8 @@ open class NewPostViewModel() : ViewModel() {
// NIP24 Wrapped DMs / Group messages
var nip24 by mutableStateOf(false)
val draftTextChanges = Channel<String>(Channel.CONFLATED)
fun lnAddress(): String? {
return account?.userProfile()?.info?.lnAddress()
}
@@ -182,127 +187,236 @@ open class NewPostViewModel() : ViewModel() {
quote: Note?,
fork: Note?,
version: Note?,
draft: Note?,
) {
this.accountViewModel = accountViewModel
this.account = accountViewModel.account
originalNote = replyingTo
replyingTo?.let { replyNote ->
if (replyNote.event is BaseTextNoteEvent) {
this.eTags = (replyNote.replyTo ?: emptyList()).plus(replyNote)
} else {
this.eTags = listOf(replyNote)
}
if (draft != null) {
loadFromDraft(draft, accountViewModel)
} else {
originalNote = replyingTo
replyingTo?.let { replyNote ->
if (replyNote.event is BaseTextNoteEvent) {
this.eTags = (replyNote.replyTo ?: emptyList()).plus(replyNote)
} else {
this.eTags = listOf(replyNote)
}
if (replyNote.event !is CommunityDefinitionEvent) {
replyNote.author?.let { replyUser ->
val currentMentions =
(replyNote.event as? TextNoteEvent)?.mentions()?.map { LocalCache.getOrCreateUser(it) }
?: emptyList()
if (replyNote.event !is CommunityDefinitionEvent) {
replyNote.author?.let { replyUser ->
val currentMentions =
(replyNote.event as? TextNoteEvent)?.mentions()?.map { LocalCache.getOrCreateUser(it) }
?: emptyList()
if (currentMentions.contains(replyUser)) {
this.pTags = currentMentions
} else {
this.pTags = currentMentions.plus(replyUser)
if (currentMentions.contains(replyUser)) {
this.pTags = currentMentions
} else {
this.pTags = currentMentions.plus(replyUser)
}
}
}
}
}
?: run {
eTags = null
pTags = null
?: run {
eTags = null
pTags = null
}
canAddInvoice = accountViewModel.userProfile().info?.lnAddress() != null
canAddZapRaiser = accountViewModel.userProfile().info?.lnAddress() != null
canUsePoll = originalNote?.event !is PrivateDmEvent && originalNote?.channelHex() == null
contentToAddUrl = null
wantsForwardZapTo = false
wantsToMarkAsSensitive = false
wantsToAddGeoHash = false
wantsZapraiser = false
zapRaiserAmount = null
forwardZapTo = Split()
forwardZapToEditting = TextFieldValue("")
quote?.let {
message = TextFieldValue(message.text + "\nnostr:${it.toNEvent()}")
urlPreview = findUrlInMessage()
it.author?.let { quotedUser ->
if (quotedUser.pubkeyHex != accountViewModel.userProfile().pubkeyHex) {
if (forwardZapTo.items.none { it.key.pubkeyHex == quotedUser.pubkeyHex }) {
forwardZapTo.addItem(quotedUser)
}
if (forwardZapTo.items.none { it.key.pubkeyHex == accountViewModel.userProfile().pubkeyHex }) {
forwardZapTo.addItem(accountViewModel.userProfile())
}
val pos = forwardZapTo.items.indexOfFirst { it.key.pubkeyHex == quotedUser.pubkeyHex }
forwardZapTo.updatePercentage(pos, 0.9f)
}
}
}
fork?.let {
message = TextFieldValue(version?.event?.content() ?: it.event?.content() ?: "")
urlPreview = findUrlInMessage()
it.event?.isSensitive()?.let {
if (it) wantsToMarkAsSensitive = true
}
it.event?.zapraiserAmount()?.let {
zapRaiserAmount = it
}
it.event?.zapSplitSetup()?.let {
val totalWeight = it.sumOf { if (it.isLnAddress) 0.0 else it.weight }
it.forEach {
if (!it.isLnAddress) {
forwardZapTo.addItem(LocalCache.getOrCreateUser(it.lnAddressOrPubKeyHex), (it.weight / totalWeight).toFloat())
}
}
}
// Only adds if it is not already set up.
if (forwardZapTo.items.isEmpty()) {
it.author?.let { forkedAuthor ->
if (forkedAuthor.pubkeyHex != accountViewModel.userProfile().pubkeyHex) {
if (forwardZapTo.items.none { it.key.pubkeyHex == forkedAuthor.pubkeyHex }) forwardZapTo.addItem(forkedAuthor)
if (forwardZapTo.items.none { it.key.pubkeyHex == accountViewModel.userProfile().pubkeyHex }) forwardZapTo.addItem(accountViewModel.userProfile())
val pos = forwardZapTo.items.indexOfFirst { it.key.pubkeyHex == forkedAuthor.pubkeyHex }
forwardZapTo.updatePercentage(pos, 0.8f)
}
}
}
it.author?.let {
if (this.pTags == null) {
this.pTags = listOf(it)
} else if (this.pTags?.contains(it) != true) {
this.pTags = listOf(it) + (this.pTags ?: emptyList())
}
}
forkedFromNote = it
} ?: run {
forkedFromNote = null
}
if (!forwardZapTo.items.isEmpty()) {
wantsForwardZapTo = true
}
}
}
private fun loadFromDraft(
draft: Note,
accountViewModel: AccountViewModel,
) {
Log.d("draft", draft.event!!.toJson())
draftTag = LocalCache.drafts.filter {
it.value.any { it.eventId == draft.event?.id() }
}.keys.firstOrNull() ?: draftTag
canAddInvoice = accountViewModel.userProfile().info?.lnAddress() != null
canAddZapRaiser = accountViewModel.userProfile().info?.lnAddress() != null
canUsePoll = originalNote?.event !is PrivateDmEvent && originalNote?.channelHex() == null
contentToAddUrl = null
wantsForwardZapTo = false
wantsToMarkAsSensitive = false
wantsToAddGeoHash = false
wantsZapraiser = false
zapRaiserAmount = null
val localfowardZapTo = draft.event?.tags()?.filter { it.size > 1 && it[0] == "zap" } ?: listOf()
forwardZapTo = Split()
localfowardZapTo.forEach {
val user = LocalCache.getOrCreateUser(it[1])
val value = it.last().toFloatOrNull() ?: 0f
forwardZapTo.addItem(user, value)
}
forwardZapToEditting = TextFieldValue("")
wantsForwardZapTo = localfowardZapTo.isNotEmpty()
quote?.let {
message = TextFieldValue(message.text + "\nnostr:${it.toNEvent()}")
urlPreview = findUrlInMessage()
it.author?.let { quotedUser ->
if (quotedUser.pubkeyHex != accountViewModel.userProfile().pubkeyHex) {
if (forwardZapTo.items.none { it.key.pubkeyHex == quotedUser.pubkeyHex }) {
forwardZapTo.addItem(quotedUser)
}
if (forwardZapTo.items.none { it.key.pubkeyHex == accountViewModel.userProfile().pubkeyHex }) {
forwardZapTo.addItem(accountViewModel.userProfile())
}
val pos = forwardZapTo.items.indexOfFirst { it.key.pubkeyHex == quotedUser.pubkeyHex }
forwardZapTo.updatePercentage(pos, 0.9f)
}
}
wantsToMarkAsSensitive = draft.event?.tags()?.any { it.size > 1 && it[0] == "content-warning" } ?: false
wantsToAddGeoHash = draft.event?.tags()?.any { it.size > 1 && it[0] == "g" } ?: false
val zapraiser = draft.event?.tags()?.filter { it.size > 1 && it[0] == "zapraiser" } ?: listOf()
wantsZapraiser = zapraiser.isNotEmpty()
zapRaiserAmount = null
if (wantsZapraiser) {
zapRaiserAmount = zapraiser.first()[1].toLongOrNull() ?: 0
}
fork?.let {
message = TextFieldValue(version?.event?.content() ?: it.event?.content() ?: "")
urlPreview = findUrlInMessage()
it.event?.isSensitive()?.let {
if (it) wantsToMarkAsSensitive = true
eTags =
draft.event?.tags()?.filter { it.size > 1 && (it[0] == "e" || it[0] == "a") && it.getOrNull(3) != "fork" }?.mapNotNull {
val note = LocalCache.checkGetOrCreateNote(it[1])
note
}
it.event?.zapraiserAmount()?.let {
zapRaiserAmount = it
pTags =
draft.event?.tags()?.filter { it.size > 1 && it[0] == "p" }?.map {
LocalCache.getOrCreateUser(it[1])
}
it.event?.zapSplitSetup()?.let {
val totalWeight = it.sumOf { if (it.isLnAddress) 0.0 else it.weight }
it.forEach {
if (!it.isLnAddress) {
forwardZapTo.addItem(LocalCache.getOrCreateUser(it.lnAddressOrPubKeyHex), (it.weight / totalWeight).toFloat())
}
}
}
// Only adds if it is not already set up.
if (forwardZapTo.items.isEmpty()) {
it.author?.let { forkedAuthor ->
if (forkedAuthor.pubkeyHex != accountViewModel.userProfile().pubkeyHex) {
if (forwardZapTo.items.none { it.key.pubkeyHex == forkedAuthor.pubkeyHex }) forwardZapTo.addItem(forkedAuthor)
if (forwardZapTo.items.none { it.key.pubkeyHex == accountViewModel.userProfile().pubkeyHex }) forwardZapTo.addItem(accountViewModel.userProfile())
val pos = forwardZapTo.items.indexOfFirst { it.key.pubkeyHex == forkedAuthor.pubkeyHex }
forwardZapTo.updatePercentage(pos, 0.8f)
}
}
}
it.author?.let {
if (this.pTags == null) {
this.pTags = listOf(it)
} else if (this.pTags?.contains(it) != true) {
this.pTags = listOf(it) + (this.pTags ?: emptyList())
}
}
forkedFromNote = it
} ?: run {
forkedFromNote = null
draft.event?.tags()?.filter { it.size > 1 && (it[0] == "e" || it[0] == "a") && it.getOrNull(3) == "fork" }?.forEach {
val note = LocalCache.checkGetOrCreateNote(it[1])
forkedFromNote = note
}
if (!forwardZapTo.items.isEmpty()) {
originalNote =
draft.event?.tags()?.filter { it.size > 1 && (it[0] == "e" || it[0] == "a") && it.getOrNull(3) == "root" }?.map {
LocalCache.checkGetOrCreateNote(it[1])
}?.firstOrNull()
canUsePoll = originalNote?.event !is PrivateDmEvent && originalNote?.channelHex() == null
if (forwardZapTo.items.isNotEmpty()) {
wantsForwardZapTo = true
}
val polls = draft.event?.tags()?.filter { it.size > 1 && it[0] == "poll_option" } ?: emptyList()
wantsPoll = polls.isNotEmpty()
polls.forEach {
pollOptions[it[1].toInt()] = it[2]
}
val minMax = draft.event?.tags()?.filter { it.size > 1 && (it[0] == "value_minimum" || it[0] == "value_maximum") } ?: listOf()
minMax.forEach {
if (it[0] == "value_maximum") {
valueMaximum = it[1].toInt()
} else if (it[0] == "value_minimum") {
valueMinimum = it[1].toInt()
}
}
wantsProduct = draft.event?.kind() == 30402
title = TextFieldValue(draft.event?.tags()?.filter { it.size > 1 && it[0] == "title" }?.map { it[1] }?.firstOrNull() ?: "")
price = TextFieldValue(draft.event?.tags()?.filter { it.size > 1 && it[0] == "price" }?.map { it[1] }?.firstOrNull() ?: "")
category = TextFieldValue(draft.event?.tags()?.filter { it.size > 1 && it[0] == "t" }?.map { it[1] }?.firstOrNull() ?: "")
locationText = TextFieldValue(draft.event?.tags()?.filter { it.size > 1 && it[0] == "location" }?.map { it[1] }?.firstOrNull() ?: "")
condition = ClassifiedsEvent.CONDITION.entries.firstOrNull {
it.value == draft.event?.tags()?.filter { it.size > 1 && it[0] == "condition" }?.map { it[1] }?.firstOrNull()
} ?: ClassifiedsEvent.CONDITION.USED_LIKE_NEW
message =
if (draft.event is PrivateDmEvent) {
val event = draft.event as PrivateDmEvent
TextFieldValue(event.cachedContentFor(accountViewModel.account.signer) ?: "")
} else {
TextFieldValue(draft.event?.content() ?: "")
}
nip24 = draft.event is ChatMessageEvent
urlPreview = findUrlInMessage()
}
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
@@ -363,6 +477,7 @@ open class NewPostViewModel() : ViewModel() {
zapRaiserAmount = localZapRaiserAmount,
geohash = geoHash,
nip94attachments = usedAttachments,
draftTag = localDraft,
)
} else {
account?.sendChannelMessage(
@@ -375,6 +490,7 @@ open class NewPostViewModel() : ViewModel() {
zapRaiserAmount = localZapRaiserAmount,
geohash = geoHash,
nip94attachments = usedAttachments,
draftTag = localDraft,
)
}
} else if (originalNote?.event is PrivateDmEvent) {
@@ -388,6 +504,7 @@ open class NewPostViewModel() : ViewModel() {
zapRaiserAmount = localZapRaiserAmount,
geohash = geoHash,
nip94attachments = usedAttachments,
draftTag = localDraft,
)
} else if (originalNote?.event is ChatMessageEvent) {
val receivers =
@@ -423,6 +540,7 @@ open class NewPostViewModel() : ViewModel() {
zapRaiserAmount = localZapRaiserAmount,
geohash = geoHash,
nip94attachments = usedAttachments,
draftTag = localDraft,
)
} else {
account?.sendPrivateMessage(
@@ -435,6 +553,7 @@ open class NewPostViewModel() : ViewModel() {
zapRaiserAmount = localZapRaiserAmount,
geohash = geoHash,
nip94attachments = usedAttachments,
draftTag = localDraft,
)
}
} else if (originalNote?.event is GitIssueEvent) {
@@ -475,6 +594,7 @@ open class NewPostViewModel() : ViewModel() {
relayList = relayList,
geohash = geoHash,
nip94attachments = usedAttachments,
draftTag = localDraft,
)
} else {
if (wantsPoll) {
@@ -493,6 +613,7 @@ open class NewPostViewModel() : ViewModel() {
relayList,
geoHash,
nip94attachments = usedAttachments,
draftTag = localDraft,
)
} else if (wantsProduct) {
account?.sendClassifieds(
@@ -511,6 +632,7 @@ open class NewPostViewModel() : ViewModel() {
relayList = relayList,
geohash = geoHash,
nip94attachments = usedAttachments,
draftTag = localDraft,
)
} else {
// adds markers
@@ -547,11 +669,13 @@ open class NewPostViewModel() : ViewModel() {
relayList = relayList,
geohash = geoHash,
nip94attachments = usedAttachments,
draftTag = localDraft,
)
}
}
cancel()
if (localDraft == null) {
cancel()
}
}
fun upload(
@@ -635,6 +759,7 @@ open class NewPostViewModel() : ViewModel() {
urlPreview = null
isUploadingImage = false
pTags = null
eTags = null
wantsDirectMessage = false
@@ -663,6 +788,11 @@ open class NewPostViewModel() : ViewModel() {
userSuggestions = emptyList()
userSuggestionAnchor = null
userSuggestionsMainMessage = null
originalNote = null
viewModelScope.launch(Dispatchers.IO) {
accountViewModel?.deleteDraft(draftTag)
}
NostrSearchEventOrUserDataSource.clear()
}
@@ -679,6 +809,10 @@ open class NewPostViewModel() : ViewModel() {
pTags = pTags?.filter { it != userToRemove }
}
open suspend fun saveDraft() {
draftTextChanges.send("")
}
open fun updateMessage(it: TextFieldValue) {
message = it
urlPreview = findUrlInMessage()
@@ -701,6 +835,10 @@ open class NewPostViewModel() : ViewModel() {
userSuggestions = emptyList()
}
}
viewModelScope.launch(Dispatchers.IO) {
saveDraft()
}
}
open fun updateToUsers(it: TextFieldValue) {
@@ -724,10 +862,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) {
@@ -754,6 +898,9 @@ open class NewPostViewModel() : ViewModel() {
userSuggestions = emptyList()
}
}
viewModelScope.launch(Dispatchers.IO) {
saveDraft()
}
}
open fun autocompleteWithUser(item: User) {
@@ -799,6 +946,10 @@ open class NewPostViewModel() : ViewModel() {
userSuggestionsMainMessage = null
userSuggestions = emptyList()
}
viewModelScope.launch(Dispatchers.IO) {
saveDraft()
}
}
private fun newStateMapPollOptions(): SnapshotStateMap<Int, String> {
@@ -869,6 +1020,9 @@ open class NewPostViewModel() : ViewModel() {
message = message.insertUrlAtCursor(imageUrl)
urlPreview = findUrlInMessage()
viewModelScope.launch(Dispatchers.IO) {
saveDraft()
}
}
},
onError = {
@@ -913,6 +1067,9 @@ open class NewPostViewModel() : ViewModel() {
}
urlPreview = findUrlInMessage()
viewModelScope.launch(Dispatchers.IO) {
saveDraft()
}
}
},
onError = {
@@ -933,6 +1090,7 @@ open class NewPostViewModel() : ViewModel() {
locUtil?.let {
location =
it.locationStateFlow.mapLatest { it.toGeoHash(GeohashPrecision.KM_5_X_5.digits).toString() }
viewModelScope.launch(Dispatchers.IO) { saveDraft() }
}
viewModelScope.launch(Dispatchers.IO) { locUtil?.start() }
}
@@ -957,6 +1115,11 @@ open class NewPostViewModel() : ViewModel() {
} else {
nip24 = !nip24
}
if (message.text.isNotBlank()) {
viewModelScope.launch(Dispatchers.IO) {
saveDraft()
}
}
}
fun updateMinZapAmountForPoll(textMin: String) {
@@ -976,6 +1139,9 @@ open class NewPostViewModel() : ViewModel() {
}
checkMinMax()
viewModelScope.launch(Dispatchers.IO) {
saveDraft()
}
}
fun updateMaxZapAmountForPoll(textMax: String) {
@@ -995,6 +1161,9 @@ open class NewPostViewModel() : ViewModel() {
}
checkMinMax()
viewModelScope.launch(Dispatchers.IO) {
saveDraft()
}
}
fun checkMinMax() {
@@ -39,6 +39,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons
import com.vitorpamplona.amethyst.commons.hashtags.Lightning
@@ -46,6 +47,8 @@ import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Composable
fun ZapRaiserRequest(
@@ -98,6 +101,9 @@ fun ZapRaiserRequest(
} else {
newPostViewModel.zapRaiserAmount = it.toLongOrNull()
}
newPostViewModel.viewModelScope.launch(Dispatchers.IO) {
newPostViewModel.saveDraft()
}
}
},
placeholder = {
@@ -88,6 +88,7 @@ import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relays.RelayPool
import com.vitorpamplona.amethyst.service.relays.RelayPoolStatus
import com.vitorpamplona.amethyst.ui.actions.NewPostView
import com.vitorpamplona.amethyst.ui.actions.NewRelayListView
import com.vitorpamplona.amethyst.ui.components.ClickableText
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
@@ -159,7 +160,10 @@ fun DrawerContent(
)
ListContent(
modifier = Modifier.fillMaxWidth().weight(1f),
modifier =
Modifier
.fillMaxWidth()
.weight(1f),
drawerState,
openSheet,
accountViewModel,
@@ -231,7 +235,8 @@ fun ProfileContentTemplate(
model = profilePicture,
contentDescription = stringResource(id = R.string.profile_image),
modifier =
Modifier.width(100.dp)
Modifier
.width(100.dp)
.height(100.dp)
.clip(shape = CircleShape)
.border(3.dp, MaterialTheme.colorScheme.background, CircleShape)
@@ -244,7 +249,10 @@ fun ProfileContentTemplate(
CreateTextWithEmoji(
text = bestDisplayName,
tags = tags,
modifier = Modifier.padding(top = 7.dp).clickable(onClick = onClick),
modifier =
Modifier
.padding(top = 7.dp)
.clickable(onClick = onClick),
fontWeight = FontWeight.Bold,
fontSize = 18.sp,
maxLines = 1,
@@ -454,8 +462,17 @@ fun ListContent(
val proxyPort = remember { mutableStateOf(accountViewModel.account.proxyPort.toString()) }
val context = LocalContext.current
var draftText by remember {
mutableStateOf<String?>(null)
}
var wantsToPost by remember { mutableStateOf(false) }
Column(
modifier = modifier.fillMaxHeight().verticalScroll(rememberScrollState()),
modifier =
modifier
.fillMaxHeight()
.verticalScroll(rememberScrollState()),
) {
NavigationRow(
title = stringResource(R.string.profile),
@@ -571,6 +588,18 @@ fun ListContent(
)
}
if (wantsToPost) {
NewPostView(
{
wantsToPost = false
draftText = null
coroutineScope.launch { drawerState.close() }
},
accountViewModel = accountViewModel,
nav = nav,
)
}
if (disconnectTorDialog) {
AlertDialog(
title = { Text(text = stringResource(R.string.do_you_really_want_to_disable_tor_title)) },
@@ -662,7 +691,8 @@ fun IconRow(
) {
Row(
modifier =
Modifier.fillMaxWidth()
Modifier
.fillMaxWidth()
.combinedClickable(
onClick = onClick,
onLongClick = onLongClick,
@@ -693,10 +723,16 @@ fun IconRowRelays(
onClick: () -> Unit,
) {
Row(
modifier = Modifier.fillMaxWidth().clickable { onClick() },
modifier =
Modifier
.fillMaxWidth()
.clickable { onClick() },
) {
Row(
modifier = Modifier.fillMaxWidth().padding(vertical = 15.dp, horizontal = 25.dp),
modifier =
Modifier
.fillMaxWidth()
.padding(vertical = 15.dp, horizontal = 25.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
@@ -737,7 +773,10 @@ fun BottomContent(
thickness = DividerThickness,
)
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 15.dp),
modifier =
Modifier
.fillMaxWidth()
.padding(horizontal = 15.dp),
verticalAlignment = Alignment.CenterVertically,
) {
ClickableText(
@@ -147,7 +147,7 @@ fun NormalChannelCard(
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel, newPostViewModel = null) { showPopup ->
CheckNewAndRenderChannelCard(
baseNote,
routeForLastRead,
@@ -64,6 +64,7 @@ import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.FeatureSetType
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
@@ -102,6 +103,7 @@ fun ChatroomMessageCompose(
innerQuote: Boolean = false,
parentBackgroundColor: MutableState<Color>? = null,
accountViewModel: AccountViewModel,
newPostViewModel: NewPostViewModel?,
nav: (String) -> Unit,
onWantsToReply: (Note) -> Unit,
) {
@@ -120,6 +122,7 @@ fun ChatroomMessageCompose(
canPreview,
parentBackgroundColor,
accountViewModel,
newPostViewModel,
nav,
onWantsToReply,
)
@@ -136,6 +139,7 @@ fun NormalChatNote(
canPreview: Boolean = true,
parentBackgroundColor: MutableState<Color>? = null,
accountViewModel: AccountViewModel,
newPostViewModel: NewPostViewModel?,
nav: (String) -> Unit,
onWantsToReply: (Note) -> Unit,
) {
@@ -255,6 +259,7 @@ fun NormalChatNote(
availableBubbleSize,
showDetails,
accountViewModel,
newPostViewModel,
nav,
)
}
@@ -265,6 +270,7 @@ fun NormalChatNote(
popupExpanded = popupExpanded,
onDismiss = { popupExpanded = false },
accountViewModel = accountViewModel,
newPostViewModel = newPostViewModel,
)
}
}
@@ -282,6 +288,7 @@ private fun RenderBubble(
availableBubbleSize: MutableState<Int>,
showDetails: State<Boolean>,
accountViewModel: AccountViewModel,
newPostViewModel: NewPostViewModel?,
nav: (String) -> Unit,
) {
val bubbleSize = remember { mutableIntStateOf(0) }
@@ -311,6 +318,7 @@ private fun RenderBubble(
canPreview,
showDetails,
accountViewModel,
newPostViewModel,
nav,
)
}
@@ -329,6 +337,7 @@ private fun MessageBubbleLines(
canPreview: Boolean,
showDetails: State<Boolean>,
accountViewModel: AccountViewModel,
newPostViewModel: NewPostViewModel?,
nav: (String) -> Unit,
) {
if (drawAuthorInfo) {
@@ -345,6 +354,7 @@ private fun MessageBubbleLines(
innerQuote = innerQuote,
backgroundBubbleColor = backgroundBubbleColor,
accountViewModel = accountViewModel,
newPostViewModel = newPostViewModel,
nav = nav,
onWantsToReply = onWantsToReply,
)
@@ -363,6 +373,9 @@ private fun MessageBubbleLines(
bubbleSize = bubbleSize,
availableBubbleSize = availableBubbleSize,
firstColumn = {
if (baseNote.isDraft()) {
DisplayDraftChat()
}
IncognitoBadge(baseNote)
ChatTimeAgo(baseNote)
RelayBadgesHorizontal(baseNote, accountViewModel, nav = nav)
@@ -394,11 +407,12 @@ private fun RenderReplyRow(
innerQuote: Boolean,
backgroundBubbleColor: MutableState<Color>,
accountViewModel: AccountViewModel,
newPostViewModel: NewPostViewModel?,
nav: (String) -> Unit,
onWantsToReply: (Note) -> Unit,
) {
if (!innerQuote && note.replyTo?.lastOrNull() != null) {
RenderReply(note, backgroundBubbleColor, accountViewModel, nav, onWantsToReply)
RenderReply(note, backgroundBubbleColor, accountViewModel, newPostViewModel, nav, onWantsToReply)
}
}
@@ -407,6 +421,7 @@ private fun RenderReply(
note: Note,
backgroundBubbleColor: MutableState<Color>,
accountViewModel: AccountViewModel,
newPostViewModel: NewPostViewModel?,
nav: (String) -> Unit,
onWantsToReply: (Note) -> Unit,
) {
@@ -425,6 +440,7 @@ private fun RenderReply(
innerQuote = true,
parentBackgroundColor = backgroundBubbleColor,
accountViewModel = accountViewModel,
newPostViewModel = newPostViewModel,
nav = nav,
onWantsToReply = onWantsToReply,
)
@@ -31,6 +31,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
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
@@ -48,6 +49,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.distinctUntilChanged
import androidx.lifecycle.map
@@ -105,8 +107,11 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
import com.vitorpamplona.amethyst.ui.theme.Font12SP
import com.vitorpamplona.amethyst.ui.theme.HalfDoubleVertSpacer
import com.vitorpamplona.amethyst.ui.theme.HalfEndPadding
import com.vitorpamplona.amethyst.ui.theme.HalfPadding
import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding
import com.vitorpamplona.amethyst.ui.theme.Size25dp
import com.vitorpamplona.amethyst.ui.theme.Size30Modifier
import com.vitorpamplona.amethyst.ui.theme.Size34dp
@@ -122,6 +127,7 @@ import com.vitorpamplona.amethyst.ui.theme.channelNotePictureModifier
import com.vitorpamplona.amethyst.ui.theme.grayText
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
import com.vitorpamplona.amethyst.ui.theme.normalWithTopMarginNoteModifier
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.amethyst.ui.theme.replyBackground
import com.vitorpamplona.amethyst.ui.theme.replyModifier
import com.vitorpamplona.quartz.events.AppDefinitionEvent
@@ -238,7 +244,7 @@ fun AcceptableNote(
nav = nav,
)
else ->
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) {
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel, newPostViewModel = null) {
showPopup,
->
CheckNewAndRenderNote(
@@ -273,7 +279,7 @@ fun AcceptableNote(
is FileHeaderEvent -> FileHeaderDisplay(baseNote, false, accountViewModel)
is FileStorageHeaderEvent -> FileStorageHeaderDisplay(baseNote, false, accountViewModel)
else ->
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) {
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel, newPostViewModel = null) {
showPopup,
->
CheckNewAndRenderNote(
@@ -868,6 +874,29 @@ fun DisplayOtsIfInOriginal(
}
}
@Composable
fun DisplayDraft() {
Text(
"Draft",
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.placeholderText,
maxLines = 1,
modifier = HalfStartPadding,
)
}
@Composable
fun DisplayDraftChat() {
Text(
"Draft",
color = MaterialTheme.colorScheme.placeholderText,
modifier = HalfEndPadding,
fontWeight = FontWeight.Bold,
fontSize = Font12SP,
maxLines = 1,
)
}
@Composable
fun FirstUserInfoRow(
baseNote: Note,
@@ -910,6 +939,10 @@ fun FirstUserInfoRow(
}
}
if (baseNote.isDraft()) {
DisplayDraft()
}
TimeAgo(baseNote)
MoreOptionsButton(baseNote, editState, accountViewModel, nav)
@@ -40,6 +40,7 @@ import androidx.compose.material.icons.filled.AlternateEmail
import androidx.compose.material.icons.filled.Block
import androidx.compose.material.icons.filled.ContentCopy
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.FormatQuote
import androidx.compose.material.icons.filled.PersonAdd
import androidx.compose.material.icons.filled.PersonRemove
@@ -84,6 +85,8 @@ import androidx.core.graphics.ColorUtils
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.actions.NewPostView
import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel
import com.vitorpamplona.amethyst.ui.components.SelectTextDialog
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ReportNoteDialog
@@ -132,6 +135,7 @@ val externalLinkForNote = { note: Note ->
fun LongPressToQuickAction(
baseNote: Note,
accountViewModel: AccountViewModel,
newPostViewModel: NewPostViewModel?,
content: @Composable (() -> Unit) -> Unit,
) {
val popupExpanded = remember { mutableStateOf(false) }
@@ -140,7 +144,7 @@ fun LongPressToQuickAction(
content(showPopup)
NoteQuickActionMenu(baseNote, popupExpanded.value, hidePopup, accountViewModel)
NoteQuickActionMenu(baseNote, popupExpanded.value, hidePopup, accountViewModel, newPostViewModel)
}
@Composable
@@ -149,20 +153,24 @@ fun NoteQuickActionMenu(
popupExpanded: Boolean,
onDismiss: () -> Unit,
accountViewModel: AccountViewModel,
newPostViewModel: NewPostViewModel?,
) {
val showSelectTextDialog = remember { mutableStateOf(false) }
val showDeleteAlertDialog = remember { mutableStateOf(false) }
val showBlockAlertDialog = remember { mutableStateOf(false) }
val showReportDialog = remember { mutableStateOf(false) }
val editDraftDialog = remember { mutableStateOf(false) }
if (popupExpanded) {
RenderMainPopup(
accountViewModel,
newPostViewModel,
note,
onDismiss,
showBlockAlertDialog,
showDeleteAlertDialog,
showReportDialog,
editDraftDialog,
)
}
@@ -199,16 +207,29 @@ fun NoteQuickActionMenu(
onDismiss()
}
}
if (editDraftDialog.value) {
NewPostView(
onClose = {
editDraftDialog.value = false
},
accountViewModel = accountViewModel,
draft = note,
nav = { },
)
}
}
@Composable
private fun RenderMainPopup(
accountViewModel: AccountViewModel,
newPostViewModel: NewPostViewModel?,
note: Note,
onDismiss: () -> Unit,
showBlockAlertDialog: MutableState<Boolean>,
showDeleteAlertDialog: MutableState<Boolean>,
showReportDialog: MutableState<Boolean>,
editDraftDialog: MutableState<Boolean>,
) {
val context = LocalContext.current
val primaryLight = lightenColor(MaterialTheme.colorScheme.primary, 0.1f)
@@ -279,6 +300,22 @@ private fun RenderMainPopup(
}
}
if (note.isDraft()) {
VerticalDivider(color = primaryLight)
NoteQuickActionItem(
Icons.Default.Edit,
stringResource(R.string.edit_draft),
) {
if (newPostViewModel != null) {
newPostViewModel.load(accountViewModel, null, null, null, null, note)
onDismiss()
} else {
editDraftDialog.value = true
onDismiss()
}
}
}
if (!isOwnNote) {
VerticalDivider(color = primaryLight)
@@ -389,14 +426,20 @@ fun NoteQuickActionItem(
onClick: () -> Unit,
) {
Column(
modifier = Modifier.size(70.dp).clickable { onClick() },
modifier =
Modifier
.size(70.dp)
.clickable { onClick() },
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
imageVector = icon,
contentDescription = null,
modifier = Modifier.size(24.dp).padding(bottom = 5.dp),
modifier =
Modifier
.size(24.dp)
.padding(bottom = 5.dp),
tint = Color.White,
)
Text(text = label, fontSize = 12.sp, color = Color.White, textAlign = TextAlign.Center)
@@ -527,7 +570,10 @@ fun QuickActionAlertDialog(
text = { Text(textContent) },
confirmButton = {
Row(
modifier = Modifier.padding(all = 8.dp).fillMaxWidth(),
modifier =
Modifier
.padding(all = 8.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
) {
TextButton(onClick = onClickDontShowAgain) {
@@ -310,7 +310,12 @@ fun RenderZapRaiser(
}
LinearProgressIndicator(
modifier = remember(details) { Modifier.fillMaxWidth().height(if (details) 24.dp else 4.dp) },
modifier =
remember(details) {
Modifier
.fillMaxWidth()
.height(if (details) 24.dp else 4.dp)
},
color = color,
progress = { zapraiserStatus.progress },
)
@@ -590,6 +595,13 @@ fun ReplyReaction(
IconButton(
modifier = iconSizeModifier,
onClick = {
if (baseNote.isDraft()) {
accountViewModel.toast(
R.string.draft_note,
R.string.it_s_not_possible_to_reply_to_a_draft_note,
)
return@IconButton
}
if (accountViewModel.isWriteable()) {
onPress()
} else {
@@ -776,7 +788,8 @@ fun LikeReaction(
Box(
contentAlignment = Center,
modifier =
Modifier.size(iconSize)
Modifier
.size(iconSize)
.combinedClickable(
role = Role.Button,
interactionSource = remember { MutableInteractionSource() },
@@ -784,6 +797,7 @@ fun LikeReaction(
onClick = {
likeClick(
accountViewModel,
baseNote,
onMultipleChoices = { wantsToReact = true },
onWantsToSignReaction = { accountViewModel.reactToOrDelete(baseNote) },
)
@@ -886,9 +900,17 @@ fun ObserveLikeText(
private fun likeClick(
accountViewModel: AccountViewModel,
baseNote: Note,
onMultipleChoices: () -> Unit,
onWantsToSignReaction: () -> Unit,
) {
if (baseNote.isDraft()) {
accountViewModel.toast(
R.string.draft_note,
R.string.it_s_not_possible_to_react_to_a_draft_note,
)
return
}
if (accountViewModel.account.reactionChoices.isEmpty()) {
accountViewModel.toast(
R.string.no_reactions_setup,
@@ -1082,6 +1104,14 @@ fun zapClick(
onError: (String, String) -> Unit,
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit,
) {
if (baseNote.isDraft()) {
accountViewModel.toast(
R.string.draft_note,
R.string.it_s_not_possible_to_zap_to_a_draft_note,
)
return
}
if (accountViewModel.account.zapAmountChoices.isEmpty()) {
accountViewModel.toast(
context.getString(R.string.error_dialog_zap_error),
@@ -52,6 +52,7 @@ fun WatchNoteEvent(
LongPressToQuickAction(
baseNote = baseNote,
accountViewModel = accountViewModel,
newPostViewModel = null,
) { showPopup ->
BlankNote(
remember {
@@ -179,7 +179,7 @@ fun ZapCustomDialog(
)
ZapButton(
isActive = postViewModel.canSend(),
isActive = postViewModel.canSend() && !baseNote.isDraft(),
) {
accountViewModel.zap(
baseNote,
@@ -185,6 +185,7 @@ class AddBountyAmountViewModel : ViewModel() {
root = null,
directMentions = setOf(),
forkedFrom = null,
draftTag = null,
)
nextAmount = TextFieldValue("")
@@ -45,6 +45,7 @@ import androidx.core.content.ContextCompat
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.actions.EditPostView
import com.vitorpamplona.amethyst.ui.actions.NewPostView
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
import com.vitorpamplona.amethyst.ui.note.VerticalDotsIcon
import com.vitorpamplona.amethyst.ui.note.externalLinkForNote
@@ -122,6 +123,11 @@ fun NoteDropDownMenu(
mutableStateOf(false)
}
val wantsToEditDraft =
remember {
mutableStateOf(false)
}
if (wantsToEditPost.value) {
// avoids changing while drafting a note and a new event shows up.
val versionLookingAt =
@@ -141,6 +147,18 @@ fun NoteDropDownMenu(
)
}
if (wantsToEditDraft.value) {
NewPostView(
onClose = {
popupExpanded.value = false
wantsToEditDraft.value = false
},
accountViewModel = accountViewModel,
draft = note,
nav = nav,
)
}
DropdownMenu(
expanded = popupExpanded.value,
onDismissRequest = onDismiss,
@@ -219,7 +237,15 @@ fun NoteDropDownMenu(
},
)
HorizontalDivider(thickness = DividerThickness)
if (note.event is TextNoteEvent) {
if (note.isDraft()) {
DropdownMenuItem(
text = { Text(stringResource(R.string.edit_draft)) },
onClick = {
wantsToEditDraft.value = true
},
)
}
if (note.event is TextNoteEvent && !note.isDraft()) {
if (state.isLoggedUser) {
DropdownMenuItem(
text = { Text(stringResource(R.string.edit_post)) },
@@ -38,6 +38,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel
import com.vitorpamplona.amethyst.ui.note.ChatroomMessageCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
@@ -48,6 +49,7 @@ import com.vitorpamplona.amethyst.ui.theme.HalfPadding
fun RefreshingChatroomFeedView(
viewModel: FeedViewModel,
accountViewModel: AccountViewModel,
newPostViewModel: NewPostViewModel,
nav: (String) -> Unit,
routeForLastRead: String,
onWantsToReply: (Note) -> Unit,
@@ -59,6 +61,7 @@ fun RefreshingChatroomFeedView(
RenderChatroomFeedView(
viewModel,
accountViewModel,
newPostViewModel,
listState,
nav,
routeForLastRead,
@@ -72,6 +75,7 @@ fun RefreshingChatroomFeedView(
fun RenderChatroomFeedView(
viewModel: FeedViewModel,
accountViewModel: AccountViewModel,
newPostViewModel: NewPostViewModel,
listState: LazyListState,
nav: (String) -> Unit,
routeForLastRead: String,
@@ -91,6 +95,7 @@ fun RenderChatroomFeedView(
ChatroomFeedLoaded(
state,
accountViewModel,
newPostViewModel,
listState,
nav,
routeForLastRead,
@@ -108,6 +113,7 @@ fun RenderChatroomFeedView(
fun ChatroomFeedLoaded(
state: FeedState.Loaded,
accountViewModel: AccountViewModel,
newPostViewModel: NewPostViewModel,
listState: LazyListState,
nav: (String) -> Unit,
routeForLastRead: String,
@@ -130,6 +136,7 @@ fun ChatroomFeedLoaded(
baseNote = item,
routeForLastRead = routeForLastRead,
accountViewModel = accountViewModel,
newPostViewModel = newPostViewModel,
nav = nav,
onWantsToReply = onWantsToReply,
)
@@ -86,6 +86,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.DisplayOtsIfInOriginal
import com.vitorpamplona.amethyst.ui.note.HiddenNote
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
@@ -446,6 +447,10 @@ fun NoteMaster(
DisplayPoW(pow)
}
if (note.isDraft()) {
DisplayDraft()
}
DisplayOtsIfInOriginal(note, editState, accountViewModel)
}
}
@@ -605,7 +610,7 @@ fun NoteMaster(
ReactionsRow(note, true, editState, accountViewModel, nav)
}
NoteQuickActionMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel)
NoteQuickActionMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel, null)
}
}
@@ -1209,6 +1209,14 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
baseNote: Note,
onMore: () -> Unit,
) {
if (baseNote.isDraft()) {
toast(
R.string.draft_note,
R.string.it_s_not_possible_to_quote_to_a_draft_note,
)
return
}
if (isWriteable()) {
if (hasBoosted(baseNote)) {
deleteBoostsTo(baseNote)
@@ -1304,6 +1312,11 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
}
}
suspend fun deleteDraft(draftTag: String) {
val notes = LocalCache.draftNotes(draftTag)
account.delete(notes)
}
val bechLinkCache = CachedLoadedBechLink(this)
class CachedLoadedBechLink(val accountViewModel: AccountViewModel) : GenericBaseCache<String, LoadedBechLink>(20) {
@@ -163,12 +163,17 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.UUID
@Composable
fun ChannelScreen(
@@ -187,6 +192,7 @@ fun ChannelScreen(
}
}
@OptIn(FlowPreview::class)
@Composable
fun PrepareChannelViewModels(
baseChannel: Channel,
@@ -204,8 +210,21 @@ fun PrepareChannelViewModels(
)
val channelScreenModel: NewPostViewModel = viewModel()
LaunchedEffect(Unit) {
launch(Dispatchers.IO) {
channelScreenModel.draftTextChanges
.receiveAsFlow()
.debounce(1000)
.collectLatest {
channelScreenModel.sendPost(localDraft = channelScreenModel.draftTag)
}
}
}
channelScreenModel.accountViewModel = accountViewModel
channelScreenModel.account = accountViewModel.account
channelScreenModel.originalNote = LocalCache.getNoteIfExists(baseChannel.idHex)
ChannelScreen(
channel = baseChannel,
@@ -287,6 +306,7 @@ fun ChannelScreen(
RefreshingChatroomFeedView(
viewModel = feedViewModel,
accountViewModel = accountViewModel,
newPostViewModel = newPostModel,
nav = nav,
routeForLastRead = "Channel/${channel.idHex}",
onWantsToReply = { replyTo.value = it },
@@ -295,7 +315,7 @@ fun ChannelScreen(
Spacer(modifier = DoubleVertSpacer)
replyTo.value?.let { DisplayReplyingToNote(it, accountViewModel, nav) { replyTo.value = null } }
replyTo.value?.let { DisplayReplyingToNote(it, accountViewModel, newPostModel, nav) { replyTo.value = null } }
val scope = rememberCoroutineScope()
@@ -323,6 +343,7 @@ fun ChannelScreen(
mentions = tagger.pTags,
wantsToMarkAsSensitive = false,
nip94attachments = usedAttachments,
draftTag = null,
)
} else if (channel is LiveActivitiesChannel) {
accountViewModel.account.sendLiveMessage(
@@ -332,10 +353,13 @@ fun ChannelScreen(
mentions = tagger.pTags,
wantsToMarkAsSensitive = false,
nip94attachments = usedAttachments,
draftTag = null,
)
}
newPostModel.message = TextFieldValue("")
replyTo.value = null
accountViewModel.deleteDraft(newPostModel.draftTag)
newPostModel.draftTag = UUID.randomUUID().toString()
feedViewModel.sendToTop()
}
}
@@ -346,6 +370,7 @@ fun ChannelScreen(
fun DisplayReplyingToNote(
replyingNote: Note?,
accountViewModel: AccountViewModel,
newPostModel: NewPostViewModel,
nav: (String) -> Unit,
onCancel: () -> Unit,
) {
@@ -364,6 +389,7 @@ fun DisplayReplyingToNote(
null,
innerQuote = true,
accountViewModel = accountViewModel,
newPostViewModel = newPostModel,
nav = nav,
onWantsToReply = {},
)
@@ -665,7 +691,12 @@ fun ShowVideoStreaming(
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = remember { Modifier.fillMaxWidth().heightIn(min = 50.dp, max = 300.dp) },
modifier =
remember {
Modifier
.fillMaxWidth()
.heightIn(min = 50.dp, max = 300.dp)
},
) {
val zoomableUrlVideo =
remember(streamingInfo) {
@@ -116,14 +116,21 @@ import com.vitorpamplona.amethyst.ui.theme.Size34dp
import com.vitorpamplona.amethyst.ui.theme.StdPadding
import com.vitorpamplona.amethyst.ui.theme.ZeroPadding
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.quartz.encoders.Hex
import com.vitorpamplona.quartz.encoders.toNpub
import com.vitorpamplona.quartz.events.ChatMessageEvent
import com.vitorpamplona.quartz.events.ChatroomKey
import com.vitorpamplona.quartz.events.findURLs
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.UUID
@Composable
fun ChatroomScreen(
@@ -206,6 +213,7 @@ fun LoadRoomByAuthor(
content(room)
}
@OptIn(FlowPreview::class)
@Composable
fun PrepareChatroomViewModels(
room: ChatroomKey,
@@ -230,8 +238,20 @@ fun PrepareChatroomViewModels(
if (newPostModel.requiresNIP24) {
newPostModel.nip24 = true
}
room.users.forEach {
newPostModel.toUsers = TextFieldValue(newPostModel.toUsers.text + " @${Hex.decode(it).toNpub()}")
}
LaunchedEffect(key1 = newPostModel) {
launch(Dispatchers.IO) {
newPostModel.draftTextChanges
.receiveAsFlow()
.debounce(1000)
.collectLatest {
newPostModel.sendPost(localDraft = newPostModel.draftTag)
}
}
launch(Dispatchers.IO) {
val hasNIP24 =
accountViewModel.userProfile().privateChatrooms[room]?.roomMessages?.any {
@@ -313,21 +333,28 @@ fun ChatroomScreen(
RefreshingChatroomFeedView(
viewModel = feedViewModel,
accountViewModel = accountViewModel,
newPostViewModel = newPostModel,
nav = nav,
routeForLastRead = "Room/${room.hashCode()}",
onWantsToReply = { replyTo.value = it },
onWantsToReply = {
replyTo.value = it
newPostModel.originalNote = it
},
)
}
Spacer(modifier = Modifier.height(10.dp))
replyTo.value?.let { DisplayReplyingToNote(it, accountViewModel, nav) { replyTo.value = null } }
replyTo.value?.let { DisplayReplyingToNote(it, accountViewModel, newPostModel, nav) { replyTo.value = null } }
val scope = rememberCoroutineScope()
// LAST ROW
PrivateMessageEditFieldRow(newPostModel, isPrivate = true, accountViewModel) {
scope.launch(Dispatchers.IO) {
accountViewModel.deleteDraft(newPostModel.draftTag)
newPostModel.draftTag = UUID.randomUUID().toString()
val urls = findURLs(newPostModel.message.text)
val usedAttachments = newPostModel.nip94attachments.filter { it.urls().intersect(urls.toSet()).isNotEmpty() }
@@ -339,6 +366,7 @@ fun ChatroomScreen(
mentions = null,
wantsToMarkAsSensitive = false,
nip94attachments = usedAttachments,
draftTag = null,
)
} else {
accountViewModel.account.sendPrivateMessage(
@@ -348,6 +376,7 @@ fun ChatroomScreen(
mentions = null,
wantsToMarkAsSensitive = false,
nip94attachments = usedAttachments,
draftTag = null,
)
}
@@ -93,6 +93,7 @@ val Size40dp = 40.dp
val Size55dp = 55.dp
val Size75dp = 75.dp
val HalfEndPadding = Modifier.padding(end = 5.dp)
val HalfStartPadding = Modifier.padding(start = 5.dp)
val StdStartPadding = Modifier.padding(start = 10.dp)
val StdTopPadding = Modifier.padding(top = 10.dp)