diff --git a/CHANGELOG.md b/CHANGELOG.md index abb588b58..a96915bac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -268,6 +268,7 @@ Updated translations: - Bengali by @npub13qtw3yu0uc9r4yj5x0rhgy8nj5q0uyeq0pavkgt9ly69uuzxgkfqwvx23t - Chinese by hypnotichemionus4 - Spanish by @npub1luhyzgce7qtcs6r6v00ryjxza8av8u4dzh3avg0zks38tjktnmxspxq903 +- Russian by Anton Zhao # [Release v1.05.1: BugFixes](https://github.com/vitorpamplona/amethyst/releases/tag/v1.05.0) - 2025-01-08 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/VideoViewInner.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/VideoViewInner.kt index cef95667c..f9e2ddb59 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/VideoViewInner.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/VideoViewInner.kt @@ -47,7 +47,7 @@ fun VideoViewInner( authorName: String? = null, nostrUriCallback: String? = null, automaticallyStartPlayback: Boolean, - controllerVisible: MutableState = mutableStateOf(true), + controllerVisible: MutableState = mutableStateOf(false), onZoom: (() -> Unit)? = null, accountViewModel: AccountViewModel, ) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/controls/HorizontalLinearProgressIndicator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/controls/HorizontalLinearProgressIndicator.kt index 88628f69a..252c9e740 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/controls/HorizontalLinearProgressIndicator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/controls/HorizontalLinearProgressIndicator.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.service.playback.composable.controls import androidx.annotation.OptIn import androidx.compose.foundation.Canvas import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectDragGestures import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth @@ -30,7 +31,9 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier @@ -103,21 +106,43 @@ private fun HorizontalLinearProgressIndicator( scrubberColor: Color = playedColor, rectHeightDp: Dp = 4.dp, ) { + var isDragging by remember { mutableStateOf(false) } + var dragProgress by remember { mutableFloatStateOf(0f) } + Canvas( Modifier .fillMaxWidth() .padding(horizontal = rectHeightDp * 2.5f) .pointerInput(Unit) { detectTapGestures { offset -> - // Capture the exact position onSeek(offset.x / this.size.width.toFloat()) } + }.pointerInput(Unit) { + detectDragGestures( + onDragStart = { offset -> + isDragging = true + dragProgress = (offset.x / this.size.width.toFloat()).coerceIn(0f, 1f) + }, + onDrag = { change, _ -> + change.consume() + dragProgress = (change.position.x / this.size.width.toFloat()).coerceIn(0f, 1f) + }, + onDragEnd = { + onSeek(dragProgress) + isDragging = false + }, + onDragCancel = { + isDragging = false + }, + ) }.padding(vertical = rectHeightDp * 2) .height(rectHeightDp) .onSizeChanged { (w, _) -> onLayoutWidthChanged(w) }, ) { - val positionX = (currentPositionProgress() * size.width).coerceAtLeast(0f) + val displayProgress = if (isDragging) dragProgress else currentPositionProgress() + val positionX = (displayProgress * size.width).coerceAtLeast(0f) val bufferX = (bufferedPositionProgress() * size.width).coerceAtLeast(0f) + val scrubberRadius = if (isDragging) size.height * 3f else size.height * 2f drawRect(unplayedColor, size = Size(size.width, size.height)) drawRect(bufferedColor, size = Size(bufferX, size.height)) @@ -125,7 +150,7 @@ private fun HorizontalLinearProgressIndicator( drawCircle( color = scrubberColor, - radius = size.height * 2f, + radius = scrubberRadius, center = Offset(x = positionX, y = size.height / 2.0f), ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt index 357f640ef..722bd77c5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt @@ -124,7 +124,6 @@ import okhttp3.coroutines.executeAsync import okio.sink import java.io.File import java.io.IOException -import kotlin.time.Duration.Companion.seconds // Delay before cleaning up shared video temp files. // Allows time for receiving app to copy the file after user confirms share. @@ -224,12 +223,7 @@ fun TwoSecondController( content: BaseMediaContent, inner: @Composable (controllerVisible: MutableState) -> Unit, ) { - val controllerVisible = remember(content) { mutableStateOf(true) } - - LaunchedEffect(content) { - delay(2.seconds) - controllerVisible.value = false - } + val controllerVisible = remember(content) { mutableStateOf(false) } inner(controllerVisible) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/polls/PollOptionsField.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/polls/PollOptionsField.kt index 5a973d304..a95acdcdf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/polls/PollOptionsField.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/polls/PollOptionsField.kt @@ -22,7 +22,9 @@ package com.vitorpamplona.amethyst.ui.note.creators.polls import android.annotation.SuppressLint import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -32,6 +34,7 @@ import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Delete import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.FilterChip import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -47,6 +50,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.ShortNotePostViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.quartz.nip88Polls.poll.tags.OptionTag +import com.vitorpamplona.quartz.nip88Polls.poll.tags.PollType import com.vitorpamplona.quartz.utils.RandomInstance @Composable @@ -55,6 +59,13 @@ fun PollOptionsField(postViewModel: ShortNotePostViewModel) { Column( modifier = Modifier.fillMaxWidth(), ) { + PollTypeSelector( + selectedType = postViewModel.pollType, + onTypeSelected = { postViewModel.pollType = it }, + ) + + Spacer(Modifier.height(4.dp)) + optionsList.forEach { option -> OutlinedTextField( modifier = Modifier.fillMaxWidth(), @@ -118,6 +129,27 @@ fun PollOptionsField(postViewModel: ShortNotePostViewModel) { } } +@Composable +fun PollTypeSelector( + selectedType: PollType, + onTypeSelected: (PollType) -> Unit, +) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + FilterChip( + selected = selectedType == PollType.SINGLE_CHOICE, + onClick = { onTypeSelected(PollType.SINGLE_CHOICE) }, + label = { Text(stringRes(R.string.poll_single_choice)) }, + ) + FilterChip( + selected = selectedType == PollType.MULTI_CHOICE, + onClick = { onTypeSelected(PollType.MULTI_CHOICE) }, + label = { Text(stringRes(R.string.poll_multiple_choice)) }, + ) + } +} + @SuppressLint("ViewModelConstructorInComposable") @Preview @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/userSuggestions/ShowUserSuggestionList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/userSuggestions/ShowUserSuggestionList.kt index 37c1ad128..7a0ae4ef3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/userSuggestions/ShowUserSuggestionList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/userSuggestions/ShowUserSuggestionList.kt @@ -30,23 +30,33 @@ import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.commons.model.nip05DnsIdentifiers.Nip05State import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.searchCommand.UserSearchDataSourceSubscription import com.vitorpamplona.amethyst.ui.layouts.listItem.SlimListItem import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture +import com.vitorpamplona.amethyst.ui.note.ObserveAndRenderNIP05VerifiedSymbol import com.vitorpamplona.amethyst.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.WatchAndDisplayNip05Row import com.vitorpamplona.amethyst.ui.theme.DividerThickness +import com.vitorpamplona.amethyst.ui.theme.Font14SP +import com.vitorpamplona.amethyst.ui.theme.NIP05IconSize import com.vitorpamplona.amethyst.ui.theme.Size55dp +import com.vitorpamplona.amethyst.ui.theme.nip05 import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -145,3 +155,51 @@ fun UserLine( }, ) } + +@Composable +private fun WatchAndDisplayNip05Row( + user: User, + accountViewModel: AccountViewModel, +) { + val nip05StateMetadata by user.nip05State().flow.collectAsStateWithLifecycle() + + when (val nip05State = nip05StateMetadata) { + is Nip05State.Exists -> { + NonClickableObserveAndDisplayNIP05(nip05State, accountViewModel) + } + + else -> { + Text( + text = user.pubkeyDisplayHex(), + fontSize = Font14SP, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } +} + +@Composable +private fun NonClickableObserveAndDisplayNIP05( + nip05State: Nip05State.Exists, + accountViewModel: AccountViewModel, +) { + if (nip05State.nip05.name != "_") { + Text( + text = remember(nip05State) { AnnotatedString(nip05State.nip05.name) }, + fontSize = Font14SP, + color = MaterialTheme.colorScheme.nip05, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + + ObserveAndRenderNIP05VerifiedSymbol(nip05State, 1, NIP05IconSize, accountViewModel) + + Text( + text = nip05State.nip05.domain, + style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.nip05, fontSize = Font14SP), + maxLines = 1, + overflow = TextOverflow.Visible, + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt index 174a5f58d..5767a90dc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt @@ -412,8 +412,15 @@ private fun RenderResults( resultContent: @Composable RowScope.(user: User) -> Unit, labelContent: @Composable (ColumnScope.(code: String, label: String) -> Unit), ) { + val showGallery = + remember { + card.options.all { + it.label.length < 50 + } + } + card.options.forEach { pollItem -> - RenderClosedItem(pollItem, resultContent) { + RenderClosedItem(pollItem, showGallery, resultContent) { labelContent(pollItem.code, pollItem.label) } } @@ -422,18 +429,20 @@ private fun RenderResults( @Composable private fun RenderClosedItem( item: PollItemCard, + showGallery: Boolean, resultContent: @Composable RowScope.(user: User) -> Unit, labelContent: @Composable ColumnScope.() -> Unit, ) { val tally by item.results.collectAsStateWithLifecycle(item.currentResults()) - RenderClosedItem(tally, item.label, resultContent, labelContent) + RenderClosedItem(tally, item.label, showGallery, resultContent, labelContent) } @Composable private fun RenderClosedItem( tally: TallyResults, label: String, + showGallery: Boolean, resultContent: @Composable RowScope.(user: User) -> Unit, labelContent: @Composable ColumnScope.() -> Unit, ) { @@ -496,7 +505,7 @@ private fun RenderClosedItem( Row( verticalAlignment = Alignment.CenterVertically, ) { - if (label.length < 30) { + if (showGallery) { UserGallery(tally, resultContent) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/ArticleListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/ArticleListView.kt index 0c08afd98..c407c60d2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/ArticleListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/ArticleListView.kt @@ -106,7 +106,7 @@ fun ArticleList( contentPadding = FeedPadding, state = listState, ) { - itemsIndexed(articles, key = { _, item -> item.toNAddr() }) { _, item -> + itemsIndexed(articles, key = { _, item -> item.address }) { _, item -> NoteCompose( baseNote = item, modifier = Modifier.animateContentSize(), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index 2bbded69b..c44877bc5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt @@ -117,6 +117,7 @@ import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent import com.vitorpamplona.quartz.nip88Polls.poll.tags.OptionTag +import com.vitorpamplona.quartz.nip88Polls.poll.tags.PollType import com.vitorpamplona.quartz.nip92IMeta.IMetaTagBuilder import com.vitorpamplona.quartz.nip92IMeta.imetas import com.vitorpamplona.quartz.nip94FileMetadata.alt @@ -238,6 +239,7 @@ open class ShortNotePostViewModel : var canUsePoll by mutableStateOf(false) var wantsPoll by mutableStateOf(false) var pollOptions: SnapshotStateMap = newStateMapPollOptions() + var pollType by mutableStateOf(PollType.SINGLE_CHOICE) var closedAt by mutableLongStateOf(TimeUtils.oneDayAhead()) // ZapPolls @@ -336,6 +338,7 @@ open class ShortNotePostViewModel : val currentMentions = (replyNote.event as? TextNoteEvent) ?.mentions() + ?.toSet() ?.map { LocalCache.getOrCreateUser(it.pubKey) } ?: emptyList() @@ -490,8 +493,8 @@ open class ShortNotePostViewModel : } pTags = - draftEvent.tags.filter { it.size > 1 && it[0] == "p" }.map { - LocalCache.getOrCreateUser(it[1]) + draftEvent.tags.filter { it.size > 1 && it[0] == "p" }.mapNotNull { + LocalCache.checkGetOrCreateUser(it[1]) } draftEvent.tags.filter { it.size > 3 && (it[0] == "e" || it[0] == "a") && it[3] == "fork" }.forEach { @@ -576,8 +579,8 @@ open class ShortNotePostViewModel : } pTags = - draftEvent.tags.filter { it.size > 1 && it[0] == "p" }.map { - LocalCache.getOrCreateUser(it[1]) + draftEvent.tags.filter { it.size > 1 && it[0] == "p" }.mapNotNull { + LocalCache.checkGetOrCreateUser(it[1]) } canUsePoll = originalNote == null @@ -596,6 +599,7 @@ open class ShortNotePostViewModel : pollOptions[index] = tag } + pollType = draftEvent.pollType() ?: PollType.SINGLE_CHOICE closedAt = draftEvent.endsAt() ?: TimeUtils.oneDayAhead() message = TextFieldValue(draftEvent.content) @@ -647,8 +651,8 @@ open class ShortNotePostViewModel : } pTags = - draftEvent.tags.filter { it.size > 1 && it[0] == "p" }.map { - LocalCache.getOrCreateUser(it[1]) + draftEvent.tags.filter { it.size > 1 && it[0] == "p" }.mapNotNull { + LocalCache.checkGetOrCreateUser(it[1]) } canUsePoll = originalNote == null @@ -828,7 +832,7 @@ open class ShortNotePostViewModel : accountViewModel.account.nip65RelayList.outboxFlow.value .toList() - PollEvent.build(tagger.message, options, closedAt, relays) { + PollEvent.build(tagger.message, options, closedAt, relays, pollType) { pTags(tagger.directMentionsUsers.map { it.toPTag() }) quotes(quotes) hashtags(findHashtags(tagger.message)) @@ -1054,6 +1058,7 @@ open class ShortNotePostViewModel : wantsPoll = false pollOptions = newStateMapPollOptions() + pollType = PollType.SINGLE_CHOICE closedAt = TimeUtils.oneDayAhead() wantsZapPoll = false diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/newUser/ImportFollowListPickFollowsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/newUser/ImportFollowListPickFollowsScreen.kt index f2c663598..d49c29101 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/newUser/ImportFollowListPickFollowsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/newUser/ImportFollowListPickFollowsScreen.kt @@ -125,6 +125,7 @@ fun DisplayFollowList( val contactsState by observeNoteEventAndMap?>(contactListNote, accountViewModel) { contactList -> contactList ?.unverifiedFollowKeySet() + ?.toSet() ?.mapNotNull { accountViewModel.checkGetOrCreateUser(it) }?.toPersistentList() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Color.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Color.kt index d34ba4609..98c40b26b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Color.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Color.kt @@ -47,7 +47,7 @@ val FollowsFollow = Color.Yellow val NIP05Verified = Color.Blue val Nip05EmailColor = Color(0xFFb198ec) -val Nip05EmailColorDark = Color(0xFF6e5490) +val Nip05EmailColorDark = Color(0xFF765AA2) val Nip05EmailColorLight = Color(0xFFa770f3) val DarkerGreen = Color.Green.copy(alpha = 0.32f) diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index b31cf4974..28f63ffea 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -483,6 +483,8 @@ Zap maximum Consensus (0–100)% + Single choice + Multiple choice Poll Closing Date & Time Poll closes in %1$s Close after diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/TagArrayBuilder.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/TagArrayBuilder.kt index f388b1dcd..9241a884d 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/TagArrayBuilder.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/TagArrayBuilder.kt @@ -72,6 +72,15 @@ class TagArrayBuilder { return this } + fun addUniqueValueIfNew(tag: Array): TagArrayBuilder { + if (tag.has(1) || tag[0].isEmpty() || tag[1].isEmpty()) return this + val list = tagList.getOrPut(tag[0], ::mutableListOf) + if (list.none { it.valueOrNull() == tag[1] }) { + list.add(tag) + } + return this + } + fun addAll(tag: List>): TagArrayBuilder { tag.forEach(::add) return this @@ -82,6 +91,16 @@ class TagArrayBuilder { return this } + fun addAllUnique(tag: Array>): TagArrayBuilder { + tag.forEach(::addUnique) + return this + } + + fun addAllUniqueValueIfNew(tag: List>): TagArrayBuilder { + tag.forEach(::addUniqueValueIfNew) + return this + } + fun toTypedArray() = tagList.flatMap { it.value }.toTypedArray() fun build() = toTypedArray() diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/tags/references/ReferenceTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/tags/references/ReferenceTag.kt index 2446661ed..91d38f0ae 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/tags/references/ReferenceTag.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/tags/references/ReferenceTag.kt @@ -52,6 +52,12 @@ class ReferenceTag { fun assemble(url: String) = arrayOf(TAG_NAME, HttpUrlFormatter.normalize(url)) - fun assemble(urls: List): List> = urls.mapTo(HashSet()) { HttpUrlFormatter.normalize(it) }.map { arrayOf(TAG_NAME, it) } + fun assemble(urls: List): List> = + urls + .mapTo(HashSet()) { + HttpUrlFormatter.normalize(it) + }.map { + arrayOf(TAG_NAME, it) + } } } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip10Notes/content/Urls.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip10Notes/content/Urls.kt index ad56ebffa..c4b864ec5 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip10Notes/content/Urls.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip10Notes/content/Urls.kt @@ -20,15 +20,25 @@ */ package com.vitorpamplona.quartz.nip10Notes.content -import com.vitorpamplona.quartz.nip01Core.tags.references.HttpUrlFormatter -import com.vitorpamplona.quartz.utils.fastFindURLs +import com.vitorpamplona.quartz.utils.DualCase +import com.vitorpamplona.quartz.utils.startsWithAny +import com.vitorpamplona.quartz.utils.urldetector.detection.UrlDetector -fun findURLs(text: String) = fastFindURLs(text) +val rejectSchemes = + listOf( + DualCase("ftp:"), + DualCase("ftps:"), + DualCase("ws:"), + DualCase("wss:"), + DualCase("nostr:"), + DualCase("blossom:"), + ) -fun buildUrlRefs(urls: List): List> = - urls - .mapTo(HashSet()) { url -> - HttpUrlFormatter.normalize(url) - }.map { - arrayOf("r", it) +fun findURLs(text: String) = + UrlDetector(text).detect().mapNotNull { + if (it.originalUrl.startsWithAny(rejectSchemes)) { + null + } else { + it.originalUrl } + } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip18Reposts/quotes/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip18Reposts/quotes/TagArrayBuilderExt.kt index 7a5bf1d0f..5fc0de6e7 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip18Reposts/quotes/TagArrayBuilderExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip18Reposts/quotes/TagArrayBuilderExt.kt @@ -31,18 +31,18 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.NNote import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile import com.vitorpamplona.quartz.nip19Bech32.entities.NPub -fun TagArrayBuilder.quote(tag: QTag) = add(tag.toTagArray()) +fun TagArrayBuilder.quote(tag: QTag) = addUniqueValueIfNew(tag.toTagArray()) -fun TagArrayBuilder.quotes(tag: List) = addAll(tag.map { it.toTagArray() }) +fun TagArrayBuilder.quotes(tag: List) = addAllUniqueValueIfNew(tag.map { it.toTagArray() }) fun TagArrayBuilder.quote(entity: Entity) = when (entity) { - is NNote -> add(entity.toQuoteTagArray()) - is NEvent -> add(entity.toQuoteTagArray()) - is NAddress -> add(entity.toQuoteTagArray()) - is NEmbed -> add(entity.toQuoteTagArray()) - is NPub -> add(entity.toQuoteTagArray()) - is NProfile -> add(entity.toQuoteTagArray()) + is NNote -> addUniqueValueIfNew(entity.toQuoteTagArray()) + is NEvent -> addUniqueValueIfNew(entity.toQuoteTagArray()) + is NAddress -> addUniqueValueIfNew(entity.toQuoteTagArray()) + is NEmbed -> addUniqueValueIfNew(entity.toQuoteTagArray()) + is NPub -> addUniqueValueIfNew(entity.toQuoteTagArray()) + is NProfile -> addUniqueValueIfNew(entity.toQuoteTagArray()) else -> this } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/Urls.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/Urls.kt deleted file mode 100644 index fe9785bb2..000000000 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/Urls.kt +++ /dev/null @@ -1,23 +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.quartz.utils - -expect fun fastFindURLs(text: String): List diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip10Notes/urls/UrlsDetectorTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip10Notes/urls/UrlsDetectorTest.kt index aae685bf2..02fbdc7b4 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip10Notes/urls/UrlsDetectorTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip10Notes/urls/UrlsDetectorTest.kt @@ -21,7 +21,6 @@ package com.vitorpamplona.quartz.nip10Notes.urls import com.vitorpamplona.quartz.nip10Notes.content.findURLs -import com.vitorpamplona.quartz.utils.fastFindURLs import kotlin.test.Test import kotlin.test.assertContains import kotlin.test.assertEquals @@ -31,7 +30,7 @@ class UrlsDetectorTest { @Test fun detectUrlNumber() { - val detectedLinks = fastFindURLs(testSentence) + val detectedLinks = findURLs(testSentence) assertEquals(2, detectedLinks.size) } @@ -48,7 +47,7 @@ class UrlsDetectorTest { */ @Test fun doesNotCrashOnJapaneseText() { - val detectedLinks = fastFindURLs("今北産業") + val detectedLinks = findURLs("今北産業") assertEquals(0, detectedLinks.size) } } diff --git a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/URLs.ios.kt b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/URLs.ios.kt deleted file mode 100644 index bdb01fd24..000000000 --- a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/URLs.ios.kt +++ /dev/null @@ -1,25 +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.quartz.utils - -import com.vitorpamplona.quartz.utils.urldetector.detection.UrlDetector - -actual fun fastFindURLs(text: String): List = UrlDetector(text).detect().map { it.originalUrl } diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/Urls.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/Urls.kt deleted file mode 100644 index bdb01fd24..000000000 --- a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/Urls.kt +++ /dev/null @@ -1,25 +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.quartz.utils - -import com.vitorpamplona.quartz.utils.urldetector.detection.UrlDetector - -actual fun fastFindURLs(text: String): List = UrlDetector(text).detect().map { it.originalUrl }