wip(nests): start copying ChannelNewMessageViewModel into nest package
Pivot from sharing ChannelNewMessageViewModel (previous commit) to a
literal per-nest copy as requested.
State of this commit (incomplete — DO NOT MERGE):
- EditFieldRow.kt: reverted to original (no interceptBackPress flag)
- NestNewMessageViewModel.kt: new file, copy of ChannelNewMessageViewModel
narrowed to MeetingSpaceEvent + LiveActivitiesChatMessageEvent only
Still TODO before this branch is usable:
- Copy EditFieldRow into nests/room/send/NestEditFieldRow.kt
- Copy ChannelFileUploadDialog into nests/room/send/NestFileUploadDialog.kt
- Rewire NestChatPanel to use these copies (it currently still calls
ChannelNewMessageViewModel + EditFieldRow from the previous commit's
approach, which won't compile against the reverted EditFieldRow
signature once NestChatPanel is also reverted)
User asked to stop mid-work — committing as WIP so the tree is clean.
This commit is contained in:
+5
-8
@@ -63,16 +63,13 @@ fun EditFieldRow(
|
||||
accountViewModel: AccountViewModel,
|
||||
onSendNewMessage: suspend () -> Unit,
|
||||
nav: INav,
|
||||
interceptBackPress: Boolean = true,
|
||||
) {
|
||||
if (interceptBackPress) {
|
||||
BackHandler {
|
||||
accountViewModel.launchSigner {
|
||||
channelScreenModel.sendDraftSync()
|
||||
channelScreenModel.cancel()
|
||||
}
|
||||
nav.popBack()
|
||||
BackHandler {
|
||||
accountViewModel.launchSigner {
|
||||
channelScreenModel.sendDraftSync()
|
||||
channelScreenModel.cancel()
|
||||
}
|
||||
nav.popBack()
|
||||
}
|
||||
|
||||
StrippingFailureDialog(channelScreenModel.strippingFailureConfirmation)
|
||||
|
||||
+642
@@ -0,0 +1,642 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.send
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.text.input.TextFieldState
|
||||
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableLongStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.compose.currentWord
|
||||
import com.vitorpamplona.amethyst.commons.compose.insertUrlAtCursor
|
||||
import com.vitorpamplona.amethyst.commons.compose.replaceCurrentWord
|
||||
import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState
|
||||
import com.vitorpamplona.amethyst.commons.richtext.UrlParser
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.location.LocationState
|
||||
import com.vitorpamplona.amethyst.service.uploads.MediaCompressor
|
||||
import com.vitorpamplona.amethyst.service.uploads.SuspendableConfirmation
|
||||
import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.draftTags.DraftTagState
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.EmojiSuggestionState
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.expiration.IExpiration
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.SplitBuilder
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadState
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.UserSuggestionAnchor
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.experimental.nip95.data.FileStorageEvent
|
||||
import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohash
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.geohash.getGeoHash
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.toPTag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.references.references
|
||||
import com.vitorpamplona.quartz.nip10Notes.content.findHashtags
|
||||
import com.vitorpamplona.quartz.nip10Notes.content.findNostrUris
|
||||
import com.vitorpamplona.quartz.nip10Notes.content.findURLs
|
||||
import com.vitorpamplona.quartz.nip18Reposts.quotes.quotes
|
||||
import com.vitorpamplona.quartz.nip30CustomEmoji.CustomEmoji
|
||||
import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag
|
||||
import com.vitorpamplona.quartz.nip30CustomEmoji.emojis
|
||||
import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning
|
||||
import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarningReason
|
||||
import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitive
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
||||
import com.vitorpamplona.quartz.nip40Expiration.expiration
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.chat.notify
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent
|
||||
import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup
|
||||
import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplitSetup
|
||||
import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount
|
||||
import com.vitorpamplona.quartz.nip92IMeta.imetas
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Composer-side ViewModel for nest-room (kind-30312 MeetingSpace) chat.
|
||||
* Mirrors `ChannelNewMessageViewModel` so the chat field inside the
|
||||
* audio room gets full @-mention picker, file/image/video upload,
|
||||
* reply preview, NIP-37 draft auto-save, emoji suggestions, content
|
||||
* warnings, expiration, geohash, and zap-split forwarding.
|
||||
*
|
||||
* Kept as a separate class (not parameterising the channel one)
|
||||
* because every event built here is exclusively a kind-1311
|
||||
* `LiveActivitiesChatMessageEvent` anchored to a `MeetingSpaceEvent`'s
|
||||
* a-tag — a single, narrow path with no `PublicChatChannel` /
|
||||
* `EphemeralChatChannel` branches to maintain.
|
||||
*/
|
||||
@Stable
|
||||
open class NestNewMessageViewModel :
|
||||
ViewModel(),
|
||||
ILocationGrabber,
|
||||
IExpiration {
|
||||
val draftTag = DraftTagState()
|
||||
|
||||
init {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
draftTag.versions.collectLatest {
|
||||
// don't save the first
|
||||
if (it > 0) {
|
||||
accountViewModel.launchSigner {
|
||||
sendDraftSync()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lateinit var accountViewModel: AccountViewModel
|
||||
lateinit var account: Account
|
||||
var room: MeetingSpaceEvent? = null
|
||||
|
||||
val replyTo = mutableStateOf<Note?>(null)
|
||||
|
||||
var uploadState by mutableStateOf<ChatFileUploadState?>(null)
|
||||
|
||||
// Stripping failure dialog
|
||||
val strippingFailureConfirmation = SuspendableConfirmation()
|
||||
|
||||
val iMetaAttachments = IMetaAttachments()
|
||||
var nip95attachments by mutableStateOf<List<Pair<FileStorageEvent, FileStorageHeaderEvent>>>(emptyList())
|
||||
|
||||
val message = TextFieldState()
|
||||
var urlPreview by mutableStateOf<String?>(null)
|
||||
val isUploadingImage: Boolean get() = uploadState?.isUploadingImage ?: false
|
||||
val isUploadingFile: Boolean get() = uploadState?.isUploadingFile ?: false
|
||||
|
||||
var userSuggestions: UserSuggestionState? = null
|
||||
var userSuggestionsMainMessage: UserSuggestionAnchor? = null
|
||||
|
||||
var emojiSuggestions: EmojiSuggestionState? = null
|
||||
|
||||
// Invoices
|
||||
var canAddInvoice by mutableStateOf(false)
|
||||
var wantsInvoice by mutableStateOf(false)
|
||||
|
||||
// Forward Zap to
|
||||
var wantsForwardZapTo by mutableStateOf(false)
|
||||
var forwardZapTo by mutableStateOf<SplitBuilder<User>>(SplitBuilder())
|
||||
var forwardZapToEditting by mutableStateOf(TextFieldValue(""))
|
||||
|
||||
// NSFW, Sensitive
|
||||
var wantsToMarkAsSensitive by mutableStateOf(false)
|
||||
var contentWarningDescription by mutableStateOf("")
|
||||
|
||||
// Expiration Date (NIP-40)
|
||||
var wantsExpirationDate by mutableStateOf(false)
|
||||
override var expirationDate by mutableLongStateOf(TimeUtils.oneDayAhead())
|
||||
|
||||
// GeoHash
|
||||
var wantsToAddGeoHash by mutableStateOf(false)
|
||||
var location: StateFlow<LocationState.LocationResult>? = null
|
||||
|
||||
// ZapRaiser
|
||||
var canAddZapRaiser by mutableStateOf(false)
|
||||
var wantsZapraiser by mutableStateOf(false)
|
||||
var zapRaiserAmount by mutableStateOf<Long?>(null)
|
||||
|
||||
fun lnAddress(): String? = account.userProfile().lnAddress()
|
||||
|
||||
fun hasLnAddress(): Boolean = account.userProfile().lnAddress() != null
|
||||
|
||||
fun user(): User = account.userProfile()
|
||||
|
||||
open fun init(accountVM: AccountViewModel) {
|
||||
this.accountViewModel = accountVM
|
||||
this.account = accountVM.account
|
||||
this.canAddInvoice = hasLnAddress()
|
||||
this.canAddZapRaiser = hasLnAddress()
|
||||
|
||||
this.userSuggestions?.reset()
|
||||
this.userSuggestions = UserSuggestionState(accountVM.account, accountVM.nip05ClientBuilder())
|
||||
|
||||
this.emojiSuggestions?.reset()
|
||||
this.emojiSuggestions = EmojiSuggestionState(accountVM.account)
|
||||
|
||||
this.uploadState = ChatFileUploadState(account.settings.defaultFileServer, account.settings.stripLocationOnUpload)
|
||||
}
|
||||
|
||||
open fun load(room: MeetingSpaceEvent) {
|
||||
this.room = room
|
||||
}
|
||||
|
||||
open fun reply(replyNote: Note) {
|
||||
replyTo.value = replyNote
|
||||
draftTag.newVersion()
|
||||
}
|
||||
|
||||
fun clearReply() {
|
||||
replyTo.value = null
|
||||
draftTag.newVersion()
|
||||
}
|
||||
|
||||
open fun editFromDraft(draft: Note) {
|
||||
val noteEvent = draft.event
|
||||
val noteAuthor = draft.author
|
||||
|
||||
if (noteEvent is DraftWrapEvent && noteAuthor != null) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
accountViewModel.createTempDraftNote(noteEvent)?.let { innerNote ->
|
||||
val oldTag = (draft.event as? AddressableEvent)?.dTag()
|
||||
if (oldTag != null) {
|
||||
draftTag.set(oldTag)
|
||||
}
|
||||
loadFromDraft(innerNote)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadFromDraft(draft: Note) {
|
||||
val draftEvent = draft.event ?: return
|
||||
|
||||
val localForwardZapTo = draftEvent.tags.zapSplitSetup()
|
||||
val totalWeight = localForwardZapTo.sumOf { it.weight }
|
||||
forwardZapTo = SplitBuilder()
|
||||
localForwardZapTo.forEach {
|
||||
if (it is ZapSplitSetup) {
|
||||
val user = LocalCache.getOrCreateUser(it.pubKeyHex)
|
||||
forwardZapTo.addItem(user, (it.weight / totalWeight).toFloat())
|
||||
}
|
||||
// don't support edditing old-style splits.
|
||||
}
|
||||
forwardZapToEditting = TextFieldValue("")
|
||||
wantsForwardZapTo = localForwardZapTo.isNotEmpty()
|
||||
|
||||
wantsToMarkAsSensitive = draftEvent.isSensitive()
|
||||
contentWarningDescription = draftEvent.contentWarningReason() ?: ""
|
||||
|
||||
val draftExpiration = draftEvent.expiration()
|
||||
wantsExpirationDate = draftExpiration != null
|
||||
expirationDate = draftExpiration ?: TimeUtils.oneDayAhead()
|
||||
|
||||
val geohash = draftEvent.getGeoHash()
|
||||
wantsToAddGeoHash = geohash != null
|
||||
|
||||
val zapraiser = draftEvent.zapraiserAmount()
|
||||
wantsZapraiser = zapraiser != null
|
||||
zapRaiserAmount = null
|
||||
if (zapraiser != null) {
|
||||
zapRaiserAmount = zapraiser
|
||||
}
|
||||
|
||||
if (forwardZapTo.items.isNotEmpty()) {
|
||||
wantsForwardZapTo = true
|
||||
}
|
||||
|
||||
if (draftEvent as? LiveActivitiesChatMessageEvent != null) {
|
||||
val replyId = draftEvent.reply()?.eventId
|
||||
if (replyId != null) {
|
||||
replyTo.value = accountViewModel.checkGetOrCreateNote(replyId)
|
||||
}
|
||||
}
|
||||
|
||||
message.setTextAndPlaceCursorAtEnd(draftEvent.content)
|
||||
|
||||
iMetaAttachments.addAll(draftEvent.imetas())
|
||||
|
||||
urlPreview = findUrlInMessage()
|
||||
}
|
||||
|
||||
fun sendPost(onDone: suspend () -> Unit) {
|
||||
accountViewModel.launchSigner {
|
||||
sendPostSync()
|
||||
onDone()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun sendPostSync() {
|
||||
val template = createTemplate() ?: return
|
||||
|
||||
val version = draftTag.current
|
||||
cancel()
|
||||
|
||||
accountViewModel.account.signAndSendPrivately(template, emptySet())
|
||||
accountViewModel.viewModelScope.launch(Dispatchers.IO) {
|
||||
accountViewModel.account.deleteDraftIgnoreErrors(version)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun sendDraftSync() {
|
||||
if (message.text.toString().isBlank()) {
|
||||
account.deleteDraftIgnoreErrors(draftTag.current)
|
||||
} else {
|
||||
val attachments = mutableSetOf<Event>()
|
||||
nip95attachments.forEach {
|
||||
attachments.add(it.first)
|
||||
attachments.add(it.second)
|
||||
}
|
||||
|
||||
val template = createTemplate() ?: return
|
||||
accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template, attachments)
|
||||
}
|
||||
}
|
||||
|
||||
fun pickedMedia(list: ImmutableList<SelectedMedia>) {
|
||||
uploadState?.load(list)
|
||||
}
|
||||
|
||||
fun upload(
|
||||
onError: (title: String, message: String) -> Unit,
|
||||
context: Context,
|
||||
onceUploaded: suspend () -> Unit,
|
||||
) = try {
|
||||
uploadUnsafe(onError, context, onceUploaded)
|
||||
} catch (_: SignerExceptions.ReadOnlyException) {
|
||||
onError(
|
||||
stringRes(context, R.string.read_only_user),
|
||||
stringRes(context, R.string.login_with_a_private_key_to_be_able_to_sign_events),
|
||||
)
|
||||
}
|
||||
|
||||
fun uploadUnsafe(
|
||||
onError: (title: String, message: String) -> Unit,
|
||||
context: Context,
|
||||
onceUploaded: suspend () -> Unit,
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val uploadState = uploadState ?: return@launch
|
||||
|
||||
val myMultiOrchestrator = uploadState.multiOrchestrator ?: return@launch
|
||||
|
||||
uploadState.mediaUploadTracker.startUpload(myMultiOrchestrator.hasNonMedia())
|
||||
|
||||
val results =
|
||||
myMultiOrchestrator.upload(
|
||||
uploadState.caption,
|
||||
uploadState.contentWarningReason,
|
||||
MediaCompressor.intToCompressorQuality(uploadState.mediaQualitySlider),
|
||||
uploadState.selectedServer,
|
||||
account,
|
||||
context,
|
||||
stripMetadata = uploadState.stripMetadata,
|
||||
onStrippingFailed = strippingFailureConfirmation::awaitConfirmation,
|
||||
)
|
||||
|
||||
if (results.allGood) {
|
||||
val urls =
|
||||
results.successful.mapNotNull { upload ->
|
||||
if (upload.result is UploadOrchestrator.OrchestratorResult.NIP95Result) {
|
||||
val nip95 = account.createNip95(upload.result.bytes, headerInfo = upload.result.fileHeader, uploadState.caption, uploadState.contentWarningReason)
|
||||
nip95attachments = nip95attachments + nip95
|
||||
val note = nip95.let { it1 -> account.consumeNip95(it1.first, it1.second) }
|
||||
|
||||
note?.toNostrUri()
|
||||
} else if (upload.result is UploadOrchestrator.OrchestratorResult.ServerResult) {
|
||||
iMetaAttachments.add(upload.result, uploadState.caption, uploadState.contentWarningReason)
|
||||
|
||||
upload.result.url
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
message.insertUrlAtCursor(urls.joinToString(" "))
|
||||
urlPreview = findUrlInMessage()
|
||||
|
||||
uploadState.reset()
|
||||
onceUploaded()
|
||||
draftTag.newVersion()
|
||||
} else {
|
||||
val errorMessages = results.errors.map { stringRes(context, it.errorResource, *it.params) }.distinct()
|
||||
|
||||
onError(stringRes(context, R.string.failed_to_upload_media_no_details), errorMessages.joinToString(".\n"))
|
||||
}
|
||||
|
||||
uploadState.mediaUploadTracker.finishUpload()
|
||||
}
|
||||
}
|
||||
|
||||
private fun roomATag(): ATag? {
|
||||
val r = room ?: return null
|
||||
return ATag(
|
||||
kind = r.kind,
|
||||
pubKeyHex = r.pubKey,
|
||||
dTag = r.dTag(),
|
||||
relay = null,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun createTemplate(): EventTemplate<out Event>? {
|
||||
val activity = roomATag() ?: return null
|
||||
val messageText = message.text.toString()
|
||||
val tagger =
|
||||
NewMessageTagger(
|
||||
message = messageText,
|
||||
pTags = listOfNotNull(replyTo.value?.author),
|
||||
eTags = listOfNotNull(replyTo.value),
|
||||
dao = accountViewModel,
|
||||
)
|
||||
tagger.run()
|
||||
|
||||
val urls = findURLs(messageText)
|
||||
val usedAttachments = iMetaAttachments.filterIsIn(urls.toSet())
|
||||
val emojis = findEmoji(messageText, accountViewModel.account.emoji.myEmojis.value)
|
||||
|
||||
val geoHash = if (wantsToAddGeoHash) (location?.value as? LocationState.LocationResult.Success)?.geoHash?.toString() else null
|
||||
|
||||
val contentWarningReason = if (wantsToMarkAsSensitive) contentWarningDescription else null
|
||||
val localExpirationDate = if (wantsExpirationDate) expirationDate else null
|
||||
|
||||
val replyingToEvent = replyTo.value?.toEventHint<LiveActivitiesChatMessageEvent>()
|
||||
|
||||
return if (replyingToEvent != null) {
|
||||
LiveActivitiesChatMessageEvent.reply(tagger.message, replyingToEvent) {
|
||||
notify(replyingToEvent.toPTag())
|
||||
|
||||
hashtags(findHashtags(tagger.message))
|
||||
references(findURLs(tagger.message))
|
||||
quotes(findNostrUris(tagger.message))
|
||||
contentWarningReason?.let { contentWarning(it) }
|
||||
localExpirationDate?.let { expiration(it) }
|
||||
|
||||
geoHash?.let { geohash(it) }
|
||||
|
||||
emojis(emojis)
|
||||
imetas(usedAttachments)
|
||||
}
|
||||
} else {
|
||||
LiveActivitiesChatMessageEvent.message(tagger.message, activity) {
|
||||
hashtags(findHashtags(tagger.message))
|
||||
references(findURLs(tagger.message))
|
||||
quotes(findNostrUris(tagger.message))
|
||||
contentWarningReason?.let { contentWarning(it) }
|
||||
localExpirationDate?.let { expiration(it) }
|
||||
|
||||
geoHash?.let { geohash(it) }
|
||||
|
||||
emojis(emojis)
|
||||
imetas(usedAttachments)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun findEmoji(
|
||||
message: String,
|
||||
myEmojiSet: List<EmojiPackState.EmojiMedia>?,
|
||||
): List<EmojiUrlTag> {
|
||||
if (myEmojiSet == null) return emptyList()
|
||||
return CustomEmoji.findAllEmojiCodes(message).mapNotNull { possibleEmoji ->
|
||||
myEmojiSet.firstOrNull { it.code == possibleEmoji }?.let { EmojiUrlTag(it.code, it.link) }
|
||||
}
|
||||
}
|
||||
|
||||
open fun cancel() {
|
||||
draftTag.rotate()
|
||||
|
||||
message.setTextAndPlaceCursorAtEnd("")
|
||||
|
||||
replyTo.value = null
|
||||
|
||||
urlPreview = null
|
||||
|
||||
wantsInvoice = false
|
||||
wantsZapraiser = false
|
||||
zapRaiserAmount = null
|
||||
|
||||
wantsForwardZapTo = false
|
||||
wantsToMarkAsSensitive = false
|
||||
contentWarningDescription = ""
|
||||
wantsToAddGeoHash = false
|
||||
|
||||
forwardZapTo = SplitBuilder()
|
||||
forwardZapToEditting = TextFieldValue("")
|
||||
|
||||
userSuggestions?.reset()
|
||||
userSuggestionsMainMessage = null
|
||||
|
||||
uploadState?.reset()
|
||||
|
||||
iMetaAttachments.reset()
|
||||
|
||||
emojiSuggestions?.reset()
|
||||
}
|
||||
|
||||
open fun findUrlInMessage(): String? = UrlParser().parseValidUrls(message.text.toString()).withScheme.firstOrNull()
|
||||
|
||||
open fun addToMessage(it: String) {
|
||||
message.setTextAndPlaceCursorAtEnd(message.text.toString() + " " + it)
|
||||
onMessageChanged()
|
||||
}
|
||||
|
||||
open fun onMessageChanged() {
|
||||
urlPreview = findUrlInMessage()
|
||||
|
||||
if (message.selection.collapsed) {
|
||||
val lastWord = message.currentWord()
|
||||
if (lastWord.startsWith("@")) {
|
||||
userSuggestionsMainMessage = UserSuggestionAnchor.MAIN_MESSAGE
|
||||
userSuggestions?.processCurrentWord(lastWord)
|
||||
} else {
|
||||
userSuggestionsMainMessage = null
|
||||
userSuggestions?.reset()
|
||||
}
|
||||
|
||||
emojiSuggestions?.processCurrentWord(lastWord)
|
||||
}
|
||||
|
||||
draftTag.newVersion()
|
||||
}
|
||||
|
||||
open fun updateZapForwardTo(newZapForwardTo: TextFieldValue) {
|
||||
forwardZapToEditting = newZapForwardTo
|
||||
if (newZapForwardTo.selection.collapsed) {
|
||||
val lastWord = newZapForwardTo.text
|
||||
userSuggestionsMainMessage = UserSuggestionAnchor.FORWARD_ZAPS
|
||||
userSuggestions?.processCurrentWord(lastWord)
|
||||
}
|
||||
}
|
||||
|
||||
open fun autocompleteWithUser(item: User) {
|
||||
userSuggestions?.let {
|
||||
if (userSuggestionsMainMessage == UserSuggestionAnchor.MAIN_MESSAGE) {
|
||||
val lastWord = message.currentWord()
|
||||
it.replaceCurrentWord(message, lastWord, item)
|
||||
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.FORWARD_ZAPS) {
|
||||
forwardZapTo.addItem(item)
|
||||
forwardZapToEditting = TextFieldValue("")
|
||||
}
|
||||
|
||||
userSuggestionsMainMessage = null
|
||||
it.reset()
|
||||
}
|
||||
|
||||
draftTag.newVersion()
|
||||
}
|
||||
|
||||
open fun autocompleteWithEmoji(item: EmojiPackState.EmojiMedia) {
|
||||
val wordToInsert = ":${item.code}:"
|
||||
message.replaceCurrentWord(wordToInsert)
|
||||
|
||||
emojiSuggestions?.reset()
|
||||
|
||||
draftTag.newVersion()
|
||||
}
|
||||
|
||||
open fun autocompleteWithEmojiUrl(item: EmojiPackState.EmojiMedia) {
|
||||
val wordToInsert = item.link + " "
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
iMetaAttachments.downloadAndPrepare(item.link) {
|
||||
Amethyst.instance.roleBasedHttpClientBuilder.okHttpClientForImage(item.link)
|
||||
}
|
||||
}
|
||||
|
||||
message.replaceCurrentWord(wordToInsert)
|
||||
|
||||
emojiSuggestions?.reset()
|
||||
|
||||
urlPreview = findUrlInMessage()
|
||||
|
||||
draftTag.newVersion()
|
||||
}
|
||||
|
||||
fun canPost(): Boolean =
|
||||
message.text.isNotBlank() &&
|
||||
uploadState?.mediaUploadTracker?.isUploading != true &&
|
||||
!wantsInvoice &&
|
||||
(!wantsZapraiser || zapRaiserAmount != null) &&
|
||||
uploadState?.multiOrchestrator == null
|
||||
|
||||
fun insertAtCursor(newElement: String) {
|
||||
message.insertUrlAtCursor(newElement)
|
||||
}
|
||||
|
||||
override fun locationManager(): LocationState = Amethyst.instance.locationManager
|
||||
|
||||
override fun locationFlow(): StateFlow<LocationState.LocationResult> {
|
||||
if (location == null) {
|
||||
location = locationManager().geohashStateFlow
|
||||
}
|
||||
|
||||
return location!!
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
Log.d("Init") { "OnCleared: ${this.javaClass.simpleName}" }
|
||||
}
|
||||
|
||||
fun updateZapPercentage(
|
||||
index: Int,
|
||||
sliderValue: Float,
|
||||
) {
|
||||
forwardZapTo.updatePercentage(index, sliderValue)
|
||||
}
|
||||
|
||||
fun updateZapFromText() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val tagger = NewMessageTagger(message.text.toString(), emptyList(), emptyList(), accountViewModel)
|
||||
tagger.run()
|
||||
tagger.pTags?.forEach { taggedUser ->
|
||||
if (!forwardZapTo.items.any { it.key == taggedUser }) {
|
||||
forwardZapTo.addItem(taggedUser)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateZapRaiserAmount(newAmount: Long?) {
|
||||
zapRaiserAmount = newAmount
|
||||
draftTag.newVersion()
|
||||
}
|
||||
|
||||
fun toggleMarkAsSensitive() {
|
||||
wantsToMarkAsSensitive = !wantsToMarkAsSensitive
|
||||
draftTag.newVersion()
|
||||
}
|
||||
|
||||
fun toggleExpirationDate() {
|
||||
wantsExpirationDate = !wantsExpirationDate
|
||||
if (wantsExpirationDate) {
|
||||
expirationDate = TimeUtils.oneDayAhead()
|
||||
}
|
||||
draftTag.newVersion()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user