diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ExpandableRichTextViewer.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ExpandableRichTextViewer.kt index 5bcc7b6c0..477e7ac2b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ExpandableRichTextViewer.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ExpandableRichTextViewer.kt @@ -43,7 +43,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.ExpandableTextParser +import com.vitorpamplona.amethyst.commons.ExpandableTextCutOffCalculator import com.vitorpamplona.amethyst.ui.note.getGradient import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.ButtonBorder @@ -63,7 +63,7 @@ fun ExpandableRichTextViewer( ) { var showFullText by remember { mutableStateOf(false) } - val whereToCut = remember(content) { ExpandableTextParser.computeWhereToCutIfPostIsTooLong(content) } + val whereToCut = remember(content) { ExpandableTextCutOffCalculator.indexToCutOff(content) } val text by remember(content) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/LoadUrlPreview.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/LoadUrlPreview.kt index f48b52a74..2d71b7079 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/LoadUrlPreview.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/LoadUrlPreview.kt @@ -29,8 +29,8 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import com.vitorpamplona.amethyst.commons.ZoomableUrlImage -import com.vitorpamplona.amethyst.commons.ZoomableUrlVideo +import com.vitorpamplona.amethyst.commons.MediaUrlImage +import com.vitorpamplona.amethyst.commons.MediaUrlVideo import com.vitorpamplona.amethyst.model.UrlCachedPreviewer import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.HalfVertPadding @@ -69,7 +69,7 @@ fun LoadUrlPreview( if (state.previewInfo.mimeType.type == "image") { Box(modifier = HalfVertPadding) { ZoomableContentView( - content = ZoomableUrlImage(url), + content = MediaUrlImage(url), roundedCorner = true, accountViewModel = accountViewModel, ) @@ -77,7 +77,7 @@ fun LoadUrlPreview( } else if (state.previewInfo.mimeType.type == "video") { Box(modifier = HalfVertPadding) { ZoomableContentView( - content = ZoomableUrlVideo(url), + content = MediaUrlVideo(url), roundedCorner = true, accountViewModel = accountViewModel, ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt index 01164e84c..51ffb3f11 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt @@ -78,6 +78,7 @@ import com.vitorpamplona.amethyst.commons.HashTagSegment import com.vitorpamplona.amethyst.commons.ImageSegment import com.vitorpamplona.amethyst.commons.InvoiceSegment import com.vitorpamplona.amethyst.commons.LinkSegment +import com.vitorpamplona.amethyst.commons.MediaUrlImage import com.vitorpamplona.amethyst.commons.PhoneSegment import com.vitorpamplona.amethyst.commons.RegularTextSegment import com.vitorpamplona.amethyst.commons.RichTextParser @@ -85,7 +86,6 @@ import com.vitorpamplona.amethyst.commons.RichTextViewerState import com.vitorpamplona.amethyst.commons.SchemelessUrlSegment import com.vitorpamplona.amethyst.commons.Segment import com.vitorpamplona.amethyst.commons.WithdrawSegment -import com.vitorpamplona.amethyst.commons.ZoomableUrlImage import com.vitorpamplona.amethyst.model.HashtagIcon import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User @@ -417,7 +417,7 @@ private fun RenderContentAsMarkdown( ZoomableContentView( content = remember(destination, tags) { - RichTextParser().parseMediaUrl(destination, tags ?: EmptyTagList) ?: ZoomableUrlImage(url = destination) + RichTextParser().parseMediaUrl(destination, tags ?: EmptyTagList) ?: MediaUrlImage(url = destination) }, roundedCorner = true, accountViewModel = accountViewModel, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt index ac9715ba3..6888e012b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt @@ -106,13 +106,13 @@ import coil.compose.AsyncImage import coil.compose.AsyncImagePainter import coil.imageLoader import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.ZoomableContent -import com.vitorpamplona.amethyst.commons.ZoomableLocalImage -import com.vitorpamplona.amethyst.commons.ZoomableLocalVideo -import com.vitorpamplona.amethyst.commons.ZoomablePreloadedContent -import com.vitorpamplona.amethyst.commons.ZoomableUrlContent -import com.vitorpamplona.amethyst.commons.ZoomableUrlImage -import com.vitorpamplona.amethyst.commons.ZoomableUrlVideo +import com.vitorpamplona.amethyst.commons.BaseMediaContent +import com.vitorpamplona.amethyst.commons.MediaLocalImage +import com.vitorpamplona.amethyst.commons.MediaLocalVideo +import com.vitorpamplona.amethyst.commons.MediaPreloadedContent +import com.vitorpamplona.amethyst.commons.MediaUrlContent +import com.vitorpamplona.amethyst.commons.MediaUrlImage +import com.vitorpamplona.amethyst.commons.MediaUrlVideo import com.vitorpamplona.amethyst.service.BlurHashRequester import com.vitorpamplona.amethyst.ui.actions.CloseButton import com.vitorpamplona.amethyst.ui.actions.InformationDialog @@ -147,8 +147,8 @@ import net.engawapg.lib.zoomable.zoomable @Composable @OptIn(ExperimentalFoundationApi::class) fun ZoomableContentView( - content: ZoomableContent, - images: ImmutableList = remember(content) { listOf(content).toImmutableList() }, + content: BaseMediaContent, + images: ImmutableList = remember(content) { listOf(content).toImmutableList() }, roundedCorner: Boolean, accountViewModel: AccountViewModel, ) { @@ -169,13 +169,13 @@ fun ZoomableContentView( Modifier.fillMaxWidth() } - if (content is ZoomableUrlContent) { + if (content is MediaUrlContent) { mainImageModifier = mainImageModifier.combinedClickable( onClick = { dialogOpen = true }, onLongClick = { shareOpen.value = true }, ) - } else if (content is ZoomablePreloadedContent) { + } else if (content is MediaPreloadedContent) { mainImageModifier = mainImageModifier.combinedClickable( onClick = { dialogOpen = true }, @@ -186,11 +186,11 @@ fun ZoomableContentView( } when (content) { - is ZoomableUrlImage -> + is MediaUrlImage -> SensitivityWarning(content.contentWarning != null, accountViewModel) { UrlImageView(content, mainImageModifier, accountViewModel = accountViewModel) } - is ZoomableUrlVideo -> + is MediaUrlVideo -> SensitivityWarning(content.contentWarning != null, accountViewModel) { VideoView( videoUri = content.url, @@ -205,9 +205,9 @@ fun ZoomableContentView( accountViewModel = accountViewModel, ) } - is ZoomableLocalImage -> + is MediaLocalImage -> LocalImageView(content, mainImageModifier, accountViewModel = accountViewModel) - is ZoomableLocalVideo -> + is MediaLocalVideo -> content.localFile?.let { VideoView( videoUri = it.toUri().toString(), @@ -229,7 +229,7 @@ fun ZoomableContentView( @Composable private fun LocalImageView( - content: ZoomableLocalImage, + content: MediaLocalImage, mainImageModifier: Modifier, topPaddingForControllers: Dp = Dp.Unspecified, accountViewModel: AccountViewModel, @@ -296,7 +296,7 @@ private fun LocalImageView( @Composable private fun UrlImageView( - content: ZoomableUrlImage, + content: MediaUrlImage, mainImageModifier: Modifier, topPaddingForControllers: Dp = Dp.Unspecified, accountViewModel: AccountViewModel, @@ -419,7 +419,7 @@ private fun InlineDownloadIcon(showImage: MutableState) = @OptIn(ExperimentalLayoutApi::class) private fun AddedImageFeatures( painter: MutableState, - content: ZoomableLocalImage, + content: MediaLocalImage, contentScale: ContentScale, myModifier: Modifier, verifiedModifier: Modifier, @@ -481,7 +481,7 @@ private fun AddedImageFeatures( @OptIn(ExperimentalLayoutApi::class) private fun AddedImageFeatures( painter: MutableState, - content: ZoomableUrlImage, + content: MediaUrlImage, contentScale: ContentScale, myModifier: Modifier, verifiedModifier: Modifier, @@ -576,8 +576,8 @@ fun aspectRatio(dim: String?): Float? { } @Composable -private fun DisplayUrlWithLoadingSymbol(content: ZoomableContent) { - var cnt by remember { mutableStateOf(null) } +private fun DisplayUrlWithLoadingSymbol(content: BaseMediaContent) { + var cnt by remember { mutableStateOf(null) } LaunchedEffect(Unit) { launch(Dispatchers.IO) { @@ -590,7 +590,7 @@ private fun DisplayUrlWithLoadingSymbol(content: ZoomableContent) { } @Composable -private fun DisplayUrlWithLoadingSymbolWait(content: ZoomableContent) { +private fun DisplayUrlWithLoadingSymbolWait(content: BaseMediaContent) { val uri = LocalUriHandler.current val primary = MaterialTheme.colorScheme.primary @@ -602,7 +602,7 @@ private fun DisplayUrlWithLoadingSymbolWait(content: ZoomableContent) { val annotatedTermsString = remember { buildAnnotatedString { - if (content is ZoomableUrlContent) { + if (content is MediaUrlContent) { withStyle(clickableTextStyle) { pushStringAnnotation("routeToImage", "") append(content.url + " ") @@ -624,7 +624,7 @@ private fun DisplayUrlWithLoadingSymbolWait(content: ZoomableContent) { val pressIndicator = remember { - if (content is ZoomableUrlContent) { + if (content is MediaUrlContent) { Modifier.clickable { runCatching { uri.openUri(content.url) } } } else { Modifier @@ -676,8 +676,8 @@ fun DisplayBlurHash( @Composable fun ZoomableImageDialog( - imageUrl: ZoomableContent, - allImages: ImmutableList = listOf(imageUrl).toImmutableList(), + imageUrl: BaseMediaContent, + allImages: ImmutableList = listOf(imageUrl).toImmutableList(), onDismiss: () -> Unit, accountViewModel: AccountViewModel, ) { @@ -740,8 +740,8 @@ fun ZoomableImageDialog( @Composable @OptIn(ExperimentalFoundationApi::class) private fun DialogContent( - allImages: ImmutableList, - imageUrl: ZoomableContent, + allImages: ImmutableList, + imageUrl: BaseMediaContent, onDismiss: () -> Unit, accountViewModel: AccountViewModel, ) { @@ -801,13 +801,13 @@ private fun DialogContent( CloseButton(onPress = onDismiss) allImages.getOrNull(pagerState.currentPage)?.let { myContent -> - if (myContent is ZoomableUrlContent) { + if (myContent is MediaUrlContent) { Row { CopyToClipboard(content = myContent) Spacer(modifier = StdHorzSpacer) SaveToGallery(url = myContent.url) } - } else if (myContent is ZoomableLocalImage && myContent.localFileExists()) { + } else if (myContent is MediaLocalImage && myContent.localFileExists()) { SaveToGallery( localFile = myContent.localFile!!, mimeType = myContent.mimeType, @@ -857,7 +857,7 @@ fun InlineCarrousel( } @Composable -private fun CopyToClipboard(content: ZoomableContent) { +private fun CopyToClipboard(content: BaseMediaContent) { val popupExpanded = remember { mutableStateOf(false) } OutlinedButton( @@ -877,7 +877,7 @@ private fun CopyToClipboard(content: ZoomableContent) { @Composable private fun ShareImageAction( popupExpanded: MutableState, - content: ZoomableContent, + content: BaseMediaContent, onDismiss: () -> Unit, ) { DropdownMenu( @@ -886,7 +886,7 @@ private fun ShareImageAction( ) { val clipboardManager = LocalClipboardManager.current - if (content is ZoomableUrlContent) { + if (content is MediaUrlContent) { DropdownMenuItem( text = { Text(stringResource(R.string.copy_url_to_clipboard)) }, onClick = { @@ -905,7 +905,7 @@ private fun ShareImageAction( } } - if (content is ZoomablePreloadedContent) { + if (content is MediaPreloadedContent) { DropdownMenuItem( text = { Text(stringResource(R.string.copy_the_note_id_to_the_clipboard)) }, onClick = { @@ -919,7 +919,7 @@ private fun ShareImageAction( @Composable private fun RenderImageOrVideo( - content: ZoomableContent, + content: BaseMediaContent, roundedCorner: Boolean, topPaddingForControllers: Dp = Dp.Unspecified, onControllerVisibilityChanged: ((Boolean) -> Unit)? = null, @@ -928,7 +928,7 @@ private fun RenderImageOrVideo( ) { val automaticallyStartPlayback = remember { mutableStateOf(true) } - if (content is ZoomableUrlImage) { + if (content is MediaUrlImage) { val mainModifier = Modifier.fillMaxSize() .zoomable( @@ -947,7 +947,7 @@ private fun RenderImageOrVideo( accountViewModel, alwayShowImage = true, ) - } else if (content is ZoomableUrlVideo) { + } else if (content is MediaUrlVideo) { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxSize(1f)) { VideoViewInner( videoUri = content.url, @@ -960,7 +960,7 @@ private fun RenderImageOrVideo( onControllerVisibilityChanged = onControllerVisibilityChanged, ) } - } else if (content is ZoomableLocalImage) { + } else if (content is MediaLocalImage) { val mainModifier = Modifier.fillMaxSize() .zoomable( @@ -979,7 +979,7 @@ private fun RenderImageOrVideo( accountViewModel, alwayShowImage = true, ) - } else if (content is ZoomableLocalVideo) { + } else if (content is MediaLocalVideo) { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxSize(1f)) { content.localFile?.let { VideoViewInner( @@ -999,7 +999,7 @@ private fun RenderImageOrVideo( @OptIn(ExperimentalCoilApi::class) private fun verifyHash( - content: ZoomableUrlContent, + content: MediaUrlContent, context: Context, ): Boolean? { if (content.hash == null) return null diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 80eb86c8f..97ac90c4b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -95,12 +95,12 @@ import coil.request.SuccessResult import com.fonfon.kgeohash.GeoHash import com.fonfon.kgeohash.toGeoHash import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.BaseMediaContent +import com.vitorpamplona.amethyst.commons.MediaLocalImage +import com.vitorpamplona.amethyst.commons.MediaLocalVideo +import com.vitorpamplona.amethyst.commons.MediaUrlImage +import com.vitorpamplona.amethyst.commons.MediaUrlVideo import com.vitorpamplona.amethyst.commons.RichTextParser -import com.vitorpamplona.amethyst.commons.ZoomableContent -import com.vitorpamplona.amethyst.commons.ZoomableLocalImage -import com.vitorpamplona.amethyst.commons.ZoomableLocalVideo -import com.vitorpamplona.amethyst.commons.ZoomableUrlImage -import com.vitorpamplona.amethyst.commons.ZoomableUrlVideo import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.Note @@ -3065,9 +3065,9 @@ fun FileHeaderDisplay( val isImage = RichTextParser.isImageUrl(fullUrl) val uri = note.toNostrUri() - mutableStateOf( + mutableStateOf( if (isImage) { - ZoomableUrlImage( + MediaUrlImage( url = fullUrl, description = description, hash = hash, @@ -3076,7 +3076,7 @@ fun FileHeaderDisplay( uri = uri, ) } else { - ZoomableUrlVideo( + MediaUrlVideo( url = fullUrl, description = description, hash = hash, @@ -3124,9 +3124,9 @@ fun VideoDisplay( val isImage = RichTextParser.isImageUrl(fullUrl) val uri = note.toNostrUri() - mutableStateOf( + mutableStateOf( if (isImage) { - ZoomableUrlImage( + MediaUrlImage( url = fullUrl, description = description, hash = hash, @@ -3135,7 +3135,7 @@ fun VideoDisplay( uri = uri, ) } else { - ZoomableUrlVideo( + MediaUrlVideo( url = fullUrl, description = description, hash = hash, @@ -3265,17 +3265,17 @@ private fun ObserverAndRenderNIP95( val newContent = if (mimeType?.startsWith("image") == true) { - ZoomableLocalImage( + MediaLocalImage( localFile = localDir, mimeType = mimeType, description = description, - blurhash = blurHash, dim = dimensions, + blurhash = blurHash, isVerified = true, uri = uri, ) } else { - ZoomableLocalVideo( + MediaLocalVideo( localFile = localDir, mimeType = mimeType, description = description, @@ -3286,7 +3286,7 @@ private fun ObserverAndRenderNIP95( ) } - mutableStateOf(newContent) + mutableStateOf(newContent) } Crossfade(targetState = content) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt index 561e4bdeb..0a586fd87 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt @@ -101,7 +101,7 @@ import androidx.lifecycle.distinctUntilChanged import androidx.lifecycle.map import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.ZoomableUrlVideo +import com.vitorpamplona.amethyst.commons.MediaUrlVideo import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.LiveActivitiesChannel @@ -681,7 +681,7 @@ fun ShowVideoStreaming( ) { val zoomableUrlVideo = remember(it) { - ZoomableUrlVideo( + MediaUrlVideo( url = url, description = title, artworkUri = artworkUri, diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/ExpandableViewComputationBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/ExpandableViewComputationBenchmark.kt index 7a9918a14..77121f17c 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/ExpandableViewComputationBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/ExpandableViewComputationBenchmark.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.benchmark import androidx.benchmark.junit4.BenchmarkRule import androidx.benchmark.junit4.measureRepeated import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.vitorpamplona.amethyst.commons.ExpandableTextParser +import com.vitorpamplona.amethyst.commons.ExpandableTextCutOffCalculator import com.vitorpamplona.amethyst.commons.nthIndexOf import junit.framework.TestCase import org.junit.Rule @@ -60,7 +60,7 @@ class ExpandableViewComputationBenchmark { benchmarkRule.measureRepeated { TestCase.assertEquals( 293, - ExpandableTextParser.computeWhereToCutIfPostIsTooLong(testCase1), + ExpandableTextCutOffCalculator.indexToCutOff(testCase1), ) } } @@ -70,7 +70,7 @@ class ExpandableViewComputationBenchmark { benchmarkRule.measureRepeated { TestCase.assertEquals( 355, - ExpandableTextParser.computeWhereToCutIfPostIsTooLong(testCase2), + ExpandableTextCutOffCalculator.indexToCutOff(testCase2), ) } } @@ -80,7 +80,7 @@ class ExpandableViewComputationBenchmark { benchmarkRule.measureRepeated { TestCase.assertEquals( 65, - ExpandableTextParser.computeWhereToCutIfPostIsTooLong(testCase3), + ExpandableTextCutOffCalculator.indexToCutOff(testCase3), ) } } diff --git a/app/src/androidTest/java/com/vitorpamplona/amethyst/RichTextParserTest.kt b/commons/src/androidTest/java/com/vitorpamplona/amethyst/commons/RichTextParserTest.kt similarity index 98% rename from app/src/androidTest/java/com/vitorpamplona/amethyst/RichTextParserTest.kt rename to commons/src/androidTest/java/com/vitorpamplona/amethyst/commons/RichTextParserTest.kt index 5df0efdfb..0ca368f6e 100644 --- a/app/src/androidTest/java/com/vitorpamplona/amethyst/RichTextParserTest.kt +++ b/commons/src/androidTest/java/com/vitorpamplona/amethyst/commons/RichTextParserTest.kt @@ -18,13 +18,10 @@ * 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 +package com.vitorpamplona.amethyst.commons import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.vitorpamplona.amethyst.commons.RichTextParser -import com.vitorpamplona.amethyst.commons.RichTextViewerState import com.vitorpamplona.quartz.events.EmptyTagList -import org.junit.Assert import org.junit.Test import org.junit.runner.RunWith @@ -688,8 +685,10 @@ class RichTextParserTest { @Test fun testTextToParse() { - val state = RichTextParser().parseText(textToParse, EmptyTagList) - Assert.assertEquals( + val state = + com.vitorpamplona.amethyst.commons.RichTextParser() + .parseText(textToParse, EmptyTagList) + org.junit.Assert.assertEquals( "relay.shitforce.one, relayable.org, universe.nostrich.land, nos.lol, universe.nostrich.land?lang=zh, universe.nostrich.land?lang=en, relay.damus.io, relay.nostr.wirednet.jp, offchain.pub, nostr.rocks, relay.wellorder.net, nostr.oxtr.dev, universe.nostrich.land?lang=ja, relay.mostr.pub, nostr.bitcoiner.social, Nostr-Check.com, MR.Rabbit, Ancap.su, zapper.lol, smies.me, baller.hodl", state.urlSet.joinToString(", "), ) @@ -4021,26 +4020,28 @@ class RichTextParserTest { .map { it.words } .flatten() .forEachIndexed { index, seg -> - Assert.assertEquals( + org.junit.Assert.assertEquals( expectedResult[index], "${seg.javaClass.simpleName.replace("Segment", "")}(${seg.segmentText})", ) } - Assert.assertTrue(state.imagesForPager.isEmpty()) - Assert.assertTrue(state.imageList.isEmpty()) - Assert.assertTrue(state.customEmoji.isEmpty()) - Assert.assertEquals(651, state.paragraphs.size) + org.junit.Assert.assertTrue(state.imagesForPager.isEmpty()) + org.junit.Assert.assertTrue(state.imageList.isEmpty()) + org.junit.Assert.assertTrue(state.customEmoji.isEmpty()) + org.junit.Assert.assertEquals(651, state.paragraphs.size) } @Test fun testShortTextToParse() { - val state = RichTextParser().parseText("Hi, how are you doing? ", EmptyTagList) - Assert.assertTrue(state.urlSet.isEmpty()) - Assert.assertTrue(state.imagesForPager.isEmpty()) - Assert.assertTrue(state.imageList.isEmpty()) - Assert.assertTrue(state.customEmoji.isEmpty()) - Assert.assertEquals( + val state = + com.vitorpamplona.amethyst.commons.RichTextParser() + .parseText("Hi, how are you doing? ", EmptyTagList) + org.junit.Assert.assertTrue(state.urlSet.isEmpty()) + org.junit.Assert.assertTrue(state.imagesForPager.isEmpty()) + org.junit.Assert.assertTrue(state.imageList.isEmpty()) + org.junit.Assert.assertTrue(state.customEmoji.isEmpty()) + org.junit.Assert.assertEquals( "Hi, how are you doing? ", state.paragraphs.firstOrNull()?.words?.firstOrNull()?.segmentText, ) @@ -4048,12 +4049,14 @@ class RichTextParserTest { @Test fun testShortNewLinesTextToParse() { - val state = RichTextParser().parseText("\nHi, \nhow\n\n\n are you doing? \n", EmptyTagList) - Assert.assertTrue(state.urlSet.isEmpty()) - Assert.assertTrue(state.imagesForPager.isEmpty()) - Assert.assertTrue(state.imageList.isEmpty()) - Assert.assertTrue(state.customEmoji.isEmpty()) - Assert.assertEquals( + val state = + com.vitorpamplona.amethyst.commons.RichTextParser() + .parseText("\nHi, \nhow\n\n\n are you doing? \n", EmptyTagList) + org.junit.Assert.assertTrue(state.urlSet.isEmpty()) + org.junit.Assert.assertTrue(state.imagesForPager.isEmpty()) + org.junit.Assert.assertTrue(state.imageList.isEmpty()) + org.junit.Assert.assertTrue(state.customEmoji.isEmpty()) + org.junit.Assert.assertEquals( "\nHi, \nhow\n\n\n are you doing? \n", state.paragraphs.joinToString("\n") { it.words.joinToString(" ") { it.segmentText } }, ) @@ -4071,17 +4074,22 @@ class RichTextParserTest { """ .trimIndent() - val state = RichTextParser().parseText(text, EmptyTagList) - Assert.assertEquals("https://lnshort.it/live-stream-embeds/", state.urlSet.firstOrNull()) - Assert.assertEquals( + val state = + com.vitorpamplona.amethyst.commons.RichTextParser() + .parseText(text, EmptyTagList) + org.junit.Assert.assertEquals( + "https://lnshort.it/live-stream-embeds/", + state.urlSet.firstOrNull(), + ) + org.junit.Assert.assertEquals( "https://nostr.build/i/fd53fcf5ad950fbe45127e4bcee1b59e8301d41de6beee211f45e344db214e8a.jpg", state.imagesForPager.keys.firstOrNull(), ) - Assert.assertEquals( + org.junit.Assert.assertEquals( "https://nostr.build/i/fd53fcf5ad950fbe45127e4bcee1b59e8301d41de6beee211f45e344db214e8a.jpg", state.imageList.firstOrNull()?.url, ) - Assert.assertTrue(state.customEmoji.isEmpty()) + org.junit.Assert.assertTrue(state.customEmoji.isEmpty()) printStateForDebug(state) @@ -4131,7 +4139,7 @@ class RichTextParserTest { .map { it.words } .flatten() .forEachIndexed { index, seg -> - Assert.assertEquals( + org.junit.Assert.assertEquals( expectedResult[index], "${seg.javaClass.simpleName.replace("Segment", "")}(${seg.segmentText})", ) @@ -4143,7 +4151,9 @@ class RichTextParserTest { val text = "That’s it ! That’s the #note https://cdn.nostr.build/i/1dc0726b6cb0f94a92bd66765ffb90f6c67e90c17bb957fc3d5d4782cbd73de7.jpg " - val state = RichTextParser().parseText(text, EmptyTagList) + val state = + com.vitorpamplona.amethyst.commons.RichTextParser() + .parseText(text, EmptyTagList) printStateForDebug(state) @@ -4162,7 +4172,7 @@ class RichTextParserTest { .map { it.words } .flatten() .forEachIndexed { index, seg -> - Assert.assertEquals( + org.junit.Assert.assertEquals( expectedResult[index], "${seg.javaClass.simpleName.replace("Segment", "")}(${seg.segmentText})", ) @@ -4174,7 +4184,9 @@ class RichTextParserTest { val text = "That’s it! https://cdn.nostr.build/i/1dc0726b6cb0f94a92bd66765ffb90f6c67e90c17bb957fc3d5d4782cbd73de7.jpg That’s the #note" - val state = RichTextParser().parseText(text, EmptyTagList) + val state = + com.vitorpamplona.amethyst.commons.RichTextParser() + .parseText(text, EmptyTagList) printStateForDebug(state) @@ -4192,7 +4204,7 @@ class RichTextParserTest { .map { it.words } .flatten() .forEachIndexed { index, seg -> - Assert.assertEquals( + org.junit.Assert.assertEquals( expectedResult[index], "${seg.javaClass.simpleName.replace("Segment", "")}(${seg.segmentText})", ) @@ -4203,7 +4215,9 @@ class RichTextParserTest { fun testUrlsEndingInPeriod() { val text = "That’s it! http://vitorpamplona.com/. That’s the note" - val state = RichTextParser().parseText(text, EmptyTagList) + val state = + com.vitorpamplona.amethyst.commons.RichTextParser() + .parseText(text, EmptyTagList) printStateForDebug(state) @@ -4221,14 +4235,14 @@ class RichTextParserTest { .map { it.words } .flatten() .forEachIndexed { index, seg -> - Assert.assertEquals( + org.junit.Assert.assertEquals( expectedResult[index], "${seg.javaClass.simpleName.replace("Segment", "")}(${seg.segmentText})", ) } } - private fun printStateForDebug(state: RichTextViewerState) { + private fun printStateForDebug(state: com.vitorpamplona.amethyst.commons.RichTextViewerState) { state.paragraphs.forEach { paragraph -> paragraph.words.forEach { seg -> println( diff --git a/commons/src/main/java/com/vitorpamplona/amethyst/commons/ExpandableTextParser.kt b/commons/src/main/java/com/vitorpamplona/amethyst/commons/ExpandableTextCutOffCalculator.kt similarity index 95% rename from commons/src/main/java/com/vitorpamplona/amethyst/commons/ExpandableTextParser.kt rename to commons/src/main/java/com/vitorpamplona/amethyst/commons/ExpandableTextCutOffCalculator.kt index 613b27bd4..d38dc799b 100644 --- a/commons/src/main/java/com/vitorpamplona/amethyst/commons/ExpandableTextParser.kt +++ b/commons/src/main/java/com/vitorpamplona/amethyst/commons/ExpandableTextCutOffCalculator.kt @@ -20,12 +20,12 @@ */ package com.vitorpamplona.amethyst.commons -class ExpandableTextParser { +class ExpandableTextCutOffCalculator { companion object { private const val SHORT_TEXT_LENGTH = 350 private const val SHORTEN_AFTER_LINES = 10 - fun computeWhereToCutIfPostIsTooLong(content: String): Int { + fun indexToCutOff(content: String): Int { // Cuts the text in the first space or new line after SHORT_TEXT_LENGTH characters val firstSpaceAfterCut = content.indexOf(' ', SHORT_TEXT_LENGTH).let { if (it < 0) content.length else it } diff --git a/commons/src/main/java/com/vitorpamplona/amethyst/commons/ZoomableContentModels.kt b/commons/src/main/java/com/vitorpamplona/amethyst/commons/MediaContentModels.kt similarity index 76% rename from commons/src/main/java/com/vitorpamplona/amethyst/commons/ZoomableContentModels.kt rename to commons/src/main/java/com/vitorpamplona/amethyst/commons/MediaContentModels.kt index f3489c1cb..107e02051 100644 --- a/commons/src/main/java/com/vitorpamplona/amethyst/commons/ZoomableContentModels.kt +++ b/commons/src/main/java/com/vitorpamplona/amethyst/commons/MediaContentModels.kt @@ -24,33 +24,35 @@ import androidx.compose.runtime.Immutable import java.io.File @Immutable -abstract class ZoomableContent( +abstract class BaseMediaContent( val description: String? = null, val dim: String? = null, + val blurhash: String? = null, ) @Immutable -abstract class ZoomableUrlContent( +abstract class MediaUrlContent( val url: String, description: String? = null, val hash: String? = null, dim: String? = null, + blurhash: String? = null, val uri: String? = null, -) : ZoomableContent(description, dim) +) : BaseMediaContent(description, dim, blurhash) @Immutable -class ZoomableUrlImage( +class MediaUrlImage( url: String, description: String? = null, hash: String? = null, - val blurhash: String? = null, + blurhash: String? = null, dim: String? = null, uri: String? = null, val contentWarning: String? = null, -) : ZoomableUrlContent(url, description, hash, dim, uri) +) : MediaUrlContent(url, description, hash, dim, blurhash, uri) @Immutable -class ZoomableUrlVideo( +class MediaUrlVideo( url: String, description: String? = null, hash: String? = null, @@ -58,41 +60,43 @@ class ZoomableUrlVideo( uri: String? = null, val artworkUri: String? = null, val authorName: String? = null, - val blurhash: String? = null, + blurhash: String? = null, val contentWarning: String? = null, -) : ZoomableUrlContent(url, description, hash, dim, uri) +) : MediaUrlContent(url, description, hash, dim, blurhash, uri) @Immutable -abstract class ZoomablePreloadedContent( +abstract class MediaPreloadedContent( val localFile: File?, description: String? = null, val mimeType: String? = null, val isVerified: Boolean? = null, dim: String? = null, + blurhash: String? = null, val uri: String, -) : ZoomableContent(description, dim) { +) : BaseMediaContent(description, dim, blurhash) { fun localFileExists() = localFile != null && localFile.exists() } @Immutable -class ZoomableLocalImage( +class MediaLocalImage( localFile: File?, mimeType: String? = null, description: String? = null, - val blurhash: String? = null, dim: String? = null, + blurhash: String? = null, isVerified: Boolean? = null, uri: String, -) : ZoomablePreloadedContent(localFile, description, mimeType, isVerified, dim, uri) +) : MediaPreloadedContent(localFile, description, mimeType, isVerified, dim, blurhash, uri) @Immutable -class ZoomableLocalVideo( +class MediaLocalVideo( localFile: File?, mimeType: String? = null, description: String? = null, dim: String? = null, + blurhash: String? = null, isVerified: Boolean? = null, uri: String, val artworkUri: String? = null, val authorName: String? = null, -) : ZoomablePreloadedContent(localFile, description, mimeType, isVerified, dim, uri) +) : MediaPreloadedContent(localFile, description, mimeType, isVerified, dim, blurhash, uri) diff --git a/commons/src/main/java/com/vitorpamplona/amethyst/commons/RichTextParser.kt b/commons/src/main/java/com/vitorpamplona/amethyst/commons/RichTextParser.kt index 20d9721a6..3833912f8 100644 --- a/commons/src/main/java/com/vitorpamplona/amethyst/commons/RichTextParser.kt +++ b/commons/src/main/java/com/vitorpamplona/amethyst/commons/RichTextParser.kt @@ -44,13 +44,13 @@ class RichTextParser() { fun parseMediaUrl( fullUrl: String, eventTags: ImmutableListOfLists, - ): ZoomableUrlContent? { + ): MediaUrlContent? { val removedParamsFromUrl = removeQueryParamsForExtensionComparison(fullUrl) return if (imageExtensions.any { removedParamsFromUrl.endsWith(it) }) { val frags = Nip54().parse(fullUrl) val tags = Nip92().parse(fullUrl, eventTags.lists) - ZoomableUrlImage( + MediaUrlImage( url = fullUrl, description = frags[FileHeaderEvent.ALT] ?: tags[FileHeaderEvent.ALT], hash = frags[FileHeaderEvent.HASH] ?: tags[FileHeaderEvent.HASH], @@ -61,7 +61,7 @@ class RichTextParser() { } else if (videoExtensions.any { removedParamsFromUrl.endsWith(it) }) { val frags = Nip54().parse(fullUrl) val tags = Nip92().parse(fullUrl, eventTags.lists) - ZoomableUrlVideo( + MediaUrlVideo( url = fullUrl, description = frags[FileHeaderEvent.ALT] ?: tags[FileHeaderEvent.ALT], hash = frags[FileHeaderEvent.HASH] ?: tags[FileHeaderEvent.HASH], @@ -289,7 +289,9 @@ class RichTextParser() { val hashTagsPattern: Pattern = Pattern.compile("#([^\\s!@#\$%^&*()=+./,\\[{\\]};:'\"?><]+)(.*)", Pattern.CASE_INSENSITIVE) - fun removeQueryParamsForExtensionComparison(fullUrl: String): String { + val acceptedNIP19schemes = listOf("npub1", "naddr1", "note1", "nprofile1", "nevent1") + + private fun removeQueryParamsForExtensionComparison(fullUrl: String): String { return if (fullUrl.contains("?")) { fullUrl.split("?")[0].lowercase() } else if (fullUrl.contains("#")) { @@ -327,24 +329,24 @@ class RichTextParser() { } } - fun parseImageOrVideo(fullUrl: String): ZoomableContent { + fun parseImageOrVideo(fullUrl: String): BaseMediaContent { val removedParamsFromUrl = removeQueryParamsForExtensionComparison(fullUrl) val isImage = imageExtensions.any { removedParamsFromUrl.endsWith(it) } val isVideo = videoExtensions.any { removedParamsFromUrl.endsWith(it) } return if (isImage) { - ZoomableUrlImage(fullUrl) + MediaUrlImage(fullUrl) } else if (isVideo) { - ZoomableUrlVideo(fullUrl) + MediaUrlVideo(fullUrl) } else { - ZoomableUrlImage(fullUrl) + MediaUrlImage(fullUrl) } } fun startsWithNIP19Scheme(word: String): Boolean { val cleaned = word.lowercase().removePrefix("@").removePrefix("nostr:").removePrefix("@") - return listOf("npub1", "naddr1", "note1", "nprofile1", "nevent1").any { cleaned.startsWith(it) } + return acceptedNIP19schemes.any { cleaned.startsWith(it) } } fun isUrlWithoutScheme(url: String) = noProtocolUrlValidator.matcher(url).matches() diff --git a/commons/src/main/java/com/vitorpamplona/amethyst/commons/RichTextParserSegments.kt b/commons/src/main/java/com/vitorpamplona/amethyst/commons/RichTextParserSegments.kt index e1b4419dd..1db5964a5 100644 --- a/commons/src/main/java/com/vitorpamplona/amethyst/commons/RichTextParserSegments.kt +++ b/commons/src/main/java/com/vitorpamplona/amethyst/commons/RichTextParserSegments.kt @@ -28,8 +28,8 @@ import kotlinx.collections.immutable.ImmutableSet @Immutable data class RichTextViewerState( val urlSet: ImmutableSet, - val imagesForPager: ImmutableMap, - val imageList: ImmutableList, + val imagesForPager: ImmutableMap, + val imageList: ImmutableList, val customEmoji: ImmutableMap, val paragraphs: ImmutableList, )