Compare commits

...
45 Commits
Author SHA1 Message Date
Vitor Pamplona dbaadb4595 v0.89.7 2024-07-22 18:10:15 -04:00
Vitor Pamplona 231ca44e1c Avoids recomposition of the entire line when changing status. 2024-07-22 18:07:44 -04:00
Vitor Pamplona ca00c87a6e Prints memory use in descending order by size 2024-07-22 16:59:58 -04:00
Vitor Pamplona 2a28111583 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-07-22 14:27:23 -04:00
Vitor Pamplona 4def80c914 updates the EOSE of users even if no user is found in the relay. 2024-07-22 14:27:10 -04:00
Vitor PamplonaandGitHub 916ece1edb Merge pull request #992 from greenart7c3/main
Remove focus from status text field when closing the drawer
2024-07-22 14:00:24 -04:00
Vitor Pamplona 3c1f57fac0 Sorts keys before making the filter. 2024-07-22 13:57:31 -04:00
greenart7c3 e166c36731 Remove focus from status text field when closing the drawer 2024-07-22 14:45:39 -03:00
Vitor Pamplona a636f4a786 updates coil and jackson libs 2024-07-22 11:31:57 -04:00
Vitor PamplonaandGitHub bb127506c7 Merge pull request #985 from believethehype/main
Store additional File Metadata in Gallery Entries
2024-07-22 08:10:26 -04:00
Vitor PamplonaandGitHub 92c704d227 Merge pull request #986 from greenart7c3/main
Add sendAndWaitForResponse to Client.kt
2024-07-22 08:10:11 -04:00
greenart7c3 e764ebbcb2 if the relay just connected send the event 2024-07-22 07:49:54 -03:00
greenart7c3andGitHub 68fb34a8ad Merge branch 'main' into main 2024-07-22 07:36:43 -03:00
greenart7c3 02683ba829 Merge remote-tracking branch 'origin/main' 2024-07-22 07:34:36 -03:00
greenart7c3 754a1b3a87 change the log of onRelayStateChange 2024-07-22 07:33:21 -03:00
Vitor PamplonaandGitHub 4b58da7be7 Merge pull request #991 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-07-21 15:22:31 -04:00
Crowdin Bot 1ac9c850e4 New Crowdin translations by GitHub Action 2024-07-21 18:32:48 +00:00
Vitor PamplonaandGitHub 3cc4dc1414 Merge pull request #990 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-07-21 14:31:54 -04:00
Vitor Pamplona 62ae43e863 v0.89.6 2024-07-21 14:31:17 -04:00
Crowdin Bot 8149a93f2f New Crowdin translations by GitHub Action 2024-07-21 18:31:01 +00:00
Vitor Pamplona 5a60c3a595 Fixes stringResource cache 2024-07-21 14:29:05 -04:00
Vitor Pamplona 2ef2f0d1a5 adds the key to avoid applying to a separate navcontroller 2024-07-21 11:32:08 -04:00
Vitor Pamplona 52fef33662 run faster animations 2024-07-21 11:18:56 -04:00
Vitor Pamplona a5c6f86b35 v0.89.5 2024-07-20 16:27:27 -04:00
Vitor Pamplona b80008d695 Adds more memory stat dumping for production apps 2024-07-20 16:27:19 -04:00
Vitor Pamplona fa06aefbf1 increases disk cache to 1GB and adds a memory cache policy. 2024-07-20 15:32:34 -04:00
Vitor PamplonaandGitHub ab9ede1e26 Merge pull request #987 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-07-20 15:08:00 -04:00
Crowdin Bot 479b1ab20a New Crowdin translations by GitHub Action 2024-07-20 19:04:48 +00:00
Vitor Pamplona 8ed3f84cb7 Fixes screen rendering below the 3-button navigation 2024-07-20 15:02:57 -04:00
greenart7c3andGitHub 99b7a8005a Merge branch 'main' into main 2024-07-19 14:13:36 -03:00
greenart7c3 3f3367258f return true if at least one relay replied with success 2024-07-19 13:29:09 -03:00
greenart7c3 5cf8b5aa12 change proguard to see if now the client.listener works 2024-07-19 12:49:04 -03:00
greenart7c3 eba6d03b53 check if listeners work now 2024-07-19 12:17:26 -03:00
greenart7c3 de85239046 check if listeners work now 2024-07-19 11:56:30 -03:00
greenart7c3 cbd8acbd01 check eose too 2024-07-19 11:14:31 -03:00
greenart7c3 19c9bfa819 Merge remote-tracking branch 'origin/main' 2024-07-19 11:14:18 -03:00
greenart7c3andgreenart7c3 80c369a6ae change default timeout to 15 seconds 2024-07-19 10:56:35 -03:00
greenart7c3andgreenart7c3 d0190c87a2 add more logs, add optional listener, fix await call 2024-07-19 10:56:35 -03:00
greenart7c3andgreenart7c3 62a2cba18b fix lint 2024-07-19 10:56:35 -03:00
greenart7c3andgreenart7c3 59b3ff1b94 Added sendAndWaitForResponse to Client.kt 2024-07-19 10:56:35 -03:00
greenart7c3 35423e1bb6 change default timeout to 15 seconds 2024-07-19 10:52:36 -03:00
greenart7c3 e125690000 add more logs, add optional listener, fix await call 2024-07-19 10:50:59 -03:00
greenart7c3 04bf9c07ac fix lint 2024-07-19 08:05:04 -03:00
greenart7c3 ac58f99560 Added sendAndWaitForResponse to Client.kt 2024-07-19 07:58:12 -03:00
Believethehype df328ff139 Store additional File Metadata in Gallery Entries 2024-07-18 17:53:37 +02:00
26 changed files with 304 additions and 51 deletions
+2 -2
View File
@@ -15,8 +15,8 @@ android {
applicationId "com.vitorpamplona.amethyst"
minSdk libs.versions.android.minSdk.get().toInteger()
targetSdk libs.versions.android.targetSdk.get().toInteger()
versionCode 390
versionName "0.89.4"
versionCode 393
versionName "0.89.7"
buildConfigField "String", "RELEASE_NOTES_ID", "\"1130badb252bdf62aaf93cb65bfa2bb09e7600c7d6764894f2954555b3b73018\""
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -30,6 +30,7 @@ import android.util.Log
import androidx.security.crypto.EncryptedSharedPreferences
import coil.ImageLoader
import coil.disk.DiskCache
import coil.memory.MemoryCache
import com.vitorpamplona.amethyst.service.ots.OkHttpBlockstreamExplorer
import com.vitorpamplona.amethyst.service.ots.OkHttpCalendarBuilder
import com.vitorpamplona.amethyst.service.playback.VideoCache
@@ -66,7 +67,14 @@ class Amethyst : Application() {
.Builder()
.directory(applicationContext.safeCacheDir.resolve("image_cache"))
.maxSizePercent(0.2)
.maximumMaxSizeBytes(500L * 1024 * 1024) // 250MB
.maximumMaxSizeBytes(1024 * 1024 * 1024) // 1GB
.build()
}
val coilMemCache: MemoryCache by lazy {
MemoryCache
.Builder(this)
.maxSizePercent(0.40) // memory heavy app due to profile pics and videos.
.build()
}
@@ -106,7 +114,7 @@ class Amethyst : Application() {
}
}
fun imageLoaderBuilder(): ImageLoader.Builder = ImageLoader.Builder(applicationContext).diskCache { coilCache }
fun imageLoaderBuilder(): ImageLoader.Builder = ImageLoader.Builder(this).diskCache { coilCache }.memoryCache { coilMemCache }
fun encryptedStorage(npub: String? = null): EncryptedSharedPreferences = EncryptedStorage.preferences(instance, npub)
@@ -2251,21 +2251,25 @@ class Account(
idHex: String,
url: String,
relay: String?,
blurhash: String?,
dim: String?,
hash: String?,
mimeType: String?,
) {
if (!isWriteable()) return
ProfileGalleryEntryEvent.create(
url = url,
eventid = idHex,
relayhint = relay,
blurhash = blurhash,
hash = hash,
dimensions = dim,
mimeType = mimeType,
/*magnetUri = magnetUri,
mimeType = headerInfo.mimeType,
hash = headerInfo.hash,
size = headerInfo.size.toString(),
dimensions = headerInfo.dim,
blurhash = headerInfo.blurHash,
alt = alt,
originalHash = originalHash,
sensitiveContent = sensitiveContent, */
originalHash = originalHash, */
signer = signer,
) { event ->
Client.send(event)
@@ -319,6 +319,9 @@ fun groupByEOSEPresence(notes: Set<Note>): Collection<List<Note>> =
.sorted()
.joinToString(",")
}.values
.map {
it.sortedBy { it.idHex } // important to keep in order otherwise the Relay thinks the filter has changed and we REQ again
}
fun groupByEOSEPresence(users: Iterable<User>): Collection<List<User>> =
users
@@ -327,6 +330,9 @@ fun groupByEOSEPresence(users: Iterable<User>): Collection<List<User>> =
.sorted()
.joinToString(",")
}.values
.map {
it.sortedBy { it.pubkeyHex } // important to keep in order otherwise the Relay thinks the filter has changed and we REQ again
}
fun findMinimumEOSEs(notes: List<Note>): Map<String, EOSETime> {
val minLatestEOSEs = mutableMapOf<String, EOSETime>()
@@ -64,6 +64,7 @@ object NostrSingleUserDataSource : AmethystNostrDataSource("SingleUserFeed") {
.map { group ->
val groupIds = group.map { it.pubkeyHex }
val minEOSEs = findMinimumEOSEsForUsers(group)
listOf(
TypedFilter(
types = EVENT_FINDER_TYPES,
@@ -92,13 +93,11 @@ object NostrSingleUserDataSource : AmethystNostrDataSource("SingleUserFeed") {
checkNotInMainThread()
usersToWatch.forEach {
if (it.latestMetadata != null) {
val eose = it.latestEOSEs[relayUrl]
if (eose == null) {
it.latestEOSEs = it.latestEOSEs + Pair(relayUrl, EOSETime(time))
} else {
eose.time = time
}
val eose = it.latestEOSEs[relayUrl]
if (eose == null) {
it.latestEOSEs = it.latestEOSEs + Pair(relayUrl, EOSETime(time))
} else {
eose.time = time
}
}
}
@@ -54,9 +54,9 @@ fun stringRes(
String
.format(
LocalConfiguration.current.locales.get(0),
resourceCache.get(id) ?: stringResource(id),
resourceCache.get(id) ?: stringResource(id).also { resourceCache.put(id, it) },
*args,
).also { resourceCache.put(id, it) }
)
fun stringRes(
ctx: Context,
@@ -824,7 +824,7 @@ private fun RenderVideoPlayer(
}
AnimatedShareButton(controllerVisible, Modifier.align(Alignment.TopEnd).padding(end = Size165dp)) { popupExpanded, toggle ->
ShareImageAction(accountViewModel = accountViewModel, popupExpanded, videoUri, nostrUriCallback, toggle)
ShareImageAction(accountViewModel = accountViewModel, popupExpanded, videoUri, null, null, null, null, nostrUriCallback, toggle)
}
} else {
controller.volume = 0f
@@ -605,6 +605,10 @@ fun ShareImageAction(
popupExpanded = popupExpanded,
videoUri = content.url,
postNostrUri = content.uri,
blurhash = content.blurhash,
dim = content.dim,
hash = content.hash,
mimeType = content.mimeType,
onDismiss = onDismiss,
)
} else if (content is MediaPreloadedContent) {
@@ -613,6 +617,10 @@ fun ShareImageAction(
popupExpanded = popupExpanded,
videoUri = content.localFile?.toUri().toString(),
postNostrUri = content.uri,
blurhash = content.blurhash,
dim = content.dim,
hash = null,
mimeType = content.mimeType,
onDismiss = onDismiss,
)
}
@@ -625,6 +633,10 @@ fun ShareImageAction(
popupExpanded: MutableState<Boolean>,
videoUri: String?,
postNostrUri: String?,
blurhash: String?,
dim: String?,
hash: String?,
mimeType: String?,
onDismiss: () -> Unit,
) {
DropdownMenu(
@@ -660,7 +672,7 @@ fun ShareImageAction(
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.addMediaToGallery(n19.hex, videoUri, n19.relay[0], blurhash, dim, hash, mimeType) // TODO Whole list or first?
accountViewModel.toast(R.string.image_saved_to_the_gallery, R.string.image_saved_to_the_gallery)
}
}
@@ -27,6 +27,7 @@ import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.windowInsetsPadding
@@ -135,6 +136,7 @@ private fun RenderBottomMenu(
Modifier
.fillMaxWidth()
.windowInsetsPadding(windowInsets)
.consumeWindowInsets(windowInsets)
.height(50.dp),
) {
HorizontalDivider(
@@ -100,7 +100,7 @@ fun AppNavigation(
) {
val scope = rememberCoroutineScope()
val nav =
remember {
remember(navController) {
{ route: String ->
scope.launch {
if (getRouteWithArguments(navController) != route) {
@@ -114,8 +114,8 @@ fun AppNavigation(
NavHost(
navController,
startDestination = Route.Home.route,
enterTransition = { fadeIn(animationSpec = tween(400)) },
exitTransition = { fadeOut(animationSpec = tween(400)) },
enterTransition = { fadeIn(animationSpec = tween(200)) },
exitTransition = { fadeOut(animationSpec = tween(200)) },
) {
Route.Home.let { route ->
composable(
@@ -20,7 +20,9 @@
*/
package com.vitorpamplona.amethyst.ui.navigation
import android.app.ActivityManager
import android.content.Context
import android.content.pm.ApplicationInfo
import android.os.Debug
import android.util.Log
import androidx.compose.foundation.clickable
@@ -66,6 +68,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.core.content.getSystemService
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -1034,6 +1037,14 @@ fun debugState(context: Context) {
Log.d("STATE DUMP", "Total Native Heap Allocated: " + nativeHeap + " MB")
val activityManager: ActivityManager? = context.getSystemService()
if (activityManager != null) {
val isLargeHeap = (context.applicationInfo.flags and ApplicationInfo.FLAG_LARGE_HEAP) != 0
val memClass = if (isLargeHeap) activityManager.largeMemoryClass else activityManager.memoryClass
Log.d("STATE DUMP", "Memory Class " + memClass + " MB (largeHeap $isLargeHeap)")
}
Log.d("STATE DUMP", "Connected Relays: " + RelayPool.connectedRelays())
val imageLoader = Coil.imageLoader(context)
@@ -1092,6 +1103,13 @@ fun debugState(context: Context) {
LocalCache.observablesByKindAndAuthor.size,
)
Log.d(
"STATE DUMP",
"Spam: " +
LocalCache.antiSpam.recentMessages.size() +
" / " + LocalCache.antiSpam.spamMessages.size(),
)
Log.d(
"STATE DUMP",
"Memory used by Events: " +
@@ -1109,11 +1127,11 @@ fun debugState(context: Context) {
LocalCache.addressables
.sumByGroup(groupMap = { _, it -> it.event?.kind() }, sumOf = { _, it -> it.event?.countMemory() ?: 0L })
qttNotes.forEach { kind, qtt ->
Log.d("STATE DUMP", "Kind $kind:\t$qtt elements\t${bytesNotes.get(kind)?.div((1024 * 1024))}MB ")
qttNotes.toList().sortedByDescending { bytesNotes.get(it.first) }.forEach { (kind, qtt) ->
Log.d("STATE DUMP", "Kind ${kind.toString().padStart(5,' ')}:\t${qtt.toString().padStart(6,' ')} elements\t${bytesNotes.get(kind)?.div((1024 * 1024))}MB ")
}
qttAddressables.forEach { kind, qtt ->
Log.d("STATE DUMP", "Kind $kind:\t$qtt elements\t${bytesAddressables.get(kind)?.div((1024 * 1024))}MB ")
qttAddressables.toList().sortedByDescending { bytesNotes.get(it.first) }.forEach { (kind, qtt) ->
Log.d("STATE DUMP", "Kind ${kind.toString().padStart(5,' ')}:\t${qtt.toString().padStart(6,' ')} elements\t${bytesAddressables.get(kind)?.div((1024 * 1024))}MB ")
}
}
@@ -136,7 +136,9 @@ fun DrawerContent(
drawerTonalElevation = 0.dp,
) {
Column(
Modifier.fillMaxHeight().verticalScroll(rememberScrollState()),
Modifier
.fillMaxHeight()
.verticalScroll(rememberScrollState()),
) {
ProfileContent(
baseAccountUser = accountViewModel.account.userProfile(),
@@ -146,7 +148,7 @@ fun DrawerContent(
)
Column(drawerSpacing) {
EditStatusBoxes(accountViewModel.account.userProfile(), accountViewModel)
EditStatusBoxes(accountViewModel.account.userProfile(), accountViewModel, drawerState)
}
FollowingAndFollowerCounts(accountViewModel.account.userProfile(), onClickUser)
@@ -263,15 +265,16 @@ fun ProfileContentTemplate(
private fun EditStatusBoxes(
baseAccountUser: User,
accountViewModel: AccountViewModel,
drawerState: DrawerState,
) {
LoadStatuses(user = baseAccountUser, accountViewModel) { statuses ->
if (statuses.isEmpty()) {
StatusEditBar(accountViewModel = accountViewModel)
StatusEditBar(accountViewModel = accountViewModel, drawerState = drawerState)
} else {
statuses.forEach {
val originalStatus by it.live().content.observeAsState()
StatusEditBar(originalStatus, it.address, accountViewModel)
StatusEditBar(originalStatus, it.address, accountViewModel, drawerState = drawerState)
}
}
}
@@ -282,11 +285,17 @@ fun StatusEditBar(
savedStatus: String? = null,
tag: ATag? = null,
accountViewModel: AccountViewModel,
drawerState: DrawerState,
) {
val focusManager = LocalFocusManager.current
val currentStatus = remember { mutableStateOf(savedStatus ?: "") }
val hasChanged = remember { derivedStateOf { currentStatus.value != (savedStatus ?: "") } }
LaunchedEffect(drawerState.isClosed) {
if (drawerState.isClosed) {
focusManager.clearFocus(true)
}
}
OutlinedTextField(
value = currentStatus.value,
@@ -991,11 +991,7 @@ fun FirstUserInfoRow(
BoostedMark()
}
if (editState.value is GenericLoadable.Loaded) {
(editState.value as? GenericLoadable.Loaded<EditState>)?.loaded?.let {
DisplayEditStatus(it)
}
}
CheckAndDisplayEditStatus(editState)
if (baseNote.isDraft()) {
DisplayDraft()
@@ -1007,6 +1003,15 @@ fun FirstUserInfoRow(
}
}
@Composable
fun CheckAndDisplayEditStatus(editState: State<GenericLoadable<EditState>>) {
if (editState.value is GenericLoadable.Loaded) {
(editState.value as? GenericLoadable.Loaded<EditState>)?.loaded?.let {
DisplayEditStatus(it)
}
}
}
@Composable
fun observeEdits(
baseNote: Note,
@@ -81,6 +81,7 @@ import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
import com.vitorpamplona.amethyst.ui.components.mockAccountViewModel
import com.vitorpamplona.amethyst.ui.navigation.routeFor
import com.vitorpamplona.amethyst.ui.navigation.routeToMessage
import com.vitorpamplona.amethyst.ui.note.CheckAndDisplayEditStatus
import com.vitorpamplona.amethyst.ui.note.CheckHiddenFeedWatchBlockAndReport
import com.vitorpamplona.amethyst.ui.note.DisplayDraft
import com.vitorpamplona.amethyst.ui.note.DisplayOtsIfInOriginal
@@ -95,7 +96,6 @@ import com.vitorpamplona.amethyst.ui.note.RenderRepost
import com.vitorpamplona.amethyst.ui.note.WatchNoteEvent
import com.vitorpamplona.amethyst.ui.note.calculateBackgroundColor
import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader
import com.vitorpamplona.amethyst.ui.note.elements.DisplayEditStatus
import com.vitorpamplona.amethyst.ui.note.elements.DisplayFollowingCommunityInPost
import com.vitorpamplona.amethyst.ui.note.elements.DisplayFollowingHashtagsInPost
import com.vitorpamplona.amethyst.ui.note.elements.DisplayLocation
@@ -421,11 +421,7 @@ private fun FullBleedNoteCompose(
DisplayFollowingHashtagsInPost(baseNote, accountViewModel, nav)
}
if (editState.value is GenericLoadable.Loaded) {
(editState.value as? GenericLoadable.Loaded<EditState>)?.loaded?.let {
DisplayEditStatus(it)
}
}
CheckAndDisplayEditStatus(editState)
TimeAgo(note = baseNote)
@@ -673,8 +673,12 @@ class AccountViewModel(
hex: String,
url: String,
relay: String?,
blurhash: String?,
dim: String?,
hash: String?,
mimeType: String?,
) {
viewModelScope.launch(Dispatchers.IO) { account.addToGallery(hex, url, relay) }
viewModelScope.launch(Dispatchers.IO) { account.addToGallery(hex, url, relay, blurhash, dim, hash, mimeType) }
}
fun removefromMediaGallery(note: Note) {
@@ -34,10 +34,12 @@ import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
@@ -486,7 +488,14 @@ private fun MainScaffold(
}
},
) {
Column(modifier = Modifier.padding(it).imePadding()) {
Column(
modifier =
Modifier
.padding(it)
.consumeWindowInsets(it)
.systemBarsPadding()
.imePadding(),
) {
AppNavigation(
homeFeedViewModel = homeFeedViewModel,
repliesFeedViewModel = repliesFeedViewModel,
@@ -9,6 +9,7 @@
<string name="post_was_hidden">Esta publicación se ocultó porque menciona tus usuarios o palabras ocultas</string>
<string name="post_was_flagged_as_inappropriate_by">Post marcado como inapropiado por</string>
<string name="post_not_found">El evento se está cargando o no se puede encontrar en la lista de relés</string>
<string name="post_not_found_short">👀</string>
<string name="channel_image">Imagen del canal</string>
<string name="referenced_event_not_found">Evento referenciado no encontrado</string>
<string name="could_not_decrypt_the_message">No se pudo desencriptar el mensaje</string>
@@ -132,6 +133,7 @@
<string name="conversations">Conversaciones</string>
<string name="notes">Notas</string>
<string name="replies">Respuestas</string>
<string name="gallery">Galería</string>
<string name="follows">"Siguiendo"</string>
<string name="reports">"Reportes"</string>
<string name="more_options">Más opciones</string>
@@ -255,6 +257,8 @@
<string name="quick_action_delete">Eliminar</string>
<string name="quick_action_unfollow">Dejar de seguir</string>
<string name="quick_action_follow">Seguir</string>
<string name="quick_action_request_deletion_gallery_title">Eliminar de la galería</string>
<string name="quick_action_request_deletion_gallery_alert_body">Elimina este contenido multimedia de la galería, aunque puedes volver a agregarlo luego.</string>
<string name="quick_action_request_deletion_alert_title">Solicitar eliminación</string>
<string name="quick_action_request_deletion_alert_body">Amethyst solicitará que se elimine su nota de los relays a los que está conectado actualmente. No hay garantía de que su nota se elimine permanentemente de esos relays, o de otros relays donde pueda almacenarse.</string>
<string name="quick_action_block_dialog_btn">Bloquear</string>
@@ -517,6 +521,7 @@
<string name="share_or_save">Compartir o guardar</string>
<string name="copy_url_to_clipboard">Copiar URL al portapapeles</string>
<string name="copy_the_note_id_to_the_clipboard">Copiar ID de la nota al portapapeles</string>
<string name="add_media_to_gallery">Agregar contenido multimedia a la galería</string>
<string name="created_at">Creado</string>
<string name="rules">Reglas</string>
<string name="login_with_external_signer">Iniciar sesión con Amber</string>
@@ -720,6 +725,8 @@
<string name="private_outbox_section_explainer">Inserta entre 1 y 3 relés para almacenar eventos que nadie más pueda ver, como los borradores o la configuración de la app. Lo ideal es que estos relés sean locales o requieran autenticación antes de descargar el contenido de cada usuario.</string>
<string name="kind_3_section">Relés generales</string>
<string name="kind_3_section_description">Amethyst usa estos relés para descargar publicaciones por ti.</string>
<string name="kind_3_recommended_section">Relés recomendados</string>
<string name="kind_3_recommended_section_description">Agrega los siguientes relés a la lista de relés generales para recibir publicaciones de los usuarios de la lista.</string>
<string name="search_section">Relés de búsqueda</string>
<string name="search_section_explainer">Lista de relés que se usarán al buscar contenido o usuarios. El etiquetado y la búsqueda no funcionarán si no hay opciones disponibles. Asegúrate de que implementen NIP-50.</string>
<string name="local_section">Relés locales</string>
@@ -9,6 +9,7 @@
<string name="post_was_hidden">Esta publicación se ocultó porque menciona tus usuarios o palabras ocultas</string>
<string name="post_was_flagged_as_inappropriate_by">La publicación fue reportada por</string>
<string name="post_not_found">El evento se está cargando o no se puede encontrar en la lista de relés</string>
<string name="post_not_found_short">👀</string>
<string name="channel_image">Imagen del canal</string>
<string name="referenced_event_not_found">No se encontró el evento referenciado</string>
<string name="could_not_decrypt_the_message">No se pudo desencriptar el mensaje</string>
@@ -132,6 +133,7 @@
<string name="conversations">Conversaciones</string>
<string name="notes">Notas</string>
<string name="replies">Respuestas</string>
<string name="gallery">Galería</string>
<string name="follows">"Seguidos"</string>
<string name="reports">"Reportes"</string>
<string name="more_options">Más opciones</string>
@@ -255,6 +257,8 @@
<string name="quick_action_delete">Eliminar</string>
<string name="quick_action_unfollow">Dejar de seguir</string>
<string name="quick_action_follow">Seguir</string>
<string name="quick_action_request_deletion_gallery_title">Eliminar de la galería</string>
<string name="quick_action_request_deletion_gallery_alert_body">Elimina este contenido multimedia de la galería, aunque puedes volver a agregarlo luego.</string>
<string name="quick_action_request_deletion_alert_title">Solicitar eliminación</string>
<string name="quick_action_request_deletion_alert_body">Amethyst solicitará que se elimine la nota de los relés con los que tienes conexión actualmente. No hay garantía de que la nota se elimine permanentemente de esos relés o de otros donde pueda estar guardada.</string>
<string name="quick_action_block_dialog_btn">Bloquear</string>
@@ -517,6 +521,7 @@
<string name="share_or_save">Compartir o guardar</string>
<string name="copy_url_to_clipboard">Copiar URL al portapapeles</string>
<string name="copy_the_note_id_to_the_clipboard">Copiar ID de la nota al portapapeles</string>
<string name="add_media_to_gallery">Agregar contenido multimedia a la galería</string>
<string name="created_at">Creado</string>
<string name="rules">Reglas</string>
<string name="login_with_external_signer">Iniciar sesión con Amber</string>
@@ -720,6 +725,8 @@
<string name="private_outbox_section_explainer">Inserta entre 1 y 3 relés para almacenar eventos que nadie más pueda ver, como los borradores o la configuración de la app. Lo ideal es que estos relés sean locales o requieran autenticación antes de descargar el contenido de cada usuario.</string>
<string name="kind_3_section">Relés generales</string>
<string name="kind_3_section_description">Amethyst usa estos relés para descargar publicaciones por ti.</string>
<string name="kind_3_recommended_section">Relés recomendados</string>
<string name="kind_3_recommended_section_description">Agrega los siguientes relés a la lista de relés generales para recibir publicaciones de los usuarios de la lista.</string>
<string name="search_section">Relés de búsqueda</string>
<string name="search_section_explainer">Lista de relés que se usarán al buscar contenido o usuarios. El etiquetado y la búsqueda no funcionarán si no hay opciones disponibles. Asegúrate de que implementen NIP-50.</string>
<string name="local_section">Relés locales</string>
@@ -9,6 +9,7 @@
<string name="post_was_hidden">Esta publicación se ocultó porque menciona tus usuarios o palabras ocultas</string>
<string name="post_was_flagged_as_inappropriate_by">La publicación fue reportada por</string>
<string name="post_not_found">El evento se está cargando o no se puede encontrar en la lista de relés</string>
<string name="post_not_found_short">👀</string>
<string name="channel_image">Imagen del canal</string>
<string name="referenced_event_not_found">No se encontró el evento referenciado</string>
<string name="could_not_decrypt_the_message">No se pudo desencriptar el mensaje</string>
@@ -132,6 +133,7 @@
<string name="conversations">Conversaciones</string>
<string name="notes">Notas</string>
<string name="replies">Respuestas</string>
<string name="gallery">Galería</string>
<string name="follows">"Seguidos"</string>
<string name="reports">"Reportes"</string>
<string name="more_options">Más opciones</string>
@@ -255,6 +257,8 @@
<string name="quick_action_delete">Eliminar</string>
<string name="quick_action_unfollow">Dejar de seguir</string>
<string name="quick_action_follow">Seguir</string>
<string name="quick_action_request_deletion_gallery_title">Eliminar de la galería</string>
<string name="quick_action_request_deletion_gallery_alert_body">Elimina este contenido multimedia de la galería, aunque puedes volver a agregarlo luego.</string>
<string name="quick_action_request_deletion_alert_title">Solicitar eliminación</string>
<string name="quick_action_request_deletion_alert_body">Amethyst solicitará que se elimine la nota de los relés con los que tienes conexión actualmente. No hay garantía de que la nota se elimine permanentemente de esos relés o de otros donde pueda estar guardada.</string>
<string name="quick_action_block_dialog_btn">Bloquear</string>
@@ -517,6 +521,7 @@
<string name="share_or_save">Compartir o guardar</string>
<string name="copy_url_to_clipboard">Copiar URL al portapapeles</string>
<string name="copy_the_note_id_to_the_clipboard">Copiar ID de la nota al portapapeles</string>
<string name="add_media_to_gallery">Agregar contenido multimedia a la galería</string>
<string name="created_at">Creado</string>
<string name="rules">Reglas</string>
<string name="login_with_external_signer">Iniciar sesión con Amber</string>
@@ -720,6 +725,8 @@
<string name="private_outbox_section_explainer">Inserta entre 1 y 3 relés para almacenar eventos que nadie más pueda ver, como los borradores o la configuración de la app. Lo ideal es que estos relés sean locales o requieran autenticación antes de descargar el contenido de cada usuario.</string>
<string name="kind_3_section">Relés generales</string>
<string name="kind_3_section_description">Amethyst usa estos relés para descargar publicaciones por ti.</string>
<string name="kind_3_recommended_section">Relés recomendados</string>
<string name="kind_3_recommended_section_description">Agrega los siguientes relés a la lista de relés generales para recibir publicaciones de los usuarios de la lista.</string>
<string name="search_section">Relés de búsqueda</string>
<string name="search_section_explainer">Lista de relés que se usarán al buscar contenido o usuarios. El etiquetado y la búsqueda no funcionarán si no hay opciones disponibles. Asegúrate de que implementen NIP-50.</string>
<string name="local_section">Relés locales</string>
@@ -725,6 +725,8 @@
<string name="private_outbox_section_explainer">१ - ३ पुनःप्रसारकों को जोडें उन घटनाओं को रखने के लिए जो कोई अन्य नहीं देख सकता जैसे कि पाण्डुलिपियाँ अथवा क्रमक के स्थापना विकल्प। आदर्शतः ये पुनःप्रसारक स्थानीय होने चाहिए अथवा प्रत्येक उपयोगकर्ता के विषयवस्तु अवरोहण के पूर्व परिचय प्रमाणीकरण अनिवार्य होना चाहिए।</string>
<string name="kind_3_section">सामान्य पुनःप्रसारक</string>
<string name="kind_3_section_description">अमेथिस्ट इन पुनःप्रसारकों का प्रयोग करता है आपके पत्रों का अवरोहण करने के लिए।</string>
<string name="kind_3_recommended_section">पुनःशंसित पुनःप्रसारक</string>
<string name="kind_3_recommended_section_description">सूचित उपयोगकर्ताओं के पत्र प्राप्त करने के लिए निम्नलिखित पुनःप्रसारकों को अपने सामान्य पुनःप्रसारक सूची में जोडें।</string>
<string name="search_section">खोज पुनःप्रसारक</string>
<string name="search_section_explainer">पुनःप्रसारकों की सूची जिनको किसी विषयवस्तु अथवा उपयोगकर्ताओं को खोजने में प्रयोग करना चाहिए। कोई विकल्प नहीं तो उपयोगकर्ता सूचक जोडना तथा खोज निष्क्रिय होंगे। सुनिश्चित करें कि वे निप॰-५० को कार्यान्वित किये हैं।</string>
<string name="local_section">स्थानीय पुनःप्रसारक</string>
@@ -20,7 +20,7 @@
<string name="impersonation">Podszywanie się</string>
<string name="illegal_behavior">Antyspołeczne zachowanie</string>
<string name="other">Inne</string>
<string name="unknown">Nieznany</string>
<string name="unknown">Nieznani</string>
<string name="relay_icon">Ikona transmitera</string>
<string name="unknown_author">Autor nieznany</string>
<string name="copy_text">Skopiuj tekst</string>
@@ -126,7 +126,7 @@
<string name="copy_channel_id_note_to_the_clipboard">Kopiuj ID kanału (wpisu) do schowka</string>
<string name="edits_the_channel_metadata">Edytuje metadane kanału</string>
<string name="join">Dołącz</string>
<string name="known">Popularne</string>
<string name="known">Znajomi</string>
<string name="new_requests">Nowe Prośby</string>
<string name="blocked_users">Zablokowani użytkownicy</string>
<string name="new_threads">Nowe Wpisy</string>
@@ -725,6 +725,8 @@
<string name="private_outbox_section_explainer">Wstaw od 1 do 3 transmiterów przechowujących dane zdarzeń, których nikt inny nie widzi, takich jak Wersje robocze i/lub ustawienia aplikacji. Idealnie byłoby, gdyby te transmitery były lokalne lub wymagały uwierzytelnienia przed pobraniem treści każdego użytkownika.</string>
<string name="kind_3_section">Transmitery ogólne</string>
<string name="kind_3_section_description">Amethyst używa tych przekaźników, aby pobrać dla Ciebie posty.</string>
<string name="kind_3_recommended_section">Polecane Transmitery</string>
<string name="kind_3_recommended_section_description">Dodaj te transmitery do głównej listy, aby otrzymywać wiadomości od wymienionych użytkowników.</string>
<string name="search_section">Transmitery wyszukujące</string>
<string name="search_section_explainer">Lista transmiterów używanych podczas wyszukiwania treści lub użytkowników. Tagowanie i wyszukiwanie nie będą działać, jeśli nie będą dostępne żadne opcje. Upewnij się, że zaimplementowano NIP-50.</string>
<string name="local_section">Lokalne Transmitery</string>
+45
View File
@@ -0,0 +1,45 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
# preserve the line number information for debugging stack traces.
-dontobfuscate
-keepattributes LocalVariableTable
-keepattributes LocalVariableTypeTable
-keepattributes *Annotation*
-keepattributes SourceFile
-keepattributes LineNumberTable
-keepattributes Signature
-keepattributes Exceptions
-keepattributes InnerClasses
-keepattributes EnclosingMethod
-keepattributes MethodParameters
-keepparameternames
-keepdirectories libs
# Keep all names
-keepnames class ** { *; }
# Keep All enums
-keep enum ** { *; }
-keep class com.vitorpamplona.ammolite.service.** { *; }
-keep class com.vitorpamplona.ammolite.relays.** { *; }
+21
View File
@@ -19,6 +19,27 @@
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
# preserve the line number information for debugging stack traces.
-dontobfuscate
-keepattributes LocalVariableTable
-keepattributes LocalVariableTypeTable
-keepattributes *Annotation*
-keepattributes SourceFile
-keepattributes LineNumberTable
-keepattributes Signature
-keepattributes Exceptions
-keepattributes InnerClasses
-keepattributes EnclosingMethod
-keepattributes MethodParameters
-keepparameternames
-keepdirectories libs
# Keep all names
-keepnames class ** { *; }
# Keep All enums
-keep enum ** { *; }
-keep class com.vitorpamplona.ammolite.service.** { *; }
-keep class com.vitorpamplona.ammolite.relays.** { *; }
@@ -28,7 +28,10 @@ import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.util.UUID
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
/**
* The Nostr Client manages multiple personae the user may switch between. Events are received and
@@ -110,7 +113,7 @@ object Client : RelayPool.Listener {
checkNotInMainThread()
subscribe(
object : Listener() {
object : Listener {
override fun onEvent(
event: Event,
subId: String,
@@ -130,6 +133,87 @@ object Client : RelayPool.Listener {
RelayPool.sendFilter(subscriptionId, filters)
}
@OptIn(DelicateCoroutinesApi::class)
suspend fun sendAndWaitForResponse(
signedEvent: EventInterface,
relay: String? = null,
feedTypes: Set<FeedType>? = null,
relayList: List<RelaySetupInfo>? = null,
onDone: (() -> Unit)? = null,
additionalListener: Listener? = null,
timeoutInSeconds: Long = 15,
): Boolean {
checkNotInMainThread()
val size = if (relay != null) 1 else relayList?.size ?: RelayPool.availableRelays()
val latch = CountDownLatch(size)
val relayErrors = mutableMapOf<String, String>()
var result = false
Log.d("sendAndWaitForResponse", "Waiting for $size responses")
val subscription =
object : Listener {
override fun onError(
error: Error,
subscriptionId: String,
relay: Relay,
) {
relayErrors[relay.url]?.let {
latch.countDown()
}
Log.d("sendAndWaitForResponse", "onError Error from relay ${relay.url} count: ${latch.count} error: $error")
}
override fun onRelayStateChange(
type: Relay.StateType,
relay: Relay,
subscriptionId: String?,
) {
if (type == Relay.StateType.DISCONNECT || type == Relay.StateType.EOSE) {
latch.countDown()
}
if (type == Relay.StateType.CONNECT) {
Log.d("sendAndWaitForResponse", "${type.name} Sending event to relay ${relay.url} count: ${latch.count}")
relay.sendOverride(signedEvent)
}
Log.d("sendAndWaitForResponse", "onRelayStateChange ${type.name} from relay ${relay.url} count: ${latch.count}")
}
override fun onSendResponse(
eventId: String,
success: Boolean,
message: String,
relay: Relay,
) {
if (eventId == signedEvent.id()) {
if (success) {
result = true
}
latch.countDown()
Log.d("sendAndWaitForResponse", "onSendResponse Received response for $eventId from relay ${relay.url} count: ${latch.count} message $message success $success")
}
}
}
subscribe(subscription)
additionalListener?.let { subscribe(it) }
val job =
GlobalScope.launch(Dispatchers.IO) {
send(signedEvent, relay, feedTypes, relayList, onDone)
}
job.join()
runBlocking {
latch.await(timeoutInSeconds, TimeUnit.SECONDS)
}
Log.d("sendAndWaitForResponse", "countdown finished")
unsubscribe(subscription)
additionalListener?.let { unsubscribe(it) }
return result
}
fun sendFilterOnlyIfDisconnected(
subscriptionId: String = UUID.randomUUID().toString().substring(0..10),
filters: List<TypedFilter> = listOf(),
@@ -206,6 +290,7 @@ object Client : RelayPool.Listener {
// }
}
@OptIn(DelicateCoroutinesApi::class)
override fun onSendResponse(
eventId: String,
success: Boolean,
@@ -219,6 +304,7 @@ object Client : RelayPool.Listener {
}
}
@OptIn(DelicateCoroutinesApi::class)
override fun onAuth(
relay: Relay,
challenge: String,
@@ -228,6 +314,7 @@ object Client : RelayPool.Listener {
GlobalScope.launch(Dispatchers.Default) { listeners.forEach { it.onAuth(relay, challenge) } }
}
@OptIn(DelicateCoroutinesApi::class)
override fun onNotify(
relay: Relay,
description: String,
@@ -239,6 +326,7 @@ object Client : RelayPool.Listener {
}
}
@OptIn(DelicateCoroutinesApi::class)
override fun onSend(
relay: Relay,
msg: String,
@@ -249,6 +337,7 @@ object Client : RelayPool.Listener {
}
}
@OptIn(DelicateCoroutinesApi::class)
override fun onBeforeSend(
relay: Relay,
event: EventInterface,
@@ -258,6 +347,7 @@ object Client : RelayPool.Listener {
}
}
@OptIn(DelicateCoroutinesApi::class)
override fun onError(
error: Error,
subscriptionId: String,
@@ -282,7 +372,7 @@ object Client : RelayPool.Listener {
fun getSubscriptionFilters(subId: String): List<TypedFilter> = subscriptions[subId] ?: emptyList()
abstract class Listener {
interface Listener {
/** A new message was received */
open fun onEvent(
event: Event,
@@ -67,7 +67,7 @@ abstract class NostrDataSource(
): Int = 31 * str1.hashCode() + str2.hashCode()
private val clientListener =
object : Client.Listener() {
object : Client.Listener {
override fun onEvent(
event: Event,
subscriptionId: String,
+2 -2
View File
@@ -13,14 +13,14 @@ benchmark = "1.2.4"
benchmarkJunit4 = "1.2.4"
biometricKtx = "1.2.0-alpha05"
blurhash = "1.0.0"
coil = "2.6.0"
coil = "2.7.0"
composeBom = "2024.06.00"
coreKtx = "1.13.1"
espressoCore = "3.6.1"
firebaseBom = "33.1.2"
fragmentKtx = "1.8.1"
gms = "4.4.2"
jacksonModuleKotlin = "2.17.1"
jacksonModuleKotlin = "2.17.2"
jna = "5.14.0"
junit = "4.13.2"
kotlin = "2.0.0"