Normalizes NIP-22 viewmodel to its own entity
This commit is contained in:
@@ -69,8 +69,8 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.DiscoverScreen
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds.NewProductScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds.NewProductScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.drafts.DraftListScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.drafts.DraftListScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.dvms.DvmContentDiscoveryScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.dvms.DvmContentDiscoveryScreen
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.GeoHashPostScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.GeoHashScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.GeoHashScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.GeoPostScreen
|
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.HashtagPostScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.HashtagPostScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.HashtagScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.HashtagScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.HomeScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.HomeScreen
|
||||||
@@ -143,7 +143,7 @@ fun AppNavigation(
|
|||||||
composableArgs<Route.EventRedirect> { LoadRedirectScreen(it.id, accountViewModel, nav) }
|
composableArgs<Route.EventRedirect> { LoadRedirectScreen(it.id, accountViewModel, nav) }
|
||||||
|
|
||||||
composableFromBottomArgs<Route.GeoPost> {
|
composableFromBottomArgs<Route.GeoPost> {
|
||||||
GeoPostScreen(
|
GeoHashPostScreen(
|
||||||
geohash = it.geohash,
|
geohash = it.geohash,
|
||||||
message = it.message,
|
message = it.message,
|
||||||
attachment = it.attachment?.ifBlank { null }?.toUri(),
|
attachment = it.attachment?.ifBlank { null }?.toUri(),
|
||||||
|
|||||||
+64
@@ -0,0 +1,64 @@
|
|||||||
|
/**
|
||||||
|
* 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.note.creators.previews
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.FillWidthQuoteBorderModifier
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.HalfHorzPadding
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.Height100Modifier
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.Size5dp
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.SquaredQuoteBorderModifier
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DisplayPreviews(
|
||||||
|
state: PreviewState,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
val urlPreviews by state.results.collectAsStateWithLifecycle(emptyList())
|
||||||
|
|
||||||
|
if (urlPreviews.isNotEmpty()) {
|
||||||
|
Row(HalfHorzPadding) {
|
||||||
|
if (urlPreviews.size > 1) {
|
||||||
|
LazyRow(Height100Modifier, horizontalArrangement = spacedBy(Size5dp)) {
|
||||||
|
items(urlPreviews) {
|
||||||
|
Box(SquaredQuoteBorderModifier) {
|
||||||
|
PreviewUrl(it, accountViewModel, nav)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Box(FillWidthQuoteBorderModifier) {
|
||||||
|
PreviewUrlFillWidth(urlPreviews[0], accountViewModel, nav)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+38
-23
@@ -18,7 +18,7 @@
|
|||||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
* 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.
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag
|
package com.vitorpamplona.amethyst.ui.note.nip22Comments
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
@@ -37,7 +37,7 @@ import com.vitorpamplona.amethyst.model.Account
|
|||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState.EmojiMedia
|
import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState
|
||||||
import com.vitorpamplona.amethyst.service.location.LocationState
|
import com.vitorpamplona.amethyst.service.location.LocationState
|
||||||
import com.vitorpamplona.amethyst.service.uploads.MediaCompressor
|
import com.vitorpamplona.amethyst.service.uploads.MediaCompressor
|
||||||
import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator
|
import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator
|
||||||
@@ -85,8 +85,8 @@ import com.vitorpamplona.quartz.nip37Drafts.DraftEvent
|
|||||||
import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplits
|
import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplits
|
||||||
import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiser
|
import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiser
|
||||||
import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount
|
import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount
|
||||||
import com.vitorpamplona.quartz.nip73ExternalIds.topics.HashtagId
|
import com.vitorpamplona.quartz.nip73ExternalIds.ExternalId
|
||||||
import com.vitorpamplona.quartz.nip73ExternalIds.topics.hashtagScope
|
import com.vitorpamplona.quartz.nip73ExternalIds.scope
|
||||||
import com.vitorpamplona.quartz.nip92IMeta.IMetaTagBuilder
|
import com.vitorpamplona.quartz.nip92IMeta.IMetaTagBuilder
|
||||||
import com.vitorpamplona.quartz.nip92IMeta.imetas
|
import com.vitorpamplona.quartz.nip92IMeta.imetas
|
||||||
import com.vitorpamplona.quartz.nip94FileMetadata.alt
|
import com.vitorpamplona.quartz.nip94FileMetadata.alt
|
||||||
@@ -104,9 +104,10 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.collections.plus
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
open class HashtagPostViewModel :
|
open class CommentPostViewModel :
|
||||||
ViewModel(),
|
ViewModel(),
|
||||||
ILocationGrabber,
|
ILocationGrabber,
|
||||||
IMessageField,
|
IMessageField,
|
||||||
@@ -125,11 +126,13 @@ open class HashtagPostViewModel :
|
|||||||
var accountViewModel: AccountViewModel? = null
|
var accountViewModel: AccountViewModel? = null
|
||||||
var account: Account? = null
|
var account: Account? = null
|
||||||
|
|
||||||
var externalIdentity by mutableStateOf<HashtagId?>(null)
|
var externalIdentity by mutableStateOf<ExternalId?>(null)
|
||||||
var replyingTo: Note? by mutableStateOf<Note?>(null)
|
var replyingTo: Note? by mutableStateOf<Note?>(null)
|
||||||
|
|
||||||
val iMetaAttachments = IMetaAttachments()
|
val iMetaAttachments = IMetaAttachments()
|
||||||
var nip95attachments by mutableStateOf<List<Pair<FileStorageEvent, FileStorageHeaderEvent>>>(emptyList())
|
var nip95attachments by mutableStateOf<List<Pair<FileStorageEvent, FileStorageHeaderEvent>>>(
|
||||||
|
emptyList(),
|
||||||
|
)
|
||||||
|
|
||||||
var notifying by mutableStateOf<List<User>?>(null)
|
var notifying by mutableStateOf<List<User>?>(null)
|
||||||
|
|
||||||
@@ -192,7 +195,7 @@ open class HashtagPostViewModel :
|
|||||||
this.emojiSuggestions = EmojiSuggestionState(accountVM)
|
this.emojiSuggestions = EmojiSuggestionState(accountVM)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun newPostFor(externalIdentity: HashtagId) {
|
fun newPostFor(externalIdentity: ExternalId) {
|
||||||
this.externalIdentity = externalIdentity
|
this.externalIdentity = externalIdentity
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,10 +223,10 @@ open class HashtagPostViewModel :
|
|||||||
open fun reply(post: Note) {
|
open fun reply(post: Note) {
|
||||||
val accountViewModel = accountViewModel ?: return
|
val accountViewModel = accountViewModel ?: return
|
||||||
val noteEvent = post.event as? CommentEvent ?: return
|
val noteEvent = post.event as? CommentEvent ?: return
|
||||||
val hashtag = noteEvent.hashtagScope() ?: return
|
val scope = noteEvent.scope() ?: return
|
||||||
|
|
||||||
this.replyingTo = post
|
this.replyingTo = post
|
||||||
this.externalIdentity = HashtagId(hashtag)
|
this.externalIdentity = scope
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun quote(quote: Note) {
|
open fun quote(quote: Note) {
|
||||||
@@ -260,8 +263,8 @@ open class HashtagPostViewModel :
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun loadFromDraft(draftEvent: CommentEvent) {
|
private fun loadFromDraft(draftEvent: CommentEvent) {
|
||||||
val hashtag = draftEvent.hashtagScope() ?: return
|
val scope = draftEvent.scope() ?: return
|
||||||
this.externalIdentity = HashtagId(hashtag)
|
this.externalIdentity = scope
|
||||||
|
|
||||||
canAddInvoice = accountViewModel?.userProfile()?.info?.lnAddress() != null
|
canAddInvoice = accountViewModel?.userProfile()?.info?.lnAddress() != null
|
||||||
canAddZapRaiser = accountViewModel?.userProfile()?.info?.lnAddress() != null
|
canAddZapRaiser = accountViewModel?.userProfile()?.info?.lnAddress() != null
|
||||||
@@ -371,7 +374,7 @@ open class HashtagPostViewModel :
|
|||||||
if (replyingTo != null) {
|
if (replyingTo != null) {
|
||||||
val eventHint = replyingTo.toEventHint<Event>() ?: return null
|
val eventHint = replyingTo.toEventHint<Event>() ?: return null
|
||||||
|
|
||||||
CommentEvent.replyBuilder(
|
CommentEvent.Companion.replyBuilder(
|
||||||
msg = tagger.message,
|
msg = tagger.message,
|
||||||
replyingTo = eventHint,
|
replyingTo = eventHint,
|
||||||
) {
|
) {
|
||||||
@@ -391,7 +394,7 @@ open class HashtagPostViewModel :
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val externalIdentity = externalIdentity ?: return null
|
val externalIdentity = externalIdentity ?: return null
|
||||||
CommentEvent.replyExternalIdentity(
|
CommentEvent.Companion.replyExternalIdentity(
|
||||||
msg = tagger.message,
|
msg = tagger.message,
|
||||||
extId = externalIdentity,
|
extId = externalIdentity,
|
||||||
) {
|
) {
|
||||||
@@ -416,11 +419,16 @@ open class HashtagPostViewModel :
|
|||||||
|
|
||||||
fun findEmoji(
|
fun findEmoji(
|
||||||
message: String,
|
message: String,
|
||||||
myEmojiSet: List<EmojiMedia>?,
|
myEmojiSet: List<EmojiPackState.EmojiMedia>?,
|
||||||
): List<EmojiUrlTag> {
|
): List<EmojiUrlTag> {
|
||||||
if (myEmojiSet == null) return emptyList()
|
if (myEmojiSet == null) return emptyList()
|
||||||
return CustomEmoji.findAllEmojiCodes(message).mapNotNull { possibleEmoji ->
|
return CustomEmoji.Companion.findAllEmojiCodes(message).mapNotNull { possibleEmoji ->
|
||||||
myEmojiSet.firstOrNull { it.code == possibleEmoji }?.let { EmojiUrlTag(it.code, it.link.url) }
|
myEmojiSet.firstOrNull { it.code == possibleEmoji }?.let {
|
||||||
|
EmojiUrlTag(
|
||||||
|
it.code,
|
||||||
|
it.link.url,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -443,7 +451,7 @@ open class HashtagPostViewModel :
|
|||||||
viewModelScope,
|
viewModelScope,
|
||||||
alt,
|
alt,
|
||||||
contentWarningReason,
|
contentWarningReason,
|
||||||
MediaCompressor.intToCompressorQuality(mediaQuality),
|
MediaCompressor.Companion.intToCompressorQuality(mediaQuality),
|
||||||
server,
|
server,
|
||||||
myAccount,
|
myAccount,
|
||||||
context,
|
context,
|
||||||
@@ -489,7 +497,14 @@ open class HashtagPostViewModel :
|
|||||||
|
|
||||||
multiOrchestrator = null
|
multiOrchestrator = null
|
||||||
} else {
|
} else {
|
||||||
val errorMessages = results.errors.map { stringRes(context, it.errorResource, *it.params) }.distinct()
|
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"))
|
onError(stringRes(context, R.string.failed_to_upload_media_no_details), errorMessages.joinToString(".\n"))
|
||||||
}
|
}
|
||||||
@@ -604,7 +619,7 @@ open class HashtagPostViewModel :
|
|||||||
draftTag.newVersion()
|
draftTag.newVersion()
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun autocompleteWithEmoji(item: EmojiMedia) {
|
open fun autocompleteWithEmoji(item: EmojiPackState.EmojiMedia) {
|
||||||
val wordToInsert = ":${item.code}:"
|
val wordToInsert = ":${item.code}:"
|
||||||
|
|
||||||
message = message.replaceCurrentWord(wordToInsert)
|
message = message.replaceCurrentWord(wordToInsert)
|
||||||
@@ -615,13 +630,13 @@ open class HashtagPostViewModel :
|
|||||||
draftTag.newVersion()
|
draftTag.newVersion()
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun autocompleteWithEmojiUrl(item: EmojiMedia) {
|
open fun autocompleteWithEmojiUrl(item: EmojiPackState.EmojiMedia) {
|
||||||
val wordToInsert = item.link.url + " "
|
val wordToInsert = item.link.url + " "
|
||||||
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
iMetaAttachments.downloadAndPrepare(
|
iMetaAttachments.downloadAndPrepare(
|
||||||
item.link.url,
|
item.link.url,
|
||||||
{ Amethyst.instance.okHttpClients.getHttpClient(accountViewModel?.account?.shouldUseTorForImageDownload(item.link.url) ?: false) },
|
{ Amethyst.Companion.instance.okHttpClients.getHttpClient(accountViewModel?.account?.shouldUseTorForImageDownload(item.link.url) ?: false) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -686,5 +701,5 @@ open class HashtagPostViewModel :
|
|||||||
return location!!
|
return location!!
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun locationManager(): LocationState = Amethyst.instance.locationManager
|
override fun locationManager(): LocationState = Amethyst.Companion.instance.locationManager
|
||||||
}
|
}
|
||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* 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.note.nip22Comments
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||||
|
import com.vitorpamplona.quartz.nip73ExternalIds.ExternalId
|
||||||
|
import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId
|
||||||
|
import com.vitorpamplona.quartz.nip73ExternalIds.topics.HashtagId
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DisplayExternalId(
|
||||||
|
externalId: ExternalId,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
when (externalId) {
|
||||||
|
is GeohashId -> DisplayGeohashExternalId(externalId, nav)
|
||||||
|
is HashtagId -> DisplayHashtagExternalId(externalId, nav)
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
+83
@@ -0,0 +1,83 @@
|
|||||||
|
/**
|
||||||
|
* 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.note.nip22Comments
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.LocationOn
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.LocalTextStyle
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.LinkAnnotation
|
||||||
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.withLink
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.Route
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.creators.location.LoadCityName
|
||||||
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
||||||
|
import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DisplayGeohashExternalId(
|
||||||
|
externalId: GeohashId,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
Row(modifier = MaterialTheme.colorScheme.replyModifier.padding(10.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
LoadCityName(externalId.geohash) { cityName ->
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.LocationOn,
|
||||||
|
contentDescription = stringRes(id = R.string.geohash_exclusive),
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(StdHorzSpacer)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text =
|
||||||
|
buildAnnotatedString {
|
||||||
|
withLink(
|
||||||
|
LinkAnnotation.Clickable("cityname") { nav.nav(Route.Geohash(externalId.geohash)) },
|
||||||
|
) {
|
||||||
|
append(cityName)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
style =
|
||||||
|
LocalTextStyle.current.copy(
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
),
|
||||||
|
maxLines = 1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+80
@@ -0,0 +1,80 @@
|
|||||||
|
/**
|
||||||
|
* 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.note.nip22Comments
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Tag
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.LocalTextStyle
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.LinkAnnotation
|
||||||
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.withLink
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.Route
|
||||||
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
||||||
|
import com.vitorpamplona.quartz.nip73ExternalIds.topics.HashtagId
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DisplayHashtagExternalId(
|
||||||
|
externalId: HashtagId,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
Row(modifier = MaterialTheme.colorScheme.replyModifier.padding(10.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Tag,
|
||||||
|
contentDescription = stringRes(id = R.string.hashtag_exclusive),
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(StdHorzSpacer)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text =
|
||||||
|
buildAnnotatedString {
|
||||||
|
withLink(
|
||||||
|
LinkAnnotation.Clickable("hashtag") { nav.nav(Route.Hashtag(externalId.topic)) },
|
||||||
|
) {
|
||||||
|
append(externalId.topic)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
style =
|
||||||
|
LocalTextStyle.current.copy(
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
),
|
||||||
|
maxLines = 1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-38
@@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.compose.foundation.horizontalScroll
|
import androidx.compose.foundation.horizontalScroll
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -35,8 +34,6 @@ import androidx.compose.foundation.layout.height
|
|||||||
import androidx.compose.foundation.layout.heightIn
|
import androidx.compose.foundation.layout.heightIn
|
||||||
import androidx.compose.foundation.layout.imePadding
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
@@ -60,7 +57,6 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
@@ -81,8 +77,7 @@ import com.vitorpamplona.amethyst.ui.note.creators.invoice.InvoiceRequest
|
|||||||
import com.vitorpamplona.amethyst.ui.note.creators.location.AddGeoHashButton
|
import com.vitorpamplona.amethyst.ui.note.creators.location.AddGeoHashButton
|
||||||
import com.vitorpamplona.amethyst.ui.note.creators.location.LocationAsHash
|
import com.vitorpamplona.amethyst.ui.note.creators.location.LocationAsHash
|
||||||
import com.vitorpamplona.amethyst.ui.note.creators.messagefield.MessageField
|
import com.vitorpamplona.amethyst.ui.note.creators.messagefield.MessageField
|
||||||
import com.vitorpamplona.amethyst.ui.note.creators.previews.PreviewUrl
|
import com.vitorpamplona.amethyst.ui.note.creators.previews.DisplayPreviews
|
||||||
import com.vitorpamplona.amethyst.ui.note.creators.previews.PreviewUrlFillWidth
|
|
||||||
import com.vitorpamplona.amethyst.ui.note.creators.secretEmoji.AddSecretEmojiButton
|
import com.vitorpamplona.amethyst.ui.note.creators.secretEmoji.AddSecretEmojiButton
|
||||||
import com.vitorpamplona.amethyst.ui.note.creators.secretEmoji.SecretEmojiRequest
|
import com.vitorpamplona.amethyst.ui.note.creators.secretEmoji.SecretEmojiRequest
|
||||||
import com.vitorpamplona.amethyst.ui.note.creators.uploads.ImageVideoDescription
|
import com.vitorpamplona.amethyst.ui.note.creators.uploads.ImageVideoDescription
|
||||||
@@ -96,13 +91,9 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.CloseButton
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.CloseButton
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.PostButton
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.PostButton
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.FillWidthQuoteBorderModifier
|
|
||||||
import com.vitorpamplona.amethyst.ui.theme.HalfHorzPadding
|
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Height100Modifier
|
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size5dp
|
import com.vitorpamplona.amethyst.ui.theme.Size5dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.SquaredQuoteBorderModifier
|
|
||||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
@@ -283,7 +274,7 @@ private fun NewProductBody(
|
|||||||
MessageField(R.string.description, postViewModel)
|
MessageField(R.string.description, postViewModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
DisplayPreviews(postViewModel, accountViewModel, nav)
|
DisplayPreviews(postViewModel.urlPreviews, accountViewModel, nav)
|
||||||
|
|
||||||
if (postViewModel.wantsToMarkAsSensitive) {
|
if (postViewModel.wantsToMarkAsSensitive) {
|
||||||
Row(
|
Row(
|
||||||
@@ -403,33 +394,6 @@ private fun NewProductBody(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun DisplayPreviews(
|
|
||||||
postViewModel: NewProductViewModel,
|
|
||||||
accountViewModel: AccountViewModel,
|
|
||||||
nav: INav,
|
|
||||||
) {
|
|
||||||
val urlPreviews by postViewModel.urlPreviews.results.collectAsStateWithLifecycle(emptyList())
|
|
||||||
|
|
||||||
if (urlPreviews.isNotEmpty()) {
|
|
||||||
Row(HalfHorzPadding) {
|
|
||||||
if (urlPreviews.size > 1) {
|
|
||||||
LazyRow(Height100Modifier, horizontalArrangement = spacedBy(Size5dp)) {
|
|
||||||
items(urlPreviews) {
|
|
||||||
Box(SquaredQuoteBorderModifier) {
|
|
||||||
PreviewUrl(it, accountViewModel, nav)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Box(FillWidthQuoteBorderModifier) {
|
|
||||||
PreviewUrlFillWidth(urlPreviews[0], accountViewModel, nav)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun BottomRowActions(postViewModel: NewProductViewModel) {
|
private fun BottomRowActions(postViewModel: NewProductViewModel) {
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
|
|||||||
+10
-92
@@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.compose.foundation.horizontalScroll
|
import androidx.compose.foundation.horizontalScroll
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -35,21 +34,14 @@ import androidx.compose.foundation.layout.height
|
|||||||
import androidx.compose.foundation.layout.heightIn
|
import androidx.compose.foundation.layout.heightIn
|
||||||
import androidx.compose.foundation.layout.imePadding
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.LocationOn
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.LocalTextStyle
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -59,13 +51,8 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Alignment.Companion.CenterVertically
|
import androidx.compose.ui.Alignment.Companion.CenterVertically
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.LinkAnnotation
|
|
||||||
import androidx.compose.ui.text.buildAnnotatedString
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.text.withLink
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
@@ -75,9 +62,7 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
|
|||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery
|
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton
|
import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.Nav
|
import com.vitorpamplona.amethyst.ui.navigation.Nav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.Route
|
|
||||||
import com.vitorpamplona.amethyst.ui.note.BaseUserPicture
|
import com.vitorpamplona.amethyst.ui.note.BaseUserPicture
|
||||||
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||||
import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.ContentSensitivityExplainer
|
import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.ContentSensitivityExplainer
|
||||||
@@ -86,10 +71,8 @@ import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.ShowEmojiSug
|
|||||||
import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList
|
import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList
|
||||||
import com.vitorpamplona.amethyst.ui.note.creators.invoice.AddLnInvoiceButton
|
import com.vitorpamplona.amethyst.ui.note.creators.invoice.AddLnInvoiceButton
|
||||||
import com.vitorpamplona.amethyst.ui.note.creators.invoice.InvoiceRequest
|
import com.vitorpamplona.amethyst.ui.note.creators.invoice.InvoiceRequest
|
||||||
import com.vitorpamplona.amethyst.ui.note.creators.location.LoadCityName
|
|
||||||
import com.vitorpamplona.amethyst.ui.note.creators.messagefield.MessageField
|
import com.vitorpamplona.amethyst.ui.note.creators.messagefield.MessageField
|
||||||
import com.vitorpamplona.amethyst.ui.note.creators.previews.PreviewUrl
|
import com.vitorpamplona.amethyst.ui.note.creators.previews.DisplayPreviews
|
||||||
import com.vitorpamplona.amethyst.ui.note.creators.previews.PreviewUrlFillWidth
|
|
||||||
import com.vitorpamplona.amethyst.ui.note.creators.secretEmoji.AddSecretEmojiButton
|
import com.vitorpamplona.amethyst.ui.note.creators.secretEmoji.AddSecretEmojiButton
|
||||||
import com.vitorpamplona.amethyst.ui.note.creators.secretEmoji.SecretEmojiRequest
|
import com.vitorpamplona.amethyst.ui.note.creators.secretEmoji.SecretEmojiRequest
|
||||||
import com.vitorpamplona.amethyst.ui.note.creators.uploads.ImageVideoDescription
|
import com.vitorpamplona.amethyst.ui.note.creators.uploads.ImageVideoDescription
|
||||||
@@ -98,19 +81,17 @@ import com.vitorpamplona.amethyst.ui.note.creators.zapraiser.AddZapraiserButton
|
|||||||
import com.vitorpamplona.amethyst.ui.note.creators.zapraiser.ZapRaiserRequest
|
import com.vitorpamplona.amethyst.ui.note.creators.zapraiser.ZapRaiserRequest
|
||||||
import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.ForwardZapTo
|
import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.ForwardZapTo
|
||||||
import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.ForwardZapToButton
|
import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.ForwardZapToButton
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.nip22Comments.CommentPostViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.nip22Comments.DisplayExternalId
|
||||||
import com.vitorpamplona.amethyst.ui.painterRes
|
import com.vitorpamplona.amethyst.ui.painterRes
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.CloseButton
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.CloseButton
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.Notifying
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.Notifying
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.PostButton
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.PostButton
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.FillWidthQuoteBorderModifier
|
|
||||||
import com.vitorpamplona.amethyst.ui.theme.HalfHorzPadding
|
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Height100Modifier
|
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size5dp
|
import com.vitorpamplona.amethyst.ui.theme.Size5dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.SquaredQuoteBorderModifier
|
|
||||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||||
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
||||||
@@ -122,7 +103,7 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun GeoPostScreen(
|
fun GeoHashPostScreen(
|
||||||
geohash: String? = null,
|
geohash: String? = null,
|
||||||
message: String? = null,
|
message: String? = null,
|
||||||
attachment: Uri? = null,
|
attachment: Uri? = null,
|
||||||
@@ -132,7 +113,7 @@ fun GeoPostScreen(
|
|||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: Nav,
|
nav: Nav,
|
||||||
) {
|
) {
|
||||||
val postViewModel: GeoPostViewModel = viewModel()
|
val postViewModel: CommentPostViewModel = viewModel()
|
||||||
postViewModel.init(accountViewModel)
|
postViewModel.init(accountViewModel)
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@@ -168,7 +149,7 @@ fun GeoPostScreen(
|
|||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun NewGeoPostScreenInner(
|
fun NewGeoPostScreenInner(
|
||||||
postViewModel: GeoPostViewModel,
|
postViewModel: CommentPostViewModel,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: Nav,
|
nav: Nav,
|
||||||
) {
|
) {
|
||||||
@@ -261,7 +242,7 @@ fun NewGeoPostScreenInner(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun NewGeoPostBody(
|
private fun NewGeoPostBody(
|
||||||
postViewModel: GeoPostViewModel,
|
postViewModel: CommentPostViewModel,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: Nav,
|
nav: Nav,
|
||||||
) {
|
) {
|
||||||
@@ -280,7 +261,7 @@ private fun NewGeoPostBody(
|
|||||||
Column(Modifier.fillMaxWidth().verticalScroll(scrollState)) {
|
Column(Modifier.fillMaxWidth().verticalScroll(scrollState)) {
|
||||||
postViewModel.externalIdentity?.let {
|
postViewModel.externalIdentity?.let {
|
||||||
Row {
|
Row {
|
||||||
DisplayExternalId(it, accountViewModel, nav)
|
DisplayExternalId(it, nav)
|
||||||
Spacer(modifier = StdVertSpacer)
|
Spacer(modifier = StdVertSpacer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -321,7 +302,7 @@ private fun NewGeoPostBody(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
DisplayPreviews(postViewModel, accountViewModel, nav)
|
DisplayPreviews(postViewModel.urlPreviews, accountViewModel, nav)
|
||||||
|
|
||||||
if (postViewModel.wantsToMarkAsSensitive) {
|
if (postViewModel.wantsToMarkAsSensitive) {
|
||||||
Row(
|
Row(
|
||||||
@@ -432,43 +413,7 @@ private fun NewGeoPostBody(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DisplayExternalId(
|
private fun BottomRowActions(postViewModel: CommentPostViewModel) {
|
||||||
externalId: GeohashId,
|
|
||||||
accountViewModel: AccountViewModel,
|
|
||||||
nav: INav,
|
|
||||||
) {
|
|
||||||
Row(modifier = MaterialTheme.colorScheme.replyModifier.padding(10.dp), verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
LoadCityName(externalId.geohash) { cityName ->
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.LocationOn,
|
|
||||||
contentDescription = stringRes(id = R.string.geohash_exclusive),
|
|
||||||
modifier = Modifier.size(20.dp),
|
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(StdHorzSpacer)
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text =
|
|
||||||
buildAnnotatedString {
|
|
||||||
withLink(
|
|
||||||
LinkAnnotation.Clickable("cityname") { nav.nav(Route.Geohash(externalId.geohash)) },
|
|
||||||
) {
|
|
||||||
append(cityName)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
style =
|
|
||||||
LocalTextStyle.current.copy(
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
),
|
|
||||||
maxLines = 1,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun BottomRowActions(postViewModel: GeoPostViewModel) {
|
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
@@ -517,30 +462,3 @@ private fun BottomRowActions(postViewModel: GeoPostViewModel) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun DisplayPreviews(
|
|
||||||
postViewModel: GeoPostViewModel,
|
|
||||||
accountViewModel: AccountViewModel,
|
|
||||||
nav: INav,
|
|
||||||
) {
|
|
||||||
val urlPreviews by postViewModel.urlPreviews.results.collectAsStateWithLifecycle(emptyList())
|
|
||||||
|
|
||||||
if (urlPreviews.isNotEmpty()) {
|
|
||||||
Row(HalfHorzPadding) {
|
|
||||||
if (urlPreviews.size > 1) {
|
|
||||||
LazyRow(Height100Modifier, horizontalArrangement = spacedBy(Size5dp)) {
|
|
||||||
items(urlPreviews) {
|
|
||||||
Box(SquaredQuoteBorderModifier) {
|
|
||||||
PreviewUrl(it, accountViewModel, nav)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Box(FillWidthQuoteBorderModifier) {
|
|
||||||
PreviewUrlFillWidth(urlPreviews[0], accountViewModel, nav)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-663
@@ -1,663 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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.geohash
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.compose.runtime.Stable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
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.model.Account
|
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
|
||||||
import com.vitorpamplona.amethyst.model.User
|
|
||||||
import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState.EmojiMedia
|
|
||||||
import com.vitorpamplona.amethyst.service.uploads.MediaCompressor
|
|
||||||
import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator
|
|
||||||
import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator
|
|
||||||
import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger
|
|
||||||
import com.vitorpamplona.amethyst.ui.actions.UserSuggestionAnchor
|
|
||||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
|
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing
|
|
||||||
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.messagefield.IMessageField
|
|
||||||
import com.vitorpamplona.amethyst.ui.note.creators.previews.PreviewState
|
|
||||||
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState
|
|
||||||
import com.vitorpamplona.amethyst.ui.note.creators.zapraiser.IZapRaiser
|
|
||||||
import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.IZapField
|
|
||||||
import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.SplitBuilder
|
|
||||||
import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.toZapSplitSetup
|
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments
|
|
||||||
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.tags.hashtags.hashtags
|
|
||||||
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.nip18Reposts.quotes.taggedQuoteIds
|
|
||||||
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
|
||||||
import com.vitorpamplona.quartz.nip22Comments.notify
|
|
||||||
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.isSensitive
|
|
||||||
import com.vitorpamplona.quartz.nip37Drafts.DraftEvent
|
|
||||||
import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplits
|
|
||||||
import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiser
|
|
||||||
import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount
|
|
||||||
import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId
|
|
||||||
import com.vitorpamplona.quartz.nip73ExternalIds.location.geohashedScope
|
|
||||||
import com.vitorpamplona.quartz.nip92IMeta.IMetaTagBuilder
|
|
||||||
import com.vitorpamplona.quartz.nip92IMeta.imetas
|
|
||||||
import com.vitorpamplona.quartz.nip94FileMetadata.alt
|
|
||||||
import com.vitorpamplona.quartz.nip94FileMetadata.blurhash
|
|
||||||
import com.vitorpamplona.quartz.nip94FileMetadata.dims
|
|
||||||
import com.vitorpamplona.quartz.nip94FileMetadata.hash
|
|
||||||
import com.vitorpamplona.quartz.nip94FileMetadata.magnet
|
|
||||||
import com.vitorpamplona.quartz.nip94FileMetadata.mimeType
|
|
||||||
import com.vitorpamplona.quartz.nip94FileMetadata.originalHash
|
|
||||||
import com.vitorpamplona.quartz.nip94FileMetadata.sensitiveContent
|
|
||||||
import com.vitorpamplona.quartz.nip94FileMetadata.size
|
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
@Stable
|
|
||||||
open class GeoPostViewModel :
|
|
||||||
ViewModel(),
|
|
||||||
IMessageField,
|
|
||||||
IZapField,
|
|
||||||
IZapRaiser {
|
|
||||||
val draftTag = DraftTagState()
|
|
||||||
|
|
||||||
init {
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
draftTag.versions.collectLatest {
|
|
||||||
sendDraftSync()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var accountViewModel: AccountViewModel? = null
|
|
||||||
var account: Account? = null
|
|
||||||
|
|
||||||
var externalIdentity by mutableStateOf<GeohashId?>(null)
|
|
||||||
var replyingTo: Note? by mutableStateOf<Note?>(null)
|
|
||||||
|
|
||||||
val iMetaAttachments = IMetaAttachments()
|
|
||||||
var nip95attachments by mutableStateOf<List<Pair<FileStorageEvent, FileStorageHeaderEvent>>>(emptyList())
|
|
||||||
|
|
||||||
var notifying by mutableStateOf<List<User>?>(null)
|
|
||||||
|
|
||||||
override var message by mutableStateOf(TextFieldValue(""))
|
|
||||||
|
|
||||||
val urlPreviews = PreviewState()
|
|
||||||
|
|
||||||
var isUploadingImage by mutableStateOf(false)
|
|
||||||
|
|
||||||
var userSuggestions: UserSuggestionState? = null
|
|
||||||
var userSuggestionsMainMessage: UserSuggestionAnchor? = null
|
|
||||||
|
|
||||||
var emojiSuggestions: EmojiSuggestionState? = null
|
|
||||||
|
|
||||||
// Images and Videos
|
|
||||||
var multiOrchestrator by mutableStateOf<MultiOrchestrator?>(null)
|
|
||||||
|
|
||||||
// Invoices
|
|
||||||
var canAddInvoice by mutableStateOf(false)
|
|
||||||
var wantsInvoice by mutableStateOf(false)
|
|
||||||
|
|
||||||
var wantsSecretEmoji by mutableStateOf(false)
|
|
||||||
|
|
||||||
// Forward Zap to
|
|
||||||
var wantsForwardZapTo by mutableStateOf(false)
|
|
||||||
override var forwardZapTo = mutableStateOf<SplitBuilder<User>>(SplitBuilder())
|
|
||||||
override var forwardZapToEditting = mutableStateOf(TextFieldValue(""))
|
|
||||||
|
|
||||||
// NSFW, Sensitive
|
|
||||||
var wantsToMarkAsSensitive by mutableStateOf(false)
|
|
||||||
|
|
||||||
// ZapRaiser
|
|
||||||
var canAddZapRaiser by mutableStateOf(false)
|
|
||||||
var wantsZapraiser by mutableStateOf(false)
|
|
||||||
override val zapRaiserAmount = mutableStateOf<Long?>(null)
|
|
||||||
|
|
||||||
var showRelaysDialog by mutableStateOf(false)
|
|
||||||
var relayList by mutableStateOf<ImmutableList<String>?>(null)
|
|
||||||
|
|
||||||
fun lnAddress(): String? = account?.userProfile()?.info?.lnAddress()
|
|
||||||
|
|
||||||
fun hasLnAddress(): Boolean = account?.userProfile()?.info?.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)
|
|
||||||
|
|
||||||
this.emojiSuggestions?.reset()
|
|
||||||
this.emojiSuggestions = EmojiSuggestionState(accountVM)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun newPostFor(externalIdentity: GeohashId) {
|
|
||||||
this.externalIdentity = externalIdentity
|
|
||||||
}
|
|
||||||
|
|
||||||
fun editFromDraft(draft: Note) {
|
|
||||||
val accountViewModel = accountViewModel ?: return
|
|
||||||
|
|
||||||
val noteEvent = draft.event
|
|
||||||
val noteAuthor = draft.author
|
|
||||||
|
|
||||||
if (noteEvent is DraftEvent && noteAuthor != null) {
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
accountViewModel.createTempDraftNote(noteEvent) { innerNote ->
|
|
||||||
if (innerNote != null) {
|
|
||||||
val oldTag = (draft.event as? AddressableEvent)?.dTag()
|
|
||||||
if (oldTag != null) {
|
|
||||||
draftTag.set(oldTag)
|
|
||||||
}
|
|
||||||
loadFromDraft(innerNote)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun reply(post: Note) {
|
|
||||||
val accountViewModel = accountViewModel ?: return
|
|
||||||
val noteEvent = post.event as? CommentEvent ?: return
|
|
||||||
val geohash = noteEvent.geohashedScope() ?: return
|
|
||||||
|
|
||||||
this.replyingTo = post
|
|
||||||
this.externalIdentity = GeohashId(geohash)
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun quote(quote: Note) {
|
|
||||||
val accountViewModel = accountViewModel ?: return
|
|
||||||
|
|
||||||
message = TextFieldValue(message.text + "\nnostr:${quote.toNEvent()}")
|
|
||||||
|
|
||||||
quote.author?.let { quotedUser ->
|
|
||||||
if (quotedUser.pubkeyHex != accountViewModel.userProfile().pubkeyHex) {
|
|
||||||
if (forwardZapTo.value.items.none { it.key.pubkeyHex == quotedUser.pubkeyHex }) {
|
|
||||||
forwardZapTo.value.addItem(quotedUser)
|
|
||||||
}
|
|
||||||
if (forwardZapTo.value.items.none { it.key.pubkeyHex == accountViewModel.userProfile().pubkeyHex }) {
|
|
||||||
forwardZapTo.value.addItem(accountViewModel.userProfile())
|
|
||||||
}
|
|
||||||
|
|
||||||
val pos = forwardZapTo.value.items.indexOfFirst { it.key.pubkeyHex == quotedUser.pubkeyHex }
|
|
||||||
forwardZapTo.value.updatePercentage(pos, 0.9f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!forwardZapTo.value.items.isEmpty()) {
|
|
||||||
wantsForwardZapTo = true
|
|
||||||
}
|
|
||||||
|
|
||||||
urlPreviews.update(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun loadFromDraft(draft: Note) {
|
|
||||||
val draftEvent = draft.event ?: return
|
|
||||||
if (draftEvent !is CommentEvent) return
|
|
||||||
|
|
||||||
loadFromDraft(draftEvent)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun loadFromDraft(draftEvent: CommentEvent) {
|
|
||||||
val geohash = draftEvent.geohashedScope() ?: return
|
|
||||||
this.externalIdentity = GeohashId(geohash)
|
|
||||||
|
|
||||||
canAddInvoice = accountViewModel?.userProfile()?.info?.lnAddress() != null
|
|
||||||
canAddZapRaiser = accountViewModel?.userProfile()?.info?.lnAddress() != null
|
|
||||||
multiOrchestrator = null
|
|
||||||
|
|
||||||
val localfowardZapTo = draftEvent.tags.filter { it.size > 1 && it[0] == "zap" }
|
|
||||||
forwardZapTo.value = SplitBuilder()
|
|
||||||
localfowardZapTo.forEach {
|
|
||||||
val user = LocalCache.getOrCreateUser(it[1])
|
|
||||||
val value = it.last().toFloatOrNull() ?: 0f
|
|
||||||
forwardZapTo.value.addItem(user, value)
|
|
||||||
}
|
|
||||||
forwardZapToEditting.value = TextFieldValue("")
|
|
||||||
wantsForwardZapTo = localfowardZapTo.isNotEmpty()
|
|
||||||
|
|
||||||
wantsToMarkAsSensitive = draftEvent.isSensitive()
|
|
||||||
|
|
||||||
val zapraiser = draftEvent.zapraiserAmount()
|
|
||||||
wantsZapraiser = zapraiser != null
|
|
||||||
zapRaiserAmount.value = null
|
|
||||||
if (zapraiser != null) {
|
|
||||||
zapRaiserAmount.value = zapraiser
|
|
||||||
}
|
|
||||||
|
|
||||||
draftEvent.replyingTo()?.let {
|
|
||||||
replyingTo = LocalCache.getOrCreateNote(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
notifying = draftEvent.rootAuthorKeys().mapNotNull { LocalCache.checkGetOrCreateUser(it) } +
|
|
||||||
draftEvent.replyAuthorKeys().mapNotNull { LocalCache.checkGetOrCreateUser(it) }
|
|
||||||
|
|
||||||
if (forwardZapTo.value.items.isNotEmpty()) {
|
|
||||||
wantsForwardZapTo = true
|
|
||||||
}
|
|
||||||
|
|
||||||
message = TextFieldValue(draftEvent.content)
|
|
||||||
|
|
||||||
iMetaAttachments.addAll(draftEvent.imetas())
|
|
||||||
|
|
||||||
urlPreviews.update(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun sendPostSync() {
|
|
||||||
val template = createTemplate() ?: return
|
|
||||||
val relayList = relayList
|
|
||||||
|
|
||||||
if (nip95attachments.isNotEmpty() && relayList != null) {
|
|
||||||
val usedImages = template.tags.taggedQuoteIds().toSet()
|
|
||||||
nip95attachments.forEach {
|
|
||||||
if (usedImages.contains(it.second.id) == true) {
|
|
||||||
account?.sendNip95Privately(it.first, it.second, relayList)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
accountViewModel?.account?.signAndSendPrivatelyOrBroadcast(
|
|
||||||
template,
|
|
||||||
relayList = { relayList },
|
|
||||||
)
|
|
||||||
|
|
||||||
accountViewModel?.deleteDraft(draftTag.current)
|
|
||||||
|
|
||||||
cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun sendDraftSync() {
|
|
||||||
val accountViewModel = accountViewModel ?: return
|
|
||||||
|
|
||||||
if (message.text.isBlank()) {
|
|
||||||
accountViewModel.account.deleteDraft(draftTag.current)
|
|
||||||
} else {
|
|
||||||
val template = createTemplate() ?: return
|
|
||||||
accountViewModel.account.createAndSendDraft(draftTag.current, template)
|
|
||||||
|
|
||||||
nip95attachments.forEach {
|
|
||||||
account?.sendToPrivateOutboxAndLocal(it.first)
|
|
||||||
account?.sendToPrivateOutboxAndLocal(it.second)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun createTemplate(): EventTemplate<out Event>? {
|
|
||||||
val accountViewModel = accountViewModel ?: return null
|
|
||||||
|
|
||||||
val tagger =
|
|
||||||
NewMessageTagger(
|
|
||||||
message = message.text,
|
|
||||||
dao = accountViewModel,
|
|
||||||
)
|
|
||||||
tagger.run()
|
|
||||||
|
|
||||||
val emojis = findEmoji(tagger.message, account?.emoji?.myEmojis?.value)
|
|
||||||
val urls = findURLs(tagger.message)
|
|
||||||
val usedAttachments = iMetaAttachments.filterIsIn(urls.toSet())
|
|
||||||
|
|
||||||
val zapReceiver = if (wantsForwardZapTo) forwardZapTo.value.toZapSplitSetup() else null
|
|
||||||
val localZapRaiserAmount = if (wantsZapraiser) zapRaiserAmount.value else null
|
|
||||||
val contentWarningReason = if (wantsToMarkAsSensitive) "" else null
|
|
||||||
|
|
||||||
val replyingTo = replyingTo
|
|
||||||
|
|
||||||
val template =
|
|
||||||
if (replyingTo != null) {
|
|
||||||
val eventHint = replyingTo.toEventHint<Event>() ?: return null
|
|
||||||
|
|
||||||
CommentEvent.replyBuilder(
|
|
||||||
msg = tagger.message,
|
|
||||||
replyingTo = eventHint,
|
|
||||||
) {
|
|
||||||
tagger.pTags?.let { notify(it.map { it.toPTag() }) }
|
|
||||||
|
|
||||||
hashtags(findHashtags(tagger.message))
|
|
||||||
references(findURLs(tagger.message))
|
|
||||||
quotes(findNostrUris(tagger.message))
|
|
||||||
|
|
||||||
localZapRaiserAmount?.let { zapraiser(it) }
|
|
||||||
zapReceiver?.let { zapSplits(it) }
|
|
||||||
contentWarningReason?.let { contentWarning(it) }
|
|
||||||
|
|
||||||
emojis(emojis)
|
|
||||||
imetas(usedAttachments)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val externalIdentity = externalIdentity ?: return null
|
|
||||||
CommentEvent.replyExternalIdentity(
|
|
||||||
msg = tagger.message,
|
|
||||||
extId = externalIdentity,
|
|
||||||
) {
|
|
||||||
tagger.pTags?.let { notify(it.map { it.toPTag() }) }
|
|
||||||
|
|
||||||
hashtags(findHashtags(tagger.message))
|
|
||||||
references(findURLs(tagger.message))
|
|
||||||
quotes(findNostrUris(tagger.message))
|
|
||||||
|
|
||||||
localZapRaiserAmount?.let { zapraiser(it) }
|
|
||||||
zapReceiver?.let { zapSplits(it) }
|
|
||||||
contentWarningReason?.let { contentWarning(it) }
|
|
||||||
|
|
||||||
emojis(emojis)
|
|
||||||
imetas(usedAttachments)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return template
|
|
||||||
}
|
|
||||||
|
|
||||||
fun findEmoji(
|
|
||||||
message: String,
|
|
||||||
myEmojiSet: List<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.url) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun upload(
|
|
||||||
alt: String?,
|
|
||||||
contentWarningReason: String?,
|
|
||||||
mediaQuality: Int,
|
|
||||||
server: ServerName,
|
|
||||||
onError: (title: String, message: String) -> Unit,
|
|
||||||
context: Context,
|
|
||||||
) {
|
|
||||||
viewModelScope.launch(Dispatchers.Default) {
|
|
||||||
val myAccount = account ?: return@launch
|
|
||||||
val myMultiOrchestrator = multiOrchestrator ?: return@launch
|
|
||||||
|
|
||||||
isUploadingImage = true
|
|
||||||
|
|
||||||
val results =
|
|
||||||
myMultiOrchestrator.upload(
|
|
||||||
viewModelScope,
|
|
||||||
alt,
|
|
||||||
contentWarningReason,
|
|
||||||
MediaCompressor.intToCompressorQuality(mediaQuality),
|
|
||||||
server,
|
|
||||||
myAccount,
|
|
||||||
context,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (results.allGood) {
|
|
||||||
results.successful.forEach {
|
|
||||||
if (it.result is UploadOrchestrator.OrchestratorResult.NIP95Result) {
|
|
||||||
account?.createNip95(it.result.bytes, headerInfo = it.result.fileHeader, alt, contentWarningReason) { nip95 ->
|
|
||||||
nip95attachments = nip95attachments + nip95
|
|
||||||
val note = nip95.let { it1 -> account?.consumeNip95(it1.first, it1.second) }
|
|
||||||
|
|
||||||
note?.let {
|
|
||||||
message = message.insertUrlAtCursor("nostr:" + it.toNEvent())
|
|
||||||
urlPreviews.update(message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (it.result is UploadOrchestrator.OrchestratorResult.ServerResult) {
|
|
||||||
val iMeta =
|
|
||||||
IMetaTagBuilder(it.result.url)
|
|
||||||
.apply {
|
|
||||||
hash(it.result.fileHeader.hash)
|
|
||||||
size(it.result.fileHeader.size)
|
|
||||||
it.result.fileHeader.mimeType
|
|
||||||
?.let { mimeType(it) }
|
|
||||||
it.result.fileHeader.dim
|
|
||||||
?.let { dims(it) }
|
|
||||||
it.result.fileHeader.blurHash
|
|
||||||
?.let { blurhash(it.blurhash) }
|
|
||||||
it.result.magnet?.let { magnet(it) }
|
|
||||||
it.result.uploadedHash?.let { originalHash(it) }
|
|
||||||
|
|
||||||
alt?.let { alt(it) }
|
|
||||||
contentWarningReason?.let { sensitiveContent(contentWarningReason) }
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
iMetaAttachments.replace(iMeta.url, iMeta)
|
|
||||||
|
|
||||||
message = message.insertUrlAtCursor(it.result.url)
|
|
||||||
urlPreviews.update(message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
multiOrchestrator = null
|
|
||||||
} 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"))
|
|
||||||
}
|
|
||||||
|
|
||||||
isUploadingImage = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun cancel() {
|
|
||||||
message = TextFieldValue("")
|
|
||||||
|
|
||||||
replyingTo = null
|
|
||||||
externalIdentity = null
|
|
||||||
|
|
||||||
multiOrchestrator = null
|
|
||||||
isUploadingImage = false
|
|
||||||
|
|
||||||
notifying = null
|
|
||||||
|
|
||||||
wantsInvoice = false
|
|
||||||
wantsZapraiser = false
|
|
||||||
zapRaiserAmount.value = null
|
|
||||||
|
|
||||||
wantsForwardZapTo = false
|
|
||||||
wantsToMarkAsSensitive = false
|
|
||||||
wantsSecretEmoji = false
|
|
||||||
|
|
||||||
forwardZapTo.value = SplitBuilder()
|
|
||||||
forwardZapToEditting.value = TextFieldValue("")
|
|
||||||
|
|
||||||
urlPreviews.reset()
|
|
||||||
|
|
||||||
userSuggestions?.reset()
|
|
||||||
userSuggestionsMainMessage = null
|
|
||||||
|
|
||||||
iMetaAttachments.reset()
|
|
||||||
|
|
||||||
emojiSuggestions?.reset()
|
|
||||||
|
|
||||||
showRelaysDialog = false
|
|
||||||
|
|
||||||
reloadRelaySet()
|
|
||||||
|
|
||||||
draftTag.rotate()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun reloadRelaySet() {
|
|
||||||
val account = accountViewModel?.account ?: return
|
|
||||||
|
|
||||||
val nip65 = account.normalizedNIP65WriteRelayList.value
|
|
||||||
val private = account.normalizedPrivateOutBoxRelaySet.value
|
|
||||||
val local = account.settings.localRelayServers
|
|
||||||
|
|
||||||
relayList =
|
|
||||||
if (nip65.isEmpty()) {
|
|
||||||
account.activeWriteRelays().map { it.url }.toImmutableList()
|
|
||||||
} else {
|
|
||||||
val combined: Set<String> = (nip65 + private + local)
|
|
||||||
combined.toImmutableList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deleteMediaToUpload(selected: SelectedMediaProcessing) {
|
|
||||||
this.multiOrchestrator?.remove(selected)
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun removeFromReplyList(userToRemove: User) {
|
|
||||||
notifying = notifying?.filter { it != userToRemove }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun updateMessage(it: TextFieldValue) {
|
|
||||||
message = it
|
|
||||||
urlPreviews.update(message)
|
|
||||||
|
|
||||||
if (message.selection.collapsed) {
|
|
||||||
val lastWord = message.currentWord()
|
|
||||||
|
|
||||||
userSuggestionsMainMessage = UserSuggestionAnchor.MAIN_MESSAGE
|
|
||||||
userSuggestions?.processCurrentWord(lastWord)
|
|
||||||
|
|
||||||
emojiSuggestions?.processCurrentWord(lastWord)
|
|
||||||
}
|
|
||||||
|
|
||||||
draftTag.newVersion()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun updateZapForwardTo(newZapForwardTo: TextFieldValue) {
|
|
||||||
forwardZapToEditting.value = newZapForwardTo
|
|
||||||
if (newZapForwardTo.selection.collapsed) {
|
|
||||||
val lastWord = newZapForwardTo.text
|
|
||||||
userSuggestionsMainMessage = UserSuggestionAnchor.FORWARD_ZAPS
|
|
||||||
userSuggestions?.processCurrentWord(lastWord)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun autocompleteWithUser(item: User) {
|
|
||||||
userSuggestions?.let { userSuggestions ->
|
|
||||||
if (userSuggestionsMainMessage == UserSuggestionAnchor.MAIN_MESSAGE) {
|
|
||||||
val lastWord = message.currentWord()
|
|
||||||
message = userSuggestions.replaceCurrentWord(message, lastWord, item)
|
|
||||||
urlPreviews.update(message)
|
|
||||||
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.FORWARD_ZAPS) {
|
|
||||||
forwardZapTo.value.addItem(item)
|
|
||||||
forwardZapToEditting.value = TextFieldValue("")
|
|
||||||
}
|
|
||||||
|
|
||||||
userSuggestionsMainMessage = null
|
|
||||||
userSuggestions.reset()
|
|
||||||
}
|
|
||||||
|
|
||||||
draftTag.newVersion()
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun autocompleteWithEmoji(item: EmojiMedia) {
|
|
||||||
val wordToInsert = ":${item.code}:"
|
|
||||||
|
|
||||||
message = message.replaceCurrentWord(wordToInsert)
|
|
||||||
urlPreviews.update(message)
|
|
||||||
|
|
||||||
emojiSuggestions?.reset()
|
|
||||||
|
|
||||||
draftTag.newVersion()
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun autocompleteWithEmojiUrl(item: EmojiMedia) {
|
|
||||||
val wordToInsert = item.link.url + " "
|
|
||||||
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
iMetaAttachments.downloadAndPrepare(
|
|
||||||
item.link.url,
|
|
||||||
{ Amethyst.instance.okHttpClients.getHttpClient(accountViewModel?.account?.shouldUseTorForImageDownload(item.link.url) ?: false) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
message = message.replaceCurrentWord(wordToInsert)
|
|
||||||
urlPreviews.update(message)
|
|
||||||
|
|
||||||
emojiSuggestions?.reset()
|
|
||||||
|
|
||||||
draftTag.newVersion()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun canPost(): Boolean =
|
|
||||||
message.text.isNotBlank() &&
|
|
||||||
!isUploadingImage &&
|
|
||||||
!wantsInvoice &&
|
|
||||||
(!wantsZapraiser || zapRaiserAmount.value != null) &&
|
|
||||||
multiOrchestrator == null
|
|
||||||
|
|
||||||
fun insertAtCursor(newElement: String) {
|
|
||||||
message = message.insertUrlAtCursor(newElement)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun selectImage(uris: ImmutableList<SelectedMedia>) {
|
|
||||||
multiOrchestrator = MultiOrchestrator(uris)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun updateZapPercentage(
|
|
||||||
index: Int,
|
|
||||||
sliderValue: Float,
|
|
||||||
) {
|
|
||||||
forwardZapTo.value.updatePercentage(index, sliderValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun updateZapFromText() {
|
|
||||||
viewModelScope.launch(Dispatchers.Default) {
|
|
||||||
val tagger =
|
|
||||||
NewMessageTagger(message.text, emptyList(), emptyList(), null, accountViewModel!!)
|
|
||||||
tagger.run()
|
|
||||||
tagger.pTags?.forEach { taggedUser ->
|
|
||||||
if (!forwardZapTo.value.items.any { it.key == taggedUser }) {
|
|
||||||
forwardZapTo.value.addItem(taggedUser)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun updateZapRaiserAmount(newAmount: Long?) {
|
|
||||||
zapRaiserAmount.value = newAmount
|
|
||||||
draftTag.newVersion()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toggleMarkAsSensitive() {
|
|
||||||
wantsToMarkAsSensitive = !wantsToMarkAsSensitive
|
|
||||||
draftTag.newVersion()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+9
-88
@@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.compose.foundation.horizontalScroll
|
import androidx.compose.foundation.horizontalScroll
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -35,21 +34,14 @@ import androidx.compose.foundation.layout.height
|
|||||||
import androidx.compose.foundation.layout.heightIn
|
import androidx.compose.foundation.layout.heightIn
|
||||||
import androidx.compose.foundation.layout.imePadding
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.Tag
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.LocalTextStyle
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -59,13 +51,8 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Alignment.Companion.CenterVertically
|
import androidx.compose.ui.Alignment.Companion.CenterVertically
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.LinkAnnotation
|
|
||||||
import androidx.compose.ui.text.buildAnnotatedString
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.text.withLink
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
@@ -75,9 +62,7 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
|
|||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery
|
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton
|
import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.Nav
|
import com.vitorpamplona.amethyst.ui.navigation.Nav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.Route
|
|
||||||
import com.vitorpamplona.amethyst.ui.note.BaseUserPicture
|
import com.vitorpamplona.amethyst.ui.note.BaseUserPicture
|
||||||
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||||
import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.ContentSensitivityExplainer
|
import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.ContentSensitivityExplainer
|
||||||
@@ -89,8 +74,7 @@ import com.vitorpamplona.amethyst.ui.note.creators.invoice.InvoiceRequest
|
|||||||
import com.vitorpamplona.amethyst.ui.note.creators.location.AddGeoHashButton
|
import com.vitorpamplona.amethyst.ui.note.creators.location.AddGeoHashButton
|
||||||
import com.vitorpamplona.amethyst.ui.note.creators.location.LocationAsHash
|
import com.vitorpamplona.amethyst.ui.note.creators.location.LocationAsHash
|
||||||
import com.vitorpamplona.amethyst.ui.note.creators.messagefield.MessageField
|
import com.vitorpamplona.amethyst.ui.note.creators.messagefield.MessageField
|
||||||
import com.vitorpamplona.amethyst.ui.note.creators.previews.PreviewUrl
|
import com.vitorpamplona.amethyst.ui.note.creators.previews.DisplayPreviews
|
||||||
import com.vitorpamplona.amethyst.ui.note.creators.previews.PreviewUrlFillWidth
|
|
||||||
import com.vitorpamplona.amethyst.ui.note.creators.secretEmoji.AddSecretEmojiButton
|
import com.vitorpamplona.amethyst.ui.note.creators.secretEmoji.AddSecretEmojiButton
|
||||||
import com.vitorpamplona.amethyst.ui.note.creators.secretEmoji.SecretEmojiRequest
|
import com.vitorpamplona.amethyst.ui.note.creators.secretEmoji.SecretEmojiRequest
|
||||||
import com.vitorpamplona.amethyst.ui.note.creators.uploads.ImageVideoDescription
|
import com.vitorpamplona.amethyst.ui.note.creators.uploads.ImageVideoDescription
|
||||||
@@ -99,19 +83,17 @@ import com.vitorpamplona.amethyst.ui.note.creators.zapraiser.AddZapraiserButton
|
|||||||
import com.vitorpamplona.amethyst.ui.note.creators.zapraiser.ZapRaiserRequest
|
import com.vitorpamplona.amethyst.ui.note.creators.zapraiser.ZapRaiserRequest
|
||||||
import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.ForwardZapTo
|
import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.ForwardZapTo
|
||||||
import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.ForwardZapToButton
|
import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.ForwardZapToButton
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.nip22Comments.CommentPostViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.nip22Comments.DisplayExternalId
|
||||||
import com.vitorpamplona.amethyst.ui.painterRes
|
import com.vitorpamplona.amethyst.ui.painterRes
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.CloseButton
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.CloseButton
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.Notifying
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.Notifying
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.PostButton
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.PostButton
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.FillWidthQuoteBorderModifier
|
|
||||||
import com.vitorpamplona.amethyst.ui.theme.HalfHorzPadding
|
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Height100Modifier
|
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size5dp
|
import com.vitorpamplona.amethyst.ui.theme.Size5dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.SquaredQuoteBorderModifier
|
|
||||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||||
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
||||||
@@ -133,7 +115,7 @@ fun HashtagPostScreen(
|
|||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: Nav,
|
nav: Nav,
|
||||||
) {
|
) {
|
||||||
val postViewModel: HashtagPostViewModel = viewModel()
|
val postViewModel: CommentPostViewModel = viewModel()
|
||||||
postViewModel.init(accountViewModel)
|
postViewModel.init(accountViewModel)
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@@ -169,7 +151,7 @@ fun HashtagPostScreen(
|
|||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun HashtagPostScreenInner(
|
fun HashtagPostScreenInner(
|
||||||
postViewModel: HashtagPostViewModel,
|
postViewModel: CommentPostViewModel,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: Nav,
|
nav: Nav,
|
||||||
) {
|
) {
|
||||||
@@ -262,7 +244,7 @@ fun HashtagPostScreenInner(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun HashtagPostBody(
|
private fun HashtagPostBody(
|
||||||
postViewModel: HashtagPostViewModel,
|
postViewModel: CommentPostViewModel,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: Nav,
|
nav: Nav,
|
||||||
) {
|
) {
|
||||||
@@ -281,7 +263,7 @@ private fun HashtagPostBody(
|
|||||||
Column(Modifier.fillMaxWidth().verticalScroll(scrollState)) {
|
Column(Modifier.fillMaxWidth().verticalScroll(scrollState)) {
|
||||||
postViewModel.externalIdentity?.let {
|
postViewModel.externalIdentity?.let {
|
||||||
Row {
|
Row {
|
||||||
DisplayExternalId(it, accountViewModel, nav)
|
DisplayExternalId(it, nav)
|
||||||
Spacer(modifier = StdVertSpacer)
|
Spacer(modifier = StdVertSpacer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -322,7 +304,7 @@ private fun HashtagPostBody(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
DisplayPreviews(postViewModel, accountViewModel, nav)
|
DisplayPreviews(postViewModel.urlPreviews, accountViewModel, nav)
|
||||||
|
|
||||||
if (postViewModel.wantsToMarkAsSensitive) {
|
if (postViewModel.wantsToMarkAsSensitive) {
|
||||||
Row(
|
Row(
|
||||||
@@ -442,41 +424,7 @@ private fun HashtagPostBody(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DisplayExternalId(
|
private fun BottomRowActions(postViewModel: CommentPostViewModel) {
|
||||||
externalId: HashtagId,
|
|
||||||
accountViewModel: AccountViewModel,
|
|
||||||
nav: INav,
|
|
||||||
) {
|
|
||||||
Row(modifier = MaterialTheme.colorScheme.replyModifier.padding(10.dp), verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Tag,
|
|
||||||
contentDescription = stringRes(id = R.string.hashtag_exclusive),
|
|
||||||
modifier = Modifier.size(20.dp),
|
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(StdHorzSpacer)
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text =
|
|
||||||
buildAnnotatedString {
|
|
||||||
withLink(
|
|
||||||
LinkAnnotation.Clickable("hashtag") { nav.nav(Route.Hashtag(externalId.topic)) },
|
|
||||||
) {
|
|
||||||
append(externalId.topic)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
style =
|
|
||||||
LocalTextStyle.current.copy(
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
),
|
|
||||||
maxLines = 1,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun BottomRowActions(postViewModel: HashtagPostViewModel) {
|
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
@@ -529,30 +477,3 @@ private fun BottomRowActions(postViewModel: HashtagPostViewModel) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun DisplayPreviews(
|
|
||||||
postViewModel: HashtagPostViewModel,
|
|
||||||
accountViewModel: AccountViewModel,
|
|
||||||
nav: INav,
|
|
||||||
) {
|
|
||||||
val urlPreviews by postViewModel.urlPreviews.results.collectAsStateWithLifecycle(emptyList())
|
|
||||||
|
|
||||||
if (urlPreviews.isNotEmpty()) {
|
|
||||||
Row(HalfHorzPadding) {
|
|
||||||
if (urlPreviews.size > 1) {
|
|
||||||
LazyRow(Height100Modifier, horizontalArrangement = spacedBy(Size5dp)) {
|
|
||||||
items(urlPreviews) {
|
|
||||||
Box(SquaredQuoteBorderModifier) {
|
|
||||||
PreviewUrl(it, accountViewModel, nav)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Box(FillWidthQuoteBorderModifier) {
|
|
||||||
PreviewUrlFillWidth(urlPreviews[0], accountViewModel, nav)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
|
|||||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider
|
import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider
|
||||||
import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider
|
import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider
|
||||||
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.tags.geohash.GeoHashTag
|
||||||
import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent
|
import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent
|
||||||
import com.vitorpamplona.quartz.nip21UriScheme.toNostrUri
|
import com.vitorpamplona.quartz.nip21UriScheme.toNostrUri
|
||||||
import com.vitorpamplona.quartz.nip22Comments.tags.ReplyAddressTag
|
import com.vitorpamplona.quartz.nip22Comments.tags.ReplyAddressTag
|
||||||
@@ -44,6 +45,7 @@ import com.vitorpamplona.quartz.nip22Comments.tags.RootIdentifierTag
|
|||||||
import com.vitorpamplona.quartz.nip22Comments.tags.RootKindTag
|
import com.vitorpamplona.quartz.nip22Comments.tags.RootKindTag
|
||||||
import com.vitorpamplona.quartz.nip31Alts.alt
|
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||||
import com.vitorpamplona.quartz.nip73ExternalIds.ExternalId
|
import com.vitorpamplona.quartz.nip73ExternalIds.ExternalId
|
||||||
|
import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId
|
||||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
import com.vitorpamplona.quartz.utils.lastNotNullOfOrNull
|
import com.vitorpamplona.quartz.utils.lastNotNullOfOrNull
|
||||||
|
|
||||||
@@ -172,10 +174,18 @@ class CommentEvent(
|
|||||||
) = eventTemplate(KIND, msg, createdAt) {
|
) = eventTemplate(KIND, msg, createdAt) {
|
||||||
alt(ALT + extId.toScope())
|
alt(ALT + extId.toScope())
|
||||||
|
|
||||||
rootExternalIdentity(extId)
|
if (extId is GeohashId) {
|
||||||
|
GeoHashTag.geoMipMap(extId.geohash).forEach { rootExternalIdentity(GeohashId(it, extId.hint)) }
|
||||||
|
} else {
|
||||||
|
rootExternalIdentity(extId)
|
||||||
|
}
|
||||||
rootKind(extId)
|
rootKind(extId)
|
||||||
|
|
||||||
replyExternalIdentity(extId)
|
if (extId is GeohashId) {
|
||||||
|
GeoHashTag.geoMipMap(extId.geohash).forEach { replyExternalIdentity(GeohashId(it, extId.hint)) }
|
||||||
|
} else {
|
||||||
|
replyExternalIdentity(extId)
|
||||||
|
}
|
||||||
replyKind(extId)
|
replyKind(extId)
|
||||||
|
|
||||||
initializer()
|
initializer()
|
||||||
|
|||||||
@@ -48,7 +48,9 @@ fun TagArrayBuilder<CommentEvent>.rootEvent(
|
|||||||
pubkey: String?,
|
pubkey: String?,
|
||||||
) = addUnique(RootEventTag.assemble(eventId, relayHint, pubkey))
|
) = addUnique(RootEventTag.assemble(eventId, relayHint, pubkey))
|
||||||
|
|
||||||
fun TagArrayBuilder<CommentEvent>.rootExternalIdentity(id: ExternalId) = addAll(RootIdentifierTag.assemble(id))
|
fun TagArrayBuilder<CommentEvent>.rootExternalIdentity(id: ExternalId) = add(RootIdentifierTag.assemble(id))
|
||||||
|
|
||||||
|
fun TagArrayBuilder<CommentEvent>.rootExternalIdentities(ids: List<ExternalId>) = addAll(ids.map { RootIdentifierTag.assemble(it) })
|
||||||
|
|
||||||
fun TagArrayBuilder<CommentEvent>.rootKind(kind: String) = addUnique(RootKindTag.assemble(kind))
|
fun TagArrayBuilder<CommentEvent>.rootKind(kind: String) = addUnique(RootKindTag.assemble(kind))
|
||||||
|
|
||||||
@@ -72,7 +74,9 @@ fun TagArrayBuilder<CommentEvent>.replyEvent(
|
|||||||
pubkey: String?,
|
pubkey: String?,
|
||||||
) = addUnique(ReplyEventTag.assemble(eventId, relayHint, pubkey))
|
) = addUnique(ReplyEventTag.assemble(eventId, relayHint, pubkey))
|
||||||
|
|
||||||
fun TagArrayBuilder<CommentEvent>.replyExternalIdentity(id: ExternalId) = addAll(ReplyIdentifierTag.assemble(id))
|
fun TagArrayBuilder<CommentEvent>.replyExternalIdentity(id: ExternalId) = add(ReplyIdentifierTag.assemble(id))
|
||||||
|
|
||||||
|
fun TagArrayBuilder<CommentEvent>.replyExternalIdentities(ids: List<ExternalId>) = addAll(ids.map { ReplyIdentifierTag.assemble(it) })
|
||||||
|
|
||||||
fun TagArrayBuilder<CommentEvent>.replyKind(kind: String) = addUnique(ReplyKindTag.assemble(kind))
|
fun TagArrayBuilder<CommentEvent>.replyKind(kind: String) = addUnique(ReplyKindTag.assemble(kind))
|
||||||
|
|
||||||
|
|||||||
+30
-6
@@ -23,9 +23,17 @@ package com.vitorpamplona.quartz.nip22Comments.tags
|
|||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.Tag
|
import com.vitorpamplona.quartz.nip01Core.core.Tag
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.geohash.GeoHashTag
|
import com.vitorpamplona.quartz.nip46RemoteSigner.getOrNull
|
||||||
import com.vitorpamplona.quartz.nip73ExternalIds.ExternalId
|
import com.vitorpamplona.quartz.nip73ExternalIds.ExternalId
|
||||||
|
import com.vitorpamplona.quartz.nip73ExternalIds.books.BookId
|
||||||
import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId
|
import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId
|
||||||
|
import com.vitorpamplona.quartz.nip73ExternalIds.movies.MovieId
|
||||||
|
import com.vitorpamplona.quartz.nip73ExternalIds.papers.PaperId
|
||||||
|
import com.vitorpamplona.quartz.nip73ExternalIds.podcasts.PodcastEpisodeId
|
||||||
|
import com.vitorpamplona.quartz.nip73ExternalIds.podcasts.PodcastFeedId
|
||||||
|
import com.vitorpamplona.quartz.nip73ExternalIds.podcasts.PodcastPublisherId
|
||||||
|
import com.vitorpamplona.quartz.nip73ExternalIds.topics.HashtagId
|
||||||
|
import com.vitorpamplona.quartz.nip73ExternalIds.urls.UrlId
|
||||||
import com.vitorpamplona.quartz.utils.arrayOfNotNull
|
import com.vitorpamplona.quartz.utils.arrayOfNotNull
|
||||||
import com.vitorpamplona.quartz.utils.ensure
|
import com.vitorpamplona.quartz.utils.ensure
|
||||||
|
|
||||||
@@ -82,6 +90,26 @@ class ReplyIdentifierTag {
|
|||||||
return tag[1]
|
return tag[1]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun parseExternalId(tag: Tag): ExternalId? {
|
||||||
|
ensure(tag.has(1)) { return null }
|
||||||
|
ensure(tag[0] == RootIdentifierTag.Companion.TAG_NAME) { return null }
|
||||||
|
ensure(tag[1].isNotEmpty()) { return null }
|
||||||
|
|
||||||
|
val value = tag[1]
|
||||||
|
val hint = tag.getOrNull(2)
|
||||||
|
|
||||||
|
return BookId.parse(value, hint)
|
||||||
|
?: HashtagId.parse(value, hint)
|
||||||
|
?: GeohashId.parse(value, hint)
|
||||||
|
?: MovieId.parse(value, hint)
|
||||||
|
?: PaperId.parse(value, hint)
|
||||||
|
?: PodcastEpisodeId.parse(value, hint)
|
||||||
|
?: PodcastFeedId.parse(value, hint)
|
||||||
|
?: PodcastPublisherId.parse(value, hint)
|
||||||
|
?: UrlId.parse(value, hint)
|
||||||
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun assemble(
|
fun assemble(
|
||||||
identity: String,
|
identity: String,
|
||||||
@@ -89,10 +117,6 @@ class ReplyIdentifierTag {
|
|||||||
) = arrayOfNotNull(TAG_NAME, identity, hint)
|
) = arrayOfNotNull(TAG_NAME, identity, hint)
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun assemble(id: ExternalId): List<Array<String>> =
|
fun assemble(id: ExternalId): Array<String> = assemble(id.toScope(), id.hint())
|
||||||
when (id) {
|
|
||||||
is GeohashId -> GeoHashTag.geoMipMap(id.geohash).map { assemble(GeohashId.toScope(it), id.hint) }
|
|
||||||
else -> listOf(assemble(id.toScope(), id.hint()))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+30
-6
@@ -23,9 +23,17 @@ package com.vitorpamplona.quartz.nip22Comments.tags
|
|||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.Tag
|
import com.vitorpamplona.quartz.nip01Core.core.Tag
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.geohash.GeoHashTag
|
import com.vitorpamplona.quartz.nip46RemoteSigner.getOrNull
|
||||||
import com.vitorpamplona.quartz.nip73ExternalIds.ExternalId
|
import com.vitorpamplona.quartz.nip73ExternalIds.ExternalId
|
||||||
|
import com.vitorpamplona.quartz.nip73ExternalIds.books.BookId
|
||||||
import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId
|
import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId
|
||||||
|
import com.vitorpamplona.quartz.nip73ExternalIds.movies.MovieId
|
||||||
|
import com.vitorpamplona.quartz.nip73ExternalIds.papers.PaperId
|
||||||
|
import com.vitorpamplona.quartz.nip73ExternalIds.podcasts.PodcastEpisodeId
|
||||||
|
import com.vitorpamplona.quartz.nip73ExternalIds.podcasts.PodcastFeedId
|
||||||
|
import com.vitorpamplona.quartz.nip73ExternalIds.podcasts.PodcastPublisherId
|
||||||
|
import com.vitorpamplona.quartz.nip73ExternalIds.topics.HashtagId
|
||||||
|
import com.vitorpamplona.quartz.nip73ExternalIds.urls.UrlId
|
||||||
import com.vitorpamplona.quartz.utils.arrayOfNotNull
|
import com.vitorpamplona.quartz.utils.arrayOfNotNull
|
||||||
import com.vitorpamplona.quartz.utils.ensure
|
import com.vitorpamplona.quartz.utils.ensure
|
||||||
|
|
||||||
@@ -81,6 +89,26 @@ class RootIdentifierTag<T : ExternalId> {
|
|||||||
return tag[1]
|
return tag[1]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun parseExternalId(tag: Tag): ExternalId? {
|
||||||
|
ensure(tag.has(1)) { return null }
|
||||||
|
ensure(tag[0] == TAG_NAME) { return null }
|
||||||
|
ensure(tag[1].isNotEmpty()) { return null }
|
||||||
|
|
||||||
|
val value = tag[1]
|
||||||
|
val hint = tag.getOrNull(2)
|
||||||
|
|
||||||
|
return BookId.parse(value, hint)
|
||||||
|
?: HashtagId.parse(value, hint)
|
||||||
|
?: GeohashId.parse(value, hint)
|
||||||
|
?: MovieId.parse(value, hint)
|
||||||
|
?: PaperId.parse(value, hint)
|
||||||
|
?: PodcastEpisodeId.parse(value, hint)
|
||||||
|
?: PodcastFeedId.parse(value, hint)
|
||||||
|
?: PodcastPublisherId.parse(value, hint)
|
||||||
|
?: UrlId.parse(value, hint)
|
||||||
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun assemble(
|
fun assemble(
|
||||||
identity: String,
|
identity: String,
|
||||||
@@ -88,10 +116,6 @@ class RootIdentifierTag<T : ExternalId> {
|
|||||||
) = arrayOfNotNull(TAG_NAME, identity, hint)
|
) = arrayOfNotNull(TAG_NAME, identity, hint)
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun assemble(id: ExternalId): List<Array<String>> =
|
fun assemble(id: ExternalId): Array<String> = assemble(id.toScope(), id.hint())
|
||||||
when (id) {
|
|
||||||
is GeohashId -> GeoHashTag.geoMipMap(id.geohash).map { assemble(GeohashId.toScope(it), id.hint) }
|
|
||||||
else -> listOf(assemble(id.toScope(), id.hint()))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* 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.quartz.nip73ExternalIds
|
||||||
|
|
||||||
|
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
||||||
|
import com.vitorpamplona.quartz.nip22Comments.tags.RootIdentifierTag
|
||||||
|
import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId
|
||||||
|
|
||||||
|
fun CommentEvent.scopes() = tags.mapNotNull { RootIdentifierTag.parseExternalId(it) }
|
||||||
|
|
||||||
|
fun CommentEvent.scope(): ExternalId? {
|
||||||
|
val scopes = scopes()
|
||||||
|
|
||||||
|
if (scopes.isEmpty()) return null
|
||||||
|
|
||||||
|
return if (scopes.first() is GeohashId) {
|
||||||
|
scopes.maxByOrNull { if (it is GeohashId) it.geohash else "" }
|
||||||
|
} else {
|
||||||
|
scopes().first()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -54,5 +54,13 @@ class BookId(
|
|||||||
ensure(encoded.startsWith(PREFIX_COLON)) { return null }
|
ensure(encoded.startsWith(PREFIX_COLON)) { return null }
|
||||||
return encoded.substring(PREFIX_COLON.length)
|
return encoded.substring(PREFIX_COLON.length)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun parse(
|
||||||
|
encoded: String,
|
||||||
|
hint: String?,
|
||||||
|
): BookId? {
|
||||||
|
ensure(encoded.startsWith(PREFIX_COLON)) { return null }
|
||||||
|
return BookId(encoded.substring(PREFIX_COLON.length), hint)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,5 +53,13 @@ class GeohashId(
|
|||||||
ensure(encoded.startsWith(PREFIX_COLON)) { return null }
|
ensure(encoded.startsWith(PREFIX_COLON)) { return null }
|
||||||
return encoded.substring(PREFIX_COLON.length)
|
return encoded.substring(PREFIX_COLON.length)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun parse(
|
||||||
|
encoded: String,
|
||||||
|
hint: String?,
|
||||||
|
): GeohashId? {
|
||||||
|
ensure(encoded.startsWith(PREFIX_COLON)) { return null }
|
||||||
|
return GeohashId(encoded.substring(PREFIX_COLON.length), hint)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,5 +80,13 @@ class MovieId(
|
|||||||
ensure(encoded.startsWith(PREFIX_COLON)) { return null }
|
ensure(encoded.startsWith(PREFIX_COLON)) { return null }
|
||||||
return encoded.substring(PREFIX_COLON.length)
|
return encoded.substring(PREFIX_COLON.length)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun parse(
|
||||||
|
encoded: String,
|
||||||
|
hint: String?,
|
||||||
|
): MovieId? {
|
||||||
|
ensure(encoded.startsWith(PREFIX_COLON)) { return null }
|
||||||
|
return MovieId(encoded.substring(PREFIX_COLON.length), hint)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,5 +54,13 @@ class PaperId(
|
|||||||
ensure(encoded.startsWith(PREFIX_COLON)) { return null }
|
ensure(encoded.startsWith(PREFIX_COLON)) { return null }
|
||||||
return encoded.substring(PREFIX_COLON.length)
|
return encoded.substring(PREFIX_COLON.length)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun parse(
|
||||||
|
encoded: String,
|
||||||
|
hint: String?,
|
||||||
|
): PaperId? {
|
||||||
|
ensure(encoded.startsWith(PREFIX_COLON)) { return null }
|
||||||
|
return PaperId(encoded.substring(PREFIX_COLON.length), hint)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
@@ -53,5 +53,13 @@ class PodcastEpisodeId(
|
|||||||
ensure(encoded.startsWith(PREFIX_COLON)) { return null }
|
ensure(encoded.startsWith(PREFIX_COLON)) { return null }
|
||||||
return encoded.substring(PREFIX_COLON.length)
|
return encoded.substring(PREFIX_COLON.length)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun parse(
|
||||||
|
encoded: String,
|
||||||
|
hint: String?,
|
||||||
|
): PodcastEpisodeId? {
|
||||||
|
ensure(encoded.startsWith(PREFIX_COLON)) { return null }
|
||||||
|
return PodcastEpisodeId(encoded.substring(PREFIX_COLON.length), hint)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
@@ -54,5 +54,13 @@ class PodcastFeedId(
|
|||||||
ensure(encoded.startsWith(PREFIX_COLON)) { return null }
|
ensure(encoded.startsWith(PREFIX_COLON)) { return null }
|
||||||
return encoded.substring(PREFIX_COLON.length)
|
return encoded.substring(PREFIX_COLON.length)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun parse(
|
||||||
|
encoded: String,
|
||||||
|
hint: String?,
|
||||||
|
): PodcastFeedId? {
|
||||||
|
ensure(encoded.startsWith(PREFIX_COLON)) { return null }
|
||||||
|
return PodcastFeedId(encoded.substring(PREFIX_COLON.length), hint)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
@@ -54,5 +54,13 @@ class PodcastPublisherId(
|
|||||||
ensure(encoded.startsWith(PREFIX_COLON)) { return null }
|
ensure(encoded.startsWith(PREFIX_COLON)) { return null }
|
||||||
return encoded.substring(PREFIX_COLON.length)
|
return encoded.substring(PREFIX_COLON.length)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun parse(
|
||||||
|
encoded: String,
|
||||||
|
hint: String?,
|
||||||
|
): PodcastPublisherId? {
|
||||||
|
ensure(encoded.startsWith(PREFIX_COLON)) { return null }
|
||||||
|
return PodcastPublisherId(encoded.substring(PREFIX_COLON.length), hint)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,5 +77,16 @@ class HashtagId(
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun parse(
|
||||||
|
encoded: String,
|
||||||
|
hint: String?,
|
||||||
|
): HashtagId? {
|
||||||
|
return if (encoded.length > 1 && encoded[0] == KIND_CHR) {
|
||||||
|
HashtagId(encoded.substring(1), hint)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,5 +56,13 @@ class UrlId(
|
|||||||
ensure(encoded.startsWith(PREFIX1) || encoded.startsWith(PREFIX2)) { return null }
|
ensure(encoded.startsWith(PREFIX1) || encoded.startsWith(PREFIX2)) { return null }
|
||||||
return encoded
|
return encoded
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun parse(
|
||||||
|
encoded: String,
|
||||||
|
hint: String?,
|
||||||
|
): UrlId? {
|
||||||
|
ensure(encoded.startsWith(PREFIX1) || encoded.startsWith(PREFIX2)) { return null }
|
||||||
|
return UrlId(encoded, hint)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user