Merge branch 'upstream-main' into kmp-completeness
This commit is contained in:
@@ -268,6 +268,7 @@ Updated translations:
|
||||
- Bengali by @npub13qtw3yu0uc9r4yj5x0rhgy8nj5q0uyeq0pavkgt9ly69uuzxgkfqwvx23t
|
||||
- Chinese by hypnotichemionus4
|
||||
- Spanish by @npub1luhyzgce7qtcs6r6v00ryjxza8av8u4dzh3avg0zks38tjktnmxspxq903
|
||||
- Russian by Anton Zhao
|
||||
|
||||
<a id="v1.05.1"></a>
|
||||
# [Release v1.05.1: BugFixes](https://github.com/vitorpamplona/amethyst/releases/tag/v1.05.0) - 2025-01-08
|
||||
|
||||
+1
-1
@@ -47,7 +47,7 @@ fun VideoViewInner(
|
||||
authorName: String? = null,
|
||||
nostrUriCallback: String? = null,
|
||||
automaticallyStartPlayback: Boolean,
|
||||
controllerVisible: MutableState<Boolean> = mutableStateOf(true),
|
||||
controllerVisible: MutableState<Boolean> = mutableStateOf(false),
|
||||
onZoom: (() -> Unit)? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
|
||||
+28
-3
@@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
+1
-7
@@ -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<Boolean>) -> Unit,
|
||||
) {
|
||||
val controllerVisible = remember(content) { mutableStateOf(true) }
|
||||
|
||||
LaunchedEffect(content) {
|
||||
delay(2.seconds)
|
||||
controllerVisible.value = false
|
||||
}
|
||||
val controllerVisible = remember(content) { mutableStateOf(false) }
|
||||
|
||||
inner(controllerVisible)
|
||||
}
|
||||
|
||||
+32
@@ -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
|
||||
|
||||
+59
-1
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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(),
|
||||
|
||||
+12
-7
@@ -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<Int, OptionTag> = 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
|
||||
|
||||
+1
@@ -125,6 +125,7 @@ fun DisplayFollowList(
|
||||
val contactsState by observeNoteEventAndMap<ContactListEvent, ImmutableList<User>?>(contactListNote, accountViewModel) { contactList ->
|
||||
contactList
|
||||
?.unverifiedFollowKeySet()
|
||||
?.toSet()
|
||||
?.mapNotNull {
|
||||
accountViewModel.checkGetOrCreateUser(it)
|
||||
}?.toPersistentList()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -483,6 +483,8 @@
|
||||
<string name="poll_zap_value_max">Zap maximum</string>
|
||||
<string name="poll_consensus_threshold">Consensus</string>
|
||||
<string name="poll_consensus_threshold_percent">(0–100)%</string>
|
||||
<string name="poll_single_choice">Single choice</string>
|
||||
<string name="poll_multiple_choice">Multiple choice</string>
|
||||
<string name="poll_closing_date_time">Poll Closing Date & Time</string>
|
||||
<string name="poll_closing_in">Poll closes in %1$s</string>
|
||||
<string name="poll_closing_time">Close after</string>
|
||||
|
||||
+19
@@ -72,6 +72,15 @@ class TagArrayBuilder<T : IEvent> {
|
||||
return this
|
||||
}
|
||||
|
||||
fun addUniqueValueIfNew(tag: Array<String>): TagArrayBuilder<T> {
|
||||
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<Array<String>>): TagArrayBuilder<T> {
|
||||
tag.forEach(::add)
|
||||
return this
|
||||
@@ -82,6 +91,16 @@ class TagArrayBuilder<T : IEvent> {
|
||||
return this
|
||||
}
|
||||
|
||||
fun addAllUnique(tag: Array<Array<String>>): TagArrayBuilder<T> {
|
||||
tag.forEach(::addUnique)
|
||||
return this
|
||||
}
|
||||
|
||||
fun addAllUniqueValueIfNew(tag: List<Array<String>>): TagArrayBuilder<T> {
|
||||
tag.forEach(::addUniqueValueIfNew)
|
||||
return this
|
||||
}
|
||||
|
||||
fun toTypedArray() = tagList.flatMap { it.value }.toTypedArray()
|
||||
|
||||
fun build() = toTypedArray()
|
||||
|
||||
+7
-1
@@ -52,6 +52,12 @@ class ReferenceTag {
|
||||
|
||||
fun assemble(url: String) = arrayOf(TAG_NAME, HttpUrlFormatter.normalize(url))
|
||||
|
||||
fun assemble(urls: List<String>): List<Array<String>> = urls.mapTo(HashSet()) { HttpUrlFormatter.normalize(it) }.map { arrayOf(TAG_NAME, it) }
|
||||
fun assemble(urls: List<String>): List<Array<String>> =
|
||||
urls
|
||||
.mapTo(HashSet()) {
|
||||
HttpUrlFormatter.normalize(it)
|
||||
}.map {
|
||||
arrayOf(TAG_NAME, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String>): List<Array<String>> =
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
+8
-8
@@ -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 <T : Event> TagArrayBuilder<T>.quote(tag: QTag) = add(tag.toTagArray())
|
||||
fun <T : Event> TagArrayBuilder<T>.quote(tag: QTag) = addUniqueValueIfNew(tag.toTagArray())
|
||||
|
||||
fun <T : Event> TagArrayBuilder<T>.quotes(tag: List<QTag>) = addAll(tag.map { it.toTagArray() })
|
||||
fun <T : Event> TagArrayBuilder<T>.quotes(tag: List<QTag>) = addAllUniqueValueIfNew(tag.map { it.toTagArray() })
|
||||
|
||||
fun <T : Event> TagArrayBuilder<T>.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
|
||||
}
|
||||
|
||||
|
||||
@@ -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<String>
|
||||
+2
-3
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String> = UrlDetector(text).detect().map { it.originalUrl }
|
||||
@@ -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<String> = UrlDetector(text).detect().map { it.originalUrl }
|
||||
Reference in New Issue
Block a user