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 }