* 'main' of https://github.com/vitorpamplona/amethyst: (35 commits)
  New Crowdin translations by GitHub Action
  fix chess tests
  Replace subsequent checks with '!isNullOrEmpty()' call
  use forEach: no intermediate list created.
  Use filterIsInstance to avoid dual iteration
  Lambda argument should be moved out of parentheses
  replace null checks with Elvis
  merge call chain with flatMap (slight performance improvement)
  New Crowdin translations by GitHub Action
  New Crowdin translations by GitHub Action
  update cz, pt, de, sv
  was this meant to be withContext?
  add names to boolean params
  remove unused imports
  Explicit type arguments can be inferred
  remove redundant modifiers
  correct package name
  If-Null return/break/... foldable to '?:'
  Declaration has type inferred from a platform call, which can lead to unchecked nullability issues. Specify type explicitly as nullable or non-nullable.
  Declaration has type inferred from a platform call, which can lead to unchecked nullability issues. Specify type explicitly as nullable or non-nullable.
  ...

# Conflicts:
#	amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ReactionsSettingsScreen.kt
This commit is contained in:
Vitor Pamplona
2026-03-07 16:47:16 -05:00
144 changed files with 807 additions and 370 deletions
@@ -80,7 +80,7 @@ class ImageUploadTesting {
.addInterceptor(DefaultContentTypeInterceptor("Amethyst/${BuildConfig.VERSION_NAME}"))
.build()
private suspend fun getBitmap(): ByteArray {
private fun getBitmap(): ByteArray {
val bitmap = Bitmap.createBitmap(200, 300, Bitmap.Config.ARGB_8888)
for (x in 0 until bitmap.width) {
for (y in 0 until bitmap.height) {
@@ -127,7 +127,7 @@ class ImageUploadTesting {
// assertEquals(server.baseUrl, "${server.baseUrl}/$initialHash", result.url?.removeSuffix(".png"))
val imageData: ByteArray =
ImageDownloader().waitAndGetImage(result.url!!, { client })?.bytes
ImageDownloader().waitAndGetImage(result.url!!) { client }?.bytes
?: run {
fail("${server.name}: Should not be null")
return
@@ -142,8 +142,7 @@ class ImageUploadTesting {
ServerInfoRetriever()
.loadInfo(
server.baseUrl,
{ client },
)
) { client }
val payload = getBitmap()
val inputStream = payload.inputStream()
@@ -172,7 +171,7 @@ class ImageUploadTesting {
Assert.assertTrue("${server.name}: Invalid result url", url.startsWith("http"))
val imageData: ByteArray =
ImageDownloader().waitAndGetImage(url, { client })?.bytes
ImageDownloader().waitAndGetImage(url) { client }?.bytes
?: run {
fail("${server.name}: Should not be null")
return
@@ -147,12 +147,14 @@ class AppModules(
// Custom fetcher that considers tor settings and avoids forwarding.
val nip05Fetcher = OkHttpNip05Fetcher(roleBasedHttpClientBuilder::okHttpClientForNip05)
val namecoinElectrumxClient =
ElectrumXClient(
socketFactory = { roleBasedHttpClientBuilder.socketFactoryForNip05() },
)
val namecoinResolver =
NamecoinNameResolver(
electrumxClient =
ElectrumXClient(
socketFactory = { roleBasedHttpClientBuilder.socketFactoryForNip05() },
),
electrumxClient = namecoinElectrumxClient,
serverListProvider = {
if (roleBasedHttpClientBuilder.shouldUseTorForNIP05("https://electrumx.example.com")) {
TOR_ELECTRUMX_SERVERS
@@ -226,7 +228,7 @@ class AppModules(
val relayStats = RelayStats(client)
// Logs debug messages when needed
val detailedLogger = if (isDebug) RelayLogger(client, false, false) else null
val detailedLogger = if (isDebug) RelayLogger(client, debugSending = false, debugReceiving = false) else null
val relayReqStats = if (isDebug) RelayReqStats(client) else null
val logger = if (isDebug) RelaySpeedLogger(client) else null
@@ -1240,7 +1240,7 @@ class Account(
val event = signer.sign(template)
cache.justConsumeMyOwnEvent(event)
val relays = relayList(event)
if (relays != null && relays.isNotEmpty()) {
if (!relays.isNullOrEmpty()) {
client.send(event, relays.toSet())
} else {
client.send(event, computeRelayListToBroadcast(event))
@@ -1898,7 +1898,7 @@ class Account(
fun getRelevantReports(note: Note): Set<Note> {
val innerReports =
if (note.event is RepostEvent || note.event is GenericRepostEvent) {
note.replyTo?.map { getRelevantReports(it) }?.flatten() ?: emptyList()
note.replyTo?.flatMap { getRelevantReports(it) } ?: emptyList()
} else {
emptyList()
}
@@ -186,8 +186,8 @@ class AccountSettings(
var backupEphemeralChatList: EphemeralChatListEvent? = null,
var backupTrustProviderList: TrustProviderListEvent? = null,
val lastReadPerRoute: MutableStateFlow<Map<String, MutableStateFlow<Long>>> = MutableStateFlow(mapOf()),
val hasDonatedInVersion: MutableStateFlow<Set<String>> = MutableStateFlow(setOf<String>()),
val pendingAttestations: MutableStateFlow<Map<HexKey, String>> = MutableStateFlow<Map<HexKey, String>>(mapOf()),
val hasDonatedInVersion: MutableStateFlow<Set<String>> = MutableStateFlow(setOf()),
val pendingAttestations: MutableStateFlow<Map<HexKey, String>> = MutableStateFlow(mapOf()),
var backupNipA3PaymentTargets: PaymentTargetsEvent? = null,
) : EphemeralChatRepository,
PublicChatListRepository {
@@ -612,7 +612,7 @@ class AccountSettings(
route: String,
timestampInSecs: Long,
): MutableStateFlow<Long> =
MutableStateFlow<Long>(timestampInSecs).also { newFlow ->
MutableStateFlow(timestampInSecs).also { newFlow ->
lastReadPerRoute.update { it + Pair(route, newFlow) }
saveAccountSettings()
}
@@ -170,7 +170,7 @@ class AntiSpamFilter {
}
}
val flowSpam = MutableStateFlow<AntiSpamState>(AntiSpamState(this))
val flowSpam = MutableStateFlow(AntiSpamState(this))
}
class AntiSpamState(
@@ -25,14 +25,13 @@ import java.lang.ref.WeakReference
import java.util.concurrent.ConcurrentSkipListMap
import java.util.function.BiConsumer
class LargeSoftCache<K, V> : CacheOperations<K, V> {
protected val cache = ConcurrentSkipListMap<K, WeakReference<V>>()
class LargeSoftCache<K : Any, V : Any> : CacheOperations<K, V> {
private val cache = ConcurrentSkipListMap<K, WeakReference<V>>()
fun keys() = cache.keys
fun get(key: K): V? {
val softRef = cache.get(key)
if (softRef == null) return null
val softRef = cache.get(key) ?: return null
val value = softRef.get()
return if (value != null) {
@@ -99,7 +98,7 @@ class LargeSoftCache<K, V> : CacheOperations<K, V> {
// another thread may put in between
cache.remove(key, softRef)
val newObject = builder(key)
return cache.putIfAbsent(key, WeakReference(newObject))?.get() ?: newObject
cache.putIfAbsent(key, WeakReference(newObject))?.get() ?: newObject
}
}
}
@@ -135,7 +134,7 @@ class LargeSoftCache<K, V> : CacheOperations<K, V> {
.forEach(BiConsumerWrapper(this, consumer))
}
class BiConsumerWrapper<K, V>(
class BiConsumerWrapper<K : Any, V : Any>(
val cache: LargeSoftCache<K, V>,
val inner: BiConsumer<K, V>,
) : BiConsumer<K, WeakReference<V>> {
@@ -2770,10 +2770,9 @@ object LocalCache : ILocalCache, ICacheProvider {
val childrenToBeRemoved = mutableListOf<Note>()
val toBeRemoved =
account.hiddenUsers.flow.value.hiddenUsers
.map { userHex ->
(notes.filter { _, it -> it.event?.pubKey == userHex } + addressables.filter { _, it -> it.event?.pubKey == userHex }).toSet()
}.flatten()
account.hiddenUsers.flow.value.hiddenUsers.flatMap { userHex ->
(notes.filter { _, it -> it.event?.pubKey == userHex } + addressables.filter { _, it -> it.event?.pubKey == userHex }).toSet()
}
toBeRemoved.forEach {
removeFromCache(it)
@@ -34,9 +34,9 @@ class TorAwareOkHttpOtsResolverBuilder(
) : OtsResolverBuilder {
fun getAPI(usingTor: Boolean) =
if (usingTor) {
OkHttpBitcoinExplorer.Companion.MEMPOOL_API_URL
OkHttpBitcoinExplorer.MEMPOOL_API_URL
} else {
OkHttpBitcoinExplorer.Companion.BLOCKSTREAM_API_URL
OkHttpBitcoinExplorer.BLOCKSTREAM_API_URL
}
override fun build(): OtsResolver =
@@ -44,9 +44,9 @@ class TorAwareOkHttpOtsResolverBuilder(
explorer =
OkHttpBitcoinExplorer(
baseUrl = {
getAPI(usingTor = isTorActive(OkHttpBitcoinExplorer.Companion.MEMPOOL_API_URL))
getAPI(usingTor = isTorActive(OkHttpBitcoinExplorer.MEMPOOL_API_URL))
},
client = okHttpClient(OkHttpBitcoinExplorer.Companion.MEMPOOL_API_URL),
client = okHttpClient(OkHttpBitcoinExplorer.MEMPOOL_API_URL),
cache = cache,
),
calendar = OkHttpCalendar(okHttpClient),
@@ -62,7 +62,7 @@ class BookmarkListState(
fun getBookmarkList(): BookmarkListEvent? = bookmarkList.event as? BookmarkListEvent
suspend fun publicBookmarks(note: Note): List<BookmarkIdTag> {
fun publicBookmarks(note: Note): List<BookmarkIdTag> {
val noteEvent = note.event as? BookmarkListEvent
return noteEvent?.publicBookmarks() ?: emptyList()
}
@@ -47,7 +47,7 @@ class HiddenUsersState(
) {
var transientHiddenUsers: MutableStateFlow<Set<String>> = MutableStateFlow(setOf())
suspend fun assembleLiveHiddenUsers(
fun assembleLiveHiddenUsers(
blockList: List<MuteTag>,
muteList: List<MuteTag>,
transientHiddenUsers: Set<String>,
@@ -34,9 +34,9 @@ data class LabeledBookmarkList(
val privateBookmarks: Set<BookmarkIdTag> = emptySet(),
val publicBookmarks: Set<BookmarkIdTag> = emptySet(),
) {
val privatePostBookmarks = privateBookmarks.filter { it is EventBookmark }.map { bookmarkIdTag -> bookmarkIdTag as EventBookmark }
val publicPostBookmarks = publicBookmarks.filter { it is EventBookmark }.map { bookmarkIdTag -> bookmarkIdTag as EventBookmark }
val privatePostBookmarks = privateBookmarks.filterIsInstance<EventBookmark>()
val publicPostBookmarks = publicBookmarks.filterIsInstance<EventBookmark>()
val privateArticleBookmarks = privateBookmarks.filter { it is AddressBookmark }.map { bookmarkIdTag -> bookmarkIdTag as AddressBookmark }
val publicArticleBookmarks = publicBookmarks.filter { it is AddressBookmark }.map { bookmarkIdTag -> bookmarkIdTag as AddressBookmark }
val privateArticleBookmarks = privateBookmarks.filterIsInstance<AddressBookmark>()
val publicArticleBookmarks = publicBookmarks.filterIsInstance<AddressBookmark>()
}
@@ -51,7 +51,7 @@ class SearchRelayListState(
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
val searchListNote = cache.getOrCreateAddressableNote(getSearchRelayListAddress())
fun getSearchRelayListAddress() = SearchRelayListEvent.Companion.createAddress(signer.pubKey)
fun getSearchRelayListAddress() = SearchRelayListEvent.createAddress(signer.pubKey)
fun getSearchRelayListFlow(): StateFlow<NoteState> = searchListNote.flow().metadata.stateFlow
@@ -70,7 +70,7 @@ class SearchRelayListState(
.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Companion.Eagerly,
SharingStarted.Eagerly,
emptySet(),
)
@@ -68,7 +68,7 @@ class TrustedRelayListState(
.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Companion.Eagerly,
SharingStarted.Eagerly,
emptySet(),
)
@@ -76,13 +76,13 @@ class TrustedRelayListState(
val relayListForTrusted = getTrustedRelayList()
return if (relayListForTrusted != null && relayListForTrusted.tags.isNotEmpty()) {
TrustedRelayListEvent.Companion.updateRelayList(
TrustedRelayListEvent.updateRelayList(
earlierVersion = relayListForTrusted,
relays = trustedRelays,
signer = signer,
)
} else {
TrustedRelayListEvent.Companion.create(
TrustedRelayListEvent.create(
relays = trustedRelays,
signer = signer,
)
@@ -120,5 +120,5 @@ class BlossomServerListState(
suspend fun createBlossomDeleteAuth(
hash: HexKey,
alt: String,
): BlossomAuthorizationEvent? = BlossomAuthorizationEvent.createDeleteAuth(hash, alt, signer)
): BlossomAuthorizationEvent = BlossomAuthorizationEvent.createDeleteAuth(hash, alt, signer)
}
@@ -29,13 +29,13 @@ import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import java.io.IOException
suspend fun <T> DataStore<Preferences>.getProperty(
fun <T> DataStore<Preferences>.getProperty(
key: Preferences.Key<String>,
parser: (String) -> T,
serializer: (T) -> String,
scope: CoroutineScope,
): UpdatablePropertyFlow<T> =
UpdatablePropertyFlow<T>(
UpdatablePropertyFlow(
flow =
data
.catch { e ->
@@ -67,12 +67,12 @@ class EncryptedDataStore(
?.get(key)
?.let { decrypt(it) }
suspend fun <T> getProperty(
fun <T> getProperty(
key: Preferences.Key<String>,
parser: (String) -> T,
serializer: (T) -> String,
): UpdatablePropertyFlow<T> =
UpdatablePropertyFlow<T>(
UpdatablePropertyFlow(
flow =
store.data
.catch { e ->
@@ -60,8 +60,8 @@ class MergedFollowListsState(
val geotags: Set<String> = emptySet(),
val communities: Set<String> = emptySet(),
) {
val geotagScopes: Set<String> = geotags.mapTo(mutableSetOf<String>()) { GeohashId.toScope(it) }
val hashtagScopes: Set<String> = hashtags.mapTo(mutableSetOf<String>()) { HashtagId.toScope(it) }
val geotagScopes: Set<String> = geotags.mapTo(mutableSetOf()) { GeohashId.toScope(it) }
val hashtagScopes: Set<String> = hashtags.mapTo(mutableSetOf()) { HashtagId.toScope(it) }
}
fun mergeLists(
@@ -48,7 +48,7 @@ class OutboxLoaderState(
}.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Companion.Eagerly,
SharingStarted.Eagerly,
UnknownTopNavPerRelayFilterSet,
)
}
@@ -52,8 +52,8 @@ class AllFollowsByOutboxTopNavFilter(
val defaultRelays: StateFlow<Set<NormalizedRelayUrl>>,
val blockedRelays: StateFlow<Set<NormalizedRelayUrl>>,
) : IFeedTopNavFilter {
val geotagScopes: Set<String>? = geotags?.mapTo(mutableSetOf<String>()) { GeohashId.Companion.toScope(it) }
val hashtagScopes: Set<String>? = hashtags?.mapTo(mutableSetOf<String>()) { HashtagId.Companion.toScope(it) }
val geotagScopes: Set<String>? = geotags?.mapTo(mutableSetOf()) { GeohashId.toScope(it) }
val hashtagScopes: Set<String>? = hashtags?.mapTo(mutableSetOf()) { HashtagId.toScope(it) }
override fun matchAuthor(pubkey: HexKey): Boolean = authors == null || pubkey in authors
@@ -47,8 +47,8 @@ class AllFollowsByProxyTopNavFilter(
val communities: Set<String>? = null,
val proxyRelays: Set<NormalizedRelayUrl>,
) : IFeedTopNavFilter {
val geotagScopes: Set<String>? = geotags?.mapTo(mutableSetOf<String>()) { GeohashId.Companion.toScope(it) }
val hashtagScopes: Set<String>? = hashtags?.mapTo(mutableSetOf<String>()) { HashtagId.Companion.toScope(it) }
val geotagScopes: Set<String>? = geotags?.mapTo(mutableSetOf()) { GeohashId.toScope(it) }
val hashtagScopes: Set<String>? = hashtags?.mapTo(mutableSetOf()) { HashtagId.toScope(it) }
override fun matchAuthor(pubkey: HexKey): Boolean = authors == null || pubkey in authors
@@ -35,6 +35,6 @@ class AllFollowsTopNavPerRelayFilter(
val geotags: Set<String>? = null,
val communities: Set<String>? = null,
) : IFeedTopNavPerRelayFilter {
val geotagScopes: Set<String>? = geotags?.mapTo(mutableSetOf<String>()) { GeohashId.toScope(it) }
val hashtagScopes: Set<String>? = hashtags?.mapTo(mutableSetOf<String>()) { HashtagId.toScope(it) }
val geotagScopes: Set<String>? = geotags?.mapTo(mutableSetOf()) { GeohashId.toScope(it) }
val hashtagScopes: Set<String>? = hashtags?.mapTo(mutableSetOf()) { HashtagId.toScope(it) }
}
@@ -37,7 +37,7 @@ class LocationTopNavFilter(
val geotags: Set<String>,
val relayList: Set<NormalizedRelayUrl>,
) : IFeedTopNavFilter {
val geotagScopes: Set<String> = geotags.mapTo(mutableSetOf<String>()) { GeohashId.toScope(it) }
val geotagScopes: Set<String> = geotags.mapTo(mutableSetOf()) { GeohashId.toScope(it) }
override fun matchAuthor(pubkey: HexKey): Boolean = true
@@ -28,5 +28,5 @@ import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId
class LocationTopNavPerRelayFilter(
val geotags: Set<String>,
) : IFeedTopNavPerRelayFilter {
val geotagScopes: Set<String> = geotags.mapTo(mutableSetOf<String>()) { GeohashId.toScope(it) }
val geotagScopes: Set<String> = geotags.mapTo(mutableSetOf()) { GeohashId.toScope(it) }
}
@@ -62,7 +62,7 @@ class NoteFeedFlow(
): IFeedTopNavFilter =
when (noteEvent) {
is PeopleListEvent -> {
if (noteEvent.dTag() == PeopleListEvent.Companion.BLOCK_LIST_D_TAG) {
if (noteEvent.dTag() == PeopleListEvent.BLOCK_LIST_D_TAG) {
MutedAuthorsByOutboxTopNavFilter(caches.peopleListCache.cachedUserIdSet(noteEvent), blockedRelays)
} else {
AuthorsByOutboxTopNavFilter(caches.peopleListCache.cachedUserIdSet(noteEvent), blockedRelays)
@@ -109,7 +109,7 @@ class NoteFeedFlow(
) {
when (noteEvent) {
is PeopleListEvent -> {
if (noteEvent.dTag() == PeopleListEvent.Companion.BLOCK_LIST_D_TAG) {
if (noteEvent.dTag() == PeopleListEvent.BLOCK_LIST_D_TAG) {
emit(MutedAuthorsByOutboxTopNavFilter(caches.peopleListCache.userIdSet(noteEvent), blockedRelays))
} else {
emit(AuthorsByOutboxTopNavFilter(caches.peopleListCache.userIdSet(noteEvent), blockedRelays))
@@ -161,7 +161,7 @@ class NoteFeedFlow(
): IFeedTopNavFilter =
when (noteEvent) {
is PeopleListEvent -> {
if (noteEvent.dTag() == PeopleListEvent.Companion.BLOCK_LIST_D_TAG) {
if (noteEvent.dTag() == PeopleListEvent.BLOCK_LIST_D_TAG) {
MutedAuthorsByProxyTopNavFilter(caches.peopleListCache.cachedUserIdSet(noteEvent), proxyRelays)
} else {
AuthorsByProxyTopNavFilter(caches.peopleListCache.cachedUserIdSet(noteEvent), proxyRelays)
@@ -208,7 +208,7 @@ class NoteFeedFlow(
) {
when (noteEvent) {
is PeopleListEvent -> {
if (noteEvent.dTag() == PeopleListEvent.Companion.BLOCK_LIST_D_TAG) {
if (noteEvent.dTag() == PeopleListEvent.BLOCK_LIST_D_TAG) {
emit(MutedAuthorsByProxyTopNavFilter(caches.peopleListCache.userIdSet(noteEvent), proxyRelays))
} else {
emit(AuthorsByProxyTopNavFilter(caches.peopleListCache.userIdSet(noteEvent), proxyRelays))
@@ -272,7 +272,7 @@ class NoteFeedFlow(
override fun startValue(): IFeedTopNavFilter {
val noteEvent = metadataFlow.value?.note?.event
return if (noteEvent == null) {
return AuthorsByOutboxTopNavFilter(emptySet(), blockedRelays)
AuthorsByOutboxTopNavFilter(emptySet(), blockedRelays)
} else {
if (proxyRelays.value.isEmpty()) {
processByOutbox(noteEvent, outboxRelays.value)
@@ -83,7 +83,7 @@ class MeltProcessor {
)
}
suspend fun melt(
fun melt(
token: CashuToken,
lud16: String,
okHttpClient: (String) -> OkHttpClient,
@@ -32,7 +32,7 @@ class MemoryTrimmingService(
) {
var isTrimmingMemoryMutex = AtomicBoolean(false)
private suspend fun doTrim(
private fun doTrim(
account: Collection<Account>,
otherAccounts: List<AccountInfo>,
) {
@@ -65,14 +65,11 @@ class LightningAddressResolver {
okHttpClient: (String) -> OkHttpClient,
context: Context,
): String {
val url = assembleUrl(lnAddress)
if (url == null) {
throw LightningAddressError(
val url =
assembleUrl(lnAddress) ?: throw LightningAddressError(
stringRes(context, R.string.error_unable_to_fetch_invoice),
stringRes(context, R.string.could_not_assemble_lnurl_from_lightning_address_check_the_user_s_setup, lnAddress),
)
}
val client = okHttpClient(url)
@@ -126,12 +123,14 @@ class LightningAddressResolver {
okHttpClient: (String) -> OkHttpClient,
context: Context,
): String {
@Suppress("BlockingMethodInNonBlockingContext") // URLEncoder.encode is CPU-only, not I/O blocking
val encodedMessage = URLEncoder.encode(message, "utf-8")
val urlBinder = if (lnCallback.contains("?")) "&" else "?"
var url = "$lnCallback${urlBinder}amount=$milliSats&comment=$encodedMessage"
if (nostrRequest != null) {
@Suppress("BlockingMethodInNonBlockingContext") // URLEncoder.encode is CPU-only, not I/O blocking
val encodedNostrRequest = URLEncoder.encode(nostrRequest.toJson(), "utf-8")
url += "&nostr=$encodedNostrRequest"
}
@@ -55,7 +55,7 @@ class LocationState(
object Loading : LocationResult()
}
private var hasLocationPermission = MutableStateFlow<Boolean>(false)
private var hasLocationPermission = MutableStateFlow(false)
private var latestLocation: LocationResult = LocationResult.Loading
fun setLocationPermission(newValue: Boolean) {
@@ -20,6 +20,7 @@
*/
package com.vitorpamplona.amethyst.service.namecoin
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.DEFAULT_ELECTRUMX_SERVERS
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.ElectrumXClient
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.ElectrumxServer
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.NamecoinLookupCache
@@ -43,12 +44,17 @@ class NamecoinNameService(
) {
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val resolver = NamecoinNameResolver(electrumxClient)
private val cache = NamecoinLookupCache()
// Custom server list (user-configurable)
@Volatile
private var customServers: List<ElectrumxServer> = emptyList()
private val resolver =
NamecoinNameResolver(
electrumxClient = electrumxClient,
serverListProvider = { customServers.ifEmpty { DEFAULT_ELECTRUMX_SERVERS } },
)
private val cache = NamecoinLookupCache()
// ── Public API ─────────────────────────────────────────────────────
/**
@@ -97,7 +103,10 @@ class NamecoinNameService(
*
* Useful for composable UIs that observe resolution state.
*/
fun resolveLive(identifier: String): StateFlow<NamecoinResolveState> {
fun resolveLive(
identifier: String,
scope: CoroutineScope = this.scope,
): StateFlow<NamecoinResolveState> {
val state = MutableStateFlow<NamecoinResolveState>(NamecoinResolveState.Loading)
scope.launch {
try {
@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.service.okhttp
import okhttp3.internal.concurrent.TaskRunner
import okhttp3.internal.http2.Http2
import java.io.Closeable
import java.util.Locale
import java.util.concurrent.CopyOnWriteArraySet
import java.util.logging.ConsoleHandler
import java.util.logging.Handler
@@ -45,7 +46,7 @@ object OkHttpDebugLogging {
level = Level.FINE
formatter =
object : SimpleFormatter() {
override fun format(record: LogRecord) = String.format("[%1\$tF %1\$tT] %2\$s %n", record.millis, record.message)
override fun format(record: LogRecord) = String.format(Locale.ROOT, $$"[%1$tF %1$tT] %2$s %n", record.millis, record.message)
}
}
@@ -48,8 +48,7 @@ class OkHttpWebSocket(
override fun needsReconnect(): Boolean {
if (socket == null) return true
val myUsingOkHttp = usingOkHttp
if (myUsingOkHttp == null) return true
val myUsingOkHttp = usingOkHttp ?: return true
val currentOkHttp = httpClient(url)
@@ -102,7 +102,7 @@ fun VideoView(
) {
val automaticallyStartPlayback =
remember {
mutableStateOf<Boolean>(
mutableStateOf(
if (alwaysShowVideo) true else accountViewModel.settings.startVideoPlayback(),
)
}
@@ -30,7 +30,7 @@ import com.vitorpamplona.amethyst.service.playback.composable.mainVideo.VideoPla
import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.GetMediaItem
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
public val DEFAULT_MUTED_SETTING = mutableStateOf(true)
val DEFAULT_MUTED_SETTING = mutableStateOf(true)
@Composable
fun VideoViewInner(
@@ -76,7 +76,7 @@ fun MuteButton(
) {
val holdOn =
remember {
mutableStateOf<Boolean>(
mutableStateOf(
true,
)
}
@@ -77,7 +77,7 @@ class IntentExtras {
data.mimeType?.let { putString("mimeType", it) }
data.aspectRatio?.let { putFloat("aspectRatio", it) }
data.proxyPort?.let { putInt("proxyPort", it) }
data.keepPlaying.let { putBoolean("keepPlaying", it) }
putBoolean("keepPlaying", data.keepPlaying)
data.waveformData?.let { putFloatArray("wavefrontData", it.wave.toFloatArray()) }
bounds?.let { putInt("boundLeft", it.left) }
@@ -20,8 +20,6 @@
*/
package com.vitorpamplona.amethyst.service.playback.pip
import android.content.Context.RECEIVER_EXPORTED
import android.os.Build
import androidx.annotation.OptIn
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
@@ -34,6 +32,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.core.content.ContextCompat
import androidx.media3.common.util.UnstableApi
import androidx.media3.ui.compose.ContentFrame
import androidx.media3.ui.compose.state.rememberMuteButtonState
@@ -91,13 +90,8 @@ fun RegisterControllerReceiver(controllerState: MediaControllerState) {
onPlayPause = controllerState::togglePlayPause,
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
context.registerReceiver(receiver, receiver.filterMute, RECEIVER_EXPORTED)
context.registerReceiver(receiver, receiver.filterPlayPause, RECEIVER_EXPORTED)
} else {
context.registerReceiver(receiver, receiver.filterMute)
context.registerReceiver(receiver, receiver.filterPlayPause)
}
ContextCompat.registerReceiver(context, receiver, receiver.filterMute, ContextCompat.RECEIVER_EXPORTED)
ContextCompat.registerReceiver(context, receiver, receiver.filterPlayPause, ContextCompat.RECEIVER_EXPORTED)
onDispose {
context.unregisterReceiver(receiver)
@@ -35,7 +35,7 @@ import okhttp3.OkHttpClient
class ExoPlayerBuilder(
val okHttp: OkHttpClient,
) {
fun build(context: Context) =
fun build(context: Context): ExoPlayer =
ExoPlayer
.Builder(context)
.apply {
@@ -26,7 +26,7 @@ class ListWithUniqueSetCache<T, U>(
val key: (T) -> U,
) {
private val list = AtomicReference(listOf<T>())
private val cacheSet = AtomicReference<Set<U>?>(setOf<U>())
private val cacheSet = AtomicReference<Set<U>?>(setOf())
fun isEmpty() = list.get().isEmpty()
@@ -41,7 +41,7 @@ class ListWithUniqueSetCache<T, U>(
}
fun distinct(): Set<U> {
var currentSet = cacheSet.get()
val currentSet = cacheSet.get()
// Check if the cached set is based on the current list
if (currentSet != null) {
@@ -49,7 +49,7 @@ class ListWithUniqueSetCache<T, U>(
}
// Compute and attempt to atomically update the cache
val newSet = list.get().mapTo(mutableSetOf<U>(), key)
val newSet = list.get().mapTo(mutableSetOf(), key)
cacheSet.compareAndSet(currentSet, newSet)
return newSet
}
@@ -20,7 +20,6 @@
*/
package com.vitorpamplona.amethyst.service.relayClient.reqCommand
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManagerControls
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountFilterAssembler
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.ChannelFinderFilterAssemblyGroup
@@ -88,7 +87,7 @@ class RelaySubscriptionsCoordinator(
val nwc = NWCPaymentFilterAssembler(client)
val all =
listOf<ComposeSubscriptionManagerControls>(
listOf(
account,
home,
chatroomList,
@@ -36,7 +36,7 @@ fun filterDraftsFromKey(
pubkey: HexKey?,
since: Long?,
): List<RelayBasedFilter> {
if (pubkey == null || pubkey.isEmpty()) return emptyList()
if (pubkey.isNullOrEmpty()) return emptyList()
return listOf(
RelayBasedFilter(
@@ -72,7 +72,7 @@ class AccountFollowsLoaderSubAssembler(
* This assembler saves the EOSE per user key. That EOSE includes their metadata, etc
* and reports, but only from trusted accounts (follows of all logged in users).
*/
val hasTried: EOSEAccountFast<User> = EOSEAccountFast<User>(2000)
val hasTried: EOSEAccountFast<User> = EOSEAccountFast(2000)
// updates all filters
override fun invalidateFilters(ignoreIfDoing: Boolean) {
@@ -92,7 +92,7 @@ fun pickRelaysToLoadUsers(
val outbox = key.authorRelayList()?.writeRelaysNorm()
if (outbox != null && outbox.isNotEmpty()) {
if (!outbox.isNullOrEmpty()) {
// If there is a home, get from it.
// if it tried all outbox relays, stop.
@@ -70,7 +70,7 @@ fun filterBasicAccountInfoFromKeys(
otherAccounts: List<HexKey>?,
since: Long?,
): List<RelayBasedFilter> {
if (otherAccounts == null || otherAccounts.isEmpty()) return emptyList()
if (otherAccounts.isNullOrEmpty()) return emptyList()
return listOf(
RelayBasedFilter(
@@ -38,7 +38,7 @@ fun filterBookmarksAndReportsFromKey(
pubkey: HexKey?,
since: Long?,
): List<RelayBasedFilter> {
if (pubkey == null || pubkey.isEmpty()) return emptyList()
if (pubkey.isNullOrEmpty()) return emptyList()
return listOf(
RelayBasedFilter(
@@ -49,7 +49,7 @@ class AccountNotificationsEoseFromInboxRelaysManager(
override fun updateFilter(
key: AccountQueryState,
since: SincePerRelayMap?,
): List<RelayBasedFilter>? =
): List<RelayBasedFilter> =
key.account.notificationRelays.flow.value.flatMap {
filterSummaryNotificationsToPubkey(
relay = it,
@@ -50,7 +50,7 @@ class AccountNotificationsEoseFromRandomRelaysManager(
override fun updateFilter(
key: AccountQueryState,
since: SincePerRelayMap?,
): List<RelayBasedFilter>? {
): List<RelayBasedFilter> {
// only loads this after the feed is built
val defaultSince = key.feedContentStates.notifications.lastNoteCreatedAtIfFilled() ?: TimeUtils.oneWeekAgo()
return (key.account.followsPerRelay.value.keys - key.account.notificationRelays.flow.value).flatMap {
@@ -90,7 +90,7 @@ fun filterSummaryNotificationsToPubkey(
pubkey: HexKey?,
since: Long?,
): List<RelayBasedFilter> {
if (pubkey == null || pubkey.isEmpty()) return emptyList()
if (pubkey.isNullOrEmpty()) return emptyList()
return listOf(
RelayBasedFilter(
@@ -111,7 +111,7 @@ fun filterNotificationsToPubkey(
pubkey: HexKey?,
since: Long?,
): List<RelayBasedFilter> {
if (pubkey == null || pubkey.isEmpty()) return emptyList()
if (pubkey.isNullOrEmpty()) return emptyList()
return listOf(
RelayBasedFilter(
@@ -142,7 +142,7 @@ fun filterJustTheLatestNotificationsToPubkeyFromRandomRelays(
pubkey: HexKey?,
since: Long?,
): List<RelayBasedFilter> {
if (pubkey == null || pubkey.isEmpty()) return emptyList()
if (pubkey.isNullOrEmpty()) return emptyList()
return listOf(
RelayBasedFilter(
@@ -32,7 +32,7 @@ fun filterGiftWrapsToPubkey(
pubkey: HexKey?,
since: Long?,
): List<RelayBasedFilter> {
if (pubkey == null || pubkey.isEmpty()) return emptyList()
if (pubkey.isNullOrEmpty()) return emptyList()
return listOf(
RelayBasedFilter(
@@ -38,7 +38,7 @@ class ChannelLoaderSubAssembler(
client: INostrClient,
allKeys: () -> Set<ChannelFinderQueryState>,
) : SingleSubNoEoseCacheEoseManager<ChannelFinderQueryState>(client, allKeys, invalidateAfterEose = true) {
override fun updateFilter(keys: List<ChannelFinderQueryState>): List<RelayBasedFilter>? = filterMissingChannelsById(keys)
override fun updateFilter(keys: List<ChannelFinderQueryState>): List<RelayBasedFilter> = filterMissingChannelsById(keys)
override fun distinct(key: ChannelFinderQueryState) = key.channel
}
@@ -40,7 +40,7 @@ fun potentialRelaysToFindAddress(note: AddressableNote): Set<NormalizedRelayUrl>
LocalCache.getAnyChannel(note)?.relays()?.let { set.addAll(it) }
note.replyTo?.map { parentNote ->
note.replyTo?.forEach { parentNote ->
set.addAll(parentNote.relays)
LocalCache.getAnyChannel(parentNote)?.relays()?.let { set.addAll(it) }
@@ -48,7 +48,7 @@ fun potentialRelaysToFindAddress(note: AddressableNote): Set<NormalizedRelayUrl>
parentNote.author?.inboxRelays()?.let { set.addAll(it) }
}
note.replies.map { childNote ->
note.replies.forEach { childNote ->
set.addAll(childNote.relays)
LocalCache.getAnyChannel(childNote)?.relays()?.let { set.addAll(it) }
@@ -63,7 +63,7 @@ fun potentialRelaysToFindAddress(note: AddressableNote): Set<NormalizedRelayUrl>
}
}
note.boosts.map { childNote ->
note.boosts.forEach { childNote ->
set.addAll(childNote.relays)
childNote.author?.outboxRelays()?.let { set.addAll(it) }
}
@@ -71,7 +71,7 @@ fun potentialRelaysToFindAddress(note: AddressableNote): Set<NormalizedRelayUrl>
return set
}
fun filterMissingAddressables(keys: List<EventFinderQueryState>): List<RelayBasedFilter>? {
fun filterMissingAddressables(keys: List<EventFinderQueryState>): List<RelayBasedFilter> {
val addressesPerRelay =
mapOfSet {
keys.forEach { key ->
@@ -36,7 +36,7 @@ fun potentialRelaysToFindEvent(note: Note): Set<NormalizedRelayUrl> {
LocalCache.getAnyChannel(note)?.relays()?.let { set.addAll(it) }
note.replyTo?.map { parentNote ->
note.replyTo?.forEach { parentNote ->
set.addAll(parentNote.relays)
LocalCache.getAnyChannel(parentNote)?.relays()?.let { set.addAll(it) }
@@ -44,7 +44,7 @@ fun potentialRelaysToFindEvent(note: Note): Set<NormalizedRelayUrl> {
parentNote.author?.inboxRelays()?.let { set.addAll(it) }
}
note.replies.map { childNote ->
note.replies.forEach { childNote ->
set.addAll(childNote.relays)
LocalCache.getAnyChannel(childNote)?.relays()?.let { set.addAll(it) }
@@ -59,7 +59,7 @@ fun potentialRelaysToFindEvent(note: Note): Set<NormalizedRelayUrl> {
}
}
note.boosts.map { childNote ->
note.boosts.forEach { childNote ->
set.addAll(childNote.relays)
childNote.author?.outboxRelays()?.let { set.addAll(it) }
}
@@ -67,7 +67,7 @@ fun potentialRelaysToFindEvent(note: Note): Set<NormalizedRelayUrl> {
return set
}
fun filterMissingEvents(keys: List<EventFinderQueryState>): List<RelayBasedFilter>? {
fun filterMissingEvents(keys: List<EventFinderQueryState>): List<RelayBasedFilter> {
val eventsPerRelay =
mapOfSet {
keys.forEach { key ->
@@ -37,7 +37,7 @@ class EventWatcherSubAssembler(
allKeys: () -> Set<EventFinderQueryState>,
) : SingleSubEoseManager<EventFinderQueryState>(client, allKeys) {
var lastNotesOnFilter = emptyList<Note>()
var latestEOSEs: EOSEAccountFast<Note> = EOSEAccountFast<Note>(1000)
var latestEOSEs: EOSEAccountFast<Note> = EOSEAccountFast(1000)
override fun newEose(
relay: NormalizedRelayUrl,
@@ -49,7 +49,7 @@ class UserOutboxFinderSubAssembler(
* This assembler saves the EOSE per user key. That EOSE includes their metadata, etc
* and reports, but only from trusted accounts (follows of all logged in users).
*/
var hasTried: EOSEAccountFast<User> = EOSEAccountFast<User>(200)
var hasTried: EOSEAccountFast<User> = EOSEAccountFast(200)
fun newEose(
relay: NormalizedRelayUrl,
@@ -68,7 +68,7 @@ class UserCardsSubAssembler(
val trustedAccounts: Map<NormalizedRelayUrl, Set<HexKey>> =
mapOfSet {
accounts.forEach { account ->
account.homeRelays.flow.value.map {
account.homeRelays.flow.value.forEach {
add(it, account.userProfile().pubkeyHex)
}
}
@@ -30,8 +30,8 @@ import kotlin.concurrent.atomics.ExperimentalAtomicApi
class KindGroup(
var count: AtomicInteger = AtomicInteger(0),
var memory: AtomicInteger = AtomicInteger(0),
val subs: LargeCache<String, AtomicInteger> = LargeCache<String, AtomicInteger>(),
val relays: LargeCache<NormalizedRelayUrl, AtomicInteger> = LargeCache<NormalizedRelayUrl, AtomicInteger>(),
val subs: LargeCache<String, AtomicInteger> = LargeCache(),
val relays: LargeCache<NormalizedRelayUrl, AtomicInteger> = LargeCache(),
) {
companion object {
const val MB: Int = 1024
@@ -32,7 +32,7 @@ typealias MutableTime = com.vitorpamplona.amethyst.commons.relays.MutableTime
open class EOSEByKey<U : Any>(
cacheSize: Int = 200,
) {
var followList: LruCache<U, EOSERelayList> = LruCache<U, EOSERelayList>(cacheSize)
var followList: LruCache<U, EOSERelayList> = LruCache(cacheSize)
fun addOrUpdate(
listCode: U,
@@ -99,7 +99,7 @@ open class EOSEAccountKey<U : Any>(
class EOSEAccountFast<T : Any>(
cacheSize: Int = 20,
) {
private val users: LruCache<T, EOSERelayList> = LruCache<T, EOSERelayList>(cacheSize)
private val users: LruCache<T, EOSERelayList> = LruCache(cacheSize)
private val lock = Any()
fun addOrUpdate(
@@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.service.tts
import android.content.Context
import android.media.AudioAttributes
import android.os.Build
import android.speech.tts.TextToSpeech
import android.speech.tts.UtteranceProgressListener
import java.util.Locale
@@ -79,15 +78,13 @@ class TextToSpeechEngine private constructor() {
engine.setPitch(defaultPitch)
engine.setSpeechRate(defaultSpeed)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val audioAttributes =
AudioAttributes
.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.setUsage(AudioAttributes.USAGE_MEDIA)
.build()
engine.setAudioAttributes(audioAttributes)
}
val audioAttributes =
AudioAttributes
.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.setUsage(AudioAttributes.USAGE_MEDIA)
.build()
engine.setAudioAttributes(audioAttributes)
engine.setListener(
onStart = { onStartListener?.invoke() },
@@ -170,7 +170,7 @@ internal fun <T> Iterable<T>.fillToSize(
transform: (List<T>) -> T,
): List<T> {
val capacity = ceil(size.safeDiv(count())).roundToInt()
return map { data -> List(capacity) { data } }.flatten().chunkToSize(size, transform)
return flatMap { data -> List(capacity) { data } }.chunkToSize(size, transform)
}
internal fun <T> Iterable<T>.chunkToSize(
@@ -207,6 +207,4 @@ internal fun Iterable<Float>.normalize(
return values.map { scale * ((it - currentMin) / range) + min }
}
private fun Int.safeDiv(value: Int): Float {
return if (value == 0) return 0F else this / value.toFloat()
}
private fun Int.safeDiv(value: Int): Float = if (value == 0) 0F else this / value.toFloat()
@@ -108,7 +108,7 @@ fun LoadOrCreateNote(
content: @Composable (Note?) -> Unit,
) {
var note by
remember(event.id) { mutableStateOf<Note?>(accountViewModel.getNoteIfExists(event.id)) }
remember(event.id) { mutableStateOf(accountViewModel.getNoteIfExists(event.id)) }
if (note == null) {
LaunchedEffect(key1 = event.id) {
@@ -117,7 +117,7 @@ fun SensitivityWarning(
@Composable
fun ContentWarningNotePreview() {
ThemeComparisonColumn {
ContentWarningNote({})
ContentWarningNote {}
}
}
@@ -49,7 +49,7 @@ class ChannelFeedContentState(
val feedContent = _feedContent.asStateFlow()
// Simple counter that changes when it needs to invalidate everything
private val _scrollToTop = MutableStateFlow<Int>(0)
private val _scrollToTop = MutableStateFlow(0)
val scrollToTop = _scrollToTop.asStateFlow()
var scrolltoTopPending = false
@@ -64,7 +64,7 @@ class ChannelFeedContentState(
viewModelScope.launch(Dispatchers.IO) { _scrollToTop.emit(_scrollToTop.value + 1) }
}
suspend fun sentToTop() {
fun sentToTop() {
scrolltoTopPending = false
}
@@ -98,10 +98,10 @@ class ChannelFeedContentState(
if (notes.isEmpty()) {
_feedContent.tryEmit(ChannelFeedState.Empty)
} else if (currentState is ChannelFeedState.Loaded) {
currentState.feed.tryEmit(LoadedFeedState<Channel>(notes, localFilter.showHiddenKey()))
currentState.feed.tryEmit(LoadedFeedState(notes, localFilter.showHiddenKey()))
} else {
_feedContent.tryEmit(
ChannelFeedState.Loaded(MutableStateFlow(LoadedFeedState<Channel>(notes, localFilter.showHiddenKey()))),
ChannelFeedState.Loaded(MutableStateFlow(LoadedFeedState(notes, localFilter.showHiddenKey()))),
)
}
}
@@ -18,7 +18,7 @@
* 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.myapplication
package com.vitorpamplona.amethyst.ui.layouts
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.AnimationState
@@ -18,7 +18,7 @@
* 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.myapplication
package com.vitorpamplona.amethyst.ui.layouts
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
@@ -32,8 +32,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.myapplication.DisappearingBottomBar
import com.vitorpamplona.myapplication.DisappearingFloatingButton
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -130,7 +130,7 @@ fun DisplayAccount(
accountSessionManager: AccountSessionManager,
) {
var baseUser by remember(acc) {
mutableStateOf<User?>(
mutableStateOf(
decodePublicKeyAsHexOrNull(acc.npub)?.let {
LocalCache.getUserIfExists(it)
},
@@ -87,7 +87,7 @@ class Nav(
) {
navigationScope.launch {
controller.navigate(route) {
popUpTo<T>(klass) { inclusive = true }
popUpTo(klass) { inclusive = true }
}
}
}
@@ -323,7 +323,7 @@ suspend fun routeEditDraftTo(
is ChatroomKeyable -> {
val room = draft.chatroomKey(account.userProfile().pubkeyHex)
account.chatroomList.getOrCreatePrivateChatroom(room)
return Route.Room(room, draftId = note.idHex)
Route.Room(room, draftId = note.idHex)
}
is TextNoteEvent -> {
@@ -98,7 +98,7 @@ fun HiddenNotePreview() {
ThemeComparisonColumn(
toPreview = {
HiddenNote(
reports = persistentSetOf<Note>(),
reports = persistentSetOf(),
isHiddenAuthor = true,
accountViewModel = mockAccountViewModel(),
nav = EmptyNav(),
@@ -167,7 +167,7 @@ fun ObserveAllStatusesToAvoidSwitchigAllTheTime(
statuses: ImmutableList<AddressableNote>,
accountViewModel: AccountViewModel,
) {
statuses.map {
statuses.forEach {
EventFinderFilterAssemblerSubscription(it, accountViewModel)
}
}
@@ -240,6 +240,7 @@ import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent
import com.vitorpamplona.quartz.nipA0VoiceMessages.BaseVoiceEvent
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
@Composable
fun NoteCompose(
@@ -536,7 +537,7 @@ fun ClickableNote(
nav.nav {
if (redirectToNote.event is DraftWrapEvent) {
with(Dispatchers.IO) {
withContext(Dispatchers.IO) {
routeEditDraftTo(redirectToNote, accountViewModel.account)
}
} else {
@@ -481,7 +481,7 @@ private fun ReactionDetailGallery(
accountViewModel: AccountViewModel,
) {
val defaultBackgroundColor = MaterialTheme.colorScheme.background
val backgroundColor = remember { mutableStateOf<Color>(defaultBackgroundColor) }
val backgroundColor = remember { mutableStateOf(defaultBackgroundColor) }
val hasReactions by observeNoteReferences(baseNote, accountViewModel)
@@ -757,7 +757,7 @@ private fun SlidingAnimationCount(
if (accountViewModel.settings.isPerformanceMode()) {
TextCount(baseCount, textColor)
} else {
AnimatedContent<Int>(
AnimatedContent(
targetState = baseCount,
transitionSpec = AnimatedContentTransitionScope<Int>::transitionSpec,
label = "SlidingAnimationCount",
@@ -86,8 +86,8 @@ fun ReplyInformationChannel(
onUserTagClick: (User) -> Unit,
) {
FlowRow {
if (mentions != null && mentions.isNotEmpty()) {
if (replyTo != null && replyTo.isNotEmpty()) {
if (!mentions.isNullOrEmpty()) {
if (!replyTo.isNullOrEmpty()) {
Text(
stringRes(id = R.string.replying_to),
fontSize = 13.sp,
@@ -32,7 +32,7 @@ import kotlin.math.round
private const val YEAR_DATE_FORMAT = "MMM dd, yyyy"
private const val MONTH_DATE_FORMAT = "MMM dd"
var locale = Locale.getDefault()
var locale: Locale = Locale.getDefault()
var yearFormatter = SimpleDateFormat(YEAR_DATE_FORMAT, locale)
var monthFormatter = SimpleDateFormat(MONTH_DATE_FORMAT, locale)
@@ -262,7 +262,7 @@ class PollNoteViewModel : ViewModel() {
valueMinimum?.let { minimum ->
valueMaximum?.let { maximum ->
if (minimum != maximum) {
options.add(((minimum + maximum) / 2).toLong())
options.add(((minimum + maximum) / 2))
}
}
}
@@ -34,9 +34,9 @@ import kotlin.uuid.Uuid
@Stable
class DraftTagState {
var current: String by mutableStateOf(newTag())
var usedDraftTags by mutableStateOf(setOf<String>(current))
var usedDraftTags by mutableStateOf(setOf(current))
private val _versions = MutableStateFlow<Int>(0)
private val _versions = MutableStateFlow(0)
@OptIn(FlowPreview::class)
val versions = _versions.debounce(1000)
@@ -43,7 +43,7 @@ class UserSuggestionState(
val account: Account,
val requireAtSymbol: Boolean = true,
) {
val invalidations = MutableStateFlow<Int>(0)
val invalidations = MutableStateFlow(0)
val currentWord = MutableStateFlow("")
val searchDataSourceState = SearchQueryState(MutableStateFlow(""), account)
@@ -51,7 +51,7 @@ fun DisplayZapSplits(
remember(noteEvent) {
val list = noteEvent.zapSplitSetup()
if (list.isEmpty() && useAuthorIfEmpty) {
listOf<ZapSplitSetup>(
listOf(
ZapSplitSetup(
pubKeyHex = noteEvent.pubKey,
relay = null,
@@ -116,13 +116,13 @@ private fun RenderPledgeAmount(
) {
val repliesState by observeNoteReplies(baseNote, accountViewModel)
var reward by remember {
mutableStateOf<String>(
mutableStateOf(
showAmount(baseReward.amount),
)
}
var hasPledge by remember {
mutableStateOf<Boolean>(
mutableStateOf(
false,
)
}
@@ -58,7 +58,7 @@ fun DisplayUncitedHashtags(
nav: INav,
) {
val unusedHashtags by
produceState(initialValue = emptyList<String>()) {
produceState(initialValue = emptyList()) {
val tagsInEvent = event.hashtags()
if (tagsInEvent.isNotEmpty()) {
val state = CachedRichTextParser.parseText(content, event.tags.toImmutableListOfLists(), callbackUri)
@@ -66,15 +66,15 @@ fun DisplayUncitedHashtags(
val tagsInContent =
state
.paragraphs
.map {
it.words.mapNotNull {
.flatMap { paragraphState ->
paragraphState.words.mapNotNull {
if (it is HashTagSegment) {
it.hashtag
} else {
null
}
}
}.flatten()
}
val unusedHashtags =
tagsInEvent.filterNot { eventTag ->
@@ -474,7 +474,7 @@ open class CommentPostViewModel :
myMultiOrchestrator.upload(
alt,
contentWarningReason,
MediaCompressor.Companion.intToCompressorQuality(mediaQuality),
MediaCompressor.intToCompressorQuality(mediaQuality),
server,
account,
context,
@@ -703,5 +703,5 @@ open class CommentPostViewModel :
return location!!
}
override fun locationManager(): LocationState = Amethyst.Companion.instance.locationManager
override fun locationManager(): LocationState = Amethyst.instance.locationManager
}
@@ -63,7 +63,7 @@ import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent
import com.vitorpamplona.quartz.nip30CustomEmoji.taggedEmojis
@Composable
public fun RenderEmojiPack(
fun RenderEmojiPack(
baseNote: Note,
actionable: Boolean,
backgroundColor: MutableState<Color>,
@@ -86,7 +86,7 @@ public fun RenderEmojiPack(
@OptIn(ExperimentalLayoutApi::class)
@Composable
public fun RenderEmojiPack(
fun RenderEmojiPack(
noteEvent: EmojiPackEvent,
baseNote: Note,
actionable: Boolean,
@@ -649,12 +649,12 @@ fun RenderPollGameResultsPreview() {
Column(Modifier.padding(10.dp)) {
RenderPoll(
note,
false,
true,
2,
remember { mutableStateOf(Color.Transparent) },
mockAccountViewModel(),
EmptyNav(),
makeItShort = false,
canPreview = true,
quotesLeft = 2,
backgroundColor = remember { mutableStateOf(Color.Transparent) },
accountViewModel = mockAccountViewModel(),
nav = EmptyNav(),
)
}
}
@@ -758,12 +758,12 @@ fun RenderPollColorResultsPreview() {
Column(Modifier.padding(10.dp)) {
RenderPoll(
note,
false,
true,
2,
remember { mutableStateOf(Color.Transparent) },
mockAccountViewModel(),
EmptyNav(),
makeItShort = false,
canPreview = true,
quotesLeft = 2,
backgroundColor = remember { mutableStateOf(Color.Transparent) },
accountViewModel = mockAccountViewModel(),
nav = EmptyNav(),
)
}
}
@@ -35,7 +35,6 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextOverflow
import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.components.RenderUserAsClickableText
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
@@ -102,7 +101,7 @@ fun DisplayUncitedUsers(
nav: INav,
) {
@Suppress("ProduceStateDoesNotAssignValue")
val uncitedUsers by produceState(initialValue = emptyList<User>()) {
val uncitedUsers by produceState(initialValue = emptyList()) {
val users = event.groupKeySetWithoutOwner() - event.citedUsers()
if (users.isNotEmpty()) {
val newUsers = accountViewModel.loadUsersSync(users.toList())
@@ -139,12 +139,12 @@ fun TorrentCommentPreview() {
toPreview = {
RenderTorrentComment(
comment,
false,
true,
3,
ReplyRenderType.FULL,
remember { mutableStateOf(Color.Transparent) },
EmptyState,
makeItShort = false,
canPreview = true,
quotesLeft = 3,
unPackReply = ReplyRenderType.FULL,
backgroundColor = remember { mutableStateOf(Color.Transparent) },
editState = EmptyState,
accountViewModel = accountViewModel,
nav = nav,
)
@@ -118,7 +118,7 @@ class AccountSessionManager(
}
}
private suspend fun requestLoginUI() = _accountContent.update { AccountState.LoggedOff }
private fun requestLoginUI() = _accountContent.update { AccountState.LoggedOff }
suspend fun loginAndStartUI(
key: String,
@@ -65,7 +65,9 @@ class CompositionObserver(
private val vmStore: StoreOwnerRegistry,
private val key: Any,
) : RememberObserver {
override fun onRemembered() {}
override fun onRemembered() {
// No action needed when remembered — registration happens at construction time.
}
override fun onForgotten() = vmStore.composableDetached(key)
@@ -357,10 +357,10 @@ class AccountViewModel(
return if (isPostHidden || isDecryptedPostHidden) {
// Spam + Blocked Users + Hidden Words + Sensitive Content
NoteComposeReportState(isPostHidden, false, false, isHiddenAuthor)
NoteComposeReportState(isPostHidden, isAcceptable = false, canPreview = false, isHiddenAuthor = isHiddenAuthor)
} else if (isFromLoggedIn || isFromLoggedInFollow) {
// No need to process if from trusted people
NoteComposeReportState(isPostHidden, true, true, isHiddenAuthor)
NoteComposeReportState(isPostHidden, isAcceptable = true, canPreview = true, isHiddenAuthor = isHiddenAuthor)
} else {
val newCanPreview = !note.hasAnyReports()
@@ -368,7 +368,7 @@ class AccountViewModel(
if (newCanPreview && newIsAcceptable) {
// No need to process reports if nothing is wrong
NoteComposeReportState(isPostHidden, true, true, false)
NoteComposeReportState(isPostHidden, isAcceptable = true, canPreview = true, isHiddenAuthor = false)
} else {
NoteComposeReportState(
isPostHidden,
@@ -511,7 +511,7 @@ class AccountViewModel(
}.toMutableMap()
val results =
mapNotNullAsync<CombinedZap, DecryptedInfo>(
mapNotNullAsync(
zaps.filter { (it.request.event as? LnZapRequestEvent)?.isPrivateZap() == true },
) { next ->
val info = innerDecryptAmountMessage(next.request, next.response)
@@ -1609,7 +1609,7 @@ class AccountViewModel(
}.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5000),
emptySet<HexKey>(),
emptySet(),
)
val draftNoteCache = CachedDraftNotes(this)
@@ -173,7 +173,7 @@ fun ChatBubbleLayout(
private fun BubblePreview() {
val bgColor =
remember {
mutableStateOf<Color>(Color.Transparent)
mutableStateOf(Color.Transparent)
}
Column {
@@ -222,8 +222,8 @@ fun RenderRelayLinePreview() {
"wss://nos.lol",
"http://icon.com/icon.ico",
Modifier,
true,
true,
showPicture = true,
loadRobohash = true,
)
}
}
@@ -35,7 +35,7 @@ fun filterNip04DMs(
account: Account?,
since: SincePerRelayMap?,
): List<RelayBasedFilter>? {
if (group == null || group.isEmpty() || account == null) return null
if (group.isNullOrEmpty() || account == null) return null
val userOutboxRelays = account.homeRelays.flow.value
val userInboxRelays = account.dmRelays.flow.value
@@ -73,14 +73,14 @@ fun NewChatroomSubjectDialog(
Surface {
val groupName =
remember {
mutableStateOf<String>(
mutableStateOf(
accountViewModel.account.chatroomList.rooms
.get(room)
?.subject
?.value ?: "",
)
}
val message = remember { mutableStateOf<String>("") }
val message = remember { mutableStateOf("") }
val scope = rememberCoroutineScope()
Column(
@@ -596,7 +596,7 @@ class ChatNewMessageViewModel :
toUsersTagger.run()
val users = toUsersTagger.pTags?.mapTo(mutableSetOf()) { it.pubkeyHex }
if (users == null || users.isEmpty()) {
if (users.isNullOrEmpty()) {
room = null
updateNIP17StatusFromRoom()
} else {
@@ -29,7 +29,7 @@ import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
fun filterMessagesToPublicChat(
channel: PublicChatChannel,
since: SincePerRelayMap?,
): List<RelayBasedFilter>? =
): List<RelayBasedFilter> =
channel.relays().toSet().map {
RelayBasedFilter(
relay = it,
@@ -31,7 +31,7 @@ fun filterMyMessagesToLiveActivities(
channel: LiveActivitiesChannel,
pubKey: HexKey,
since: SincePerRelayMap?,
): List<RelayBasedFilter>? =
): List<RelayBasedFilter> =
channel.relays().toSet().map {
RelayBasedFilter(
relay = it,
@@ -129,7 +129,7 @@ class ChannelMetadataViewModel : ViewModel() {
val template =
if (event != null) {
val hint = EventHintBundle<ChannelCreateEvent>(event, channel.relays().firstOrNull())
val hint = EventHintBundle(event, channel.relays().firstOrNull())
ChannelMetadataEvent.build(
channelName.value.text,
@@ -40,7 +40,7 @@ class FollowingEphemeralChatSubAssembler(
override fun updateFilter(
key: ChatroomListState,
since: SincePerRelayMap?,
): List<RelayBasedFilter>? =
): List<RelayBasedFilter> =
listOfNotNull(
filterFollowingEphemeralChats(key.account.ephemeralChatList.liveEphemeralChatList.value, since),
).flatten()
@@ -40,7 +40,7 @@ class FollowingPublicChatSubAssembler(
override fun updateFilter(
key: ChatroomListState,
since: SincePerRelayMap?,
): List<RelayBasedFilter>? =
): List<RelayBasedFilter> =
listOfNotNull(
filterLastMessageFollowingPublicChats(key.account.publicChatList.flowSet.value, since),
filterFollowingPublicChatsCreationEvent(key.account.publicChatList.flowSet.value, since),
@@ -29,7 +29,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
@@ -42,7 +42,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
@@ -117,8 +117,7 @@ open class DiscoverLiveFeedFilter(
fun convertStatusToOrder(event: LiveActivitiesEvent?): Int {
if (event == null) return 0
val url = event.streaming()
if (url == null) return 0
val url = event.streaming() ?: return 0
return when (event.status()) {
StatusTag.STATUS.LIVE -> {
if (OnlineChecker.isCachedAndOffline(url)) {

Some files were not shown because too many files have changed in this diff Show More