mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-09 08:04:45 +00:00
Merge pull request #3315 from LubuSeb/lubu/url-comments-304
Add URL-scoped comment feeds
This commit is contained in:
+3
@@ -71,6 +71,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.datasource.RelayInfo
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.shorts.datasource.ShortsFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.softwareapps.datasource.SoftwareAppsFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.ThreadFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.url.datasource.UrlFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.VideoFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.datasource.OnchainZapsFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.workouts.datasource.WorkoutsFilterAssembler
|
||||
@@ -116,6 +117,7 @@ class RelaySubscriptionsCoordinator(
|
||||
val profile = UserProfileFilterAssembler(client)
|
||||
val hashtags = HashtagFilterAssembler(client)
|
||||
val geohashes = GeoHashFilterAssembler(client)
|
||||
val urls = UrlFilterAssembler(client)
|
||||
val relayFeed = RelayFeedFilterAssembler(client)
|
||||
val relayInfoNip66 = RelayInfoNip66FilterAssembler(client)
|
||||
val followPacks = FollowPackFeedFilterAssembler(client)
|
||||
@@ -209,6 +211,7 @@ class RelaySubscriptionsCoordinator(
|
||||
profile,
|
||||
hashtags,
|
||||
geohashes,
|
||||
urls,
|
||||
relayFeed,
|
||||
relayInfoNip66,
|
||||
chess,
|
||||
|
||||
@@ -51,8 +51,10 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.NPub
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect
|
||||
import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent
|
||||
import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent
|
||||
import com.vitorpamplona.quartz.nip73ExternalIds.urls.UrlId
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import com.vitorpamplona.quartz.utils.UriParser
|
||||
import java.net.URLDecoder
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -147,6 +149,18 @@ fun isNotificationRoute(uri: String) = uri.startsWith("notifications", true) ||
|
||||
|
||||
fun isHashtagRoute(uri: String) = uri.startsWith("hashtag?id=") || uri.startsWith("nostr:hashtag?id=")
|
||||
|
||||
fun isUrlRoute(uri: String) = uri.startsWith("url?id=") || uri.startsWith("nostr:url?id=")
|
||||
|
||||
fun urlRoute(uri: String): Route.Url? {
|
||||
val url =
|
||||
runCatching {
|
||||
val rawUrl = java.net.URI(uri.removePrefix("nostr:")).findParameterValue("id") ?: return null
|
||||
URLDecoder.decode(rawUrl, Charsets.UTF_8.name())
|
||||
}.getOrNull() ?: return null
|
||||
|
||||
return UrlId.toScopeOrNull(url)?.let { Route.Url(it) }
|
||||
}
|
||||
|
||||
fun isWalletConnectRoute(uri: String) = uri.startsWith("dlnwc?value=") || uri.startsWith("amethyst+walletconnect:dlnwc?value=") || uri.startsWith("amethyst+walletconnect://dlnwc?value=")
|
||||
|
||||
fun isMarmotGroupRoute(uri: String) = uri.startsWith("marmot:")
|
||||
@@ -164,6 +178,9 @@ fun uriToRoute(
|
||||
if (isHashtagRoute(uri)) {
|
||||
return Route.Hashtag(uri.removePrefix("nostr:").removePrefix("hashtag?id=").lowercase())
|
||||
}
|
||||
if (isUrlRoute(uri)) {
|
||||
return urlRoute(uri)
|
||||
}
|
||||
|
||||
val nip19 = Nip19Parser.uriToRoute(uri)?.entity
|
||||
if (nip19 != null) {
|
||||
|
||||
@@ -30,6 +30,8 @@ import com.vitorpamplona.amethyst.commons.richtext.MediaUrlPdf
|
||||
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo
|
||||
import com.vitorpamplona.amethyst.model.UrlCachedPreviewer
|
||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfVertPadding
|
||||
|
||||
@@ -39,11 +41,12 @@ fun LoadUrlPreview(
|
||||
urlText: String,
|
||||
callbackUri: String? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav? = null,
|
||||
) {
|
||||
if (!accountViewModel.settings.showUrlPreview()) {
|
||||
ClickableUrl(urlText, url)
|
||||
} else {
|
||||
LoadUrlPreviewDirect(url, urlText, callbackUri, accountViewModel)
|
||||
LoadUrlPreviewDirect(url, urlText, callbackUri, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +56,7 @@ fun LoadUrlPreviewDirect(
|
||||
urlText: String,
|
||||
callbackUri: String? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav? = null,
|
||||
) {
|
||||
@Suppress("ProduceStateDoesNotAssignValue")
|
||||
val urlPreviewState by
|
||||
@@ -72,7 +76,7 @@ fun LoadUrlPreviewDirect(
|
||||
) { state ->
|
||||
when (state) {
|
||||
is UrlPreviewState.Loaded -> {
|
||||
RenderLoaded(state, url, callbackUri, accountViewModel)
|
||||
RenderLoaded(state, url, callbackUri, accountViewModel, nav)
|
||||
}
|
||||
|
||||
is UrlPreviewState.Loading -> {
|
||||
@@ -94,6 +98,7 @@ fun RenderLoaded(
|
||||
url: String,
|
||||
callbackUri: String? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav? = null,
|
||||
) {
|
||||
when {
|
||||
state.previewInfo.mimeType.startsWith("image") -> {
|
||||
@@ -130,7 +135,14 @@ fun RenderLoaded(
|
||||
}
|
||||
|
||||
else -> {
|
||||
UrlPreviewCard(url, state.previewInfo)
|
||||
UrlPreviewCard(
|
||||
url,
|
||||
state.previewInfo,
|
||||
onUrlComments =
|
||||
nav?.let {
|
||||
{ it.nav(Route.Url(url)) }
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -549,7 +549,7 @@ private fun RenderWordWithPreview(
|
||||
is ImageSegment -> ZoomableContentView(word.segmentText, state, accountViewModel)
|
||||
is VideoSegment -> ZoomableContentView(word.segmentText, state, accountViewModel)
|
||||
is PdfSegment -> ZoomableContentView(word.segmentText, state, accountViewModel)
|
||||
is LinkSegment -> LoadUrlPreview(word.segmentText, word.segmentText, callbackUri, accountViewModel)
|
||||
is LinkSegment -> LoadUrlPreview(word.segmentText, word.segmentText, callbackUri, accountViewModel, nav)
|
||||
is NowhereLinkSegment -> NowhereLinkCard(word)
|
||||
is EmojiSegment -> RenderCustomEmoji(word.segmentText, state)
|
||||
is InvoiceSegment -> MayBeInvoicePreview(word.segmentText, accountViewModel)
|
||||
|
||||
@@ -52,6 +52,7 @@ import kotlinx.coroutines.launch
|
||||
fun UrlPreviewCard(
|
||||
url: String,
|
||||
previewInfo: UrlInfoItem,
|
||||
onUrlComments: (() -> Unit)? = null,
|
||||
) {
|
||||
val uri = LocalUriHandler.current
|
||||
val popupExpanded =
|
||||
@@ -76,6 +77,15 @@ fun UrlPreviewCard(
|
||||
popupExpanded.value = false
|
||||
}
|
||||
}
|
||||
onUrlComments?.let {
|
||||
M3ActionRow(
|
||||
icon = MaterialSymbols.Link,
|
||||
text = stringRes(R.string.kind_comments),
|
||||
) {
|
||||
popupExpanded.value = false
|
||||
it()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -132,7 +132,7 @@ class MarkdownMediaRenderer(
|
||||
renderAsCompleteLink(title ?: uri, uri, richTextStringBuilder)
|
||||
} else {
|
||||
renderInlineFullWidth(richTextStringBuilder) {
|
||||
LoadUrlPreview(uri, title ?: uri, callbackUri, accountViewModel)
|
||||
LoadUrlPreview(uri, title ?: uri, callbackUri, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,6 +214,8 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.shorts.ShortsScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.softwareapps.SoftwareAppDetailScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.softwareapps.SoftwareAppsScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.ThreadScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.url.UrlPostScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.url.UrlScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.VideoScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.hls.NewHlsVideoScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.AddCashuWalletScreen
|
||||
@@ -460,6 +462,7 @@ fun BuildNavigation(
|
||||
composableFromEndArgs<Route.ContactListUsers> { ContactListUsersScreen(it.noteId, accountViewModel, nav) }
|
||||
composableFromEndArgs<Route.Hashtag> { HashtagScreen(it, accountViewModel, nav) }
|
||||
composableFromEndArgs<Route.Geohash> { GeoHashScreen(it, accountViewModel, nav) }
|
||||
composableFromEndArgs<Route.Url> { UrlScreen(it, accountViewModel, nav) }
|
||||
composableFromEndArgs<Route.RelayFeed> { RelayFeedScreen(it, accountViewModel, nav) }
|
||||
composableFromEndArgs<Route.ChessGame> { ChessGameScreen(it.gameId, accountViewModel, nav) }
|
||||
composableFromEndArgs<Route.RelayInfo> { RelayInformationScreen(it.url, accountViewModel, nav) }
|
||||
@@ -570,6 +573,19 @@ fun BuildNavigation(
|
||||
)
|
||||
}
|
||||
|
||||
composableFromBottomArgs<Route.UrlPost> {
|
||||
UrlPostScreen(
|
||||
url = it.url,
|
||||
message = it.message,
|
||||
attachment = it.attachment,
|
||||
replyId = it.replyTo,
|
||||
quoteId = it.quote,
|
||||
draftId = it.draft,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
|
||||
composableFromBottomArgs<Route.GenericCommentPost> {
|
||||
ReplyCommentPostScreen(
|
||||
replyId = it.replyTo,
|
||||
|
||||
+5
@@ -34,6 +34,7 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NPub
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import java.net.URLEncoder
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
@@ -144,6 +145,10 @@ class BouncingIntentNav(
|
||||
NOSTR_URI_PREFIX + "hashtag?id=" + route.hashtag
|
||||
}
|
||||
|
||||
is Route.Url -> {
|
||||
NOSTR_URI_PREFIX + "url?id=" + URLEncoder.encode(route.url, Charsets.UTF_8.name())
|
||||
}
|
||||
|
||||
is Route.LiveActivityChannel -> {
|
||||
NOSTR_URI_PREFIX + NAddress.create(route.kind, route.pubKeyHex, route.dTag, null)
|
||||
}
|
||||
|
||||
@@ -477,6 +477,10 @@ sealed class Route {
|
||||
val geohash: String,
|
||||
) : Route()
|
||||
|
||||
@Serializable data class Url(
|
||||
val url: String,
|
||||
) : Route()
|
||||
|
||||
@Serializable data class ChessGame(
|
||||
val gameId: String,
|
||||
) : Route()
|
||||
@@ -691,6 +695,16 @@ sealed class Route {
|
||||
val draft: String? = null,
|
||||
) : Route()
|
||||
|
||||
@Serializable
|
||||
data class UrlPost(
|
||||
val url: String? = null,
|
||||
val message: String? = null,
|
||||
val attachment: String? = null,
|
||||
val replyTo: String? = null,
|
||||
val quote: String? = null,
|
||||
val draft: String? = null,
|
||||
) : Route()
|
||||
|
||||
@Serializable
|
||||
data class GenericCommentPost(
|
||||
val message: String? = null,
|
||||
|
||||
+11
-5
@@ -84,7 +84,7 @@ fun PreviewUrl(
|
||||
}
|
||||
|
||||
else -> {
|
||||
MyLoadUrlPreviewDirect(myUrlPreview, myUrlPreview, accountViewModel)
|
||||
MyLoadUrlPreviewDirect(myUrlPreview, myUrlPreview, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,7 +104,7 @@ fun PreviewUrl(
|
||||
}
|
||||
|
||||
RichTextParser.isUrlWithoutScheme(myUrlPreview) -> {
|
||||
MyLoadUrlPreviewDirect("https://$myUrlPreview", myUrlPreview, accountViewModel)
|
||||
MyLoadUrlPreviewDirect("https://$myUrlPreview", myUrlPreview, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -138,7 +138,7 @@ fun PreviewUrlFillWidth(
|
||||
}
|
||||
|
||||
else -> {
|
||||
MyLoadUrlPreviewDirectFillWidth(myUrlPreview, myUrlPreview, accountViewModel)
|
||||
MyLoadUrlPreviewDirectFillWidth(myUrlPreview, myUrlPreview, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
} else if (RichTextParser.startsWithNIP19Scheme(myUrlPreview)) {
|
||||
@@ -154,7 +154,7 @@ fun PreviewUrlFillWidth(
|
||||
nav = nav,
|
||||
)
|
||||
} else if (RichTextParser.isUrlWithoutScheme(myUrlPreview)) {
|
||||
MyLoadUrlPreviewDirectFillWidth("https://$myUrlPreview", myUrlPreview, accountViewModel)
|
||||
MyLoadUrlPreviewDirectFillWidth("https://$myUrlPreview", myUrlPreview, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,6 +200,7 @@ private fun MyLoadUrlPreviewDirect(
|
||||
url: String,
|
||||
urlText: String,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
@Suppress("ProduceStateDoesNotAssignValue")
|
||||
val urlPreviewState by
|
||||
@@ -268,6 +269,7 @@ private fun MyLoadUrlPreviewDirectFillWidth(
|
||||
url: String,
|
||||
urlText: String,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
@Suppress("ProduceStateDoesNotAssignValue")
|
||||
val urlPreviewState by
|
||||
@@ -304,7 +306,11 @@ private fun MyLoadUrlPreviewDirectFillWidth(
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
} else {
|
||||
UrlPreviewCard(url, previewInfo = state.previewInfo)
|
||||
UrlPreviewCard(
|
||||
url,
|
||||
previewInfo = state.previewInfo,
|
||||
onUrlComments = { nav.nav(com.vitorpamplona.amethyst.ui.navigation.routes.Route.Url(url)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+7
-5
@@ -30,7 +30,6 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.text.LinkAnnotation
|
||||
import androidx.compose.ui.text.LinkInteractionListener
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
@@ -43,6 +42,7 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
@@ -68,7 +68,7 @@ fun DisplayExternalId(
|
||||
}
|
||||
|
||||
is UrlId -> {
|
||||
DisplayUrlExternalId(externalId)
|
||||
DisplayUrlExternalId(externalId, nav)
|
||||
}
|
||||
|
||||
else -> {
|
||||
@@ -78,13 +78,15 @@ fun DisplayExternalId(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DisplayUrlExternalId(externalId: UrlId) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
fun DisplayUrlExternalId(
|
||||
externalId: UrlId,
|
||||
nav: INav,
|
||||
) {
|
||||
DisplayExternalIdChip(
|
||||
symbol = MaterialSymbols.Link,
|
||||
contentDescription = stringRes(id = R.string.external_url_scope),
|
||||
label = externalId.url,
|
||||
linkInteractionListener = { runCatching { uriHandler.openUri(externalId.url) } },
|
||||
linkInteractionListener = { nav.nav(Route.Url(externalId.url)) },
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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.url
|
||||
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.ui.painterRes
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size26Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
|
||||
|
||||
@Composable
|
||||
fun NewUrlPostButton(
|
||||
url: String,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
nav.nav(Route.UrlPost(url))
|
||||
},
|
||||
modifier = Size55Modifier,
|
||||
shape = CircleShape,
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterRes(R.drawable.ic_compose, 1),
|
||||
contentDescription = stringRes(id = R.string.new_community_note),
|
||||
modifier = Size26Modifier,
|
||||
tint = Color.White,
|
||||
)
|
||||
}
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* 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.url
|
||||
|
||||
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.Nav
|
||||
import com.vitorpamplona.amethyst.ui.note.nip22Comments.CommentPostViewModel
|
||||
import com.vitorpamplona.amethyst.ui.note.nip22Comments.GenericCommentPostScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip73ExternalIds.urls.UrlId
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@Composable
|
||||
fun UrlPostScreen(
|
||||
url: String? = null,
|
||||
message: String? = null,
|
||||
attachment: String? = null,
|
||||
replyId: HexKey? = null,
|
||||
quoteId: HexKey? = null,
|
||||
draftId: HexKey? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: Nav,
|
||||
) {
|
||||
val postViewModel: CommentPostViewModel = viewModel()
|
||||
postViewModel.init(accountViewModel)
|
||||
|
||||
val context = LocalContext.current
|
||||
|
||||
LaunchedEffect(postViewModel, accountViewModel) {
|
||||
url?.let {
|
||||
UrlId.toScopeOrNull(it)?.let { normalizedUrl ->
|
||||
postViewModel.newPostFor(UrlId(normalizedUrl))
|
||||
}
|
||||
}
|
||||
replyId?.let { accountViewModel.getNoteIfExists(it) }?.let {
|
||||
postViewModel.reply(it)
|
||||
}
|
||||
draftId?.let { accountViewModel.getNoteIfExists(it) }?.let {
|
||||
postViewModel.editFromDraft(it)
|
||||
}
|
||||
quoteId?.let { accountViewModel.getNoteIfExists(it) }?.let {
|
||||
postViewModel.quote(it)
|
||||
}
|
||||
message?.ifBlank { null }?.let {
|
||||
postViewModel.message.setTextAndPlaceCursorAtEnd(it)
|
||||
postViewModel.onMessageChanged()
|
||||
}
|
||||
attachment?.ifBlank { null }?.toUri()?.let {
|
||||
withContext(Dispatchers.IO) {
|
||||
val mediaType = context.contentResolver.getType(it)
|
||||
postViewModel.selectImage(persistentListOf(SelectedMedia(it, mediaType)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GenericCommentPostScreen(postViewModel, accountViewModel, nav)
|
||||
}
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* 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.url
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel
|
||||
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
|
||||
import com.vitorpamplona.amethyst.ui.navigation.bottombars.FabBottomBarPadded
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarExtensibleWithBackButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.url.dal.UrlFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.url.datasource.UrlFilterAssemblerSubscription
|
||||
import com.vitorpamplona.quartz.nip73ExternalIds.urls.UrlId
|
||||
|
||||
@Composable
|
||||
fun UrlScreen(
|
||||
route: Route.Url,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val url = UrlId.toScopeOrNull(route.url) ?: return
|
||||
|
||||
PrepareViewModelsUrlScreen(url, accountViewModel, nav)
|
||||
}
|
||||
|
||||
@SuppressLint("StateFlowValueCalledInComposition")
|
||||
@Composable
|
||||
fun PrepareViewModelsUrlScreen(
|
||||
url: String,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val urlFeedViewModel: UrlFeedViewModel =
|
||||
viewModel(
|
||||
key = url + "UrlFeedViewModel",
|
||||
factory =
|
||||
UrlFeedViewModel.Factory(
|
||||
url,
|
||||
accountViewModel.account.followOutboxesOrProxy.flow.value,
|
||||
accountViewModel.account,
|
||||
),
|
||||
)
|
||||
|
||||
UrlScreen(url, urlFeedViewModel, accountViewModel, nav)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UrlScreen(
|
||||
url: String,
|
||||
feedViewModel: UrlFeedViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
WatchLifecycleAndUpdateModel(feedViewModel)
|
||||
UrlFilterAssemblerSubscription(url, accountViewModel)
|
||||
|
||||
DisappearingScaffold(
|
||||
isInvertedLayout = false,
|
||||
topBar = {
|
||||
TopBarExtensibleWithBackButton(
|
||||
title = {
|
||||
DisplayUrlHeader(url, Modifier.weight(1f))
|
||||
},
|
||||
popBack = nav::popBack,
|
||||
)
|
||||
},
|
||||
floatingButton = {
|
||||
FabBottomBarPadded(nav) {
|
||||
NewUrlPostButton(url, accountViewModel, nav)
|
||||
}
|
||||
},
|
||||
accountViewModel = accountViewModel,
|
||||
) {
|
||||
RefresheableFeedView(
|
||||
feedViewModel,
|
||||
null,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DisplayUrlHeader(
|
||||
url: String,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
Text(
|
||||
url,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = modifier,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* 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.url.dal
|
||||
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
||||
import com.vitorpamplona.quartz.nip73ExternalIds.urls.UrlId
|
||||
|
||||
class UrlFeedFilter(
|
||||
url: String,
|
||||
val relays: Set<NormalizedRelayUrl>,
|
||||
val account: Account,
|
||||
val cache: LocalCache,
|
||||
) : AdditiveFeedFilter<Note>() {
|
||||
val normalizedUrl = UrlId.toScopeOrNull(url).orEmpty()
|
||||
|
||||
override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + normalizedUrl
|
||||
|
||||
override fun feed(): List<Note> {
|
||||
val notes =
|
||||
cache.notes.filterIntoSet { _, it ->
|
||||
acceptableEvent(it, normalizedUrl)
|
||||
}
|
||||
|
||||
return sort(notes)
|
||||
}
|
||||
|
||||
override fun applyFilter(newItems: Set<Note>): Set<Note> = newItems.filterTo(HashSet()) { acceptableEvent(it, normalizedUrl) }
|
||||
|
||||
fun acceptableEvent(
|
||||
it: Note,
|
||||
url: String,
|
||||
): Boolean =
|
||||
acceptableViaScope(it.event, url) &&
|
||||
!it.isHiddenFor(account.hiddenUsers.flow.value) &&
|
||||
account.isAcceptable(it)
|
||||
|
||||
fun acceptableViaScope(
|
||||
event: com.vitorpamplona.quartz.nip01Core.core.Event?,
|
||||
url: String,
|
||||
): Boolean = UrlId.toScopeOrNull(url)?.let { event is CommentEvent && event.isTaggedScope(it, UrlId::match) } ?: false
|
||||
|
||||
override fun sort(items: Set<Note>): List<Note> = items.sortedByDefaultFeedOrder()
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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.url.dal
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.ui.screen.AndroidFeedViewModel
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
|
||||
@Stable
|
||||
class UrlFeedViewModel(
|
||||
val url: String,
|
||||
val relays: Set<NormalizedRelayUrl>,
|
||||
val account: Account,
|
||||
) : AndroidFeedViewModel(
|
||||
UrlFeedFilter(url, relays, account, LocalCache),
|
||||
) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
class Factory(
|
||||
val url: String,
|
||||
val relays: Set<NormalizedRelayUrl>,
|
||||
val account: Account,
|
||||
) : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T = UrlFeedViewModel(url, relays, account) as T
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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.url.datasource
|
||||
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip22Comments.CommentKinds
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip73ExternalIds.urls.UrlId
|
||||
|
||||
fun filterPostsByUrl(
|
||||
url: String,
|
||||
relays: Set<NormalizedRelayUrl>,
|
||||
since: SincePerRelayMap?,
|
||||
): List<RelayBasedFilter> {
|
||||
val normalizedUrl = UrlId.toScopeOrNull(url) ?: return emptyList()
|
||||
val urlScopeMap = mapOf("I" to listOf(normalizedUrl))
|
||||
|
||||
return relays.map { relay ->
|
||||
val since = since?.get(relay)?.time
|
||||
RelayBasedFilter(
|
||||
relay = relay,
|
||||
filter =
|
||||
Filter(
|
||||
tags = urlScopeMap,
|
||||
kinds = CommentKinds,
|
||||
limit = 100,
|
||||
since = since,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 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.url.datasource
|
||||
|
||||
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
|
||||
class UrlFeedFilterSubAssembler(
|
||||
client: INostrClient,
|
||||
allKeys: () -> Set<UrlQueryState>,
|
||||
) : PerUniqueIdEoseManager<UrlQueryState, String>(client, allKeys) {
|
||||
override fun updateFilter(
|
||||
key: UrlQueryState,
|
||||
since: SincePerRelayMap?,
|
||||
): List<RelayBasedFilter> = filterPostsByUrl(key.normalizedUrl, key.relays, since)
|
||||
|
||||
override fun id(key: UrlQueryState) = key.normalizedUrl
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 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.url.datasource
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip73ExternalIds.urls.UrlId
|
||||
|
||||
class UrlQueryState(
|
||||
val url: String,
|
||||
val relays: Set<NormalizedRelayUrl>,
|
||||
) {
|
||||
val normalizedUrl = UrlId.toScopeOrNull(url).orEmpty()
|
||||
}
|
||||
|
||||
@Stable
|
||||
class UrlFilterAssembler(
|
||||
client: INostrClient,
|
||||
) : ComposeSubscriptionManager<UrlQueryState>() {
|
||||
val group =
|
||||
listOf(
|
||||
UrlFeedFilterSubAssembler(client, ::allKeys),
|
||||
)
|
||||
|
||||
override fun invalidateKeys() = invalidateFilters()
|
||||
|
||||
override fun invalidateFilters() = group.forEach { it.invalidateFilters() }
|
||||
|
||||
override fun destroy() = group.forEach { it.destroy() }
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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.url.datasource
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
@SuppressLint("StateFlowValueCalledInComposition")
|
||||
@Composable
|
||||
fun UrlFilterAssemblerSubscription(
|
||||
url: String,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
val state =
|
||||
remember(url) {
|
||||
UrlQueryState(url, accountViewModel.account.followOutboxesOrProxy.flow.value)
|
||||
}
|
||||
|
||||
LifecycleAwareKeyDataSourceSubscription(state, accountViewModel.dataSources().urls)
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.ui.urlRoute
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Test
|
||||
import java.net.URLEncoder
|
||||
|
||||
class URIParserTest {
|
||||
@Test
|
||||
fun parsesEncodedUrlRoutes() {
|
||||
val url = "HTTPS://Example.com/path?b=2&a=1#section"
|
||||
val route = urlRoute("nostr:url?id=${URLEncoder.encode(url, Charsets.UTF_8.name())}")
|
||||
|
||||
assertEquals(Route.Url("https://example.com/path?b=2&a=1"), route)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ignoresMalformedEncodedUrlRoutes() {
|
||||
assertNull(urlRoute("nostr:url?id=%"))
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 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.url.datasource
|
||||
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip22Comments.CommentKinds
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class FilterPostsByUrlTest {
|
||||
@Test
|
||||
fun normalizesUrlAndQueriesNip22UrlScope() {
|
||||
val relay = NormalizedRelayUrl("wss://relay.example/")
|
||||
val filters = filterPostsByUrl("HTTPS://Example.com/path?b=2&a=1#section", setOf(relay), null)
|
||||
|
||||
assertEquals(1, filters.size)
|
||||
assertEquals(relay, filters[0].relay)
|
||||
assertEquals(CommentKinds, filters[0].filter.kinds)
|
||||
assertEquals(mapOf("I" to listOf("https://example.com/path?b=2&a=1")), filters[0].filter.tags)
|
||||
assertEquals(100, filters[0].filter.limit)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun returnsNoFiltersForMalformedUrls() {
|
||||
val relay = NormalizedRelayUrl("wss://relay.example/")
|
||||
|
||||
val filters = filterPostsByUrl("https://", setOf(relay), null)
|
||||
|
||||
assertTrue(filters.isEmpty())
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,11 @@ class UrlId(
|
||||
|
||||
fun toScope(url: String) = Rfc3986.normalizeAndRemoveFragment(url)
|
||||
|
||||
fun toScopeOrNull(url: String) =
|
||||
runCatching { toScope(url) }
|
||||
.getOrNull()
|
||||
?.takeIf { parse(it) != null }
|
||||
|
||||
fun toKind(url: String) = KIND
|
||||
|
||||
fun match(
|
||||
|
||||
Reference in New Issue
Block a user