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

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