mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-03-17 21:31:57 +01:00
Merge branch 'main' into base64
This commit is contained in:
commit
c57c190344
@ -1,3 +1,5 @@
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.androidApplication)
|
||||
alias(libs.plugins.jetbrainsKotlinAndroid)
|
||||
@ -293,35 +295,3 @@ dependencies {
|
||||
debugImplementation libs.androidx.ui.test.manifest
|
||||
}
|
||||
|
||||
// https://gitlab.com/fdroid/wiki/-/wikis/HOWTO:-diff-&-fix-APKs-for-Reproducible-Builds#differing-assetsdexoptbaselineprofm-easy-to-fix
|
||||
// NB: Android Studio can't find the imports; this does not affect the
|
||||
// actual build since Gradle can find them just fine.
|
||||
import com.android.tools.profgen.ArtProfileKt
|
||||
import com.android.tools.profgen.ArtProfileSerializer
|
||||
import com.android.tools.profgen.DexFile
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
project.afterEvaluate {
|
||||
tasks.each { task ->
|
||||
if (task.name.startsWith("compile") && task.name.endsWith("ReleaseArtProfile")) {
|
||||
task.doLast {
|
||||
outputs.files.each { file ->
|
||||
if (file.name.endsWith(".profm")) {
|
||||
println("Sorting ${file} ...")
|
||||
def version = ArtProfileSerializer.valueOf("METADATA_0_0_2")
|
||||
def profile = ArtProfileKt.ArtProfile(file)
|
||||
def keys = new ArrayList(profile.profileData.keySet())
|
||||
def sortedData = new LinkedHashMap()
|
||||
Collections.sort keys, new DexFile.Companion()
|
||||
keys.each { key -> sortedData[key] = profile.profileData[key] }
|
||||
new FileOutputStream(file).with {
|
||||
write(version.magicBytes$profgen)
|
||||
write(version.versionBytes$profgen)
|
||||
version.write$profgen(it, sortedData, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -91,6 +91,7 @@ import com.vitorpamplona.quartz.events.PollNoteEvent
|
||||
import com.vitorpamplona.quartz.events.Price
|
||||
import com.vitorpamplona.quartz.events.PrivateDmEvent
|
||||
import com.vitorpamplona.quartz.events.PrivateOutboxRelayListEvent
|
||||
import com.vitorpamplona.quartz.events.ProfileGalleryEntryEvent
|
||||
import com.vitorpamplona.quartz.events.ReactionEvent
|
||||
import com.vitorpamplona.quartz.events.RelayAuthEvent
|
||||
import com.vitorpamplona.quartz.events.ReportEvent
|
||||
@ -118,7 +119,7 @@ import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combineTransform
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flattenMerge
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
@ -480,10 +481,9 @@ class Account(
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val liveNotificationList: Flow<ListNameNotePair> by lazy {
|
||||
defaultNotificationFollowList
|
||||
.transformLatest { listName ->
|
||||
emit(loadPeopleListFlowFromListName(listName))
|
||||
}.flattenMerge()
|
||||
defaultNotificationFollowList.flatMapLatest { listName ->
|
||||
loadPeopleListFlowFromListName(listName)
|
||||
}
|
||||
}
|
||||
|
||||
val liveNotificationFollowLists: StateFlow<LiveFollowLists?> by lazy {
|
||||
@ -493,10 +493,9 @@ class Account(
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val liveStoriesList: Flow<ListNameNotePair> by lazy {
|
||||
defaultStoriesFollowList
|
||||
.transformLatest { listName ->
|
||||
emit(loadPeopleListFlowFromListName(listName))
|
||||
}.flattenMerge()
|
||||
defaultStoriesFollowList.flatMapLatest { listName ->
|
||||
loadPeopleListFlowFromListName(listName)
|
||||
}
|
||||
}
|
||||
|
||||
val liveStoriesFollowLists: StateFlow<LiveFollowLists?> by lazy {
|
||||
@ -506,10 +505,9 @@ class Account(
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val liveDiscoveryList: Flow<ListNameNotePair> by lazy {
|
||||
defaultDiscoveryFollowList
|
||||
.transformLatest { listName ->
|
||||
emit(loadPeopleListFlowFromListName(listName))
|
||||
}.flattenMerge()
|
||||
defaultDiscoveryFollowList.flatMapLatest { listName ->
|
||||
loadPeopleListFlowFromListName(listName)
|
||||
}
|
||||
}
|
||||
|
||||
val liveDiscoveryFollowLists: StateFlow<LiveFollowLists?> by lazy {
|
||||
@ -2197,6 +2195,35 @@ class Account(
|
||||
}
|
||||
}
|
||||
|
||||
fun addToGallery(
|
||||
idHex: String,
|
||||
url: String,
|
||||
relay: String?,
|
||||
) {
|
||||
if (!isWriteable()) return
|
||||
ProfileGalleryEntryEvent.create(
|
||||
url = url,
|
||||
eventid = idHex,
|
||||
/*magnetUri = magnetUri,
|
||||
mimeType = headerInfo.mimeType,
|
||||
hash = headerInfo.hash,
|
||||
size = headerInfo.size.toString(),
|
||||
dimensions = headerInfo.dim,
|
||||
blurhash = headerInfo.blurHash,
|
||||
alt = alt,
|
||||
originalHash = originalHash,
|
||||
sensitiveContent = sensitiveContent, */
|
||||
signer = signer,
|
||||
) { event ->
|
||||
Client.send(event)
|
||||
LocalCache.consume(event, null)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeFromGallery(note: Note) {
|
||||
delete(note)
|
||||
}
|
||||
|
||||
fun addBookmark(
|
||||
note: Note,
|
||||
isPrivate: Boolean,
|
||||
|
@ -103,6 +103,7 @@ import com.vitorpamplona.quartz.events.PinListEvent
|
||||
import com.vitorpamplona.quartz.events.PollNoteEvent
|
||||
import com.vitorpamplona.quartz.events.PrivateDmEvent
|
||||
import com.vitorpamplona.quartz.events.PrivateOutboxRelayListEvent
|
||||
import com.vitorpamplona.quartz.events.ProfileGalleryEntryEvent
|
||||
import com.vitorpamplona.quartz.events.ReactionEvent
|
||||
import com.vitorpamplona.quartz.events.RecommendRelayEvent
|
||||
import com.vitorpamplona.quartz.events.RelaySetEvent
|
||||
@ -1668,6 +1669,26 @@ object LocalCache {
|
||||
refreshObservers(note)
|
||||
}
|
||||
|
||||
fun consume(
|
||||
event: ProfileGalleryEntryEvent,
|
||||
relay: Relay?,
|
||||
) {
|
||||
val note = getOrCreateNote(event.id)
|
||||
val author = getOrCreateUser(event.pubKey)
|
||||
|
||||
if (relay != null) {
|
||||
author.addRelayBeingUsed(relay, event.createdAt)
|
||||
note.addRelay(relay)
|
||||
}
|
||||
|
||||
// Already processed this event.
|
||||
if (note.event != null) return
|
||||
|
||||
note.loadEvent(event, author, emptyList())
|
||||
|
||||
refreshObservers(note)
|
||||
}
|
||||
|
||||
fun consume(
|
||||
event: FileStorageHeaderEvent,
|
||||
relay: Relay?,
|
||||
@ -2529,6 +2550,7 @@ object LocalCache {
|
||||
}
|
||||
is FhirResourceEvent -> consume(event, relay)
|
||||
is FileHeaderEvent -> consume(event, relay)
|
||||
is ProfileGalleryEntryEvent -> consume(event, relay)
|
||||
is FileServersEvent -> consume(event, relay)
|
||||
is FileStorageEvent -> consume(event, relay)
|
||||
is FileStorageHeaderEvent -> consume(event, relay)
|
||||
|
@ -139,6 +139,8 @@ open class Note(
|
||||
var relays = listOf<RelayBriefInfoCache.RelayBriefInfo>()
|
||||
private set
|
||||
|
||||
var associatedNote: Note? = null
|
||||
|
||||
var lastReactionsDownloadTime: Map<String, EOSETime> = emptyMap()
|
||||
|
||||
fun id() = Hex.decode(idHex)
|
||||
|
@ -488,6 +488,7 @@ class UserLiveSet(
|
||||
val innerRelayInfo = UserBundledRefresherLiveData(u)
|
||||
val innerZaps = UserBundledRefresherLiveData(u)
|
||||
val innerBookmarks = UserBundledRefresherLiveData(u)
|
||||
val innerGallery = UserBundledRefresherLiveData(u)
|
||||
val innerStatuses = UserBundledRefresherLiveData(u)
|
||||
|
||||
// UI Observers line up here.
|
||||
@ -500,6 +501,7 @@ class UserLiveSet(
|
||||
val relayInfo = innerRelayInfo.map { it }
|
||||
val zaps = innerZaps.map { it }
|
||||
val bookmarks = innerBookmarks.map { it }
|
||||
val gallery = innerGallery.map { it }
|
||||
val statuses = innerStatuses.map { it }
|
||||
|
||||
val profilePictureChanges = innerMetadata.map { it.user.profilePicture() }.distinctUntilChanged()
|
||||
@ -518,6 +520,7 @@ class UserLiveSet(
|
||||
relayInfo.hasObservers() ||
|
||||
zaps.hasObservers() ||
|
||||
bookmarks.hasObservers() ||
|
||||
gallery.hasObservers() ||
|
||||
statuses.hasObservers() ||
|
||||
profilePictureChanges.hasObservers() ||
|
||||
nip05Changes.hasObservers() ||
|
||||
@ -533,6 +536,7 @@ class UserLiveSet(
|
||||
innerRelayInfo.destroy()
|
||||
innerZaps.destroy()
|
||||
innerBookmarks.destroy()
|
||||
innerGallery.destroy()
|
||||
innerStatuses.destroy()
|
||||
}
|
||||
}
|
||||
|
@ -39,6 +39,7 @@ import com.vitorpamplona.quartz.events.MetadataEvent
|
||||
import com.vitorpamplona.quartz.events.PeopleListEvent
|
||||
import com.vitorpamplona.quartz.events.PinListEvent
|
||||
import com.vitorpamplona.quartz.events.PollNoteEvent
|
||||
import com.vitorpamplona.quartz.events.ProfileGalleryEntryEvent
|
||||
import com.vitorpamplona.quartz.events.RepostEvent
|
||||
import com.vitorpamplona.quartz.events.TextNoteEvent
|
||||
import com.vitorpamplona.quartz.events.WikiNoteEvent
|
||||
@ -153,6 +154,20 @@ object NostrUserProfileDataSource : AmethystNostrDataSource("UserProfileFeed") {
|
||||
)
|
||||
}
|
||||
|
||||
fun createProfileGalleryFilter() =
|
||||
user?.let {
|
||||
TypedFilter(
|
||||
types = COMMON_FEED_TYPES,
|
||||
filter =
|
||||
Filter(
|
||||
kinds =
|
||||
listOf(ProfileGalleryEntryEvent.KIND),
|
||||
authors = listOf(it.pubkeyHex),
|
||||
limit = 1000,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun createReceivedAwardsFilter() =
|
||||
user?.let {
|
||||
TypedFilter(
|
||||
@ -173,6 +188,7 @@ object NostrUserProfileDataSource : AmethystNostrDataSource("UserProfileFeed") {
|
||||
listOfNotNull(
|
||||
createUserInfoFilter(),
|
||||
createUserPostsFilter(),
|
||||
createProfileGalleryFilter(),
|
||||
createFollowFilter(),
|
||||
createFollowersFilter(),
|
||||
createUserReceivedZapsFilter(),
|
||||
|
@ -630,17 +630,21 @@ private fun BottomRowActions(postViewModel: NewPostViewModel) {
|
||||
|
||||
@Composable
|
||||
private fun PollField(postViewModel: NewPostViewModel) {
|
||||
val optionsList = postViewModel.pollOptions
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
postViewModel.pollOptions.values.forEachIndexed { index, _ ->
|
||||
NewPollOption(postViewModel, index)
|
||||
optionsList.forEach { value ->
|
||||
NewPollOption(postViewModel, value.key)
|
||||
}
|
||||
|
||||
NewPollVoteValueRange(postViewModel)
|
||||
|
||||
Button(
|
||||
onClick = { postViewModel.pollOptions[postViewModel.pollOptions.size] = "" },
|
||||
onClick = {
|
||||
// postViewModel.pollOptions[postViewModel.pollOptions.size] = ""
|
||||
optionsList[optionsList.size] = ""
|
||||
},
|
||||
border =
|
||||
BorderStroke(
|
||||
1.dp,
|
||||
|
@ -1278,10 +1278,26 @@ open class NewPostViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
fun removePollOption(optionIndex: Int) {
|
||||
pollOptions.remove(optionIndex)
|
||||
pollOptions.removeOrdered(optionIndex)
|
||||
saveDraft()
|
||||
}
|
||||
|
||||
private fun MutableMap<Int, String>.removeOrdered(index: Int) {
|
||||
val keyList = keys
|
||||
val elementList = values.toMutableList()
|
||||
run stop@{
|
||||
for (i in index until elementList.size) {
|
||||
val nextIndex = i + 1
|
||||
if (nextIndex == elementList.size) return@stop
|
||||
elementList[i] = elementList[nextIndex].also { elementList[nextIndex] = "null" }
|
||||
}
|
||||
}
|
||||
elementList.removeLast()
|
||||
val newEntries = keyList.zip(elementList) { key, content -> Pair(key, content) }
|
||||
this.clear()
|
||||
this.putAll(newEntries)
|
||||
}
|
||||
|
||||
fun updatePollOption(
|
||||
optionIndex: Int,
|
||||
text: String,
|
||||
|
@ -118,6 +118,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size55dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size75dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.VolumeBottomIconSize
|
||||
import com.vitorpamplona.amethyst.ui.theme.imageModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.videoGalleryModifier
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@ -204,6 +205,7 @@ fun VideoView(
|
||||
title: String? = null,
|
||||
thumb: VideoThumb? = null,
|
||||
roundedCorner: Boolean,
|
||||
gallery: Boolean = false,
|
||||
isFiniteHeight: Boolean,
|
||||
waveform: ImmutableList<Int>? = null,
|
||||
artworkUri: String? = null,
|
||||
@ -247,6 +249,7 @@ fun VideoView(
|
||||
title = title,
|
||||
thumb = thumb,
|
||||
roundedCorner = roundedCorner,
|
||||
gallery = gallery,
|
||||
isFiniteHeight = isFiniteHeight,
|
||||
waveform = waveform,
|
||||
artworkUri = artworkUri,
|
||||
@ -324,6 +327,7 @@ fun VideoViewInner(
|
||||
title: String? = null,
|
||||
thumb: VideoThumb? = null,
|
||||
roundedCorner: Boolean,
|
||||
gallery: Boolean = false,
|
||||
isFiniteHeight: Boolean,
|
||||
waveform: ImmutableList<Int>? = null,
|
||||
artworkUri: String? = null,
|
||||
@ -348,6 +352,7 @@ fun VideoViewInner(
|
||||
controller = controller,
|
||||
thumbData = thumb,
|
||||
roundedCorner = roundedCorner,
|
||||
gallery = gallery,
|
||||
isFiniteHeight = isFiniteHeight,
|
||||
nostrUriCallback = nostrUriCallback,
|
||||
waveform = waveform,
|
||||
@ -695,6 +700,7 @@ private fun RenderVideoPlayer(
|
||||
controller: MediaController,
|
||||
thumbData: VideoThumb?,
|
||||
roundedCorner: Boolean,
|
||||
gallery: Boolean = false,
|
||||
isFiniteHeight: Boolean,
|
||||
nostrUriCallback: String?,
|
||||
waveform: ImmutableList<Int>? = null,
|
||||
@ -712,13 +718,18 @@ private fun RenderVideoPlayer(
|
||||
|
||||
Box {
|
||||
val borders = MaterialTheme.colorScheme.imageModifier
|
||||
|
||||
val bordersSquare = MaterialTheme.colorScheme.videoGalleryModifier
|
||||
val myModifier =
|
||||
remember(controller) {
|
||||
if (roundedCorner) {
|
||||
modifier.then(
|
||||
borders.defaultMinSize(minHeight = 75.dp).align(Alignment.Center),
|
||||
)
|
||||
} else if (gallery) {
|
||||
Modifier
|
||||
modifier.then(
|
||||
bordersSquare.defaultMinSize(minHeight = 75.dp).align(Alignment.Center),
|
||||
)
|
||||
} else {
|
||||
modifier.fillMaxWidth().defaultMinSize(minHeight = 75.dp).align(Alignment.Center)
|
||||
}
|
||||
@ -737,6 +748,7 @@ private fun RenderVideoPlayer(
|
||||
setBackgroundColor(Color.Transparent.toArgb())
|
||||
setShutterBackgroundColor(Color.Transparent.toArgb())
|
||||
controllerAutoShow = false
|
||||
useController = !gallery
|
||||
thumbData?.thumb?.let { defaultArtwork = it }
|
||||
hideController()
|
||||
resizeMode =
|
||||
@ -745,72 +757,77 @@ private fun RenderVideoPlayer(
|
||||
} else {
|
||||
AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH
|
||||
}
|
||||
onDialog?.let { innerOnDialog ->
|
||||
setFullscreenButtonClickListener {
|
||||
controller.pause()
|
||||
innerOnDialog(it)
|
||||
if (!gallery) {
|
||||
onDialog?.let { innerOnDialog ->
|
||||
setFullscreenButtonClickListener {
|
||||
controller.pause()
|
||||
innerOnDialog(it)
|
||||
}
|
||||
}
|
||||
setControllerVisibilityListener(
|
||||
PlayerView.ControllerVisibilityListener { visible ->
|
||||
controllerVisible.value = visible == View.VISIBLE
|
||||
onControllerVisibilityChanged?.let { callback -> callback(visible == View.VISIBLE) }
|
||||
},
|
||||
)
|
||||
}
|
||||
setControllerVisibilityListener(
|
||||
PlayerView.ControllerVisibilityListener { visible ->
|
||||
controllerVisible.value = visible == View.VISIBLE
|
||||
onControllerVisibilityChanged?.let { callback -> callback(visible == View.VISIBLE) }
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
waveform?.let { Waveform(it, controller, remember { Modifier.align(Alignment.Center) }) }
|
||||
if (!gallery) {
|
||||
val startingMuteState = remember(controller) { controller.volume < 0.001 }
|
||||
|
||||
val startingMuteState = remember(controller) { controller.volume < 0.001 }
|
||||
MuteButton(
|
||||
controllerVisible,
|
||||
startingMuteState,
|
||||
Modifier.align(Alignment.TopEnd),
|
||||
) { mute: Boolean ->
|
||||
// makes the new setting the default for new creations.
|
||||
DEFAULT_MUTED_SETTING.value = mute
|
||||
|
||||
MuteButton(
|
||||
controllerVisible,
|
||||
startingMuteState,
|
||||
Modifier.align(Alignment.TopEnd),
|
||||
) { mute: Boolean ->
|
||||
// makes the new setting the default for new creations.
|
||||
DEFAULT_MUTED_SETTING.value = mute
|
||||
|
||||
// if the user unmutes a video and it's not the current playing, switches to that one.
|
||||
if (!mute && keepPlayingMutex != null && keepPlayingMutex != controller) {
|
||||
keepPlayingMutex?.stop()
|
||||
keepPlayingMutex?.release()
|
||||
keepPlayingMutex = null
|
||||
}
|
||||
|
||||
controller.volume = if (mute) 0f else 1f
|
||||
}
|
||||
|
||||
KeepPlayingButton(
|
||||
keepPlaying,
|
||||
controllerVisible,
|
||||
Modifier.align(Alignment.TopEnd).padding(end = Size55dp),
|
||||
) { newKeepPlaying: Boolean ->
|
||||
// If something else is playing and the user marks this video to keep playing, stops the other
|
||||
// one.
|
||||
if (newKeepPlaying) {
|
||||
if (keepPlayingMutex != null && keepPlayingMutex != controller) {
|
||||
// if the user unmutes a video and it's not the current playing, switches to that one.
|
||||
if (!mute && keepPlayingMutex != null && keepPlayingMutex != controller) {
|
||||
keepPlayingMutex?.stop()
|
||||
keepPlayingMutex?.release()
|
||||
}
|
||||
keepPlayingMutex = controller
|
||||
} else {
|
||||
if (keepPlayingMutex == controller) {
|
||||
keepPlayingMutex = null
|
||||
}
|
||||
|
||||
controller.volume = if (mute) 0f else 1f
|
||||
}
|
||||
|
||||
keepPlaying.value = newKeepPlaying
|
||||
}
|
||||
KeepPlayingButton(
|
||||
keepPlaying,
|
||||
controllerVisible,
|
||||
Modifier.align(Alignment.TopEnd).padding(end = Size55dp),
|
||||
) { newKeepPlaying: Boolean ->
|
||||
// If something else is playing and the user marks this video to keep playing, stops the other
|
||||
// one.
|
||||
if (newKeepPlaying) {
|
||||
if (keepPlayingMutex != null && keepPlayingMutex != controller) {
|
||||
keepPlayingMutex?.stop()
|
||||
keepPlayingMutex?.release()
|
||||
}
|
||||
keepPlayingMutex = controller
|
||||
} else {
|
||||
if (keepPlayingMutex == controller) {
|
||||
keepPlayingMutex = null
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedSaveButton(controllerVisible, Modifier.align(Alignment.TopEnd).padding(end = Size110dp)) { context ->
|
||||
saveImage(videoUri, mimeType, context, accountViewModel)
|
||||
}
|
||||
keepPlaying.value = newKeepPlaying
|
||||
}
|
||||
|
||||
AnimatedShareButton(controllerVisible, Modifier.align(Alignment.TopEnd).padding(end = Size165dp)) { popupExpanded, toggle ->
|
||||
ShareImageAction(popupExpanded, videoUri, nostrUriCallback, toggle)
|
||||
AnimatedSaveButton(controllerVisible, Modifier.align(Alignment.TopEnd).padding(end = Size110dp)) { context ->
|
||||
saveImage(videoUri, mimeType, context, accountViewModel)
|
||||
}
|
||||
|
||||
AnimatedShareButton(controllerVisible, Modifier.align(Alignment.TopEnd).padding(end = Size165dp)) { popupExpanded, toggle ->
|
||||
ShareImageAction(accountViewModel = accountViewModel, popupExpanded, videoUri, nostrUriCallback, toggle)
|
||||
}
|
||||
} else {
|
||||
controller.volume = 0f
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -269,7 +269,7 @@ private fun DialogContent(
|
||||
contentDescription = stringRes(R.string.quick_action_share),
|
||||
)
|
||||
|
||||
ShareImageAction(popupExpanded = popupExpanded, myContent, onDismiss = { popupExpanded.value = false })
|
||||
ShareImageAction(accountViewModel = accountViewModel, popupExpanded = popupExpanded, myContent, onDismiss = { popupExpanded.value = false })
|
||||
}
|
||||
|
||||
val localContext = LocalContext.current
|
||||
|
@ -95,6 +95,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size75dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.hashVerifierMark
|
||||
import com.vitorpamplona.amethyst.ui.theme.imageModifier
|
||||
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||
import com.vitorpamplona.quartz.encoders.Nip19Bech32
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
@ -596,12 +597,14 @@ fun DisplayBlurHash(
|
||||
|
||||
@Composable
|
||||
fun ShareImageAction(
|
||||
accountViewModel: AccountViewModel,
|
||||
popupExpanded: MutableState<Boolean>,
|
||||
content: BaseMediaContent,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
if (content is MediaUrlContent) {
|
||||
ShareImageAction(
|
||||
accountViewModel = accountViewModel,
|
||||
popupExpanded = popupExpanded,
|
||||
videoUri = content.url,
|
||||
postNostrUri = content.uri,
|
||||
@ -609,6 +612,7 @@ fun ShareImageAction(
|
||||
)
|
||||
} else if (content is MediaPreloadedContent) {
|
||||
ShareImageAction(
|
||||
accountViewModel = accountViewModel,
|
||||
popupExpanded = popupExpanded,
|
||||
videoUri = content.localFile?.toUri().toString(),
|
||||
postNostrUri = content.uri,
|
||||
@ -620,6 +624,7 @@ fun ShareImageAction(
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
fun ShareImageAction(
|
||||
accountViewModel: AccountViewModel,
|
||||
popupExpanded: MutableState<Boolean>,
|
||||
videoUri: String?,
|
||||
postNostrUri: String?,
|
||||
@ -650,6 +655,23 @@ fun ShareImageAction(
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
postNostrUri?.let {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringRes(R.string.add_media_to_gallery)) },
|
||||
onClick = {
|
||||
if (videoUri != null) {
|
||||
var n19 = Nip19Bech32.uriToRoute(postNostrUri)?.entity as? Nip19Bech32.NEvent
|
||||
if (n19 != null) {
|
||||
accountViewModel.addMediaToGallery(n19.hex, videoUri, n19.relay[0]) // TODO Whole list or first?
|
||||
accountViewModel.toast(R.string.image_saved_to_the_gallery, R.string.image_saved_to_the_gallery)
|
||||
}
|
||||
}
|
||||
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -94,8 +94,8 @@ class HomeNewThreadFeedFilter(
|
||||
noteEvent is ClassifiedsEvent ||
|
||||
noteEvent is RepostEvent ||
|
||||
noteEvent is GenericRepostEvent ||
|
||||
noteEvent is LongTextNoteEvent ||
|
||||
noteEvent is WikiNoteEvent ||
|
||||
(noteEvent is LongTextNoteEvent && noteEvent.content.isNotEmpty()) ||
|
||||
(noteEvent is WikiNoteEvent && noteEvent.content.isNotEmpty()) ||
|
||||
noteEvent is PollNoteEvent ||
|
||||
noteEvent is HighlightEvent ||
|
||||
noteEvent is AudioTrackEvent ||
|
||||
|
@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Copyright (c) 2024 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.dal
|
||||
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.quartz.events.MuteListEvent
|
||||
import com.vitorpamplona.quartz.events.PeopleListEvent
|
||||
import com.vitorpamplona.quartz.events.ProfileGalleryEntryEvent
|
||||
|
||||
class UserProfileGalleryFeedFilter(
|
||||
val user: User,
|
||||
val account: Account,
|
||||
) : AdditiveFeedFilter<Note>() {
|
||||
override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + "ProfileGallery"
|
||||
|
||||
override fun showHiddenKey(): Boolean =
|
||||
account.defaultStoriesFollowList.value == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) ||
|
||||
account.defaultStoriesFollowList.value == MuteListEvent.blockListFor(account.userProfile().pubkeyHex)
|
||||
|
||||
override fun feed(): List<Note> {
|
||||
val params = buildFilterParams(account)
|
||||
|
||||
val notes =
|
||||
LocalCache.notes.filterIntoSet { _, it ->
|
||||
acceptableEvent(it, params, user)
|
||||
}
|
||||
|
||||
var sorted = sort(notes)
|
||||
var finalnotes = setOf<Note>()
|
||||
for (item in sorted) {
|
||||
val note = (item.event as ProfileGalleryEntryEvent).event()?.let { LocalCache.checkGetOrCreateNote(it) }
|
||||
if (note != null) {
|
||||
note.associatedNote = item
|
||||
finalnotes = finalnotes + note
|
||||
}
|
||||
}
|
||||
|
||||
return finalnotes.toList()
|
||||
}
|
||||
|
||||
override fun applyFilter(collection: Set<Note>): Set<Note> = innerApplyFilter(collection)
|
||||
|
||||
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
||||
val params = buildFilterParams(account)
|
||||
|
||||
return collection.filterTo(HashSet()) { acceptableEvent(it, params, user) }
|
||||
}
|
||||
|
||||
fun acceptableEvent(
|
||||
it: Note,
|
||||
params: FilterByListParams,
|
||||
user: User,
|
||||
): Boolean {
|
||||
val noteEvent = it.event
|
||||
return (
|
||||
(it.event?.pubKey() == user.pubkeyHex && noteEvent is ProfileGalleryEntryEvent) && noteEvent.hasUrl() && noteEvent.hasEvent() // && noteEvent.isOneOf(SUPPORTED_VIDEO_FEED_MIME_TYPES_SET))
|
||||
) &&
|
||||
params.match(noteEvent) &&
|
||||
account.isAcceptable(it)
|
||||
}
|
||||
|
||||
fun buildFilterParams(account: Account): FilterByListParams =
|
||||
FilterByListParams.create(
|
||||
userHex = account.userProfile().pubkeyHex,
|
||||
selectedListName = account.defaultStoriesFollowList.value,
|
||||
followLists = account.liveStoriesFollowLists.value,
|
||||
hiddenUsers = account.flowHiddenUsers.value,
|
||||
)
|
||||
|
||||
override fun sort(collection: Set<Note>): List<Note> = collection.sortedWith(DefaultFeedOrder)
|
||||
}
|
@ -59,6 +59,7 @@ fun BlankNotePreview() {
|
||||
fun BlankNote(
|
||||
modifier: Modifier = Modifier,
|
||||
idHex: String? = null,
|
||||
shortPreview: Boolean = false,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
Row {
|
||||
@ -75,7 +76,12 @@ fun BlankNote(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Text(
|
||||
text = stringRes(R.string.post_not_found) + if (idHex != null) ": $idHex" else "",
|
||||
text =
|
||||
if (shortPreview) {
|
||||
stringRes(R.string.post_not_found_short)
|
||||
} else {
|
||||
stringRes(R.string.post_not_found) + if (idHex != null) ": $idHex" else ""
|
||||
},
|
||||
modifier = Modifier.padding(30.dp),
|
||||
color = Color.Gray,
|
||||
textAlign = TextAlign.Center,
|
||||
|
@ -154,6 +154,40 @@ fun LongPressToQuickAction(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LongPressToQuickActionGallery(
|
||||
baseNote: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
content: @Composable (() -> Unit) -> Unit,
|
||||
) {
|
||||
val popupExpanded = remember { mutableStateOf(false) }
|
||||
|
||||
content { popupExpanded.value = true }
|
||||
|
||||
if (popupExpanded.value) {
|
||||
if (baseNote.author == accountViewModel.account.userProfile()) {
|
||||
NoteQuickActionMenuGallery(
|
||||
note = baseNote,
|
||||
onDismiss = { popupExpanded.value = false },
|
||||
accountViewModel = accountViewModel,
|
||||
nav = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NoteQuickActionMenuGallery(
|
||||
note: Note,
|
||||
onDismiss: () -> Unit,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
DeleteFromGalleryDialog(note, accountViewModel) {
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NoteQuickActionMenu(
|
||||
note: Note,
|
||||
@ -435,6 +469,169 @@ private fun RenderMainPopup(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderDeleteFromGalleryPopup(
|
||||
accountViewModel: AccountViewModel,
|
||||
note: Note,
|
||||
showDeleteAlertDialog: MutableState<Boolean>,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val primaryLight = lightenColor(MaterialTheme.colorScheme.primary, 0.1f)
|
||||
val cardShape = RoundedCornerShape(5.dp)
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val backgroundColor =
|
||||
if (MaterialTheme.colorScheme.isLight) {
|
||||
MaterialTheme.colorScheme.primary
|
||||
} else {
|
||||
MaterialTheme.colorScheme.secondaryButtonBackground
|
||||
}
|
||||
|
||||
val showToast = { stringRes: Int ->
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
stringRes(context, stringRes),
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
val isOwnNote = accountViewModel.isLoggedUser(note.author)
|
||||
val isFollowingUser = !isOwnNote && accountViewModel.isFollowing(note.author)
|
||||
|
||||
Popup(onDismissRequest = onDismiss, alignment = Alignment.Center) {
|
||||
Card(
|
||||
modifier = Modifier.shadow(elevation = 6.dp, shape = cardShape),
|
||||
shape = cardShape,
|
||||
colors = CardDefaults.cardColors(containerColor = backgroundColor),
|
||||
) {
|
||||
Column(modifier = Modifier.width(IntrinsicSize.Min)) {
|
||||
Row(modifier = Modifier.height(IntrinsicSize.Min)) {
|
||||
NoteQuickActionItem(
|
||||
icon = Icons.Default.ContentCopy,
|
||||
label = stringRes(R.string.quick_action_copy_text),
|
||||
) {
|
||||
accountViewModel.decrypt(note) {
|
||||
clipboardManager.setText(AnnotatedString(it))
|
||||
showToast(R.string.copied_note_text_to_clipboard)
|
||||
}
|
||||
|
||||
onDismiss()
|
||||
}
|
||||
VerticalDivider(color = primaryLight)
|
||||
NoteQuickActionItem(
|
||||
Icons.Default.AlternateEmail,
|
||||
stringRes(R.string.quick_action_copy_user_id),
|
||||
) {
|
||||
note.author?.let {
|
||||
scope.launch {
|
||||
clipboardManager.setText(AnnotatedString(it.toNostrUri()))
|
||||
showToast(R.string.copied_user_id_to_clipboard)
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
VerticalDivider(color = primaryLight)
|
||||
NoteQuickActionItem(
|
||||
Icons.Default.FormatQuote,
|
||||
stringRes(R.string.quick_action_copy_note_id),
|
||||
) {
|
||||
scope.launch {
|
||||
clipboardManager.setText(AnnotatedString(note.toNostrUri()))
|
||||
showToast(R.string.copied_note_id_to_clipboard)
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
HorizontalDivider(
|
||||
color = primaryLight,
|
||||
)
|
||||
Row(modifier = Modifier.height(IntrinsicSize.Min)) {
|
||||
if (isOwnNote) {
|
||||
NoteQuickActionItem(
|
||||
Icons.Default.Delete,
|
||||
stringRes(R.string.quick_action_delete),
|
||||
) {
|
||||
if (accountViewModel.hideDeleteRequestDialog) {
|
||||
accountViewModel.delete(note)
|
||||
onDismiss()
|
||||
} else {
|
||||
showDeleteAlertDialog.value = true
|
||||
}
|
||||
}
|
||||
} else if (isFollowingUser) {
|
||||
NoteQuickActionItem(
|
||||
Icons.Default.PersonRemove,
|
||||
stringRes(R.string.quick_action_unfollow),
|
||||
) {
|
||||
accountViewModel.unfollow(note.author!!)
|
||||
onDismiss()
|
||||
}
|
||||
} else {
|
||||
NoteQuickActionItem(
|
||||
Icons.Default.PersonAdd,
|
||||
stringRes(R.string.quick_action_follow),
|
||||
) {
|
||||
accountViewModel.follow(note.author!!)
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
|
||||
VerticalDivider(color = primaryLight)
|
||||
NoteQuickActionItem(
|
||||
icon = ImageVector.vectorResource(id = R.drawable.relays),
|
||||
label = stringRes(R.string.broadcast),
|
||||
) {
|
||||
accountViewModel.broadcast(note)
|
||||
// showSelectTextDialog = true
|
||||
onDismiss()
|
||||
}
|
||||
VerticalDivider(color = primaryLight)
|
||||
if (isOwnNote && note.isDraft()) {
|
||||
NoteQuickActionItem(
|
||||
Icons.Default.Edit,
|
||||
stringRes(R.string.edit_draft),
|
||||
) {
|
||||
onDismiss()
|
||||
}
|
||||
} else {
|
||||
NoteQuickActionItem(
|
||||
icon = Icons.Default.Share,
|
||||
label = stringRes(R.string.quick_action_share),
|
||||
) {
|
||||
val sendIntent =
|
||||
Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
type = "text/plain"
|
||||
putExtra(
|
||||
Intent.EXTRA_TEXT,
|
||||
externalLinkForNote(note),
|
||||
)
|
||||
putExtra(
|
||||
Intent.EXTRA_TITLE,
|
||||
stringRes(context, R.string.quick_action_share_browser_link),
|
||||
)
|
||||
}
|
||||
|
||||
val shareIntent =
|
||||
Intent.createChooser(
|
||||
sendIntent,
|
||||
stringRes(context, R.string.quick_action_share),
|
||||
)
|
||||
ContextCompat.startActivity(context, shareIntent, null)
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NoteQuickActionItem(
|
||||
icon: ImageVector,
|
||||
@ -462,6 +659,25 @@ fun NoteQuickActionItem(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DeleteFromGalleryDialog(
|
||||
note: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
QuickActionAlertDialogOneButton(
|
||||
title = stringRes(R.string.quick_action_request_deletion_gallery_title),
|
||||
textContent = stringRes(R.string.quick_action_request_deletion_gallery_alert_body),
|
||||
buttonIcon = Icons.Default.Delete,
|
||||
buttonText = stringRes(R.string.quick_action_delete_dialog_btn),
|
||||
onClickDoOnce = {
|
||||
accountViewModel.removefromMediaGallery(note)
|
||||
onDismiss()
|
||||
},
|
||||
onDismiss = onDismiss,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DeleteAlertDialog(
|
||||
note: Note,
|
||||
@ -612,3 +828,95 @@ fun QuickActionAlertDialog(
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun QuickActionAlertDialogOneButton(
|
||||
title: String,
|
||||
textContent: String,
|
||||
buttonIcon: ImageVector,
|
||||
buttonText: String,
|
||||
buttonColors: ButtonColors = ButtonDefaults.buttonColors(),
|
||||
onClickDoOnce: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
QuickActionAlertDialogOneButton(
|
||||
title = title,
|
||||
textContent = textContent,
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = buttonIcon,
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
buttonText = buttonText,
|
||||
buttonColors = buttonColors,
|
||||
onClickDoOnce = onClickDoOnce,
|
||||
onDismiss = onDismiss,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun QuickActionAlertDialogOneButton(
|
||||
title: String,
|
||||
textContent: String,
|
||||
buttonIconResource: Int,
|
||||
buttonText: String,
|
||||
buttonColors: ButtonColors = ButtonDefaults.buttonColors(),
|
||||
onClickDoOnce: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
QuickActionAlertDialogOneButton(
|
||||
title = title,
|
||||
textContent = textContent,
|
||||
icon = {
|
||||
Icon(
|
||||
painter = painterResource(buttonIconResource),
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
buttonText = buttonText,
|
||||
buttonColors = buttonColors,
|
||||
onClickDoOnce = onClickDoOnce,
|
||||
onDismiss = onDismiss,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun QuickActionAlertDialogOneButton(
|
||||
title: String,
|
||||
textContent: String,
|
||||
icon: @Composable () -> Unit,
|
||||
buttonText: String,
|
||||
buttonColors: ButtonColors = ButtonDefaults.buttonColors(),
|
||||
onClickDoOnce: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(title) },
|
||||
text = { Text(textContent) },
|
||||
confirmButton = {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(all = 8.dp)
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Button(
|
||||
onClick = onClickDoOnce,
|
||||
colors = buttonColors,
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
icon()
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(buttonText)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
@ -37,6 +37,7 @@ fun WatchNoteEvent(
|
||||
baseNote: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
modifier: Modifier = Modifier,
|
||||
shortPreview: Boolean = false,
|
||||
onNoteEventFound: @Composable () -> Unit,
|
||||
) {
|
||||
WatchNoteEvent(
|
||||
@ -54,6 +55,7 @@ fun WatchNoteEvent(
|
||||
onLongClick = showPopup,
|
||||
)
|
||||
},
|
||||
shortPreview = shortPreview,
|
||||
)
|
||||
}
|
||||
},
|
||||
|
@ -86,6 +86,7 @@ fun VideoDisplay(
|
||||
val description = event.content.ifBlank { null } ?: event.alt()
|
||||
val isImage = event.mimeType()?.startsWith("image/") == true || RichTextParser.isImageUrl(fullUrl)
|
||||
val uri = note.toNostrUri()
|
||||
val id = note.id()
|
||||
val mimeType = event.mimeType()
|
||||
|
||||
mutableStateOf<BaseMediaContent>(
|
||||
|
@ -57,6 +57,7 @@ import com.vitorpamplona.amethyst.ui.dal.ThreadFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.UserProfileAppRecommendationsFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.UserProfileBookmarksFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.UserProfileConversationsFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.UserProfileGalleryFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.UserProfileNewThreadFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.UserProfileReportsFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.VideoFeedFilter
|
||||
@ -248,6 +249,20 @@ class NostrUserProfileReportFeedViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
class NostrUserProfileGalleryFeedViewModel(
|
||||
val user: User,
|
||||
val account: Account,
|
||||
) : FeedViewModel(UserProfileGalleryFeedFilter(user, account)) {
|
||||
class Factory(
|
||||
val user: User,
|
||||
val account: Account,
|
||||
) : ViewModelProvider.Factory {
|
||||
override fun <NostrUserProfileGalleryFeedViewModel : ViewModel> create(modelClass: Class<NostrUserProfileGalleryFeedViewModel>): NostrUserProfileGalleryFeedViewModel =
|
||||
NostrUserProfileGalleryFeedViewModel(user, account)
|
||||
as NostrUserProfileGalleryFeedViewModel
|
||||
}
|
||||
}
|
||||
|
||||
class NostrUserProfileBookmarksFeedViewModel(
|
||||
val user: User,
|
||||
val account: Account,
|
||||
|
@ -45,6 +45,7 @@ object ScrollStateKeys {
|
||||
const val DISCOVER_SCREEN = "Discover"
|
||||
val HOME_FOLLOWS = Route.Home.base + "Follows"
|
||||
val HOME_REPLIES = Route.Home.base + "FollowsReplies"
|
||||
val PROFILE_GALLERY = Route.Home.base + "ProfileGallery"
|
||||
|
||||
val DRAFTS = Route.Home.base + "Drafts"
|
||||
|
||||
|
@ -669,6 +669,18 @@ class AccountViewModel(
|
||||
viewModelScope.launch(Dispatchers.IO) { account.addEmojiPack(usersEmojiList, emojiList) }
|
||||
}
|
||||
|
||||
fun addMediaToGallery(
|
||||
hex: String,
|
||||
url: String,
|
||||
relay: String?,
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) { account.addToGallery(hex, url, relay) }
|
||||
}
|
||||
|
||||
fun removefromMediaGallery(note: Note) {
|
||||
viewModelScope.launch(Dispatchers.IO) { account.removeFromGallery(note) }
|
||||
}
|
||||
|
||||
fun addPrivateBookmark(note: Note) {
|
||||
viewModelScope.launch(Dispatchers.IO) { account.addBookmark(note, true) }
|
||||
}
|
||||
|
@ -118,13 +118,6 @@ fun DiscoverScreen(
|
||||
ScrollStateKeys.DISCOVER_CONTENT,
|
||||
AppDefinitionEvent.KIND,
|
||||
),
|
||||
TabItem(
|
||||
R.string.discover_marketplace,
|
||||
discoveryMarketplaceFeedViewModel,
|
||||
Route.Discover.base + "Marketplace",
|
||||
ScrollStateKeys.DISCOVER_MARKETPLACE,
|
||||
ClassifiedsEvent.KIND,
|
||||
),
|
||||
TabItem(
|
||||
R.string.discover_live,
|
||||
discoveryLiveFeedViewModel,
|
||||
@ -139,6 +132,13 @@ fun DiscoverScreen(
|
||||
ScrollStateKeys.DISCOVER_COMMUNITY,
|
||||
CommunityDefinitionEvent.KIND,
|
||||
),
|
||||
TabItem(
|
||||
R.string.discover_marketplace,
|
||||
discoveryMarketplaceFeedViewModel,
|
||||
Route.Discover.base + "Marketplace",
|
||||
ScrollStateKeys.DISCOVER_MARKETPLACE,
|
||||
ClassifiedsEvent.KIND,
|
||||
),
|
||||
TabItem(
|
||||
R.string.discover_chat,
|
||||
discoveryChatFeedViewModel,
|
||||
@ -168,6 +168,10 @@ fun DiscoverScreen(
|
||||
println("Discovery Start")
|
||||
NostrDiscoveryDataSource.start()
|
||||
}
|
||||
if (event == Lifecycle.Event.ON_PAUSE) {
|
||||
println("Discovery Stop")
|
||||
NostrDiscoveryDataSource.stop()
|
||||
}
|
||||
}
|
||||
|
||||
lifeCycleOwner.lifecycle.addObserver(observer)
|
||||
|
@ -0,0 +1,370 @@
|
||||
/**
|
||||
* Copyright (c) 2024 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn
|
||||
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyGridState
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.itemsIndexed
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment.Companion.BottomStart
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.distinctUntilChanged
|
||||
import androidx.lifecycle.map
|
||||
import coil.compose.AsyncImage
|
||||
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser.Companion.isVideoUrl
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
|
||||
import com.vitorpamplona.amethyst.ui.components.VideoView
|
||||
import com.vitorpamplona.amethyst.ui.note.CheckHiddenFeedWatchBlockAndReport
|
||||
import com.vitorpamplona.amethyst.ui.note.ClickableNote
|
||||
import com.vitorpamplona.amethyst.ui.note.LongPressToQuickActionGallery
|
||||
import com.vitorpamplona.amethyst.ui.note.WatchAuthor
|
||||
import com.vitorpamplona.amethyst.ui.note.WatchNoteEvent
|
||||
import com.vitorpamplona.amethyst.ui.note.calculateBackgroundColor
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.BannerImage
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedEmpty
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedError
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedState
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.LoadingFeed
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
|
||||
import com.vitorpamplona.quartz.events.ProfileGalleryEntryEvent
|
||||
|
||||
@Composable
|
||||
fun RenderGalleryFeed(
|
||||
viewModel: FeedViewModel,
|
||||
routeForLastRead: String?,
|
||||
forceEventKind: Int?,
|
||||
listState: LazyGridState,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
val feedState by viewModel.feedContent.collectAsStateWithLifecycle()
|
||||
CrossfadeIfEnabled(
|
||||
targetState = feedState,
|
||||
animationSpec = tween(durationMillis = 100),
|
||||
label = "RenderDiscoverFeed",
|
||||
accountViewModel = accountViewModel,
|
||||
) { state ->
|
||||
when (state) {
|
||||
is FeedState.Empty -> {
|
||||
FeedEmpty { viewModel.invalidateData() }
|
||||
}
|
||||
is FeedState.FeedError -> {
|
||||
FeedError(state.errorMessage) { viewModel.invalidateData() }
|
||||
}
|
||||
is FeedState.Loaded -> {
|
||||
GalleryFeedLoaded(
|
||||
state,
|
||||
routeForLastRead,
|
||||
listState,
|
||||
forceEventKind,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
is FeedState.Loading -> {
|
||||
LoadingFeed()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun GalleryFeedLoaded(
|
||||
state: FeedState.Loaded,
|
||||
routeForLastRead: String?,
|
||||
listState: LazyGridState,
|
||||
forceEventKind: Int?,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(3),
|
||||
contentPadding = FeedPadding,
|
||||
state = listState,
|
||||
) {
|
||||
itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { _, item ->
|
||||
val defaultModifier = remember { Modifier.fillMaxWidth().animateItemPlacement() }
|
||||
|
||||
Row(defaultModifier) {
|
||||
GalleryCardCompose(
|
||||
baseNote = item,
|
||||
routeForLastRead = routeForLastRead,
|
||||
modifier = Modifier,
|
||||
forceEventKind = forceEventKind,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
thickness = DividerThickness,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GalleryCardCompose(
|
||||
baseNote: Note,
|
||||
routeForLastRead: String? = null,
|
||||
modifier: Modifier = Modifier,
|
||||
parentBackgroundColor: MutableState<Color>? = null,
|
||||
forceEventKind: Int?,
|
||||
isHiddenFeed: Boolean = false,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
WatchNoteEvent(baseNote = baseNote, accountViewModel = accountViewModel, shortPreview = true) {
|
||||
CheckHiddenFeedWatchBlockAndReport(
|
||||
note = baseNote,
|
||||
modifier = modifier,
|
||||
ignoreAllBlocksAndReports = isHiddenFeed,
|
||||
showHiddenWarning = false,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
) { canPreview ->
|
||||
|
||||
if (baseNote.associatedNote != null) {
|
||||
if (baseNote.associatedNote!!.event != null) {
|
||||
val image = (baseNote.associatedNote!!.event as ProfileGalleryEntryEvent).url()
|
||||
if (image != null) {
|
||||
GalleryCard(
|
||||
galleryNote = baseNote.associatedNote!!,
|
||||
baseNote = baseNote,
|
||||
image = image,
|
||||
modifier = modifier,
|
||||
parentBackgroundColor = parentBackgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GalleryCard(
|
||||
galleryNote: Note,
|
||||
baseNote: Note,
|
||||
image: String,
|
||||
modifier: Modifier = Modifier,
|
||||
parentBackgroundColor: MutableState<Color>? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
// baseNote.event?.let { Text(text = it.pubKey()) }
|
||||
LongPressToQuickActionGallery(baseNote = galleryNote, accountViewModel = accountViewModel) { showPopup ->
|
||||
CheckNewAndRenderChannelCard(
|
||||
baseNote,
|
||||
image,
|
||||
modifier,
|
||||
parentBackgroundColor,
|
||||
accountViewModel,
|
||||
showPopup,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CheckNewAndRenderChannelCard(
|
||||
baseNote: Note,
|
||||
image: String,
|
||||
modifier: Modifier = Modifier,
|
||||
parentBackgroundColor: MutableState<Color>? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
showPopup: () -> Unit,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
val backgroundColor =
|
||||
calculateBackgroundColor(
|
||||
createdAt = baseNote.createdAt(),
|
||||
parentBackgroundColor = parentBackgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
|
||||
ClickableNote(
|
||||
baseNote = baseNote,
|
||||
backgroundColor = backgroundColor,
|
||||
modifier = modifier,
|
||||
accountViewModel = accountViewModel,
|
||||
showPopup = showPopup,
|
||||
nav = nav,
|
||||
) {
|
||||
InnerGalleryCardBox(baseNote, image, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun InnerGalleryCardBox(
|
||||
baseNote: Note,
|
||||
image: String,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
Column(HalfPadding) {
|
||||
SensitivityWarning(
|
||||
note = baseNote,
|
||||
accountViewModel = accountViewModel,
|
||||
) {
|
||||
RenderGalleryThumb(baseNote, image, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class GalleryThumb(
|
||||
val id: String?,
|
||||
val image: String?,
|
||||
val title: String?,
|
||||
// val price: Price?,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun RenderGalleryThumb(
|
||||
baseNote: Note,
|
||||
image: String,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
val card by
|
||||
baseNote
|
||||
.live()
|
||||
.metadata
|
||||
.map {
|
||||
GalleryThumb(
|
||||
id = "",
|
||||
image = image,
|
||||
title = "",
|
||||
// noteEvent?.title(),
|
||||
// price = noteEvent?.price(),
|
||||
)
|
||||
}.distinctUntilChanged()
|
||||
.observeAsState(
|
||||
GalleryThumb(
|
||||
id = "",
|
||||
image = image,
|
||||
title = "",
|
||||
),
|
||||
)
|
||||
|
||||
InnerRenderGalleryThumb(card as GalleryThumb, baseNote, accountViewModel)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun RenderGalleryThumbPreview(accountViewModel: AccountViewModel) {
|
||||
Surface(Modifier.size(200.dp)) {
|
||||
InnerRenderGalleryThumb(
|
||||
card =
|
||||
GalleryThumb(
|
||||
id = "",
|
||||
image = null,
|
||||
title = "Like New",
|
||||
// price = Price("800000", "SATS", null),
|
||||
),
|
||||
note = Note("hex"),
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun InnerRenderGalleryThumb(
|
||||
card: GalleryThumb,
|
||||
note: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(1f),
|
||||
contentAlignment = BottomStart,
|
||||
) {
|
||||
card.image?.let {
|
||||
if (isVideoUrl(it)) {
|
||||
VideoView(
|
||||
videoUri = it,
|
||||
mimeType = null,
|
||||
title = "",
|
||||
authorName = note.author?.toBestDisplayName(),
|
||||
roundedCorner = false,
|
||||
gallery = true,
|
||||
isFiniteHeight = false,
|
||||
alwaysShowVideo = true,
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
} else {
|
||||
AsyncImage(
|
||||
model = it,
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
}
|
||||
?: run { DisplayGalleryAuthorBanner(note) }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DisplayGalleryAuthorBanner(note: Note) {
|
||||
WatchAuthor(note) {
|
||||
BannerImage(
|
||||
it,
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.clip(QuoteBorder),
|
||||
)
|
||||
}
|
||||
}
|
@ -148,13 +148,17 @@ import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileBookmarksFeedViewMod
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileConversationsFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileFollowersUserFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileFollowsUserFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileGalleryFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileNewThreadsFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileReportFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileZapsFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.RefresheableBox
|
||||
import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView
|
||||
import com.vitorpamplona.amethyst.ui.screen.RefreshingFeedUserFeedView
|
||||
import com.vitorpamplona.amethyst.ui.screen.RelayFeedView
|
||||
import com.vitorpamplona.amethyst.ui.screen.RelayFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.SaveableGridFeedState
|
||||
import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys
|
||||
import com.vitorpamplona.amethyst.ui.screen.UserFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
@ -294,6 +298,16 @@ fun PrepareViewModels(
|
||||
),
|
||||
)
|
||||
|
||||
val galleryFeedViewModel: NostrUserProfileGalleryFeedViewModel =
|
||||
viewModel(
|
||||
key = baseUser.pubkeyHex + "UserGalleryFeedViewModel",
|
||||
factory =
|
||||
NostrUserProfileGalleryFeedViewModel.Factory(
|
||||
baseUser,
|
||||
accountViewModel.account,
|
||||
),
|
||||
)
|
||||
|
||||
val reportsFeedViewModel: NostrUserProfileReportFeedViewModel =
|
||||
viewModel(
|
||||
key = baseUser.pubkeyHex + "UserProfileReportFeedViewModel",
|
||||
@ -312,6 +326,7 @@ fun PrepareViewModels(
|
||||
appRecommendations,
|
||||
zapFeedViewModel,
|
||||
bookmarksFeedViewModel,
|
||||
galleryFeedViewModel,
|
||||
reportsFeedViewModel,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
@ -328,6 +343,7 @@ fun ProfileScreen(
|
||||
appRecommendations: NostrUserAppRecommendationsFeedViewModel,
|
||||
zapFeedViewModel: NostrUserProfileZapsFeedViewModel,
|
||||
bookmarksFeedViewModel: NostrUserProfileBookmarksFeedViewModel,
|
||||
galleryFeedViewModel: NostrUserProfileGalleryFeedViewModel,
|
||||
reportsFeedViewModel: NostrUserProfileReportFeedViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
@ -372,6 +388,7 @@ fun ProfileScreen(
|
||||
followersFeedViewModel,
|
||||
zapFeedViewModel,
|
||||
bookmarksFeedViewModel,
|
||||
galleryFeedViewModel,
|
||||
reportsFeedViewModel,
|
||||
accountViewModel,
|
||||
nav,
|
||||
@ -388,6 +405,7 @@ private fun RenderSurface(
|
||||
followersFeedViewModel: NostrUserProfileFollowersUserFeedViewModel,
|
||||
zapFeedViewModel: NostrUserProfileZapsFeedViewModel,
|
||||
bookmarksFeedViewModel: NostrUserProfileBookmarksFeedViewModel,
|
||||
galleryFeedViewModel: NostrUserProfileGalleryFeedViewModel,
|
||||
reportsFeedViewModel: NostrUserProfileReportFeedViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
@ -447,6 +465,7 @@ private fun RenderSurface(
|
||||
followersFeedViewModel,
|
||||
zapFeedViewModel,
|
||||
bookmarksFeedViewModel,
|
||||
galleryFeedViewModel,
|
||||
reportsFeedViewModel,
|
||||
accountViewModel,
|
||||
nav,
|
||||
@ -469,6 +488,7 @@ private fun RenderScreen(
|
||||
followersFeedViewModel: NostrUserProfileFollowersUserFeedViewModel,
|
||||
zapFeedViewModel: NostrUserProfileZapsFeedViewModel,
|
||||
bookmarksFeedViewModel: NostrUserProfileBookmarksFeedViewModel,
|
||||
galleryFeedViewModel: NostrUserProfileGalleryFeedViewModel,
|
||||
reportsFeedViewModel: NostrUserProfileReportFeedViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
@ -500,6 +520,7 @@ private fun RenderScreen(
|
||||
followersFeedViewModel,
|
||||
zapFeedViewModel,
|
||||
bookmarksFeedViewModel,
|
||||
galleryFeedViewModel,
|
||||
reportsFeedViewModel,
|
||||
accountViewModel,
|
||||
nav,
|
||||
@ -518,6 +539,7 @@ private fun CreateAndRenderPages(
|
||||
followersFeedViewModel: NostrUserProfileFollowersUserFeedViewModel,
|
||||
zapFeedViewModel: NostrUserProfileZapsFeedViewModel,
|
||||
bookmarksFeedViewModel: NostrUserProfileBookmarksFeedViewModel,
|
||||
galleryFeedViewModel: NostrUserProfileGalleryFeedViewModel,
|
||||
reportsFeedViewModel: NostrUserProfileReportFeedViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
@ -532,13 +554,14 @@ private fun CreateAndRenderPages(
|
||||
when (page) {
|
||||
0 -> TabNotesNewThreads(threadsViewModel, accountViewModel, nav)
|
||||
1 -> TabNotesConversations(repliesViewModel, accountViewModel, nav)
|
||||
2 -> TabFollows(baseUser, followsFeedViewModel, accountViewModel, nav)
|
||||
3 -> TabFollowers(baseUser, followersFeedViewModel, accountViewModel, nav)
|
||||
4 -> TabReceivedZaps(baseUser, zapFeedViewModel, accountViewModel, nav)
|
||||
5 -> TabBookmarks(bookmarksFeedViewModel, accountViewModel, nav)
|
||||
6 -> TabFollowedTags(baseUser, accountViewModel, nav)
|
||||
7 -> TabReports(baseUser, reportsFeedViewModel, accountViewModel, nav)
|
||||
8 -> TabRelays(baseUser, accountViewModel, nav)
|
||||
2 -> TabGallery(galleryFeedViewModel, accountViewModel, nav)
|
||||
3 -> TabFollows(baseUser, followsFeedViewModel, accountViewModel, nav)
|
||||
4 -> TabFollowers(baseUser, followersFeedViewModel, accountViewModel, nav)
|
||||
5 -> TabReceivedZaps(baseUser, zapFeedViewModel, accountViewModel, nav)
|
||||
6 -> TabBookmarks(bookmarksFeedViewModel, accountViewModel, nav)
|
||||
7 -> TabFollowedTags(baseUser, accountViewModel, nav)
|
||||
8 -> TabReports(baseUser, reportsFeedViewModel, accountViewModel, nav)
|
||||
9 -> TabRelays(baseUser, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
|
||||
@ -573,6 +596,7 @@ private fun CreateAndRenderTabs(
|
||||
listOf<@Composable (() -> Unit)?>(
|
||||
{ Text(text = stringRes(R.string.notes)) },
|
||||
{ Text(text = stringRes(R.string.replies)) },
|
||||
{ Text(text = stringRes(R.string.gallery)) },
|
||||
{ FollowTabHeader(baseUser) },
|
||||
{ FollowersTabHeader(baseUser) },
|
||||
{ ZapTabHeader(baseUser) },
|
||||
@ -1534,6 +1558,77 @@ fun TabNotesConversations(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun TabGallery(
|
||||
feedViewModel: NostrUserProfileGalleryFeedViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
LaunchedEffect(Unit) { feedViewModel.invalidateData() }
|
||||
|
||||
// Column(Modifier.fillMaxHeight()) {
|
||||
|
||||
RefresheableBox(feedViewModel, true) {
|
||||
SaveableGridFeedState(feedViewModel, scrollStateKey = ScrollStateKeys.PROFILE_GALLERY) { listState ->
|
||||
RenderGalleryFeed(
|
||||
feedViewModel,
|
||||
null,
|
||||
0,
|
||||
listState,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// }
|
||||
}
|
||||
|
||||
/*@Composable
|
||||
fun Gallery(
|
||||
baseUser: User,
|
||||
feedViewModel: UserFeedViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
WatchFollowChanges(baseUser, feedViewModel)
|
||||
|
||||
Column(Modifier.fillMaxHeight()) {
|
||||
Column {
|
||||
baseUser.latestGalleryList?.let {
|
||||
// val note2 = getOrCreateAddressableNoteInternal(aTag)
|
||||
val note = LocalCache.getOrCreateAddressableNote(it.address())
|
||||
note.event = it
|
||||
var notes = listOf<GalleryThumb>()
|
||||
for (tag in note.event?.tags()!!) {
|
||||
if (tag.size > 2) {
|
||||
if (tag[0] == "g") {
|
||||
// TODO get the node by id on main thread. LoadNote does nothing.
|
||||
val thumb =
|
||||
GalleryThumb(
|
||||
baseNote = note,
|
||||
id = tag[2],
|
||||
// TODO use the original note once it's loaded baseNote = basenote,
|
||||
image = tag[1],
|
||||
title = null,
|
||||
)
|
||||
notes = notes + thumb
|
||||
// }
|
||||
}
|
||||
}
|
||||
ProfileGallery(
|
||||
baseNotes = notes,
|
||||
modifier = Modifier,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} */
|
||||
|
||||
@Composable
|
||||
fun TabFollowedTags(
|
||||
baseUser: User,
|
||||
@ -1545,7 +1640,11 @@ fun TabFollowedTags(
|
||||
baseUser.latestContactList?.unverifiedFollowTagSet()
|
||||
}
|
||||
|
||||
Column(Modifier.fillMaxHeight().padding(vertical = 0.dp)) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxHeight()
|
||||
.padding(vertical = 0.dp),
|
||||
) {
|
||||
items?.let {
|
||||
LazyColumn {
|
||||
itemsIndexed(items) { index, hashtag ->
|
||||
|
@ -56,6 +56,7 @@ val TabRowHeight = Modifier
|
||||
val SmallBorder = RoundedCornerShape(7.dp)
|
||||
val SmallishBorder = RoundedCornerShape(9.dp)
|
||||
val QuoteBorder = RoundedCornerShape(15.dp)
|
||||
|
||||
val ButtonBorder = RoundedCornerShape(20.dp)
|
||||
val EditFieldBorder = RoundedCornerShape(25.dp)
|
||||
|
||||
|
@ -42,6 +42,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.graphics.compositeOver
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
@ -124,6 +125,18 @@ val LightImageModifier =
|
||||
.clip(shape = QuoteBorder)
|
||||
.border(1.dp, LightSubtleBorder, QuoteBorder)
|
||||
|
||||
val DarkVideoModifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(shape = RectangleShape)
|
||||
.border(1.dp, DarkSubtleBorder, RectangleShape)
|
||||
|
||||
val LightVideoModifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(shape = RectangleShape)
|
||||
.border(1.dp, LightSubtleBorder, RectangleShape)
|
||||
|
||||
val DarkProfile35dpModifier =
|
||||
Modifier
|
||||
.size(Size35dp)
|
||||
@ -148,6 +161,20 @@ val LightReplyBorderModifier =
|
||||
.clip(shape = QuoteBorder)
|
||||
.border(1.dp, LightSubtleBorder, QuoteBorder)
|
||||
|
||||
val DarkVideoBorderModifier =
|
||||
Modifier
|
||||
.padding(top = 5.dp)
|
||||
.fillMaxWidth()
|
||||
.clip(shape = RectangleShape)
|
||||
.border(1.dp, DarkSubtleBorder, RectangleShape)
|
||||
|
||||
val LightVideoBorderModifier =
|
||||
Modifier
|
||||
.padding(top = 2.dp, bottom = 0.dp, start = 0.dp, end = 0.dp)
|
||||
.fillMaxWidth()
|
||||
.clip(shape = RectangleShape)
|
||||
.border(1.dp, LightSubtleBorder, RectangleShape)
|
||||
|
||||
val DarkInnerPostBorderModifier =
|
||||
Modifier
|
||||
.padding(vertical = 5.dp)
|
||||
@ -356,6 +383,9 @@ val ColorScheme.markdownStyle: RichTextStyle
|
||||
val ColorScheme.imageModifier: Modifier
|
||||
get() = if (isLight) LightImageModifier else DarkImageModifier
|
||||
|
||||
val ColorScheme.videoGalleryModifier: Modifier
|
||||
get() = if (isLight) LightVideoModifier else DarkVideoModifier
|
||||
|
||||
val ColorScheme.profile35dpModifier: Modifier
|
||||
get() = if (isLight) LightProfile35dpModifier else DarkProfile35dpModifier
|
||||
|
||||
|
@ -14,6 +14,7 @@
|
||||
<string name="could_not_decrypt_the_message">No se pudo desencriptar el mensaje</string>
|
||||
<string name="group_picture">Imagen de grupo</string>
|
||||
<string name="explicit_content">Contenido explícito</string>
|
||||
<string name="spam">Spam</string>
|
||||
<string name="spam_description">El número de eventos de spam procedentes de este relé</string>
|
||||
<string name="impersonation">Suplantación de identidad</string>
|
||||
<string name="illegal_behavior">Comportamiento ilegal</string>
|
||||
@ -35,6 +36,7 @@
|
||||
<string name="report_impersonation">Reportar suplantación de identidad</string>
|
||||
<string name="report_explicit_content">Reportar contenido explícito</string>
|
||||
<string name="report_illegal_behaviour">Reportar comportamiento ilegal</string>
|
||||
<string name="report_malware">Reportar malware</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_reply">Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para poder responder</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_boost_posts">Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para poder impulsar publicaciones.</string>
|
||||
<string name="login_with_a_private_key_to_like_posts">Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para indicar que te gustan las publicaciones.</string>
|
||||
@ -44,11 +46,13 @@
|
||||
<string name="login_with_a_private_key_to_be_able_to_unfollow">Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para poder dejar de seguir a otros usuarios.</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_hide_word">Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para poder ocultar una palabra o frase.</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_show_word">Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para poder mostrar una palabra o frase.</string>
|
||||
<string name="zaps">Zaps</string>
|
||||
<string name="view_count">Total de visualizaciones</string>
|
||||
<string name="boost">Impulsar</string>
|
||||
<string name="boosted">Impulsada</string>
|
||||
<string name="edited">editada</string>
|
||||
<string name="edited_number">edición #%1$s</string>
|
||||
<string name="original">original</string>
|
||||
<string name="quote">Citar</string>
|
||||
<string name="fork">Bifurcar</string>
|
||||
<string name="propose_an_edit">Proponer una edición</string>
|
||||
@ -89,6 +93,7 @@
|
||||
<string name="failed_to_upload_the_image">No se pudo cargar la imagen</string>
|
||||
<string name="relay_address">Dirección del relé</string>
|
||||
<string name="posts">Publicaciones</string>
|
||||
<string name="bytes">Bytes</string>
|
||||
<string name="errors">Errores</string>
|
||||
<string name="errors_description">El número de errores de conexión en esta sesión</string>
|
||||
<string name="home_feed">Tus noticias</string>
|
||||
@ -195,6 +200,9 @@
|
||||
<string name="nip_05">Dirección de Nostr</string>
|
||||
<string name="never">nunca</string>
|
||||
<string name="now">ahora</string>
|
||||
<string name="h">h</string>
|
||||
<string name="m">m</string>
|
||||
<string name="d">d</string>
|
||||
<string name="nudity">Desnudos</string>
|
||||
<string name="profanity_hateful_speech">Blasfemias / Discurso de odio</string>
|
||||
<string name="report_hateful_speech">Denuncia discurso de odio</string>
|
||||
@ -220,6 +228,7 @@
|
||||
<string name="biometric_authentication_failed">Autenticación fallida</string>
|
||||
<string name="biometric_authentication_failed_explainer">El sistema biométrico no pudo autenticar al propietario de este teléfono</string>
|
||||
<string name="biometric_authentication_failed_explainer_with_error">El sistema biométrico no pudo autenticar al propietario de este teléfono. Error: %1$s</string>
|
||||
<string name="biometric_error">Error</string>
|
||||
<string name="badge_created_by">"Credo por %1$s"</string>
|
||||
<string name="badge_award_image_for">"Imagen del Badge para %1$s"</string>
|
||||
<string name="new_badge_award_notif">Has recibido un nuevo Badge</string>
|
||||
@ -259,6 +268,7 @@
|
||||
<string name="report_dialog_impersonation">Suplantación de identidad malintencionada</string>
|
||||
<string name="report_dialog_nudity">Desnudos o contenido gráfico</string>
|
||||
<string name="report_dialog_illegal">Comportamiento ilegal</string>
|
||||
<string name="report_dialog_malware">Malware</string>
|
||||
<string name="report_dialog_blocking_a_user">Si bloqueas a un usuario, se ocultará su contenido en tu app. Tus notas todavía son visibles públicamente, incluso para las personas que bloquees. Los usuarios bloqueados aparecen en la pantalla \"Filtros de seguridad\".</string>
|
||||
<string name="report_dialog_block_hide_user_btn"><![CDATA[Bloquear y ocultar usuario]]></string>
|
||||
<string name="report_dialog_report_btn">Reportar abuso</string>
|
||||
@ -296,6 +306,7 @@
|
||||
<string name="poll_zap_value_min">Zap mínimo</string>
|
||||
<string name="poll_zap_value_max">Zap máximo</string>
|
||||
<string name="poll_consensus_threshold">Consenso</string>
|
||||
<string name="poll_consensus_threshold_percent">(0–100)%</string>
|
||||
<string name="poll_closing_time">Cerrar después de</string>
|
||||
<string name="poll_closing_time_days">días</string>
|
||||
<string name="poll_unable_to_vote">No se puede votar</string>
|
||||
@ -326,9 +337,18 @@
|
||||
<string name="zap_type_private_explainer">El remitente y el destinatario pueden verse entre sí y leer el mensaje</string>
|
||||
<string name="zap_type_anonymous">Anónimo</string>
|
||||
<string name="zap_type_anonymous_explainer">El receptor y el público no saben quién envió el pago</string>
|
||||
<string name="zap_type_nonzap">No zap</string>
|
||||
<string name="zap_type_nonzap_explainer">No hay rastro en Nostr, solo en Lightning</string>
|
||||
<string name="file_server">Servidor de archivos</string>
|
||||
<string name="zap_forward_lnAddress">Dirección o @usuario de Lightning</string>
|
||||
<string name="media_servers">Servidores multimedia</string>
|
||||
<string name="set_preferred_media_servers">Configura tus servidores preferidos para subir contenido multimedia.</string>
|
||||
<string name="no_media_server_message">No tienes ningún servidor multimedia personalizado. Puedes utilizar la lista de Amethyst, o agregar uno de la lista a continuación ↓</string>
|
||||
<string name="built_in_media_servers_title">Servidores multimedia integrados</string>
|
||||
<string name="built_in_servers_description">Lista predeterminada de Amethyst. Puedes agregarlos individualmente o añadir la lista.</string>
|
||||
<string name="use_default_servers">Usar lista predeterminada</string>
|
||||
<string name="add_media_server">Agregar servidor multimedia</string>
|
||||
<string name="delete_media_server">Eliminar servidor multimedia</string>
|
||||
<string name="upload_server_relays_nip95">Tus relés (NIP-95)</string>
|
||||
<string name="upload_server_relays_nip95_explainer">Los archivos están alojados en tus relés. Nuevo NIP: comprueba si son compatibles.</string>
|
||||
<string name="connect_via_tor_short">Configuración de Tor/Orbot</string>
|
||||
@ -336,8 +356,10 @@
|
||||
<string name="do_you_really_want_to_disable_tor_title">¿Desconectarse de tu Orbot/Tor?</string>
|
||||
<string name="do_you_really_want_to_disable_tor_text">Los datos se transferirán de inmediato en la red normal</string>
|
||||
<string name="yes">Sí</string>
|
||||
<string name="no">No</string>
|
||||
<string name="follow_list_selection">Lista de seguidos</string>
|
||||
<string name="follow_list_kind3follows">Todos los seguidos</string>
|
||||
<string name="follow_list_global">Global</string>
|
||||
<string name="follow_list_mute_list">Lista de silenciados</string>
|
||||
<string name="connect_through_your_orbot_setup_markdown"> ## Conéctate a través de Tor con Orbot
|
||||
\n\n1. Instala [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android).
|
||||
@ -355,6 +377,7 @@
|
||||
<string name="app_notification_dms_channel_description">Te notifica cuando llega un mensaje privado</string>
|
||||
<string name="app_notification_zaps_channel_name">Zaps recibidos</string>
|
||||
<string name="app_notification_zaps_channel_description">Te notifica cuando alguien te zapea</string>
|
||||
<string name="app_notification_zaps_channel_message">%1$s sats</string>
|
||||
<string name="app_notification_zaps_channel_message_from">De %1$s</string>
|
||||
<string name="app_notification_zaps_channel_message_for">por %1$s</string>
|
||||
<string name="reply_notify">Notificar: </string>
|
||||
@ -374,6 +397,7 @@
|
||||
<string name="warn_when_posts_have_reports_from_your_follows">Avisar cuando las publicaciones tengan reportes de tus seguidos</string>
|
||||
<string name="new_reaction_symbol">Nuevo símbolo de reacción</string>
|
||||
<string name="no_reaction_type_setup_long_press_to_change">No hay tipos de reacción preseleccionados para este usuario. Deja presionado el botón de corazón para cambiarlos.</string>
|
||||
<string name="zapraiser">Recaudación de zaps</string>
|
||||
<string name="zapraiser_explainer">Añade una meta de sats para recaudar por esta publicación. Los clientes compatibles pueden mostrarla como una barra de progreso para incentivar las donaciones.</string>
|
||||
<string name="zapraiser_target_amount_in_sats">Cantidad objetivo en sats</string>
|
||||
<string name="sats_to_complete">Zapraiser en %1$s. %2$s sats hasta la meta</string>
|
||||
@ -384,6 +408,7 @@
|
||||
<string name="an_error_occurred_trying_to_get_relay_information">Se ha producido un error al intentar obtener información sobre el relé de %1$s</string>
|
||||
<string name="owner">Propietario</string>
|
||||
<string name="version">Versión</string>
|
||||
<string name="software">Software</string>
|
||||
<string name="contact">Contacto</string>
|
||||
<string name="supports">NIP compatibles</string>
|
||||
<string name="admission_fees">Tasas de admisión</string>
|
||||
@ -402,6 +427,7 @@
|
||||
<string name="maximum_event_tags">Etiquetas de evento máximas</string>
|
||||
<string name="content_length">Longitud del contenido</string>
|
||||
<string name="minimum_pow">PoW mínima</string>
|
||||
<string name="auth">Autenticación</string>
|
||||
<string name="payment">Pago</string>
|
||||
<string name="cashu">Token de Cashu</string>
|
||||
<string name="cashu_redeem">Canjear</string>
|
||||
@ -423,6 +449,7 @@
|
||||
<string name="discover_marketplace">Mercado</string>
|
||||
<string name="discover_live">En vivo</string>
|
||||
<string name="discover_community">Comunidad</string>
|
||||
<string name="discover_chat">Chats</string>
|
||||
<string name="community_approved_posts">Publicaciones aprobadas</string>
|
||||
<string name="groups_no_descriptor">Este grupo no tiene descripción ni reglas. Habla con el propietario para agregar una.</string>
|
||||
<string name="community_no_descriptor">Esta comunidad no tiene descripción. Habla con el propietario para agregar una.</string>
|
||||
@ -431,6 +458,7 @@
|
||||
<string name="settings">Configuración</string>
|
||||
<string name="connectivity_type_always">Siempre</string>
|
||||
<string name="connectivity_type_wifi_only">Solo Wi-Fi</string>
|
||||
<string name="connectivity_type_unmetered_wifi_only">WiFi sin medición</string>
|
||||
<string name="connectivity_type_never">Nunca</string>
|
||||
<string name="ui_feature_set_type_complete">Completo</string>
|
||||
<string name="ui_feature_set_type_simplified">Simplificado</string>
|
||||
@ -449,6 +477,7 @@
|
||||
<string name="ui_style">Modo de interfaz</string>
|
||||
<string name="ui_style_description">Elegir el estilo de publicación</string>
|
||||
<string name="load_image">Cargar imagen</string>
|
||||
<string name="spamming_users">Spammers</string>
|
||||
<string name="muted_button">Silenciado. Hacer clic para reactivar el sonido.</string>
|
||||
<string name="mute_button">Sonido activado. Hacer clic para silenciar.</string>
|
||||
<string name="search_button">Buscar grabaciones locales y remotas</string>
|
||||
@ -496,6 +525,7 @@
|
||||
<string name="poll_zap_value_min_max_explainer">Los votos se calculan según la cantidad de zaps. Puedes establecer una cantidad mínima para evitar spammers y una máxima para que los zappers grandes no puedan dominar la encuesta. Usa la misma cantidad en ambos campos para asegurarte de que cada voto tenga el mismo valor. Déjalos vacíos para aceptar cualquier cantidad.</string>
|
||||
<string name="error_dialog_zap_error">No se pudo enviar el zap</string>
|
||||
<string name="error_dialog_talk_to_user">Mensaje para el usuario</string>
|
||||
<string name="error_dialog_button_ok">OK</string>
|
||||
<string name="relay_information_document_error_assemble_url">No se pudo llegar a %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_failed_to_assemble_url">Error al ensamblar la url NIP-11 para %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_failed_to_reach_server">No se pudo contactar %1$s: %2$s</string>
|
||||
@ -507,6 +537,8 @@
|
||||
<string name="active_for">Activo para: </string>
|
||||
<string name="active_for_home">Inicio</string>
|
||||
<string name="active_for_msg">Mensajes directos</string>
|
||||
<string name="active_for_chats">Chats</string>
|
||||
<string name="active_for_global">Global</string>
|
||||
<string name="active_for_search">Búsqueda</string>
|
||||
<string name="zap_split_title">Dividir y reenviar zaps</string>
|
||||
<string name="zap_split_explainer">Los clientes compatibles dividirán y reenviarán los zaps a los usuarios que se agreguen aquí en lugar de enviártelos a ti.</string>
|
||||
@ -515,6 +547,7 @@
|
||||
<string name="missing_lud16">Lightning no está configurado</string>
|
||||
<string name="user_x_does_not_have_a_lightning_address_setup_to_receive_sats">El usuario %1$s no tiene una dirección de Lightning configurada para recibir sats.</string>
|
||||
<string name="zap_split_weight">Porcentaje</string>
|
||||
<string name="zap_split_weight_placeholder">25</string>
|
||||
<string name="splitting_zaps_with">Dividir zaps con</string>
|
||||
<string name="forwarding_zaps_to">Reenviar zaps a</string>
|
||||
<string name="lightning_wallets_not_found2">No se encontraron billeteras de Lightning</string>
|
||||
@ -567,6 +600,7 @@
|
||||
<string name="select_push_server">Seleccionar una aplicación UnifiedPush</string>
|
||||
<string name="push_server_title">Notificación push</string>
|
||||
<string name="push_server_explainer">De aplicaciones UnifiedPush instaladas</string>
|
||||
<string name="push_server_none">Ninguno</string>
|
||||
<string name="push_server_none_explainer">Desactiva las notificaciones push</string>
|
||||
<string name="push_server_uses_app_explainer">Usa la app %1$s</string>
|
||||
<string name="push_server_install_app">Configuración de notificaciones push</string>
|
||||
@ -580,9 +614,11 @@
|
||||
<string name="hi_there_is_this_still_available">Hola, ¿esto todavía está disponible?</string>
|
||||
<string name="classifieds">Vender un artículo</string>
|
||||
<string name="classifieds_title">Título</string>
|
||||
<string name="classifieds_title_placeholder">iPhone 13</string>
|
||||
<string name="classifieds_condition">Estado</string>
|
||||
<string name="classifieds_category">Categoría</string>
|
||||
<string name="classifieds_price">Precio (en sats)</string>
|
||||
<string name="classifieds_price_placeholder">1000</string>
|
||||
<string name="classifieds_location">Ubicación</string>
|
||||
<string name="classifieds_location_placeholder">Ciudad, estado, país.</string>
|
||||
<string name="classifieds_condition_new">Nuevo</string>
|
||||
@ -616,6 +652,8 @@
|
||||
<string name="server_did_not_provide_a_url_after_uploading">El servidor no proporcionó una URL después de la carga</string>
|
||||
<string name="could_not_download_from_the_server">No se pudo descargar el contenido cargado desde el servidor</string>
|
||||
<string name="could_not_prepare_local_file_to_upload">No se pudo preparar el archivo local para cargar: %1$s</string>
|
||||
<string name="failed_to_upload_with_message">Error al subir: %1$s</string>
|
||||
<string name="failed_to_delete_with_message">Error al eliminar: %1$s</string>
|
||||
<string name="edit_draft">Editar borrador</string>
|
||||
<string name="login_with_qr_code">Iniciar sesión con código QR</string>
|
||||
<string name="route">Ruta</string>
|
||||
@ -624,6 +662,7 @@
|
||||
<string name="route_discover">Descubrir</string>
|
||||
<string name="route_messages">Mensajes</string>
|
||||
<string name="route_notifications">Notificaciones</string>
|
||||
<string name="route_global">Global</string>
|
||||
<string name="route_video">Cortos</string>
|
||||
<string name="route_security_filters">Filtros de seguridad</string>
|
||||
<string name="new_post">Nueva publicación</string>
|
||||
@ -634,6 +673,8 @@
|
||||
<string name="reply_description">Responder</string>
|
||||
<string name="boost_or_quote_description">Impulsar o citar</string>
|
||||
<string name="like_description">Me gusta</string>
|
||||
<string name="zap_description">Zap</string>
|
||||
<string name="change_reaction">Cambiar reacciones rápidas</string>
|
||||
<string name="profile_image_of_user">Imagen de perfil de %1$s</string>
|
||||
<string name="relay_info">Relé %1$s</string>
|
||||
<string name="expand_relay_list">Ampliar lista de relés</string>
|
||||
@ -644,6 +685,7 @@
|
||||
<string name="add_bitcoin_invoice">Factura de Bitcoin</string>
|
||||
<string name="cancel_bitcoin_invoice">Cancelar factura de Bitcoin</string>
|
||||
<string name="cancel_classifieds">Cancelar la venta de un artículo</string>
|
||||
<string name="add_zapraiser">Recaudación de zaps</string>
|
||||
<string name="cancel_zapraiser">Cancelar zapraiser</string>
|
||||
<string name="add_location">Ubicación</string>
|
||||
<string name="remove_location">Eliminar ubicación</string>
|
||||
@ -694,7 +736,9 @@
|
||||
<string name="forked_from">Bifurcación de</string>
|
||||
<string name="forked_tag">BIFURCACIÓN</string>
|
||||
<string name="git_repository">Repositorio Git: %1$s</string>
|
||||
<string name="git_web_address">Web:</string>
|
||||
<string name="git_clone_address">Clon:</string>
|
||||
<string name="existed_since">OTS: %1$s</string>
|
||||
<string name="ots_info_title">Prueba de marca de tiempo</string>
|
||||
<string name="ots_info_description">Hay prueba de que esta publicación se firmó en algún momento antes de %1$s. La prueba se marcó en la cadena de bloques de Bitcoin en esa fecha y hora.</string>
|
||||
<string name="edit_post">Editar publicación</string>
|
||||
@ -722,4 +766,34 @@
|
||||
<string name="dvm_requesting_job">Solicitando trabajo a DVM</string>
|
||||
<string name="nwc_payment_request">Solicitud de pago enviada, esperando confirmación de tu monedero</string>
|
||||
<string name="dvm_waiting_to_confirm_payment">Esperando a que DVM confirme el pago o envíe resultados</string>
|
||||
<string name="http_status_400">Solicitud incorrecta: el servidor no puede procesar o no procesará la solicitud.</string>
|
||||
<string name="http_status_401">No autorizado - El usuario no tiene credenciales de autenticación válidas</string>
|
||||
<string name="http_status_402">Pago requerido: el servidor requiere pago para completar la solicitud.</string>
|
||||
<string name="http_status_403">Prohibido: el usuario no tiene derechos de acceso para realizar la solicitud.</string>
|
||||
<string name="http_status_404">No se encontró: el servidor no puede encontrar la dirección solicitada.</string>
|
||||
<string name="http_status_405">Método no permitido: el servidor admite el método de solicitud, pero no el recurso de destino.</string>
|
||||
<string name="http_status_406">No aceptable: el servidor no encuentra ningún contenido que satisfaga la solicitud.</string>
|
||||
<string name="http_status_407">Autenticación de proxy requerida: el usuario no tiene credenciales de autenticación válidas.</string>
|
||||
<string name="http_status_408">Tiempo de espera de la solicitud: se agotó el tiempo de espera del servidor.</string>
|
||||
<string name="http_status_409">Conflicto: el servidor no puede satisfacer la solicitud porque hay un conflicto con el recurso.</string>
|
||||
<string name="http_status_410">Borrado: el contenido solicitado se eliminó permanentemente del servidor y no se restablecerá.</string>
|
||||
<string name="http_status_411">Longitud requerida: el servidor rechaza la solicitud porque requiere una longitud definida.</string>
|
||||
<string name="http_status_412">Precondición fallida: las precondiciones de la solicitud en los campos de cabecera que el servidor no cumple.</string>
|
||||
<string name="http_status_413">Carga demasiado grande: la solicitud supera los límites definidos por el servidor, por lo que este se niega a procesarla.</string>
|
||||
<string name="http_status_414">URI demasiado largo: la URL solicitada por el cliente es demasiado larga para que el servidor pueda procesarla. </string>
|
||||
<string name="http_status_415">Tipo de contenido multimedia no admitido: la solicitud usa un formato multimedia que el servidor no admite.</string>
|
||||
<string name="http_status_416">Intervalo no satisfactorio: el servidor no puede satisfacer el valor indicado en el campo de cabecera del intervalo de la solicitud. </string>
|
||||
<string name="http_status_417">Expectativa fallida: el servidor no puede cumplir los requisitos indicados por el campo de cabecera de la solicitud Expect.</string>
|
||||
<string name="http_status_426">Actualización requerida: el servidor se niega a procesar la solicitud utilizando el protocolo actual a menos que el cliente se actualice a un protocolo diferente.</string>
|
||||
<string name="http_status_500">Error interno del servidor: el servidor encontró un error inesperado y no puede completar la solicitud.</string>
|
||||
<string name="http_status_501">No implementado: el servidor no puede completar la solicitud o no reconoce el método de solicitud.</string>
|
||||
<string name="http_status_502">Gateway incorrecto: el servidor actúa como gateway y obtuvo una respuesta no válida del host.</string>
|
||||
<string name="http_status_503">Servicio no disponible: esto ocurre a menudo cuando un servidor está sobrecargado o caído por mantenimiento.</string>
|
||||
<string name="http_status_504">Tiempo de espera del gateway: el servidor estaba actuando como gateway o proxy y se agotó el tiempo de espera de una respuesta.</string>
|
||||
<string name="http_status_505">Versión HTTP no admitida: el servidor no admite la versión HTTP de la solicitud.</string>
|
||||
<string name="http_status_506">La variante también negocia: el servidor tiene un error de configuración interno.</string>
|
||||
<string name="http_status_507">Almacenamiento insuficiente: el servidor no dispone de almacenamiento suficiente para procesar correctamente la solicitud.</string>
|
||||
<string name="http_status_508">Bucle detectado: el servidor detecta un bucle infinito al procesar la solicitud.</string>
|
||||
<string name="http_status_511">Autenticación de red requerida: el cliente debe estar autenticado para acceder a la red.</string>
|
||||
<string name="add_a_nip96_server">Agregar servidor NIP-96</string>
|
||||
</resources>
|
||||
|
@ -14,6 +14,7 @@
|
||||
<string name="could_not_decrypt_the_message">No se pudo desencriptar el mensaje</string>
|
||||
<string name="group_picture">Foto de grupo</string>
|
||||
<string name="explicit_content">Contenido explícito</string>
|
||||
<string name="spam">Spam</string>
|
||||
<string name="spam_description">El número de eventos de spam procedentes de este relé</string>
|
||||
<string name="impersonation">Suplantación de identidad</string>
|
||||
<string name="illegal_behavior">Comportamiento ilegal</string>
|
||||
@ -35,6 +36,7 @@
|
||||
<string name="report_impersonation">Reportar suplantación de identidad</string>
|
||||
<string name="report_explicit_content">Reportar contenido explícito</string>
|
||||
<string name="report_illegal_behaviour">Reportar comportamiento ilegal</string>
|
||||
<string name="report_malware">Reportar malware</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_reply">Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para poder responder</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_boost_posts">Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para poder impulsar publicaciones</string>
|
||||
<string name="login_with_a_private_key_to_like_posts">Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para indicar que te gustan las publicaciones.</string>
|
||||
@ -44,11 +46,13 @@
|
||||
<string name="login_with_a_private_key_to_be_able_to_unfollow">Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para poder dejar de seguir a otros usuarios.</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_hide_word">Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para poder ocultar una palabra o frase.</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_show_word">Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para poder mostrar una palabra o frase.</string>
|
||||
<string name="zaps">Zaps</string>
|
||||
<string name="view_count">Total de visualizaciones</string>
|
||||
<string name="boost">Impulsar</string>
|
||||
<string name="boosted">Impulsada</string>
|
||||
<string name="edited">editada</string>
|
||||
<string name="edited_number">edición #%1$s</string>
|
||||
<string name="original">original</string>
|
||||
<string name="quote">Cita</string>
|
||||
<string name="fork">Bifurcar</string>
|
||||
<string name="propose_an_edit">Proponer una edición</string>
|
||||
@ -89,6 +93,7 @@
|
||||
<string name="failed_to_upload_the_image">Error al subir la imagen</string>
|
||||
<string name="relay_address">Dirección del relé</string>
|
||||
<string name="posts">Publicaciones</string>
|
||||
<string name="bytes">Bytes</string>
|
||||
<string name="errors">Errores</string>
|
||||
<string name="errors_description">El número de errores de conexión en esta sesión</string>
|
||||
<string name="home_feed">Tus noticias</string>
|
||||
@ -195,6 +200,9 @@
|
||||
<string name="nip_05">Dirección de Nostr</string>
|
||||
<string name="never">nunca</string>
|
||||
<string name="now">ahora</string>
|
||||
<string name="h">h</string>
|
||||
<string name="m">m</string>
|
||||
<string name="d">d</string>
|
||||
<string name="nudity">Desnudez</string>
|
||||
<string name="profanity_hateful_speech">Groserías o lenguaje que incita al odio</string>
|
||||
<string name="report_hateful_speech">Reportar lenguaje que incita al odio</string>
|
||||
@ -220,6 +228,7 @@
|
||||
<string name="biometric_authentication_failed">Error de autenticación</string>
|
||||
<string name="biometric_authentication_failed_explainer">El sistema biométrico no pudo autenticar al propietario de este teléfono</string>
|
||||
<string name="biometric_authentication_failed_explainer_with_error">El sistema biométrico no pudo autenticar al propietario de este teléfono. Error: %1$s</string>
|
||||
<string name="biometric_error">Error</string>
|
||||
<string name="badge_created_by">"Creado por %1$s"</string>
|
||||
<string name="badge_award_image_for">"Imagen de premio de insignia por %1$s"</string>
|
||||
<string name="new_badge_award_notif">Recibiste un nuevo premio de insignia</string>
|
||||
@ -259,6 +268,7 @@
|
||||
<string name="report_dialog_impersonation">Suplantación de identidad malintencionada</string>
|
||||
<string name="report_dialog_nudity">Desnudez o contenido gráfico</string>
|
||||
<string name="report_dialog_illegal">Comportamiento ilegal</string>
|
||||
<string name="report_dialog_malware">Malware</string>
|
||||
<string name="report_dialog_blocking_a_user">Si bloqueas a un usuario, se ocultará su contenido en tu app. Tus notas todavía son visibles públicamente, incluso para las personas que bloquees. Los usuarios bloqueados aparecen en la pantalla \"Filtros de seguridad\".</string>
|
||||
<string name="report_dialog_block_hide_user_btn"><![CDATA[Bloquear y ocultar usuario]]></string>
|
||||
<string name="report_dialog_report_btn">Reportar abuso</string>
|
||||
@ -296,6 +306,7 @@
|
||||
<string name="poll_zap_value_min">Zap mínimo</string>
|
||||
<string name="poll_zap_value_max">Zap máximo</string>
|
||||
<string name="poll_consensus_threshold">Consenso</string>
|
||||
<string name="poll_consensus_threshold_percent">(0–100)%</string>
|
||||
<string name="poll_closing_time">Cerrar después de</string>
|
||||
<string name="poll_closing_time_days">días</string>
|
||||
<string name="poll_unable_to_vote">No se puede votar</string>
|
||||
@ -326,9 +337,18 @@
|
||||
<string name="zap_type_private_explainer">El remitente y el destinatario pueden verse entre sí y leer el mensaje</string>
|
||||
<string name="zap_type_anonymous">Anónimo</string>
|
||||
<string name="zap_type_anonymous_explainer">El receptor y el público no saben quién envió el pago</string>
|
||||
<string name="zap_type_nonzap">No zap</string>
|
||||
<string name="zap_type_nonzap_explainer">No hay rastro en Nostr, solo en Lightning</string>
|
||||
<string name="file_server">Servidor de archivos</string>
|
||||
<string name="zap_forward_lnAddress">Dirección o @usuario de Lightning</string>
|
||||
<string name="media_servers">Servidores multimedia</string>
|
||||
<string name="set_preferred_media_servers">Configura tus servidores preferidos para subir contenido multimedia.</string>
|
||||
<string name="no_media_server_message">No tienes ningún servidor multimedia personalizado. Puedes utilizar la lista de Amethyst, o agregar uno de la lista a continuación ↓</string>
|
||||
<string name="built_in_media_servers_title">Servidores multimedia integrados</string>
|
||||
<string name="built_in_servers_description">Lista predeterminada de Amethyst. Puedes agregarlos individualmente o añadir la lista.</string>
|
||||
<string name="use_default_servers">Usar lista predeterminada</string>
|
||||
<string name="add_media_server">Agregar servidor multimedia</string>
|
||||
<string name="delete_media_server">Eliminar servidor multimedia</string>
|
||||
<string name="upload_server_relays_nip95">Tus relés (NIP-95)</string>
|
||||
<string name="upload_server_relays_nip95_explainer">Los archivos están alojados en tus relés. Nuevo NIP: comprueba si son compatibles.</string>
|
||||
<string name="connect_via_tor_short">Configuración de Tor/Orbot</string>
|
||||
@ -336,8 +356,10 @@
|
||||
<string name="do_you_really_want_to_disable_tor_title">¿Desconectarse de tu Orbot/Tor?</string>
|
||||
<string name="do_you_really_want_to_disable_tor_text">Los datos se transferirán de inmediato en la red normal</string>
|
||||
<string name="yes">Sí</string>
|
||||
<string name="no">No</string>
|
||||
<string name="follow_list_selection">Lista de seguidos</string>
|
||||
<string name="follow_list_kind3follows">Todos los seguidos</string>
|
||||
<string name="follow_list_global">Global</string>
|
||||
<string name="follow_list_mute_list">Lista de silenciados</string>
|
||||
<string name="connect_through_your_orbot_setup_markdown"> ## Conéctate a través de Tor con Orbot
|
||||
\n\n1. Instala [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android).
|
||||
@ -355,6 +377,7 @@
|
||||
<string name="app_notification_dms_channel_description">Te notifica cuando llega un mensaje privado</string>
|
||||
<string name="app_notification_zaps_channel_name">Zaps recibidos</string>
|
||||
<string name="app_notification_zaps_channel_description">Te notifica cuando alguien te zapea</string>
|
||||
<string name="app_notification_zaps_channel_message">%1$s sats</string>
|
||||
<string name="app_notification_zaps_channel_message_from">De %1$s</string>
|
||||
<string name="app_notification_zaps_channel_message_for">por %1$s</string>
|
||||
<string name="reply_notify">Notificar: </string>
|
||||
@ -374,6 +397,7 @@
|
||||
<string name="warn_when_posts_have_reports_from_your_follows">Avisar cuando las publicaciones tengan reportes de tus seguidos</string>
|
||||
<string name="new_reaction_symbol">Nuevo símbolo de reacción</string>
|
||||
<string name="no_reaction_type_setup_long_press_to_change">No hay tipos de reacción preseleccionados para este usuario. Deja presionado el botón de corazón para cambiarlos.</string>
|
||||
<string name="zapraiser">Recaudación de zaps</string>
|
||||
<string name="zapraiser_explainer">Agrega una meta de sats para recaudar por esta publicación. Los clientes compatibles pueden mostrarla como una barra de progreso para incentivar las donaciones.</string>
|
||||
<string name="zapraiser_target_amount_in_sats">Cantidad objetivo en sats</string>
|
||||
<string name="sats_to_complete">Zapraiser en %1$s. %2$s sats hasta la meta</string>
|
||||
@ -384,6 +408,7 @@
|
||||
<string name="an_error_occurred_trying_to_get_relay_information">Se produjo un error al intentar obtener información sobre el relé de %1$s</string>
|
||||
<string name="owner">Propietario</string>
|
||||
<string name="version">Versión</string>
|
||||
<string name="software">Software</string>
|
||||
<string name="contact">Contacto</string>
|
||||
<string name="supports">NIP compatibles</string>
|
||||
<string name="admission_fees">Tarifas de admisión</string>
|
||||
@ -402,6 +427,7 @@
|
||||
<string name="maximum_event_tags">Etiquetas de evento máximas</string>
|
||||
<string name="content_length">Longitud del contenido</string>
|
||||
<string name="minimum_pow">PoW mínima</string>
|
||||
<string name="auth">Autenticación</string>
|
||||
<string name="payment">Pago</string>
|
||||
<string name="cashu">Token de Cashu</string>
|
||||
<string name="cashu_redeem">Canjear</string>
|
||||
@ -423,6 +449,7 @@
|
||||
<string name="discover_marketplace">Mercado</string>
|
||||
<string name="discover_live">En vivo</string>
|
||||
<string name="discover_community">Comunidad</string>
|
||||
<string name="discover_chat">Chats</string>
|
||||
<string name="community_approved_posts">Publicaciones aprobadas</string>
|
||||
<string name="groups_no_descriptor">Este grupo no tiene descripción ni reglas. Habla con el propietario para agregar una.</string>
|
||||
<string name="community_no_descriptor">Esta comunidad no tiene descripción. Habla con el propietario para agregar una.</string>
|
||||
@ -431,6 +458,7 @@
|
||||
<string name="settings">Configuración</string>
|
||||
<string name="connectivity_type_always">Siempre</string>
|
||||
<string name="connectivity_type_wifi_only">Solo Wi-Fi</string>
|
||||
<string name="connectivity_type_unmetered_wifi_only">WiFi sin medición</string>
|
||||
<string name="connectivity_type_never">Nunca</string>
|
||||
<string name="ui_feature_set_type_complete">Completo</string>
|
||||
<string name="ui_feature_set_type_simplified">Simplificado</string>
|
||||
@ -449,6 +477,7 @@
|
||||
<string name="ui_style">Modo de interfaz</string>
|
||||
<string name="ui_style_description">Elegir el estilo de publicación</string>
|
||||
<string name="load_image">Cargar imagen</string>
|
||||
<string name="spamming_users">Spammers</string>
|
||||
<string name="muted_button">Silenciado. Hacer clic para reactivar el sonido.</string>
|
||||
<string name="mute_button">Sonido activado. Hacer clic para silenciar.</string>
|
||||
<string name="search_button">Buscar grabaciones locales y remotas</string>
|
||||
@ -496,6 +525,7 @@
|
||||
<string name="poll_zap_value_min_max_explainer">Los votos se calculan según la cantidad de zaps. Puedes establecer una cantidad mínima para evitar spammers y una máxima para que los zappers grandes no puedan dominar la encuesta. Usa la misma cantidad en ambos campos para asegurarte de que cada voto tenga el mismo valor. Déjalos vacíos para aceptar cualquier cantidad.</string>
|
||||
<string name="error_dialog_zap_error">No se pudo enviar el zap</string>
|
||||
<string name="error_dialog_talk_to_user">Mensaje para el usuario</string>
|
||||
<string name="error_dialog_button_ok">OK</string>
|
||||
<string name="relay_information_document_error_assemble_url">No se pudo llegar a %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_failed_to_assemble_url">Error al ensamblar la url NIP-11 para %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_failed_to_reach_server">No se pudo contactar %1$s: %2$s</string>
|
||||
@ -507,6 +537,8 @@
|
||||
<string name="active_for">Activo para: </string>
|
||||
<string name="active_for_home">Inicio</string>
|
||||
<string name="active_for_msg">Mensajes directos</string>
|
||||
<string name="active_for_chats">Chats</string>
|
||||
<string name="active_for_global">Global</string>
|
||||
<string name="active_for_search">Búsqueda</string>
|
||||
<string name="zap_split_title">Dividir y reenviar zaps</string>
|
||||
<string name="zap_split_explainer">Los clientes compatibles dividirán y reenviarán los zaps a los usuarios que se agreguen aquí en lugar de a ti.</string>
|
||||
@ -515,6 +547,7 @@
|
||||
<string name="missing_lud16">Lightning no está configurado</string>
|
||||
<string name="user_x_does_not_have_a_lightning_address_setup_to_receive_sats">El usuario %1$s no tiene una dirección de Lightning configurada para recibir sats.</string>
|
||||
<string name="zap_split_weight">Porcentaje</string>
|
||||
<string name="zap_split_weight_placeholder">25</string>
|
||||
<string name="splitting_zaps_with">Dividir zaps con</string>
|
||||
<string name="forwarding_zaps_to">Reenviar zaps a</string>
|
||||
<string name="lightning_wallets_not_found2">No se encontraron billeteras de Lightning</string>
|
||||
@ -581,9 +614,11 @@
|
||||
<string name="hi_there_is_this_still_available">Hola, ¿esto todavía está disponible?</string>
|
||||
<string name="classifieds">Vender un artículo</string>
|
||||
<string name="classifieds_title">Título</string>
|
||||
<string name="classifieds_title_placeholder">iPhone 13</string>
|
||||
<string name="classifieds_condition">Estado</string>
|
||||
<string name="classifieds_category">Categoría</string>
|
||||
<string name="classifieds_price">Precio (en sats)</string>
|
||||
<string name="classifieds_price_placeholder">1000</string>
|
||||
<string name="classifieds_location">Ubicación</string>
|
||||
<string name="classifieds_location_placeholder">Ciudad, estado, país.</string>
|
||||
<string name="classifieds_condition_new">Nuevo</string>
|
||||
@ -617,6 +652,8 @@
|
||||
<string name="server_did_not_provide_a_url_after_uploading">El servidor no proporcionó una URL después de la subida</string>
|
||||
<string name="could_not_download_from_the_server">No se pudo descargar el contenido subido desde el servidor</string>
|
||||
<string name="could_not_prepare_local_file_to_upload">No se pudo preparar el archivo local para subir: %1$s</string>
|
||||
<string name="failed_to_upload_with_message">Error al subir: %1$s</string>
|
||||
<string name="failed_to_delete_with_message">Error al eliminar: %1$s</string>
|
||||
<string name="edit_draft">Editar borrador</string>
|
||||
<string name="login_with_qr_code">Iniciar sesión con código QR</string>
|
||||
<string name="route">Ruta</string>
|
||||
@ -625,6 +662,7 @@
|
||||
<string name="route_discover">Descubrir</string>
|
||||
<string name="route_messages">Mensajes</string>
|
||||
<string name="route_notifications">Notificaciones</string>
|
||||
<string name="route_global">Global</string>
|
||||
<string name="route_video">Cortos</string>
|
||||
<string name="route_security_filters">Filtros de seguridad</string>
|
||||
<string name="new_post">Nueva publicación</string>
|
||||
@ -635,6 +673,8 @@
|
||||
<string name="reply_description">Responder</string>
|
||||
<string name="boost_or_quote_description">Impulsar o citar</string>
|
||||
<string name="like_description">Me gusta</string>
|
||||
<string name="zap_description">Zap</string>
|
||||
<string name="change_reaction">Cambiar reacciones rápidas</string>
|
||||
<string name="profile_image_of_user">Imagen de perfil de %1$s</string>
|
||||
<string name="relay_info">Relé %1$s</string>
|
||||
<string name="expand_relay_list">Ampliar lista de relés</string>
|
||||
@ -645,6 +685,7 @@
|
||||
<string name="add_bitcoin_invoice">Factura de Bitcoin</string>
|
||||
<string name="cancel_bitcoin_invoice">Cancelar factura de Bitcoin</string>
|
||||
<string name="cancel_classifieds">Cancelar la venta de un artículo</string>
|
||||
<string name="add_zapraiser">Recaudación de zaps</string>
|
||||
<string name="cancel_zapraiser">Cancelar zapraiser</string>
|
||||
<string name="add_location">Ubicación</string>
|
||||
<string name="remove_location">Eliminar ubicación</string>
|
||||
@ -695,7 +736,9 @@
|
||||
<string name="forked_from">Bifurcación de</string>
|
||||
<string name="forked_tag">BIFURCACIÓN</string>
|
||||
<string name="git_repository">Repositorio Git: %1$s</string>
|
||||
<string name="git_web_address">Web:</string>
|
||||
<string name="git_clone_address">Clon:</string>
|
||||
<string name="existed_since">OTS: %1$s</string>
|
||||
<string name="ots_info_title">Prueba de marca de tiempo</string>
|
||||
<string name="ots_info_description">Hay prueba de que esta publicación se firmó en algún momento antes de %1$s. La prueba se marcó en la cadena de bloques de Bitcoin en esa fecha y hora.</string>
|
||||
<string name="edit_post">Editar publicación</string>
|
||||
@ -723,4 +766,34 @@
|
||||
<string name="dvm_requesting_job">Solicitando trabajo a DVM</string>
|
||||
<string name="nwc_payment_request">Solicitud de pago enviada, esperando confirmación de tu billetera</string>
|
||||
<string name="dvm_waiting_to_confirm_payment">Esperando a que DVM confirme el pago o envíe resultados</string>
|
||||
<string name="http_status_400">Solicitud incorrecta: el servidor no puede procesar o no procesará la solicitud.</string>
|
||||
<string name="http_status_401">No autorizado: el usuario no tiene credenciales de autenticación válidas.</string>
|
||||
<string name="http_status_402">Pago requerido: el servidor requiere pago para completar la solicitud.</string>
|
||||
<string name="http_status_403">Prohibido: el usuario no tiene derechos de acceso para realizar la solicitud.</string>
|
||||
<string name="http_status_404">No se encontró: el servidor no puede encontrar la dirección solicitada.</string>
|
||||
<string name="http_status_405">Método no permitido: el servidor admite el método de solicitud, pero no el recurso de destino.</string>
|
||||
<string name="http_status_406">No aceptable: el servidor no encuentra ningún contenido que satisfaga la solicitud.</string>
|
||||
<string name="http_status_407">Autenticación de proxy requerida: el usuario no tiene credenciales de autenticación válidas.</string>
|
||||
<string name="http_status_408">Tiempo de espera de la solicitud: se agotó el tiempo de espera del servidor.</string>
|
||||
<string name="http_status_409">Conflicto: el servidor no puede satisfacer la solicitud porque hay un conflicto con el recurso.</string>
|
||||
<string name="http_status_410">Borrado: el contenido solicitado se eliminó permanentemente del servidor y no se restablecerá.</string>
|
||||
<string name="http_status_411">Longitud requerida: el servidor rechaza la solicitud porque requiere una longitud definida.</string>
|
||||
<string name="http_status_412">Precondición fallida: las precondiciones de la solicitud en los campos de cabecera que el servidor no cumple.</string>
|
||||
<string name="http_status_413">Carga demasiado grande: la solicitud supera los límites definidos por el servidor, por lo que este se niega a procesarla.</string>
|
||||
<string name="http_status_414">URI demasiado largo: la URL solicitada por el cliente es demasiado larga para que el servidor pueda procesarla. </string>
|
||||
<string name="http_status_415">Tipo de contenido multimedia no admitido: la solicitud usa un formato multimedia que el servidor no admite.</string>
|
||||
<string name="http_status_416">Intervalo no satisfactorio: el servidor no puede satisfacer el valor indicado en el campo de cabecera del intervalo de la solicitud. </string>
|
||||
<string name="http_status_417">Expectativa fallida: el servidor no puede cumplir los requisitos indicados por el campo de cabecera de la solicitud Expect.</string>
|
||||
<string name="http_status_426">Actualización requerida: el servidor se niega a procesar la solicitud utilizando el protocolo actual a menos que el cliente se actualice a un protocolo diferente.</string>
|
||||
<string name="http_status_500">Error interno del servidor: el servidor encontró un error inesperado y no puede completar la solicitud.</string>
|
||||
<string name="http_status_501">No implementado: el servidor no puede completar la solicitud o no reconoce el método de solicitud.</string>
|
||||
<string name="http_status_502">Gateway incorrecto: el servidor actúa como gateway y obtuvo una respuesta no válida del host.</string>
|
||||
<string name="http_status_503">Servicio no disponible: esto ocurre a menudo cuando un servidor está sobrecargado o caído por mantenimiento.</string>
|
||||
<string name="http_status_504">Tiempo de espera del gateway: el servidor estaba actuando como gateway o proxy y se agotó el tiempo de espera de una respuesta.</string>
|
||||
<string name="http_status_505">Versión HTTP no admitida: el servidor no admite la versión HTTP de la solicitud.</string>
|
||||
<string name="http_status_506">La variante también negocia: el servidor tiene un error de configuración interno.</string>
|
||||
<string name="http_status_507">Almacenamiento insuficiente: el servidor no dispone de almacenamiento suficiente para procesar correctamente la solicitud.</string>
|
||||
<string name="http_status_508">Bucle detectado: el servidor detecta un bucle infinito al procesar la solicitud.</string>
|
||||
<string name="http_status_511">Autenticación de red requerida: el cliente debe estar autenticado para acceder a la red.</string>
|
||||
<string name="add_a_nip96_server">Agregar servidor NIP-96</string>
|
||||
</resources>
|
||||
|
@ -14,6 +14,7 @@
|
||||
<string name="could_not_decrypt_the_message">No se pudo desencriptar el mensaje</string>
|
||||
<string name="group_picture">Imagen del grupo</string>
|
||||
<string name="explicit_content">Contenido explícito</string>
|
||||
<string name="spam">Spam</string>
|
||||
<string name="spam_description">El número de eventos de spam procedentes de este relé</string>
|
||||
<string name="impersonation">Suplantación de identidad</string>
|
||||
<string name="illegal_behavior">Comportamiento ilegal</string>
|
||||
@ -35,6 +36,7 @@
|
||||
<string name="report_impersonation">Reportar suplantación de identidad</string>
|
||||
<string name="report_explicit_content">Reportar contenido explícito</string>
|
||||
<string name="report_illegal_behaviour">Reportar comportamiento ilegal</string>
|
||||
<string name="report_malware">Reportar malware</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_reply">Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para poder responder</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_boost_posts">Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para poder impulsar publicaciones</string>
|
||||
<string name="login_with_a_private_key_to_like_posts">Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para indicar que te gustan las publicaciones.</string>
|
||||
@ -44,11 +46,13 @@
|
||||
<string name="login_with_a_private_key_to_be_able_to_unfollow">Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para poder dejar de seguir a otros usuarios.</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_hide_word">Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para poder ocultar una palabra o frase.</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_show_word">Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para poder mostrar una palabra o frase.</string>
|
||||
<string name="zaps">Zaps</string>
|
||||
<string name="view_count">Total vistas</string>
|
||||
<string name="boost">Impulsar</string>
|
||||
<string name="boosted">Impulsada</string>
|
||||
<string name="edited">editada</string>
|
||||
<string name="edited_number">edición #%1$s</string>
|
||||
<string name="original">original</string>
|
||||
<string name="quote">Cita</string>
|
||||
<string name="fork">Bifurcar</string>
|
||||
<string name="propose_an_edit">Proponer una edición</string>
|
||||
@ -89,6 +93,7 @@
|
||||
<string name="failed_to_upload_the_image">Error al subir la imagen</string>
|
||||
<string name="relay_address">Dirección del relé</string>
|
||||
<string name="posts">Publicaciones</string>
|
||||
<string name="bytes">Bytes</string>
|
||||
<string name="errors">Errores</string>
|
||||
<string name="errors_description">El número de errores de conexión en esta sesión</string>
|
||||
<string name="home_feed">Tus noticias</string>
|
||||
@ -195,6 +200,9 @@
|
||||
<string name="nip_05">Dirección de Nostr</string>
|
||||
<string name="never">nunca</string>
|
||||
<string name="now">ahora</string>
|
||||
<string name="h">h</string>
|
||||
<string name="m">m</string>
|
||||
<string name="d">d</string>
|
||||
<string name="nudity">Desnudez</string>
|
||||
<string name="profanity_hateful_speech">Groserías o lenguaje que incita al odio</string>
|
||||
<string name="report_hateful_speech">Reportar lenguaje que incita al odio</string>
|
||||
@ -220,6 +228,7 @@
|
||||
<string name="biometric_authentication_failed">Error de autenticación</string>
|
||||
<string name="biometric_authentication_failed_explainer">El sistema biométrico no pudo autenticar al propietario de este teléfono</string>
|
||||
<string name="biometric_authentication_failed_explainer_with_error">El sistema biométrico no pudo autenticar al propietario de este teléfono. Error: %1$s</string>
|
||||
<string name="biometric_error">Error</string>
|
||||
<string name="badge_created_by">"Creado por %1$s"</string>
|
||||
<string name="badge_award_image_for">"Imagen de premio de insignia por %1$s"</string>
|
||||
<string name="new_badge_award_notif">Recibiste un nuevo premio de insignia</string>
|
||||
@ -259,6 +268,7 @@
|
||||
<string name="report_dialog_impersonation">Suplantación de identidad malintencionada</string>
|
||||
<string name="report_dialog_nudity">Desnudez o contenido gráfico</string>
|
||||
<string name="report_dialog_illegal">Comportamiento ilegal</string>
|
||||
<string name="report_dialog_malware">Malware</string>
|
||||
<string name="report_dialog_blocking_a_user">Si bloqueas a un usuario, se ocultará su contenido en tu app. Tus notas todavía son visibles públicamente, incluso para las personas que bloquees. Los usuarios bloqueados aparecen en la pantalla \"Filtros de seguridad\".</string>
|
||||
<string name="report_dialog_block_hide_user_btn"><![CDATA[Bloquear y ocultar usuario]]></string>
|
||||
<string name="report_dialog_report_btn">Reportar abuso</string>
|
||||
@ -296,6 +306,7 @@
|
||||
<string name="poll_zap_value_min">Zap mínimo</string>
|
||||
<string name="poll_zap_value_max">Zap máximo</string>
|
||||
<string name="poll_consensus_threshold">Consenso</string>
|
||||
<string name="poll_consensus_threshold_percent">(0–100)%</string>
|
||||
<string name="poll_closing_time">Cerrar después de</string>
|
||||
<string name="poll_closing_time_days">días</string>
|
||||
<string name="poll_unable_to_vote">No se puede votar</string>
|
||||
@ -326,9 +337,18 @@
|
||||
<string name="zap_type_private_explainer">El remitente y el receptor pueden verse entre sí y leer el mensaje</string>
|
||||
<string name="zap_type_anonymous">Anónimo</string>
|
||||
<string name="zap_type_anonymous_explainer">El receptor y el público no saben quién envió el pago</string>
|
||||
<string name="zap_type_nonzap">No zap</string>
|
||||
<string name="zap_type_nonzap_explainer">No hay rastro en Nostr, solo en Lightning</string>
|
||||
<string name="file_server">Servidor de archivos</string>
|
||||
<string name="zap_forward_lnAddress">Dirección o @usuario de Lightning</string>
|
||||
<string name="media_servers">Servidores multimedia</string>
|
||||
<string name="set_preferred_media_servers">Configura tus servidores preferidos para subir contenido multimedia.</string>
|
||||
<string name="no_media_server_message">No tienes ningún servidor multimedia personalizado. Puedes utilizar la lista de Amethyst, o agregar uno de la lista a continuación ↓</string>
|
||||
<string name="built_in_media_servers_title">Servidores multimedia integrados</string>
|
||||
<string name="built_in_servers_description">Lista predeterminada de Amethyst. Puedes agregarlos individualmente o añadir la lista.</string>
|
||||
<string name="use_default_servers">Usar lista predeterminada</string>
|
||||
<string name="add_media_server">Agregar servidor multimedia</string>
|
||||
<string name="delete_media_server">Eliminar servidor multimedia</string>
|
||||
<string name="upload_server_relays_nip95">Tus relés (NIP-95)</string>
|
||||
<string name="upload_server_relays_nip95_explainer">Los archivos están alojados en tus relés. Nuevo NIP: comprueba si son compatibles.</string>
|
||||
<string name="connect_via_tor_short">Configuración de Tor/Orbot</string>
|
||||
@ -336,8 +356,10 @@
|
||||
<string name="do_you_really_want_to_disable_tor_title">¿Desconectarse de tu Orbot/Tor?</string>
|
||||
<string name="do_you_really_want_to_disable_tor_text">Los datos se transferirán de inmediato en la red normal</string>
|
||||
<string name="yes">Sí</string>
|
||||
<string name="no">No</string>
|
||||
<string name="follow_list_selection">Lista de seguidos</string>
|
||||
<string name="follow_list_kind3follows">Todos los seguidos</string>
|
||||
<string name="follow_list_global">Global</string>
|
||||
<string name="follow_list_mute_list">Lista de silenciados</string>
|
||||
<string name="connect_through_your_orbot_setup_markdown"> ## Conéctate a través de Tor con Orbot
|
||||
\n\n1. Instala [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android).
|
||||
@ -355,6 +377,7 @@
|
||||
<string name="app_notification_dms_channel_description">Te notifica cuando llega un mensaje privado</string>
|
||||
<string name="app_notification_zaps_channel_name">Zaps recibidos</string>
|
||||
<string name="app_notification_zaps_channel_description">Te notifica cuando alguien te zapea</string>
|
||||
<string name="app_notification_zaps_channel_message">%1$s sats</string>
|
||||
<string name="app_notification_zaps_channel_message_from">De %1$s</string>
|
||||
<string name="app_notification_zaps_channel_message_for">por %1$s</string>
|
||||
<string name="reply_notify">Notificar: </string>
|
||||
@ -374,6 +397,7 @@
|
||||
<string name="warn_when_posts_have_reports_from_your_follows">Avisar cuando las publicaciones tengan reportes de tus seguidos</string>
|
||||
<string name="new_reaction_symbol">Nuevo símbolo de reacción</string>
|
||||
<string name="no_reaction_type_setup_long_press_to_change">No hay tipos de reacción preseleccionados para este usuario. Deja presionado el botón de corazón para cambiarlos.</string>
|
||||
<string name="zapraiser">Recaudación de zaps</string>
|
||||
<string name="zapraiser_explainer">Agrega una meta de sats para recaudar por esta publicación. Los clientes compatibles pueden mostrarla como una barra de progreso para incentivar las donaciones.</string>
|
||||
<string name="zapraiser_target_amount_in_sats">Cantidad objetivo en sats</string>
|
||||
<string name="sats_to_complete">Zapraiser en %1$s. %2$s sats hasta la meta</string>
|
||||
@ -384,6 +408,7 @@
|
||||
<string name="an_error_occurred_trying_to_get_relay_information">Se produjo un error al intentar obtener información sobre el relé de %1$s</string>
|
||||
<string name="owner">Propietario</string>
|
||||
<string name="version">Versión</string>
|
||||
<string name="software">Software</string>
|
||||
<string name="contact">Contacto</string>
|
||||
<string name="supports">NIP compatibles</string>
|
||||
<string name="admission_fees">Comisiones de admisión</string>
|
||||
@ -402,6 +427,7 @@
|
||||
<string name="maximum_event_tags">Etiquetas de evento máximas</string>
|
||||
<string name="content_length">Longitud del contenido</string>
|
||||
<string name="minimum_pow">PoW mínima</string>
|
||||
<string name="auth">Autenticación</string>
|
||||
<string name="payment">Pago</string>
|
||||
<string name="cashu">Token de Cashu</string>
|
||||
<string name="cashu_redeem">Canjear</string>
|
||||
@ -423,6 +449,7 @@
|
||||
<string name="discover_marketplace">Mercado</string>
|
||||
<string name="discover_live">En vivo</string>
|
||||
<string name="discover_community">Comunidad</string>
|
||||
<string name="discover_chat">Chats</string>
|
||||
<string name="community_approved_posts">Publicaciones aprobadas</string>
|
||||
<string name="groups_no_descriptor">Este grupo no tiene descripción ni reglas. Habla con el propietario para agregar una.</string>
|
||||
<string name="community_no_descriptor">Esta comunidad no tiene descripción. Habla con el propietario para agregar una.</string>
|
||||
@ -431,6 +458,7 @@
|
||||
<string name="settings">Configuración</string>
|
||||
<string name="connectivity_type_always">Siempre</string>
|
||||
<string name="connectivity_type_wifi_only">Solo Wi-Fi</string>
|
||||
<string name="connectivity_type_unmetered_wifi_only">WiFi sin medición</string>
|
||||
<string name="connectivity_type_never">Nunca</string>
|
||||
<string name="ui_feature_set_type_complete">Completo</string>
|
||||
<string name="ui_feature_set_type_simplified">Simplificado</string>
|
||||
@ -449,6 +477,7 @@
|
||||
<string name="ui_style">Modo de interfaz</string>
|
||||
<string name="ui_style_description">Elegir el estilo de publicación</string>
|
||||
<string name="load_image">Cargar imagen</string>
|
||||
<string name="spamming_users">Spammers</string>
|
||||
<string name="muted_button">Silenciado. Hacer clic para reactivar el sonido.</string>
|
||||
<string name="mute_button">Sonido activado. Hacer clic para silenciar.</string>
|
||||
<string name="search_button">Buscar grabaciones locales y remotas</string>
|
||||
@ -496,6 +525,7 @@
|
||||
<string name="poll_zap_value_min_max_explainer">Los votos se calculan según la cantidad de zaps. Puedes establecer una cantidad mínima para evitar spammers y una máxima para que los zappers grandes no puedan dominar la encuesta. Usa la misma cantidad en ambos campos para asegurarte de que cada voto tenga el mismo valor. Déjalos vacíos para aceptar cualquier cantidad.</string>
|
||||
<string name="error_dialog_zap_error">No se pudo enviar el zap</string>
|
||||
<string name="error_dialog_talk_to_user">Mensaje para el usuario</string>
|
||||
<string name="error_dialog_button_ok">OK</string>
|
||||
<string name="relay_information_document_error_assemble_url">No se pudo llegar a %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_failed_to_assemble_url">Error al ensamblar la url NIP-11 para %1$s: %2$s</string>
|
||||
<string name="relay_information_document_error_failed_to_reach_server">No se pudo contactar %1$s: %2$s</string>
|
||||
@ -507,6 +537,8 @@
|
||||
<string name="active_for">Activo para: </string>
|
||||
<string name="active_for_home">Inicio</string>
|
||||
<string name="active_for_msg">Mensajes directos</string>
|
||||
<string name="active_for_chats">Chats</string>
|
||||
<string name="active_for_global">Global</string>
|
||||
<string name="active_for_search">Búsqueda</string>
|
||||
<string name="zap_split_title">Dividir y reenviar zaps</string>
|
||||
<string name="zap_split_explainer">Los clientes compatibles dividirán y reenviarán los zaps a los usuarios que se agreguen aquí en lugar de a ti.</string>
|
||||
@ -515,6 +547,7 @@
|
||||
<string name="missing_lud16">Lightning no está configurado</string>
|
||||
<string name="user_x_does_not_have_a_lightning_address_setup_to_receive_sats">El usuario %1$s no tiene una dirección de Lightning configurada para recibir sats.</string>
|
||||
<string name="zap_split_weight">Porcentaje</string>
|
||||
<string name="zap_split_weight_placeholder">25</string>
|
||||
<string name="splitting_zaps_with">Dividir zaps con</string>
|
||||
<string name="forwarding_zaps_to">Reenviar zaps a</string>
|
||||
<string name="lightning_wallets_not_found2">No se encontraron billeteras de Lightning</string>
|
||||
@ -581,9 +614,11 @@
|
||||
<string name="hi_there_is_this_still_available">Hola, ¿esto todavía está disponible?</string>
|
||||
<string name="classifieds">Vender un artículo</string>
|
||||
<string name="classifieds_title">Título</string>
|
||||
<string name="classifieds_title_placeholder">iPhone 13</string>
|
||||
<string name="classifieds_condition">Estado</string>
|
||||
<string name="classifieds_category">Categoría</string>
|
||||
<string name="classifieds_price">Precio (en sats)</string>
|
||||
<string name="classifieds_price_placeholder">1000</string>
|
||||
<string name="classifieds_location">Ubicación</string>
|
||||
<string name="classifieds_location_placeholder">Ciudad, estado, país.</string>
|
||||
<string name="classifieds_condition_new">Nuevo</string>
|
||||
@ -617,6 +652,8 @@
|
||||
<string name="server_did_not_provide_a_url_after_uploading">El servidor no proporcionó una URL después de la subida</string>
|
||||
<string name="could_not_download_from_the_server">No se pudo descargar el contenido subido desde el servidor</string>
|
||||
<string name="could_not_prepare_local_file_to_upload">No se pudo preparar el archivo local para subir: %1$s</string>
|
||||
<string name="failed_to_upload_with_message">Error al subir: %1$s</string>
|
||||
<string name="failed_to_delete_with_message">Error al eliminar: %1$s</string>
|
||||
<string name="edit_draft">Editar borrador</string>
|
||||
<string name="login_with_qr_code">Iniciar sesión con código QR</string>
|
||||
<string name="route">Ruta</string>
|
||||
@ -625,6 +662,7 @@
|
||||
<string name="route_discover">Descubrir</string>
|
||||
<string name="route_messages">Mensajes</string>
|
||||
<string name="route_notifications">Notificaciones</string>
|
||||
<string name="route_global">Global</string>
|
||||
<string name="route_video">Cortos</string>
|
||||
<string name="route_security_filters">Filtros de seguridad</string>
|
||||
<string name="new_post">Nueva publicación</string>
|
||||
@ -635,6 +673,8 @@
|
||||
<string name="reply_description">Responder</string>
|
||||
<string name="boost_or_quote_description">Impulsar o citar</string>
|
||||
<string name="like_description">Me gusta</string>
|
||||
<string name="zap_description">Zap</string>
|
||||
<string name="change_reaction">Cambiar reacciones rápidas</string>
|
||||
<string name="profile_image_of_user">Imagen de perfil de %1$s</string>
|
||||
<string name="relay_info">Relé %1$s</string>
|
||||
<string name="expand_relay_list">Ampliar lista de relés</string>
|
||||
@ -645,6 +685,7 @@
|
||||
<string name="add_bitcoin_invoice">Factura de Bitcoin</string>
|
||||
<string name="cancel_bitcoin_invoice">Cancelar factura de Bitcoin</string>
|
||||
<string name="cancel_classifieds">Cancelar la venta de un artículo</string>
|
||||
<string name="add_zapraiser">Recaudación de zaps</string>
|
||||
<string name="cancel_zapraiser">Cancelar zapraiser</string>
|
||||
<string name="add_location">Ubicación</string>
|
||||
<string name="remove_location">Eliminar ubicación</string>
|
||||
@ -695,7 +736,9 @@
|
||||
<string name="forked_from">Bifurcación de</string>
|
||||
<string name="forked_tag">BIFURCACIÓN</string>
|
||||
<string name="git_repository">Repositorio Git: %1$s</string>
|
||||
<string name="git_web_address">Web:</string>
|
||||
<string name="git_clone_address">Clon:</string>
|
||||
<string name="existed_since">OTS: %1$s</string>
|
||||
<string name="ots_info_title">Prueba de marca de tiempo</string>
|
||||
<string name="ots_info_description">Hay prueba de que esta publicación se firmó en algún momento antes de %1$s. La prueba se marcó en la cadena de bloques de Bitcoin en esa fecha y hora.</string>
|
||||
<string name="edit_post">Editar publicación</string>
|
||||
@ -723,4 +766,34 @@
|
||||
<string name="dvm_requesting_job">Solicitando trabajo a DVM</string>
|
||||
<string name="nwc_payment_request">Solicitud de pago enviada, esperando confirmación de tu billetera</string>
|
||||
<string name="dvm_waiting_to_confirm_payment">Esperando a que DVM confirme el pago o envíe resultados</string>
|
||||
<string name="http_status_400">Solicitud incorrecta: el servidor no puede procesar o no procesará la solicitud.</string>
|
||||
<string name="http_status_401">No autorizado: el usuario no tiene credenciales de autenticación válidas.</string>
|
||||
<string name="http_status_402">Pago requerido: el servidor requiere pago para completar la solicitud.</string>
|
||||
<string name="http_status_403">Prohibido: el usuario no tiene derechos de acceso para realizar la solicitud.</string>
|
||||
<string name="http_status_404">No se encontró: el servidor no puede encontrar la dirección solicitada.</string>
|
||||
<string name="http_status_405">Método no permitido: el servidor admite el método de solicitud, pero no el recurso de destino.</string>
|
||||
<string name="http_status_406">No aceptable: el servidor no encuentra ningún contenido que satisfaga la solicitud.</string>
|
||||
<string name="http_status_407">Autenticación de proxy requerida: el usuario no tiene credenciales de autenticación válidas.</string>
|
||||
<string name="http_status_408">Tiempo de espera de la solicitud: se agotó el tiempo de espera del servidor.</string>
|
||||
<string name="http_status_409">Conflicto: el servidor no puede satisfacer la solicitud porque hay un conflicto con el recurso.</string>
|
||||
<string name="http_status_410">Borrado: el contenido solicitado se eliminó permanentemente del servidor y no se restablecerá.</string>
|
||||
<string name="http_status_411">Longitud requerida: el servidor rechaza la solicitud porque requiere una longitud definida.</string>
|
||||
<string name="http_status_412">Precondición fallida: las precondiciones de la solicitud en los campos de cabecera que el servidor no cumple.</string>
|
||||
<string name="http_status_413">Carga demasiado grande: la solicitud supera los límites definidos por el servidor, por lo que este se niega a procesarla.</string>
|
||||
<string name="http_status_414">URI demasiado largo: la URL solicitada por el cliente es demasiado larga para que el servidor pueda procesarla. </string>
|
||||
<string name="http_status_415">Tipo de contenido multimedia no admitido: la solicitud usa un formato multimedia que el servidor no admite.</string>
|
||||
<string name="http_status_416">Intervalo no satisfactorio: el servidor no puede satisfacer el valor indicado en el campo de cabecera del intervalo de la solicitud. </string>
|
||||
<string name="http_status_417">Expectativa fallida: el servidor no puede cumplir los requisitos indicados por el campo de cabecera de la solicitud Expect.</string>
|
||||
<string name="http_status_426">Actualización requerida: el servidor se niega a procesar la solicitud utilizando el protocolo actual a menos que el cliente se actualice a un protocolo diferente.</string>
|
||||
<string name="http_status_500">Error interno del servidor: el servidor encontró un error inesperado y no puede completar la solicitud.</string>
|
||||
<string name="http_status_501">No implementado: el servidor no puede completar la solicitud o no reconoce el método de solicitud.</string>
|
||||
<string name="http_status_502">Gateway incorrecto: el servidor actúa como gateway y obtuvo una respuesta no válida del host.</string>
|
||||
<string name="http_status_503">Servicio no disponible: esto ocurre a menudo cuando un servidor está sobrecargado o caído por mantenimiento.</string>
|
||||
<string name="http_status_504">Tiempo de espera del gateway: el servidor estaba actuando como gateway o proxy y se agotó el tiempo de espera de una respuesta.</string>
|
||||
<string name="http_status_505">Versión HTTP no admitida: el servidor no admite la versión HTTP de la solicitud.</string>
|
||||
<string name="http_status_506">La variante también negocia: el servidor tiene un error de configuración interno.</string>
|
||||
<string name="http_status_507">Almacenamiento insuficiente: el servidor no dispone de almacenamiento suficiente para procesar correctamente la solicitud.</string>
|
||||
<string name="http_status_508">Bucle detectado: el servidor detecta un bucle infinito al procesar la solicitud.</string>
|
||||
<string name="http_status_511">Autenticación de red requerida: el cliente debe estar autenticado para acceder a la red.</string>
|
||||
<string name="add_a_nip96_server">Agregar servidor NIP-96</string>
|
||||
</resources>
|
||||
|
@ -36,6 +36,7 @@
|
||||
<string name="report_impersonation">Signaler une falsification d\'identité</string>
|
||||
<string name="report_explicit_content">Signaler un contenu choquant</string>
|
||||
<string name="report_illegal_behaviour">Signaler un comportement illégal</string>
|
||||
<string name="report_malware">Signaler un programme malveillant</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_reply">Connectez vous avec une clé privée pour pouvoir répondre</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_boost_posts">Connectez vous avec une clé privée pour pouvoir booster des posts</string>
|
||||
<string name="login_with_a_private_key_to_like_posts">Connectez vous avec une clé privée pour pouvoir aimer des posts</string>
|
||||
@ -267,6 +268,7 @@
|
||||
<string name="report_dialog_impersonation">Usurpation d\'identité</string>
|
||||
<string name="report_dialog_nudity">Nudité ou contenu graphique</string>
|
||||
<string name="report_dialog_illegal">Comportement illégal</string>
|
||||
<string name="report_dialog_malware">Programme malveillant</string>
|
||||
<string name="report_dialog_blocking_a_user">Bloquer un utilisateur cachera son contenu dans votre application. Vos notes sont toujours visibles publiquement, y compris pour les personnes que vous bloquez. Les utilisateurs bloqués sont listés sur l\'écran Filtres de Sécurité.</string>
|
||||
<string name="report_dialog_block_hide_user_btn"><![CDATA[Bloquer et Masquer l\'Utilisateur]]></string>
|
||||
<string name="report_dialog_report_btn">Signaler un Abus</string>
|
||||
@ -339,6 +341,14 @@
|
||||
<string name="zap_type_nonzap_explainer">Aucune trace sur Nostr, seulement sur le Lightning</string>
|
||||
<string name="file_server">Serveur fichier</string>
|
||||
<string name="zap_forward_lnAddress">Adresse LN ou @Utilisateur</string>
|
||||
<string name="media_servers">Serveurs média</string>
|
||||
<string name="set_preferred_media_servers">Définissez vos serveurs de téléversement de média préférés.</string>
|
||||
<string name="no_media_server_message">Vous n\'avez pas de serveur média personnalisé. Vous pouvez utiliser la liste d\'Amethyst, ou en ajouter un ci-dessous</string>
|
||||
<string name="built_in_media_servers_title">Serveurs média intégrés</string>
|
||||
<string name="built_in_servers_description">Liste par défaut d\'Amethyst. Vous pouvez les ajouter individuellement ou ajouter la liste.</string>
|
||||
<string name="use_default_servers">Utiliser la liste par défaut</string>
|
||||
<string name="add_media_server">Ajouter un serveur média</string>
|
||||
<string name="delete_media_server">Supprimer le serveur média</string>
|
||||
<string name="upload_server_relays_nip95">Vos relais (NIP-95)</string>
|
||||
<string name="upload_server_relays_nip95_explainer">Les fichiers sont hébergés par vos relais. Nouveau NIP: vérifiez s\'ils sont supportés</string>
|
||||
<string name="connect_via_tor_short">Configuration Tor/Orbot</string>
|
||||
@ -642,6 +652,8 @@
|
||||
<string name="server_did_not_provide_a_url_after_uploading">Le serveur n\'a pas fourni d\'URL après le téléversement</string>
|
||||
<string name="could_not_download_from_the_server">Impossible de télécharger le média depuis le serveur</string>
|
||||
<string name="could_not_prepare_local_file_to_upload">Impossible de préparer le fichier local à téléverser: %1$s</string>
|
||||
<string name="failed_to_upload_with_message">Échec du téléversement: %1$s</string>
|
||||
<string name="failed_to_delete_with_message">Échec de la suppression : %1$s</string>
|
||||
<string name="edit_draft">Modifier le brouillon</string>
|
||||
<string name="login_with_qr_code">Se connecter avec un QR Code</string>
|
||||
<string name="route">Route</string>
|
||||
@ -754,4 +766,5 @@
|
||||
<string name="dvm_requesting_job">Tâche demandée par DVM</string>
|
||||
<string name="nwc_payment_request">Demande de paiement envoyée, en attente de confirmation depuis votre portefeuille</string>
|
||||
<string name="dvm_waiting_to_confirm_payment">En attente que DVM confirme le paiement ou envoie les résultats</string>
|
||||
<string name="add_a_nip96_server">Ajouter un serveur NIP-96</string>
|
||||
</resources>
|
||||
|
@ -36,6 +36,7 @@
|
||||
<string name="report_impersonation">पररूपण की सूचना दें</string>
|
||||
<string name="report_explicit_content">अभद्र विषयवस्तु की सूचना दें</string>
|
||||
<string name="report_illegal_behaviour">अवैध बरताव की सूचना दें</string>
|
||||
<string name="report_malware">दुष्क्रमक की सूचना दें</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_reply">आप ख्याप्य कुंचिका का प्रयोग कर रहे हैं जिससे केवल पढ सकते हैं। उत्तर देने के लिए गुप्त कुंचिका के साथ प्रवेशांकन करें</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_boost_posts">आप ख्याप्य कुंचिका का प्रयोग कर रहे हैं जिससे केवल पढ सकते हैं। प्रकाशित पत्रों को उद्धृत करने के लिए गुप्त कुंचिका के साथ प्रवेशांकन करें</string>
|
||||
<string name="login_with_a_private_key_to_like_posts">आप ख्याप्य कुंचिका का प्रयोग कर रहे हैं जिससे केवल पढ सकते हैं। प्रकाशित पत्रों को चाहने के लिए गुप्त कुंचिका के साथ प्रवेशांकन करें</string>
|
||||
@ -267,6 +268,7 @@
|
||||
<string name="report_dialog_impersonation">दुर्भावनापूर्ण पररूपण</string>
|
||||
<string name="report_dialog_nudity">नग्नता अथवा आपत्तिजनक विषयवस्तु</string>
|
||||
<string name="report_dialog_illegal">अवैध बरताव</string>
|
||||
<string name="report_dialog_malware">दुष्क्रमक</string>
|
||||
<string name="report_dialog_blocking_a_user">उपयोगकर्ता को बाधित करने से उनका विषयवस्तु आपके लिए छिपाया जाएगा क्रमक में। आपके टीकाएँ सार्वजनिक रूप से दृश्य रहेंगे, उन जनों के लिए भी जिनहे आपने बाधित किया। बाधित उपयोगकर्ता सूचित हैं सुरक्षा छलनियाँ पटल पर।</string>
|
||||
<string name="report_dialog_block_hide_user_btn"><![CDATA[उपयोगकर्ता को बाधित करें तथा छिपाएँ]]></string>
|
||||
<string name="report_dialog_report_btn">दुर्व्यवहार की सूचना दें</string>
|
||||
@ -339,6 +341,14 @@
|
||||
<string name="zap_type_nonzap_explainer">नोस्ट्र में कोई पदचिह्न नहीं, केवल लैटनिंग पर</string>
|
||||
<string name="file_server">अभिलेख सेवासंगणक</string>
|
||||
<string name="zap_forward_lnAddress">लै॰जाल पता अथवा @उपयोगकर्ता</string>
|
||||
<string name="media_servers">प्रसारसंगणक</string>
|
||||
<string name="set_preferred_media_servers">आपके प्रसारसंगणक आद्यताएँ स्थापित करें।</string>
|
||||
<string name="no_media_server_message">आपका कोई विशिष्ट प्रसारसंगणक स्थापित नहीं। आप अमेथिस्ट की सूची का प्रयोग कर सकते हैं अथवा प्रसारसंगणक नीचे जोड सकते हैं ↓</string>
|
||||
<string name="built_in_media_servers_title">अन्तर्निहित प्रसारसंगणक</string>
|
||||
<string name="built_in_servers_description">अमेथिस्त की मूलविकल्प सूची। आप एक एक करके जोड सकते हैं अथवा सूची जोड सकते हैं।</string>
|
||||
<string name="use_default_servers">मूलविकल्प सूची का प्रयोग करें</string>
|
||||
<string name="add_media_server">प्रसारसंगणक जोडें</string>
|
||||
<string name="delete_media_server">प्रसारसंगणक मिटाएँ</string>
|
||||
<string name="upload_server_relays_nip95">आपके पुनःप्रसारक (निप॰-९५)</string>
|
||||
<string name="upload_server_relays_nip95_explainer">अभिलेख आपके पुनःप्रसारक द्वारा रखे जाते हैं। नया निप॰: जाँच करें यदि वे अवलम्बन करते हैं</string>
|
||||
<string name="connect_via_tor_short">टोर / ओर्बोट स्थापन</string>
|
||||
@ -785,4 +795,5 @@
|
||||
<string name="http_status_507">स्मृतिस्थान का अभाव - सेवासंगणक में पर्याप्त स्मृतिस्थान उपलब्ध नहीं अनुरोध पर सफलतापूर्वक काम करने के लिए</string>
|
||||
<string name="http_status_508">क्रमचक्र दृष्ट - सेवासंगणक को अनन्त क्रमचक्र का पता चला अनुरोध पर काम करते हुए</string>
|
||||
<string name="http_status_511">जाल प्रमाणीकरण आवश्यक - जाल उपलब्ध होने के लिए ग्राहक का प्रमाणीकरण अनिवार्य</string>
|
||||
<string name="add_a_nip96_server">निप॰-९६ सेवासंगणक जोडें</string>
|
||||
</resources>
|
||||
|
@ -36,6 +36,7 @@
|
||||
<string name="report_impersonation">Zgłoś podszywanie się</string>
|
||||
<string name="report_explicit_content">Zgłoś niedozwoloną zawartość</string>
|
||||
<string name="report_illegal_behaviour">Zgłoś antyspołeczne działania</string>
|
||||
<string name="report_malware">Zgłoś złośliwe oprogramowanie</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_reply">Używasz klucza publicznego, a klucze publiczne są tylko do odczytu. Zaloguj się za pomocą klucza prywatnego, aby móc odpowiedzieć</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_boost_posts">Używasz klucza publicznego, a klucze publiczne są tylko do odczytu. Zaloguj się za pomocą klucza prywatnego, aby zwiększyć liczbę postów</string>
|
||||
<string name="login_with_a_private_key_to_like_posts">Używasz klucza publicznego, a klucze publiczne są tylko do odczytu. Zaloguj się za pomocą klucza prywatnego, aby polubić posty</string>
|
||||
@ -267,6 +268,7 @@
|
||||
<string name="report_dialog_impersonation">Złośliwe podszywanie się</string>
|
||||
<string name="report_dialog_nudity">Nagość lub obraźliwa grafika</string>
|
||||
<string name="report_dialog_illegal">Antyspołeczne zachowanie</string>
|
||||
<string name="report_dialog_malware">Złośliwe oprogramowanie</string>
|
||||
<string name="report_dialog_blocking_a_user">Zablokowanie użytkownika ukryje jego zawartość w aplikacji. Twoje notatki są nadal widoczne publicznie, w tym dla osób, które blokujesz. Zablokowani użytkownicy są wymienieni na ekranie filtrów bezpieczeństwa.</string>
|
||||
<string name="report_dialog_block_hide_user_btn"><![CDATA[Zablokuj i ukryj użytkownika]]></string>
|
||||
<string name="report_dialog_report_btn">Zgłoś nadużycie</string>
|
||||
@ -339,6 +341,14 @@
|
||||
<string name="zap_type_nonzap_explainer">Brak śladu w Nostr, tylko w Lightning</string>
|
||||
<string name="file_server">Serwer Plików</string>
|
||||
<string name="zap_forward_lnAddress">LnAdres lub @Użytkownik</string>
|
||||
<string name="media_servers">Serwery Multimediów</string>
|
||||
<string name="set_preferred_media_servers">Ustaw swoje preferowane serwery przesyłania multimediów.</string>
|
||||
<string name="no_media_server_message">Nie masz ustawionych własnych serwerów multimediów. Możesz użyć listy Amethyst\'a lub dodać jeden poniżej ↓</string>
|
||||
<string name="built_in_media_servers_title">Wbudowane serwery multimediów</string>
|
||||
<string name="built_in_servers_description">Domyślna lista Amethyst-a. Możesz dodawać je pojedynczo lub dodać pełną listę.</string>
|
||||
<string name="use_default_servers">Użyj domyślnej listy</string>
|
||||
<string name="add_media_server">Dodaj serwer multimediów</string>
|
||||
<string name="delete_media_server">Usuń serwer multimediów</string>
|
||||
<string name="upload_server_relays_nip95">Twoje transmitery (NIP-95)</string>
|
||||
<string name="upload_server_relays_nip95_explainer">Pliki są przechowywane przez Twoje retransmitery. Nowy NIP: sprawdź, czy jest obsługiwany</string>
|
||||
<string name="connect_via_tor_short">Tor/Orbot - Ustawienia</string>
|
||||
@ -426,6 +436,8 @@
|
||||
<string name="cashu_copy_token">Skopiuj token</string>
|
||||
<string name="no_lightning_address_set">Nie ustawiono adresu Lightning</string>
|
||||
<string name="copied_token_to_clipboard">Skopiowano token do schowka</string>
|
||||
<string name="live_stream_live_tag">NA ŻYWO</string>
|
||||
<string name="live_stream_offline_tag">OFFLINE</string>
|
||||
<string name="live_stream_ended_tag">Zakończony</string>
|
||||
<string name="live_stream_planned_tag">ZAPLANOWANE</string>
|
||||
<string name="live_stream_is_offline">Transmisja wyłączona</string>
|
||||
@ -776,4 +788,5 @@
|
||||
<string name="http_status_507">Niewystarczająca pojemność - serwer nie ma wystarczającej pojemność, aby pomyślnie przetworzyć żądanie</string>
|
||||
<string name="http_status_508">Wykryta pętla - serwer wykrywa nieskończoną pętlę podczas przetwarzania żądania</string>
|
||||
<string name="http_status_511">Wymagane uwierzytelnienie sieciowe - Klient musi być uwierzytelniony aby uzyskać dostęp do sieci</string>
|
||||
<string name="add_a_nip96_server">Dodaj serwer NIP-96</string>
|
||||
</resources>
|
||||
|
@ -10,6 +10,7 @@
|
||||
<string name="post_was_hidden">This post was hidden because it mentions your hidden users or words</string>
|
||||
<string name="post_was_flagged_as_inappropriate_by">Post was muted or reported by</string>
|
||||
<string name="post_not_found">Event is loading or can\'t be found in your relay list</string>
|
||||
<string name="post_not_found_short">👀</string>
|
||||
<string name="channel_image">Channel Image</string>
|
||||
<string name="referenced_event_not_found">Referenced event not found</string>
|
||||
<string name="could_not_decrypt_the_message">Could not decrypt the message</string>
|
||||
@ -135,6 +136,7 @@
|
||||
<string name="conversations">Conversations</string>
|
||||
<string name="notes">Notes</string>
|
||||
<string name="replies">Replies</string>
|
||||
<string name="gallery">Gallery</string>
|
||||
<string name="follows">"Follows"</string>
|
||||
<string name="reports">"Reports"</string>
|
||||
<string name="more_options">More Options</string>
|
||||
@ -271,6 +273,8 @@
|
||||
<string name="quick_action_delete">Delete</string>
|
||||
<string name="quick_action_unfollow">Unfollow</string>
|
||||
<string name="quick_action_follow">Follow</string>
|
||||
<string name="quick_action_request_deletion_gallery_title">Delete from Gallery</string>
|
||||
<string name="quick_action_request_deletion_gallery_alert_body">Remove this media from your Gallery, you can readd it later</string>
|
||||
<string name="quick_action_request_deletion_alert_title">Request Deletion</string>
|
||||
<string name="quick_action_request_deletion_alert_body">Amethyst will request that your note be deleted from the relays you are currently connected to. There is no guarantee that your note will be permanently deleted from those relays, or from other relays where it may be stored.</string>
|
||||
<string name="quick_action_block_dialog_btn">Block</string>
|
||||
@ -598,6 +602,7 @@
|
||||
<string name="share_or_save">Share or Save</string>
|
||||
<string name="copy_url_to_clipboard">Copy URL to clipboard</string>
|
||||
<string name="copy_the_note_id_to_the_clipboard">Copy Note ID to clipboard</string>
|
||||
<string name="add_media_to_gallery">Add Media to Gallery</string>
|
||||
|
||||
<string name="created_at">Created at</string>
|
||||
<string name="rules">Rules</string>
|
||||
|
@ -75,7 +75,7 @@ class Relay(
|
||||
private var afterEOSEPerSubscription = mutableMapOf<String, Boolean>()
|
||||
|
||||
private val authResponse = mutableMapOf<HexKey, Boolean>()
|
||||
private val sendWhenReady = mutableListOf<EventInterface>()
|
||||
private val outboxCache = mutableMapOf<HexKey, EventInterface>()
|
||||
|
||||
fun register(listener: Listener) {
|
||||
listeners = listeners.plus(listener)
|
||||
@ -93,6 +93,15 @@ class Relay(
|
||||
|
||||
// Sends everything.
|
||||
renewFilters()
|
||||
sendOutbox()
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendOutbox() {
|
||||
synchronized(outboxCache) {
|
||||
outboxCache.values.forEach {
|
||||
send(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -157,13 +166,6 @@ class Relay(
|
||||
// Log.w("Relay", "Relay OnOpen, Loading All subscriptions $url")
|
||||
onConnected(this@Relay)
|
||||
|
||||
synchronized(sendWhenReady) {
|
||||
sendWhenReady.forEach {
|
||||
send(it)
|
||||
}
|
||||
sendWhenReady.clear()
|
||||
}
|
||||
|
||||
listeners.forEach { it.onRelayStateChange(this@Relay, StateType.CONNECT, null) }
|
||||
}
|
||||
|
||||
@ -307,15 +309,22 @@ class Relay(
|
||||
val success = msgArray[2].asBoolean()
|
||||
val message = if (msgArray.size() > 2) msgArray[3].asText() else ""
|
||||
|
||||
Log.w("Relay", "Relay on OK $url, $eventId, $success, $message")
|
||||
|
||||
if (authResponse.containsKey(eventId)) {
|
||||
val wasAlreadyAuthenticated = authResponse.get(eventId)
|
||||
authResponse.put(eventId, success)
|
||||
if (wasAlreadyAuthenticated != true && success) {
|
||||
renewFilters()
|
||||
sendOutbox()
|
||||
}
|
||||
}
|
||||
|
||||
Log.w("Relay", "Relay on OK $url, $eventId, $success, $message")
|
||||
if (outboxCache.contains(eventId) && !message.startsWith("auth-required")) {
|
||||
synchronized(outboxCache) {
|
||||
outboxCache.remove(eventId)
|
||||
}
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
RelayStats.newNotice(url, "Failed to receive $eventId: $message")
|
||||
@ -325,7 +334,7 @@ class Relay(
|
||||
}
|
||||
"AUTH" ->
|
||||
listeners.forEach {
|
||||
// Log.w("Relay", "Relay onAuth $url, ${msg[1].asString}")
|
||||
Log.w("Relay", "Relay onAuth $url, ${ msgArray[1].asText()}")
|
||||
it.onAuth(this@Relay, msgArray[1].asText())
|
||||
}
|
||||
"NOTIFY" ->
|
||||
@ -477,23 +486,7 @@ class Relay(
|
||||
sendAuth(signedEvent)
|
||||
} else {
|
||||
if (write) {
|
||||
if (isConnected()) {
|
||||
if (isReady) {
|
||||
writeToSocket("""["EVENT",${signedEvent.toJson()}]""")
|
||||
} else {
|
||||
synchronized(sendWhenReady) {
|
||||
sendWhenReady.add(signedEvent)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// sends all filters after connection is successful.
|
||||
connectAndRun {
|
||||
writeToSocket("""["EVENT",${signedEvent.toJson()}]""")
|
||||
|
||||
// Sends everything.
|
||||
renewFilters()
|
||||
}
|
||||
}
|
||||
sendEvent(signedEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -504,21 +497,20 @@ class Relay(
|
||||
}
|
||||
|
||||
private fun sendEvent(signedEvent: EventInterface) {
|
||||
synchronized(outboxCache) {
|
||||
outboxCache.put(signedEvent.id(), signedEvent)
|
||||
}
|
||||
|
||||
if (isConnected()) {
|
||||
if (isReady) {
|
||||
writeToSocket("""["EVENT",${signedEvent.toJson()}]""")
|
||||
} else {
|
||||
synchronized(sendWhenReady) {
|
||||
sendWhenReady.add(signedEvent)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// sends all filters after connection is successful.
|
||||
connectAndRun {
|
||||
writeToSocket("""["EVENT",${signedEvent.toJson()}]""")
|
||||
|
||||
// Sends everything.
|
||||
renewFilters()
|
||||
sendOutbox()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ android {
|
||||
debug {
|
||||
// Since debuggable can"t be modified by gradle for library modules,
|
||||
// it must be done in a manifest - see src/androidTest/AndroidManifest.xml
|
||||
minifyEnabled true
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "benchmark-proguard-rules.pro"
|
||||
}
|
||||
release {
|
||||
|
41
build.gradle
41
build.gradle
@ -4,34 +4,47 @@ plugins {
|
||||
alias(libs.plugins.androidLibrary) apply false
|
||||
alias(libs.plugins.jetbrainsKotlinJvm) apply false
|
||||
alias(libs.plugins.androidBenchmark) apply false
|
||||
alias(libs.plugins.diffplugSpotless) apply false
|
||||
alias(libs.plugins.diffplugSpotless)
|
||||
alias(libs.plugins.googleServices) apply false
|
||||
alias(libs.plugins.jetbrainsComposeCompiler) apply false
|
||||
}
|
||||
|
||||
subprojects {
|
||||
allprojects {
|
||||
apply plugin: 'com.diffplug.spotless'
|
||||
spotless {
|
||||
kotlin {
|
||||
target '**/*.kt'
|
||||
targetExclude("$layout.buildDirectory/**/*.kt")
|
||||
|
||||
ktlint("1.3.1")
|
||||
licenseHeaderFile rootProject.file('spotless/copyright.kt'), "package|import|class|object|sealed|open|interface|abstract "
|
||||
if (project === rootProject) {
|
||||
spotless {
|
||||
predeclareDeps()
|
||||
}
|
||||
|
||||
groovyGradle {
|
||||
target '*.gradle'
|
||||
spotlessPredeclare {
|
||||
kotlin {
|
||||
ktlint("1.3.1")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
spotless {
|
||||
kotlin {
|
||||
target '**/*.kt'
|
||||
targetExclude("$layout.buildDirectory/**/*.kt")
|
||||
|
||||
afterEvaluate {
|
||||
tasks.named("preBuild") {
|
||||
dependsOn("spotlessApply")
|
||||
ktlint("1.3.1")
|
||||
licenseHeaderFile rootProject.file('spotless/copyright.kt'), "package|import|class|object|sealed|open|interface|abstract "
|
||||
}
|
||||
|
||||
groovyGradle {
|
||||
target '*.gradle'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subprojects {
|
||||
afterEvaluate {
|
||||
tasks.named("preBuild") {
|
||||
dependsOn("spotlessApply")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register('installGitHook', Copy) {
|
||||
from new File(rootProject.rootDir, 'git-hooks/pre-commit')
|
||||
|
@ -38,6 +38,7 @@ abstract class MediaUrlContent(
|
||||
dim: String? = null,
|
||||
blurhash: String? = null,
|
||||
val uri: String? = null,
|
||||
val id: String? = null,
|
||||
val mimeType: String? = null,
|
||||
) : BaseMediaContent(description, dim, blurhash)
|
||||
|
||||
@ -49,6 +50,7 @@ class MediaUrlImage(
|
||||
blurhash: String? = null,
|
||||
dim: String? = null,
|
||||
uri: String? = null,
|
||||
id: String? = null,
|
||||
val contentWarning: String? = null,
|
||||
mimeType: String? = null,
|
||||
) : MediaUrlContent(url, description, hash, dim, blurhash, uri, mimeType)
|
||||
@ -60,6 +62,7 @@ class MediaUrlVideo(
|
||||
hash: String? = null,
|
||||
dim: String? = null,
|
||||
uri: String? = null,
|
||||
id: String? = null,
|
||||
val artworkUri: String? = null,
|
||||
val authorName: String? = null,
|
||||
blurhash: String? = null,
|
||||
@ -76,6 +79,7 @@ abstract class MediaPreloadedContent(
|
||||
dim: String? = null,
|
||||
blurhash: String? = null,
|
||||
val uri: String,
|
||||
val id: String? = null,
|
||||
) : BaseMediaContent(description, dim, blurhash) {
|
||||
fun localFileExists() = localFile != null && localFile.exists()
|
||||
}
|
||||
|
@ -24,4 +24,4 @@ android.nonTransitiveRClass=true
|
||||
android.enableR8.fullMode=true
|
||||
android.nonFinalResIds=false
|
||||
|
||||
kotlin.daemon.jvmargs=-Xmx4096m
|
||||
kotlin.daemon.jvmargs=-Xmx6g -XX:MaxMetaspaceSize=3g
|
@ -47,8 +47,6 @@ zelory = "3.0.1"
|
||||
zoomable = "1.6.1"
|
||||
zxing = "3.5.3"
|
||||
zxingAndroidEmbedded = "4.3.0"
|
||||
material = "1.10.0"
|
||||
materialVersion = "1.12.0"
|
||||
|
||||
[libraries]
|
||||
abedElazizShe-image-compressor = { group = "com.github.AbedElazizShe", name = "LightCompressor", version.ref = "lightcompressor" }
|
||||
@ -119,7 +117,6 @@ zelory-video-compressor = { group = "id.zelory", name = "compressor", version.re
|
||||
zoomable = { group = "net.engawapg.lib", name = "zoomable", version.ref = "zoomable" }
|
||||
zxing = { group = "com.google.zxing", name = "core", version.ref = "zxing" }
|
||||
zxing-embedded = { group = "com.journeyapps", name = "zxing-android-embedded", version.ref = "zxingAndroidEmbedded" }
|
||||
material = { group = "com.google.android.material", name = "material", version.ref = "materialVersion" }
|
||||
|
||||
[plugins]
|
||||
androidApplication = { id = "com.android.application", version.ref = "agp" }
|
||||
|
@ -85,6 +85,7 @@ class EventFactory {
|
||||
EmojiPackSelectionEvent.KIND ->
|
||||
EmojiPackSelectionEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
FileHeaderEvent.KIND -> FileHeaderEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
ProfileGalleryEntryEvent.KIND -> ProfileGalleryEntryEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
FileServersEvent.KIND -> FileServersEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
FileStorageEvent.KIND -> FileStorageEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
FileStorageHeaderEvent.KIND ->
|
||||
|
@ -0,0 +1,169 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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.events
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
@Immutable
|
||||
class GalleryListEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : GeneralListEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
companion object {
|
||||
const val KIND = 10011
|
||||
const val ALT = "Profile Gallery"
|
||||
const val GALLERYTAGNAME = "url"
|
||||
|
||||
fun addEvent(
|
||||
earlierVersion: GalleryListEvent?,
|
||||
eventId: HexKey,
|
||||
url: String,
|
||||
relay: String?,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (GalleryListEvent) -> Unit,
|
||||
) = addTag(earlierVersion, GALLERYTAGNAME, eventId, url, relay, signer, createdAt, onReady)
|
||||
|
||||
fun addTag(
|
||||
earlierVersion: GalleryListEvent?,
|
||||
tagName: String,
|
||||
eventid: HexKey,
|
||||
url: String,
|
||||
relay: String?,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (GalleryListEvent) -> Unit,
|
||||
) {
|
||||
var tags = arrayOf(tagName, url, eventid)
|
||||
if (relay != null) {
|
||||
tags + relay
|
||||
}
|
||||
|
||||
add(
|
||||
earlierVersion,
|
||||
arrayOf(tags),
|
||||
signer,
|
||||
createdAt,
|
||||
onReady,
|
||||
)
|
||||
}
|
||||
|
||||
fun add(
|
||||
earlierVersion: GalleryListEvent?,
|
||||
listNewTags: Array<Array<String>>,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (GalleryListEvent) -> Unit,
|
||||
) {
|
||||
create(
|
||||
content = earlierVersion?.content ?: "",
|
||||
tags = listNewTags.plus(earlierVersion?.tags ?: arrayOf()),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
onReady = onReady,
|
||||
)
|
||||
}
|
||||
|
||||
fun removeEvent(
|
||||
earlierVersion: GalleryListEvent,
|
||||
eventId: HexKey,
|
||||
url: String,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (GalleryListEvent) -> Unit,
|
||||
) = removeTag(earlierVersion, GALLERYTAGNAME, eventId, url, signer, createdAt, onReady)
|
||||
|
||||
fun removeReplaceable(
|
||||
earlierVersion: GalleryListEvent,
|
||||
aTag: ATag,
|
||||
url: String,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (GalleryListEvent) -> Unit,
|
||||
) = removeTag(earlierVersion, GALLERYTAGNAME, aTag.toTag(), url, signer, createdAt, onReady)
|
||||
|
||||
private fun removeTag(
|
||||
earlierVersion: GalleryListEvent,
|
||||
tagName: String,
|
||||
eventid: HexKey,
|
||||
url: String,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (GalleryListEvent) -> Unit,
|
||||
) {
|
||||
create(
|
||||
content = earlierVersion.content,
|
||||
tags =
|
||||
earlierVersion.tags
|
||||
.filter { it.size <= 1 || !(it[0] == tagName && it[1] == url && it[2] == eventid) }
|
||||
.toTypedArray(),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
onReady = onReady,
|
||||
)
|
||||
}
|
||||
|
||||
fun create(
|
||||
content: String,
|
||||
tags: Array<Array<String>>,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (GalleryListEvent) -> Unit,
|
||||
) {
|
||||
val newTags =
|
||||
if (tags.any { it.size > 1 && it[0] == "alt" }) {
|
||||
tags
|
||||
} else {
|
||||
tags + arrayOf("alt", ALT)
|
||||
}
|
||||
|
||||
signer.sign(createdAt, KIND, newTags, content, onReady)
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class GalleryUrl(
|
||||
val url: String,
|
||||
val id: String,
|
||||
val relay: String?,
|
||||
) {
|
||||
fun encode(): String = ":$url:$id:$relay"
|
||||
|
||||
companion object {
|
||||
fun decode(encodedGallerySetup: String): GalleryUrl? {
|
||||
val galleryParts = encodedGallerySetup.split(":", limit = 3)
|
||||
return if (galleryParts.size > 3) {
|
||||
GalleryUrl(galleryParts[1], galleryParts[2], galleryParts[3])
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,130 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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.events
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
@Immutable
|
||||
class ProfileGalleryEntryEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
fun url() = tags.firstOrNull { it.size > 1 && it[0] == URL }?.get(1)
|
||||
|
||||
fun urls() = tags.filter { it.size > 1 && it[0] == URL }.map { it[1] }
|
||||
|
||||
fun encryptionKey() = tags.firstOrNull { it.size > 2 && it[0] == ENCRYPTION_KEY }?.let { AESGCM(it[1], it[2]) }
|
||||
|
||||
fun mimeType() = tags.firstOrNull { it.size > 1 && it[0] == MIME_TYPE }?.get(1)
|
||||
|
||||
fun hash() = tags.firstOrNull { it.size > 1 && it[0] == HASH }?.get(1)
|
||||
|
||||
fun size() = tags.firstOrNull { it.size > 1 && it[0] == FILE_SIZE }?.get(1)
|
||||
|
||||
fun alt() = tags.firstOrNull { it.size > 1 && it[0] == ALT }?.get(1)
|
||||
|
||||
fun dimensions() = tags.firstOrNull { it.size > 1 && it[0] == DIMENSION }?.get(1)
|
||||
|
||||
fun magnetURI() = tags.firstOrNull { it.size > 1 && it[0] == MAGNET_URI }?.get(1)
|
||||
|
||||
fun torrentInfoHash() = tags.firstOrNull { it.size > 1 && it[0] == TORRENT_INFOHASH }?.get(1)
|
||||
|
||||
fun blurhash() = tags.firstOrNull { it.size > 1 && it[0] == BLUR_HASH }?.get(1)
|
||||
|
||||
fun hasUrl() = tags.any { it.size > 1 && it[0] == URL }
|
||||
|
||||
fun event() = tags.firstOrNull { it.size > 1 && it[0] == "e" }?.get(1)
|
||||
|
||||
fun hasEvent() = tags.any { it.size > 1 && it[0] == "e" }
|
||||
|
||||
fun isOneOf(mimeTypes: Set<String>) = tags.any { it.size > 1 && it[0] == MIME_TYPE && mimeTypes.contains(it[1]) }
|
||||
|
||||
companion object {
|
||||
const val KIND = 1163
|
||||
const val ALT_DESCRIPTION = "Profile Gallery Entry"
|
||||
|
||||
const val URL = "url"
|
||||
const val ENCRYPTION_KEY = "aes-256-gcm"
|
||||
const val MIME_TYPE = "m"
|
||||
const val FILE_SIZE = "size"
|
||||
const val DIMENSION = "dim"
|
||||
const val HASH = "x"
|
||||
const val MAGNET_URI = "magnet"
|
||||
const val TORRENT_INFOHASH = "i"
|
||||
const val BLUR_HASH = "blurhash"
|
||||
const val ORIGINAL_HASH = "ox"
|
||||
const val ALT = "alt"
|
||||
|
||||
fun create(
|
||||
url: String,
|
||||
eventid: String? = null,
|
||||
magnetUri: String? = null,
|
||||
mimeType: String? = null,
|
||||
alt: String? = null,
|
||||
hash: String? = null,
|
||||
size: String? = null,
|
||||
dimensions: String? = null,
|
||||
blurhash: String? = null,
|
||||
originalHash: String? = null,
|
||||
magnetURI: String? = null,
|
||||
torrentInfoHash: String? = null,
|
||||
encryptionKey: AESGCM? = null,
|
||||
sensitiveContent: Boolean? = null,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
onReady: (ProfileGalleryEntryEvent) -> Unit,
|
||||
) {
|
||||
val tags =
|
||||
listOfNotNull(
|
||||
arrayOf(URL, url),
|
||||
eventid?.let { arrayOf("e", it) },
|
||||
magnetUri?.let { arrayOf(MAGNET_URI, it) },
|
||||
mimeType?.let { arrayOf(MIME_TYPE, it) },
|
||||
alt?.ifBlank { null }?.let { arrayOf(ALT, it) } ?: arrayOf("alt", ALT_DESCRIPTION),
|
||||
hash?.let { arrayOf(HASH, it) },
|
||||
size?.let { arrayOf(FILE_SIZE, it) },
|
||||
dimensions?.let { arrayOf(DIMENSION, it) },
|
||||
blurhash?.let { arrayOf(BLUR_HASH, it) },
|
||||
originalHash?.let { arrayOf(ORIGINAL_HASH, it) },
|
||||
magnetURI?.let { arrayOf(MAGNET_URI, it) },
|
||||
torrentInfoHash?.let { arrayOf(TORRENT_INFOHASH, it) },
|
||||
encryptionKey?.let { arrayOf(ENCRYPTION_KEY, it.key, it.nonce) },
|
||||
sensitiveContent?.let {
|
||||
if (it) {
|
||||
arrayOf("content-warning", "")
|
||||
} else {
|
||||
null
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
val content = alt ?: ""
|
||||
signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user