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.drafts.DraftListScreen
|
||||
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.GeoPostScreen
|
||||
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.home.HomeScreen
|
||||
@@ -143,7 +143,7 @@ fun AppNavigation(
|
||||
composableArgs<Route.EventRedirect> { LoadRedirectScreen(it.id, accountViewModel, nav) }
|
||||
|
||||
composableFromBottomArgs<Route.GeoPost> {
|
||||
GeoPostScreen(
|
||||
GeoHashPostScreen(
|
||||
geohash = it.geohash,
|
||||
message = it.message,
|
||||
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
|
||||
* 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 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.Note
|
||||
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.uploads.MediaCompressor
|
||||
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.zapraiser.zapraiser
|
||||
import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount
|
||||
import com.vitorpamplona.quartz.nip73ExternalIds.topics.HashtagId
|
||||
import com.vitorpamplona.quartz.nip73ExternalIds.topics.hashtagScope
|
||||
import com.vitorpamplona.quartz.nip73ExternalIds.ExternalId
|
||||
import com.vitorpamplona.quartz.nip73ExternalIds.scope
|
||||
import com.vitorpamplona.quartz.nip92IMeta.IMetaTagBuilder
|
||||
import com.vitorpamplona.quartz.nip92IMeta.imetas
|
||||
import com.vitorpamplona.quartz.nip94FileMetadata.alt
|
||||
@@ -104,9 +104,10 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.collections.plus
|
||||
|
||||
@Stable
|
||||
open class HashtagPostViewModel :
|
||||
open class CommentPostViewModel :
|
||||
ViewModel(),
|
||||
ILocationGrabber,
|
||||
IMessageField,
|
||||
@@ -125,11 +126,13 @@ open class HashtagPostViewModel :
|
||||
var accountViewModel: AccountViewModel? = null
|
||||
var account: Account? = null
|
||||
|
||||
var externalIdentity by mutableStateOf<HashtagId?>(null)
|
||||
var externalIdentity by mutableStateOf<ExternalId?>(null)
|
||||
var replyingTo: Note? by mutableStateOf<Note?>(null)
|
||||
|
||||
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)
|
||||
|
||||
@@ -192,7 +195,7 @@ open class HashtagPostViewModel :
|
||||
this.emojiSuggestions = EmojiSuggestionState(accountVM)
|
||||
}
|
||||
|
||||
fun newPostFor(externalIdentity: HashtagId) {
|
||||
fun newPostFor(externalIdentity: ExternalId) {
|
||||
this.externalIdentity = externalIdentity
|
||||
}
|
||||
|
||||
@@ -220,10 +223,10 @@ open class HashtagPostViewModel :
|
||||
open fun reply(post: Note) {
|
||||
val accountViewModel = accountViewModel ?: return
|
||||
val noteEvent = post.event as? CommentEvent ?: return
|
||||
val hashtag = noteEvent.hashtagScope() ?: return
|
||||
val scope = noteEvent.scope() ?: return
|
||||
|
||||
this.replyingTo = post
|
||||
this.externalIdentity = HashtagId(hashtag)
|
||||
this.externalIdentity = scope
|
||||
}
|
||||
|
||||
open fun quote(quote: Note) {
|
||||
@@ -260,8 +263,8 @@ open class HashtagPostViewModel :
|
||||
}
|
||||
|
||||
private fun loadFromDraft(draftEvent: CommentEvent) {
|
||||
val hashtag = draftEvent.hashtagScope() ?: return
|
||||
this.externalIdentity = HashtagId(hashtag)
|
||||
val scope = draftEvent.scope() ?: return
|
||||
this.externalIdentity = scope
|
||||
|
||||
canAddInvoice = accountViewModel?.userProfile()?.info?.lnAddress() != null
|
||||
canAddZapRaiser = accountViewModel?.userProfile()?.info?.lnAddress() != null
|
||||
@@ -371,7 +374,7 @@ open class HashtagPostViewModel :
|
||||
if (replyingTo != null) {
|
||||
val eventHint = replyingTo.toEventHint<Event>() ?: return null
|
||||
|
||||
CommentEvent.replyBuilder(
|
||||
CommentEvent.Companion.replyBuilder(
|
||||
msg = tagger.message,
|
||||
replyingTo = eventHint,
|
||||
) {
|
||||
@@ -391,7 +394,7 @@ open class HashtagPostViewModel :
|
||||
}
|
||||
} else {
|
||||
val externalIdentity = externalIdentity ?: return null
|
||||
CommentEvent.replyExternalIdentity(
|
||||
CommentEvent.Companion.replyExternalIdentity(
|
||||
msg = tagger.message,
|
||||
extId = externalIdentity,
|
||||
) {
|
||||
@@ -416,11 +419,16 @@ open class HashtagPostViewModel :
|
||||
|
||||
fun findEmoji(
|
||||
message: String,
|
||||
myEmojiSet: List<EmojiMedia>?,
|
||||
myEmojiSet: List<EmojiPackState.EmojiMedia>?,
|
||||
): List<EmojiUrlTag> {
|
||||
if (myEmojiSet == null) return emptyList()
|
||||
return CustomEmoji.findAllEmojiCodes(message).mapNotNull { possibleEmoji ->
|
||||
myEmojiSet.firstOrNull { it.code == possibleEmoji }?.let { EmojiUrlTag(it.code, it.link.url) }
|
||||
return CustomEmoji.Companion.findAllEmojiCodes(message).mapNotNull { possibleEmoji ->
|
||||
myEmojiSet.firstOrNull { it.code == possibleEmoji }?.let {
|
||||
EmojiUrlTag(
|
||||
it.code,
|
||||
it.link.url,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -443,7 +451,7 @@ open class HashtagPostViewModel :
|
||||
viewModelScope,
|
||||
alt,
|
||||
contentWarningReason,
|
||||
MediaCompressor.intToCompressorQuality(mediaQuality),
|
||||
MediaCompressor.Companion.intToCompressorQuality(mediaQuality),
|
||||
server,
|
||||
myAccount,
|
||||
context,
|
||||
@@ -489,7 +497,14 @@ open class HashtagPostViewModel :
|
||||
|
||||
multiOrchestrator = null
|
||||
} 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"))
|
||||
}
|
||||
@@ -604,7 +619,7 @@ open class HashtagPostViewModel :
|
||||
draftTag.newVersion()
|
||||
}
|
||||
|
||||
open fun autocompleteWithEmoji(item: EmojiMedia) {
|
||||
open fun autocompleteWithEmoji(item: EmojiPackState.EmojiMedia) {
|
||||
val wordToInsert = ":${item.code}:"
|
||||
|
||||
message = message.replaceCurrentWord(wordToInsert)
|
||||
@@ -615,13 +630,13 @@ open class HashtagPostViewModel :
|
||||
draftTag.newVersion()
|
||||
}
|
||||
|
||||
open fun autocompleteWithEmojiUrl(item: EmojiMedia) {
|
||||
open fun autocompleteWithEmojiUrl(item: EmojiPackState.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) },
|
||||
{ Amethyst.Companion.instance.okHttpClients.getHttpClient(accountViewModel?.account?.shouldUseTorForImageDownload(item.link.url) ?: false) },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -686,5 +701,5 @@ open class HashtagPostViewModel :
|
||||
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 androidx.compose.foundation.horizontalScroll
|
||||
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.Column
|
||||
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.imePadding
|
||||
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.verticalScroll
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
@@ -60,7 +57,6 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
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.LocationAsHash
|
||||
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.PreviewUrlFillWidth
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.previews.DisplayPreviews
|
||||
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.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.PostButton
|
||||
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.Size35dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size5dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.SquaredQuoteBorderModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
@@ -283,7 +274,7 @@ private fun NewProductBody(
|
||||
MessageField(R.string.description, postViewModel)
|
||||
}
|
||||
|
||||
DisplayPreviews(postViewModel, accountViewModel, nav)
|
||||
DisplayPreviews(postViewModel.urlPreviews, accountViewModel, nav)
|
||||
|
||||
if (postViewModel.wantsToMarkAsSensitive) {
|
||||
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
|
||||
private fun BottomRowActions(postViewModel: NewProductViewModel) {
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
+10
-92
@@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
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.Column
|
||||
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.imePadding
|
||||
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.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.LocationOn
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
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.Modifier
|
||||
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.withLink
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
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.SelectedMedia
|
||||
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.Route
|
||||
import com.vitorpamplona.amethyst.ui.note.BaseUserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||
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.invoice.AddLnInvoiceButton
|
||||
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.previews.PreviewUrl
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.previews.PreviewUrlFillWidth
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.previews.DisplayPreviews
|
||||
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.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.zapsplits.ForwardZapTo
|
||||
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.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.CloseButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.Notifying
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.PostButton
|
||||
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.Size35dp
|
||||
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.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
||||
@@ -122,7 +103,7 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@Composable
|
||||
fun GeoPostScreen(
|
||||
fun GeoHashPostScreen(
|
||||
geohash: String? = null,
|
||||
message: String? = null,
|
||||
attachment: Uri? = null,
|
||||
@@ -132,7 +113,7 @@ fun GeoPostScreen(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: Nav,
|
||||
) {
|
||||
val postViewModel: GeoPostViewModel = viewModel()
|
||||
val postViewModel: CommentPostViewModel = viewModel()
|
||||
postViewModel.init(accountViewModel)
|
||||
|
||||
val context = LocalContext.current
|
||||
@@ -168,7 +149,7 @@ fun GeoPostScreen(
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun NewGeoPostScreenInner(
|
||||
postViewModel: GeoPostViewModel,
|
||||
postViewModel: CommentPostViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: Nav,
|
||||
) {
|
||||
@@ -261,7 +242,7 @@ fun NewGeoPostScreenInner(
|
||||
|
||||
@Composable
|
||||
private fun NewGeoPostBody(
|
||||
postViewModel: GeoPostViewModel,
|
||||
postViewModel: CommentPostViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: Nav,
|
||||
) {
|
||||
@@ -280,7 +261,7 @@ private fun NewGeoPostBody(
|
||||
Column(Modifier.fillMaxWidth().verticalScroll(scrollState)) {
|
||||
postViewModel.externalIdentity?.let {
|
||||
Row {
|
||||
DisplayExternalId(it, accountViewModel, nav)
|
||||
DisplayExternalId(it, nav)
|
||||
Spacer(modifier = StdVertSpacer)
|
||||
}
|
||||
}
|
||||
@@ -321,7 +302,7 @@ private fun NewGeoPostBody(
|
||||
)
|
||||
}
|
||||
|
||||
DisplayPreviews(postViewModel, accountViewModel, nav)
|
||||
DisplayPreviews(postViewModel.urlPreviews, accountViewModel, nav)
|
||||
|
||||
if (postViewModel.wantsToMarkAsSensitive) {
|
||||
Row(
|
||||
@@ -432,43 +413,7 @@ private fun NewGeoPostBody(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DisplayExternalId(
|
||||
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) {
|
||||
private fun BottomRowActions(postViewModel: CommentPostViewModel) {
|
||||
val scrollState = rememberScrollState()
|
||||
Row(
|
||||
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 androidx.compose.foundation.horizontalScroll
|
||||
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.Column
|
||||
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.imePadding
|
||||
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.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Tag
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
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.Modifier
|
||||
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.withLink
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
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.SelectedMedia
|
||||
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.Route
|
||||
import com.vitorpamplona.amethyst.ui.note.BaseUserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||
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.LocationAsHash
|
||||
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.PreviewUrlFillWidth
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.previews.DisplayPreviews
|
||||
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.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.zapsplits.ForwardZapTo
|
||||
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.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.CloseButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.Notifying
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.PostButton
|
||||
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.Size35dp
|
||||
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.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
||||
@@ -133,7 +115,7 @@ fun HashtagPostScreen(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: Nav,
|
||||
) {
|
||||
val postViewModel: HashtagPostViewModel = viewModel()
|
||||
val postViewModel: CommentPostViewModel = viewModel()
|
||||
postViewModel.init(accountViewModel)
|
||||
|
||||
val context = LocalContext.current
|
||||
@@ -169,7 +151,7 @@ fun HashtagPostScreen(
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun HashtagPostScreenInner(
|
||||
postViewModel: HashtagPostViewModel,
|
||||
postViewModel: CommentPostViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: Nav,
|
||||
) {
|
||||
@@ -262,7 +244,7 @@ fun HashtagPostScreenInner(
|
||||
|
||||
@Composable
|
||||
private fun HashtagPostBody(
|
||||
postViewModel: HashtagPostViewModel,
|
||||
postViewModel: CommentPostViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: Nav,
|
||||
) {
|
||||
@@ -281,7 +263,7 @@ private fun HashtagPostBody(
|
||||
Column(Modifier.fillMaxWidth().verticalScroll(scrollState)) {
|
||||
postViewModel.externalIdentity?.let {
|
||||
Row {
|
||||
DisplayExternalId(it, accountViewModel, nav)
|
||||
DisplayExternalId(it, nav)
|
||||
Spacer(modifier = StdVertSpacer)
|
||||
}
|
||||
}
|
||||
@@ -322,7 +304,7 @@ private fun HashtagPostBody(
|
||||
)
|
||||
}
|
||||
|
||||
DisplayPreviews(postViewModel, accountViewModel, nav)
|
||||
DisplayPreviews(postViewModel.urlPreviews, accountViewModel, nav)
|
||||
|
||||
if (postViewModel.wantsToMarkAsSensitive) {
|
||||
Row(
|
||||
@@ -442,41 +424,7 @@ private fun HashtagPostBody(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DisplayExternalId(
|
||||
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) {
|
||||
private fun BottomRowActions(postViewModel: CommentPostViewModel) {
|
||||
val scrollState = rememberScrollState()
|
||||
Row(
|
||||
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.PubKeyHintProvider
|
||||
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.nip21UriScheme.toNostrUri
|
||||
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.nip31Alts.alt
|
||||
import com.vitorpamplona.quartz.nip73ExternalIds.ExternalId
|
||||
import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import com.vitorpamplona.quartz.utils.lastNotNullOfOrNull
|
||||
|
||||
@@ -172,10 +174,18 @@ class CommentEvent(
|
||||
) = eventTemplate(KIND, msg, createdAt) {
|
||||
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)
|
||||
|
||||
replyExternalIdentity(extId)
|
||||
if (extId is GeohashId) {
|
||||
GeoHashTag.geoMipMap(extId.geohash).forEach { replyExternalIdentity(GeohashId(it, extId.hint)) }
|
||||
} else {
|
||||
replyExternalIdentity(extId)
|
||||
}
|
||||
replyKind(extId)
|
||||
|
||||
initializer()
|
||||
|
||||
@@ -48,7 +48,9 @@ fun TagArrayBuilder<CommentEvent>.rootEvent(
|
||||
pubkey: String?,
|
||||
) = 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))
|
||||
|
||||
@@ -72,7 +74,9 @@ fun TagArrayBuilder<CommentEvent>.replyEvent(
|
||||
pubkey: String?,
|
||||
) = 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))
|
||||
|
||||
|
||||
+30
-6
@@ -23,9 +23,17 @@ package com.vitorpamplona.quartz.nip22Comments.tags
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Tag
|
||||
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.books.BookId
|
||||
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.ensure
|
||||
|
||||
@@ -82,6 +90,26 @@ class ReplyIdentifierTag {
|
||||
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
|
||||
fun assemble(
|
||||
identity: String,
|
||||
@@ -89,10 +117,6 @@ class ReplyIdentifierTag {
|
||||
) = arrayOfNotNull(TAG_NAME, identity, hint)
|
||||
|
||||
@JvmStatic
|
||||
fun assemble(id: ExternalId): List<Array<String>> =
|
||||
when (id) {
|
||||
is GeohashId -> GeoHashTag.geoMipMap(id.geohash).map { assemble(GeohashId.toScope(it), id.hint) }
|
||||
else -> listOf(assemble(id.toScope(), id.hint()))
|
||||
}
|
||||
fun assemble(id: ExternalId): Array<String> = assemble(id.toScope(), id.hint())
|
||||
}
|
||||
}
|
||||
|
||||
+30
-6
@@ -23,9 +23,17 @@ package com.vitorpamplona.quartz.nip22Comments.tags
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Tag
|
||||
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.books.BookId
|
||||
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.ensure
|
||||
|
||||
@@ -81,6 +89,26 @@ class RootIdentifierTag<T : ExternalId> {
|
||||
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
|
||||
fun assemble(
|
||||
identity: String,
|
||||
@@ -88,10 +116,6 @@ class RootIdentifierTag<T : ExternalId> {
|
||||
) = arrayOfNotNull(TAG_NAME, identity, hint)
|
||||
|
||||
@JvmStatic
|
||||
fun assemble(id: ExternalId): List<Array<String>> =
|
||||
when (id) {
|
||||
is GeohashId -> GeoHashTag.geoMipMap(id.geohash).map { assemble(GeohashId.toScope(it), id.hint) }
|
||||
else -> listOf(assemble(id.toScope(), id.hint()))
|
||||
}
|
||||
fun assemble(id: ExternalId): Array<String> = 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 }
|
||||
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 }
|
||||
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 }
|
||||
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 }
|
||||
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 }
|
||||
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 }
|
||||
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 }
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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 }
|
||||
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