* 'main' of https://github.com/vitorpamplona/amethyst:
  New Crowdin translations by GitHub Action
  drawerGesturesEnabled is true whenever the drawer is in transition (target != current)
  Split the logic between onPreScroll and onPostScroll
  New Crowdin translations by GitHub Action
  New Crowdin translations by GitHub Action
  Unnecessary ?. / ?: / !! on non-null Missing @OptIn annotations Parameter name mismatches Icons.Filled.*  / Icons.AutoMirrored.* No cast needed / remove inline LocalLifecycleOwner import fix NIP-51 name() → title() Chess gameId → startEventId Nullable DecimalFormat safe calls DelicateCoroutinesApi opt-in
  use 50/50 split for pager vs dock zones
  fix: allow swipe-to-close when drawer is open on pager screens
This commit is contained in:
Vitor Pamplona
2026-03-16 16:28:55 -04:00
41 changed files with 255 additions and 60 deletions
@@ -2016,6 +2016,7 @@ class Account(
}
scope.launch(Dispatchers.IO) {
@OptIn(kotlinx.coroutines.FlowPreview::class)
settings.saveable.debounce(1000).collect {
if (it.accountSettings != null) {
LocalPreferences.saveToEncryptedStorage(it.accountSettings)
@@ -358,19 +358,19 @@ object LocalCache : ILocalCache, ICacheProvider {
fun load(keys: Set<String>): Set<User> = keys.mapNotNullTo(mutableSetOf(), ::checkGetOrCreateUser)
override fun getOrCreateUser(key: HexKey): User {
require(isValidHex(key = key)) { "$key is not a valid hex" }
override fun getOrCreateUser(pubkey: HexKey): User {
require(isValidHex(key = pubkey)) { "$pubkey is not a valid hex" }
return users.getOrCreate(key) {
val nip65RelayListNote = getOrCreateAddressableNoteInternal(AdvertisedRelayListEvent.createAddress(key))
val dmRelayListNote = getOrCreateAddressableNoteInternal(ChatMessageRelayListEvent.createAddress(key))
return users.getOrCreate(pubkey) {
val nip65RelayListNote = getOrCreateAddressableNoteInternal(AdvertisedRelayListEvent.createAddress(pubkey))
val dmRelayListNote = getOrCreateAddressableNoteInternal(ChatMessageRelayListEvent.createAddress(pubkey))
User(it, nip65RelayListNote, dmRelayListNote)
}
}
override fun getUserIfExists(key: String): User? {
if (key.isEmpty()) return null
return users.get(key)
override fun getUserIfExists(pubkey: String): User? {
if (pubkey.isEmpty()) return null
return users.get(pubkey)
}
override fun countUsers(predicate: (String, User) -> Boolean): Int {
@@ -394,7 +394,7 @@ object LocalCache : ILocalCache, ICacheProvider {
fun getAddressableNoteIfExists(address: Address): AddressableNote? = addressables.get(address)
override fun getNoteIfExists(key: String): Note? = if (key.length == 64) notes.get(key) else Address.parse(key)?.let { addressables.get(it) }
override fun getNoteIfExists(hexKey: String): Note? = if (hexKey.length == 64) notes.get(hexKey) else Address.parse(hexKey)?.let { addressables.get(it) }
fun getNoteIfExists(key: ETag): Note? = notes.get(key.eventId)
@@ -2250,6 +2250,7 @@ object LocalCache : ILocalCache, ICacheProvider {
requestNote?.let { request -> zappedNote?.addZapPayment(request, note) }
@OptIn(kotlinx.coroutines.DelicateCoroutinesApi::class)
GlobalScope.launch(Dispatchers.IO) {
responseCallback(event)
}
@@ -114,7 +114,7 @@ class NwcSignerState(
fun hasWalletConnectSetup(): Boolean = nip47Setup.value != null
override fun isNIP47Author(pubkey: HexKey?): Boolean = nip47Signer.value.pubKey == pubkey
override fun isNIP47Author(pubKey: HexKey?): Boolean = nip47Signer.value.pubKey == pubKey
/**
* Decrypts a NIP-47 payment request using the current signer.
@@ -36,7 +36,7 @@ import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.BookmarkIdTag
import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.LabeledBookmarkListEvent
import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.description
import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.image
import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.name
import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.title
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -181,7 +181,7 @@ class LabeledBookmarkListsState(
val template =
listEvent.update {
if (listName != null) name(listName)
if (listName != null) title(listName)
if (listDescription != null) description(listDescription)
if (listImage != null) image(listImage)
}
@@ -38,7 +38,7 @@ import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
import com.vitorpamplona.quartz.nip51Lists.peopleList.description
import com.vitorpamplona.quartz.nip51Lists.peopleList.image
import com.vitorpamplona.quartz.nip51Lists.peopleList.name
import com.vitorpamplona.quartz.nip51Lists.peopleList.title
import com.vitorpamplona.quartz.utils.flattenToSet
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -226,7 +226,7 @@ class PeopleListsState(
val template =
listEvent.update {
if (listName != null) name(listName)
if (listName != null) title(listName)
if (listDescription != null) description(listDescription)
if (listImage != null) image(listImage)
}
@@ -79,6 +79,7 @@ class MergedFollowListsState(
communities = community.mapTo(mutableSetOf()) { it.address.toValue() },
)
@OptIn(kotlinx.coroutines.FlowPreview::class)
val flow: StateFlow<AllFollows> =
combine(
listOf(
@@ -20,6 +20,7 @@
*/
package com.vitorpamplona.amethyst.service.cashu.v4
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.cbor.ByteString
@@ -34,6 +35,7 @@ class V4Token(
val t: Array<V4T>?,
)
@OptIn(ExperimentalSerializationApi::class)
@Serializable
class V4T(
// identifier
@@ -42,6 +44,7 @@ class V4T(
val p: Array<V4Proof>,
)
@OptIn(ExperimentalSerializationApi::class)
@Serializable
class V4Proof(
// amount
@@ -57,6 +60,7 @@ class V4Proof(
val w: String? = null,
)
@OptIn(ExperimentalSerializationApi::class)
@Serializable
class V4DleqProof(
@ByteString
@@ -51,14 +51,14 @@ class EventWatcherSubAssembler(
}
override fun updateFilter(
key: List<EventFinderQueryState>,
keys: List<EventFinderQueryState>,
since: SincePerRelayMap?,
): List<RelayBasedFilter>? {
if (key.isEmpty()) {
if (keys.isEmpty()) {
return null
}
lastNotesOnFilter = key.map { it.note }
lastNotesOnFilter = keys.map { it.note }
return groupByRelayPresence(lastNotesOnFilter, latestEOSEs)
.map { group ->
@@ -53,7 +53,7 @@ class BlossomServersViewModel : ViewModel() {
fun refresh() {
isModified = false
_fileServers.update {
val obtainedFileServers = obtainFileServers() ?: emptyList()
val obtainedFileServers = obtainFileServers()
obtainedFileServers.mapNotNull { serverUrl ->
try {
ServerName(
@@ -0,0 +1,108 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.components
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.pager.PagerState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onSizeChanged
private const val PAGER_ZONE_FRACTION = 0.5f
fun Modifier.zonedDrawerSwipe(
pagerState: PagerState,
openDrawer: () -> Unit,
): Modifier =
composed {
var widthPx by remember { mutableFloatStateOf(1f) }
var gestureStartX by remember { mutableFloatStateOf(0f) }
var gestureStartPage by remember { mutableIntStateOf(0) }
var drawerOpened by remember { mutableStateOf(false) }
val connection =
remember {
object : NestedScrollConnection {
override fun onPreScroll(
available: Offset,
source: NestedScrollSource,
): Offset {
if (source != NestedScrollSource.UserInput) return Offset.Zero
if (drawerOpened) return Offset(available.x, 0f)
// Non-first pages in the drawer zone: intercept before the
// pager consumes the delta to page backwards.
if (available.x > 0f) {
val wasOnFirstPage = gestureStartPage == 0
val isInPagerZone = gestureStartX < widthPx * PAGER_ZONE_FRACTION
if (!wasOnFirstPage && !isInPagerZone) {
drawerOpened = true
openDrawer()
return Offset(available.x, 0f)
}
}
return Offset.Zero
}
override fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource,
): Offset {
if (source != NestedScrollSource.UserInput) return Offset.Zero
if (drawerOpened) return Offset(available.x, 0f)
// First page: open drawer only with unconsumed right-swipe
// so child LazyRows can scroll first.
if (available.x > 0f && gestureStartPage == 0) {
drawerOpened = true
openDrawer()
return Offset(available.x, 0f)
}
return Offset.Zero
}
}
}
this
.onSizeChanged { widthPx = it.width.toFloat() }
.pointerInput(Unit) {
awaitEachGesture {
val down = awaitFirstDown(requireUnconsumed = false)
gestureStartX = down.position.x
gestureStartPage = pagerState.currentPage
drawerOpened = false
}
}.nestedScroll(connection)
}
@@ -37,8 +37,10 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import androidx.core.net.toUri
import androidx.core.util.Consumer
import androidx.navigation.NavDestination.Companion.hasRoute
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.crashreports.DisplayCrashMessages
@@ -142,7 +144,17 @@ fun AppNavigation(
) {
val nav = rememberNav()
AccountSwitcherAndLeftDrawerLayout(accountViewModel, accountSessionManager, nav) {
val navBackStackEntry by nav.controller.currentBackStackEntryAsState()
val isTabPagerRoute =
navBackStackEntry?.destination?.let { dest ->
dest.hasRoute<Route.Home>() || dest.hasRoute<Route.Message>()
} ?: false
val drawerGesturesEnabled =
!isTabPagerRoute ||
nav.drawerState.isOpen ||
nav.drawerState.targetValue != nav.drawerState.currentValue
AccountSwitcherAndLeftDrawerLayout(accountViewModel, accountSessionManager, nav, drawerGesturesEnabled) {
NavHost(
navController = nav.controller,
startDestination = Route.Home,
@@ -62,7 +62,7 @@ fun BadgeCompose(
nav: INav,
) {
val noteState by observeNote(likeSetCard.note, accountViewModel)
val note = noteState?.note
val note = noteState.note
val context = LocalContext.current.applicationContext
@@ -30,10 +30,10 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.compose.LocalLifecycleOwner
import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
@@ -49,10 +49,10 @@ fun showAmountInteger(amount: BigDecimal?): String {
if (amount.abs() < BigDecimal(0.01)) return ""
return when {
amount >= OneGiga -> dfG.get().format(amount.div(OneGiga).setScale(0, RoundingMode.HALF_UP))
amount >= OneMega -> dfM.get().format(amount.div(OneMega).setScale(0, RoundingMode.HALF_UP))
amount >= TenKilo -> dfK.get().format(amount.div(OneKilo).setScale(0, RoundingMode.HALF_UP))
else -> dfN.get().format(amount)
amount >= OneGiga -> dfG.get()?.format(amount.div(OneGiga).setScale(0, RoundingMode.HALF_UP)) ?: ""
amount >= OneMega -> dfM.get()?.format(amount.div(OneMega).setScale(0, RoundingMode.HALF_UP)) ?: ""
amount >= TenKilo -> dfK.get()?.format(amount.div(OneKilo).setScale(0, RoundingMode.HALF_UP)) ?: ""
else -> dfN.get()?.format(amount) ?: ""
}
}
@@ -118,13 +118,13 @@ class PollNoteViewModel : ViewModel() {
it.zappedValue.value = zappedValue
it.tally.value = tallyValue.toFloat()
it.consensusThreadhold.value = consensusThreshold != null && tallyValue >= consensusThreshold!!
it.zappedByLoggedIn.value = account?.userProfile()?.let { it1 -> cachedIsPollOptionZappedBy(it.option, it1) } ?: false
it.zappedByLoggedIn.value = account.userProfile().let { it1 -> cachedIsPollOptionZappedBy(it.option, it1) }
}
}
}
fun checkIfCanZap(): Boolean {
val account = account ?: return false
val account = account
val note = pollNote ?: return false
return account.userProfile() != note.author && !wasZappedByLoggedInAccount
}
@@ -96,7 +96,7 @@ fun RenderLiveChessChallenge(
nav: INav,
) {
val event = (note.event as? LiveChessGameChallengeEvent) ?: return
val gameId = event.gameId() ?: return
val gameId = event.gameId()
val chessViewModel: ChessViewModelNew =
viewModel(
@@ -72,9 +72,9 @@ private fun ObserverAndRenderNIP95(
val content by
remember(noteState) {
// Creates a new object when the event arrives to force an update of the image.
val note = noteState?.note
val note = noteState.note
val uri = header.toNostrUri()
val localDir = note?.idHex?.let { File(Amethyst.instance.nip95cache, it) }
val localDir = note.idHex.let { File(Amethyst.instance.nip95cache, it) }
val blurHash = eventHeader.blurhash()
val dimensions = eventHeader.dimensions()
val description = eventHeader.alt() ?: eventHeader.content
@@ -163,7 +163,7 @@ fun RenderTextModificationEvent(
}
LaunchedEffect(key1 = noteState) {
val newAuthor = accountViewModel.isLoggedUser(noteState?.note?.author)
val newAuthor = accountViewModel.isLoggedUser(noteState.note.author)
if (isAuthorTheLoggedUser.value != newAuthor) {
isAuthorTheLoggedUser.value = newAuthor
@@ -49,6 +49,7 @@ fun AccountSwitcherAndLeftDrawerLayout(
accountViewModel: AccountViewModel,
accountSessionManager: AccountSessionManager,
nav: INav,
gesturesEnabled: Boolean = true,
content: @Composable () -> Unit,
) {
val scope = rememberCoroutineScope()
@@ -83,6 +84,7 @@ fun AccountSwitcherAndLeftDrawerLayout(
ModalNavigationDrawer(
drawerState = nav.drawerState,
gesturesEnabled = gesturesEnabled,
drawerContent = {
DrawerContent(nav, openSheetFunction, accountViewModel)
BackHandler(enabled = nav.drawerState.isOpen, nav::closeDrawer)
@@ -104,7 +104,7 @@ class ChannelMetadataViewModel : ViewModel() {
fun createOrUpdate(onDone: (PublicChatChannel) -> Unit) {
viewModelScope.launch(Dispatchers.IO) {
account?.let { account ->
account.let { account ->
val channel = originalChannel
if (channel == null) {
val template =
@@ -204,7 +204,7 @@ class ChannelMetadataViewModel : ViewModel() {
onUploaded: (String) -> Unit,
onError: (String, String) -> Unit,
) {
val account = account ?: return
val account = account
onUploading(true)
val compResult = MediaCompressor().compress(galleryUri.uri, galleryUri.mimeType, CompressorQuality.MEDIUM, context.applicationContext)
@@ -48,6 +48,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.ui.components.zonedDrawerSwipe
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
@@ -124,7 +125,12 @@ fun MessagesPager(
HorizontalPager(
contentPadding = paddingValues,
state = pagerState,
userScrollEnabled = false,
userScrollEnabled = true,
modifier =
Modifier.zonedDrawerSwipe(
pagerState = pagerState,
openDrawer = nav::openDrawer,
),
) { page ->
ChatroomListFeedView(
feedContentState = tabs[page].feedContentState,
@@ -220,7 +220,7 @@ open class NewProductViewModel :
}
open fun quote(quote: Note) {
val accountViewModel = accountViewModel ?: return
val accountViewModel = accountViewModel
message = TextFieldValue(message.text + "\nnostr:${quote.toNEvent()}")
@@ -307,7 +307,6 @@ open class NewProductViewModel :
}
suspend fun sendPostSync() {
val accountViewModel = accountViewModel ?: return
val template = createTemplate() ?: return
val version = draftTag.current
@@ -320,8 +319,6 @@ open class NewProductViewModel :
}
suspend fun sendDraftSync() {
val accountViewModel = accountViewModel ?: return
if (message.text.isBlank()) {
accountViewModel.account.deleteDraftIgnoreErrors(draftTag.current)
} else {
@@ -331,7 +328,7 @@ open class NewProductViewModel :
}
private suspend fun createTemplate(): EventTemplate<out Event>? {
val accountViewModel = accountViewModel ?: return null
val accountViewModel = accountViewModel
val tagger =
NewMessageTagger(
@@ -340,7 +337,7 @@ open class NewProductViewModel :
)
tagger.run()
val emojis = findEmoji(tagger.message, account?.emoji?.myEmojis?.value)
val emojis = findEmoji(tagger.message, account.emoji.myEmojis.value)
val urls = findURLs(tagger.message)
val usedAttachments = iMetaDescription.filterIsIn(urls.toSet()) + productImages.map { it.toIMeta() }
@@ -399,7 +396,7 @@ open class NewProductViewModel :
context: Context,
) {
viewModelScope.launch(Dispatchers.IO) {
val myAccount = account ?: return@launch
val myAccount = account
val myMultiOrchestrator = multiOrchestrator ?: return@launch
mediaUploadTracker.startUpload(myMultiOrchestrator.hasNonMedia())
@@ -501,8 +498,8 @@ open class NewProductViewModel :
this.multiOrchestrator?.remove(selected)
}
override fun updateMessage(it: TextFieldValue) {
message = it
override fun updateMessage(newMessage: TextFieldValue) {
message = newMessage
urlPreviews.update(message)
if (message.selection.collapsed) {
@@ -616,7 +613,7 @@ open class NewProductViewModel :
override fun updateZapFromText() {
viewModelScope.launch(Dispatchers.IO) {
val tagger = NewMessageTagger(message.text, emptyList(), emptyList(), accountViewModel!!)
val tagger = NewMessageTagger(message.text, emptyList(), emptyList(), accountViewModel)
tagger.run()
tagger.pTags?.forEach { taggedUser ->
if (!forwardZapTo.value.items.any { it.key == taggedUser }) {
@@ -63,6 +63,7 @@ import com.vitorpamplona.amethyst.model.TopFilter
import com.vitorpamplona.amethyst.service.OnlineChecker
import com.vitorpamplona.amethyst.service.location.LocationState
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.components.zonedDrawerSwipe
import com.vitorpamplona.amethyst.ui.feeds.ChannelFeedContentState
import com.vitorpamplona.amethyst.ui.feeds.ChannelFeedState
import com.vitorpamplona.amethyst.ui.feeds.PagerStateKeys
@@ -211,7 +212,12 @@ private fun HomePages(
HorizontalPager(
contentPadding = it,
state = pagerState,
userScrollEnabled = false,
userScrollEnabled = true,
modifier =
Modifier.zonedDrawerSwipe(
pagerState = pagerState,
openDrawer = nav::openDrawer,
),
) { page ->
HomeFeeds(
feedState = tabs[page].feedState,
@@ -322,7 +322,7 @@ class NewPublicMessageViewModel :
}
suspend fun sendPostSync() {
val template = createTemplate() ?: return
val template = createTemplate()
val extraNotesToBroadcast = mutableListOf<Event>()
if (nip95attachments.isNotEmpty()) {
@@ -354,7 +354,7 @@ class NewPublicMessageViewModel :
broadcast.add(it.second)
}
val template = createTemplate() ?: return
val template = createTemplate()
accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template, broadcast)
}
}
@@ -459,7 +459,7 @@ class NewPublicMessageViewModel :
if (state.result is UploadOrchestrator.OrchestratorResult.NIP95Result) {
val nip95 = account.createNip95(state.result.bytes, headerInfo = state.result.fileHeader, alt, contentWarningReason)
nip95attachments = nip95attachments + nip95
val note = nip95.let { it1 -> account?.consumeNip95(it1.first, it1.second) }
val note = nip95.let { it1 -> account.consumeNip95(it1.first, it1.second) }
note?.let {
message = message.insertUrlAtCursor("nostr:" + it.toNEvent())
@@ -63,6 +63,7 @@ class UserProfileFollowersUserFeedViewModel(
}
}
@OptIn(kotlinx.coroutines.FlowPreview::class)
val followersFlow: StateFlow<List<User>> =
account.cache
.observeEvents(followerFilter)
@@ -55,6 +55,7 @@ class UserProfileFollowsUserFeedViewModel(
return LocalCache.load(nonHiddenFollows).sortedWith(sortingModel)
}
@OptIn(kotlinx.coroutines.FlowPreview::class)
val followsFlow: StateFlow<List<User>> =
contactList
.flow()
@@ -56,7 +56,7 @@ fun WatchApp(
LaunchedEffect(key1 = appState) {
withContext(Dispatchers.IO) {
(appState?.note?.event as? AppDefinitionEvent)?.appMetaData()?.let { metaData ->
(appState.note.event as? AppDefinitionEvent)?.appMetaData()?.let { metaData ->
metaData.picture?.ifBlank { null }?.let { newLogo ->
if (newLogo != appLogo) appLogo = newLogo
}
@@ -112,6 +112,7 @@ class UserProfileZapsViewModel(
return results.map { (user, amount) -> ZapAmount(user, amount) }.sortedWith(sortingModel)
}
@OptIn(kotlinx.coroutines.FlowPreview::class)
val receivedZapAmountsByUser: StateFlow<List<ZapAmount>> =
account.cache
.observeEvents(zapsToUser)
@@ -47,6 +47,7 @@ import androidx.compose.material.icons.automirrored.filled.Feed
import androidx.compose.material.icons.automirrored.filled.Label
import androidx.compose.material.icons.automirrored.filled.List
import androidx.compose.material.icons.automirrored.filled.Message
import androidx.compose.material.icons.automirrored.filled.Send
import androidx.compose.material.icons.filled.AttachMoney
import androidx.compose.material.icons.filled.Bolt
import androidx.compose.material.icons.filled.Code
@@ -61,7 +62,6 @@ import androidx.compose.material.icons.filled.Language
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.Payment
import androidx.compose.material.icons.filled.PrivacyTip
import androidx.compose.material.icons.filled.Send
import androidx.compose.material.icons.filled.Storage
import androidx.compose.material.icons.filled.Tag
import androidx.compose.material.icons.filled.Topic
@@ -931,7 +931,7 @@ private fun OutboxEventsCard(eventIds: Set<HexKey>) {
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Icon(
imageVector = Icons.Default.Send,
imageVector = Icons.AutoMirrored.Filled.Send,
contentDescription = null,
modifier = Modifier.size(12.dp),
tint = MaterialTheme.colorScheme.onTertiaryContainer,
@@ -90,6 +90,7 @@ class SearchBarViewModel(
val listState: LazyListState = LazyListState(0, 0)
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
val directNip05Resolver: Flow<User?> =
searchTerm
.debounce(400)
@@ -55,7 +55,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
@@ -63,6 +62,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
@@ -99,7 +99,7 @@ class WalletViewModel : ViewModel() {
fun init(account: Account) {
this.account = account
_hasWalletSetup.value = account.nip47SignerState?.hasWalletConnectSetup() == true
_hasWalletSetup.value = account.nip47SignerState.hasWalletConnectSetup()
}
fun refreshWalletSetup() {
@@ -669,6 +669,7 @@
<string name="write_to_relay">Zapisovat do Relay</string>
<string name="write_to_relay_description">Množství bajtů, které bylo odesláno na toto relé, včetně filtrů a událostí</string>
<string name="read_from_relay_description">Množství bajtů, které bylo přijato z tohoto relé, včetně filtrů a událostí</string>
<string name="relay_event_count">Uložené události</string>
<string name="an_error_occurred_trying_to_get_relay_information">Při pokusu o získání informací z Relay se vyskytla chyba z %1$s</string>
<string name="owner">Vlastník</string>
<string name="used_by">Používáno</string>
@@ -1553,4 +1554,8 @@
<string name="uptime">%1$d%% dostupnost</string>
<string name="namecoin_settings">Nastavení Namecoin</string>
<string name="ots_explorer_settings">Průzkumník Bitcoin (OTS)</string>
<string name="events">události</string>
<string name="dms">DMs</string>
<string name="profiles">profily</string>
<string name="relay_settings_lower">nastavení relé</string>
</resources>
@@ -674,6 +674,7 @@ anz der Bedingungen ist erforderlich</string>
<string name="write_to_relay">In Relay schreiben</string>
<string name="write_to_relay_description">Die Menge in Bytes, die an dieses Relais gesendet wurde, einschließlich Filter und Ereignisse</string>
<string name="read_from_relay_description">Die Menge in Bytes, die von diesem Relais empfangen wurde, einschließlich Filter und Ereignisse</string>
<string name="relay_event_count">Ereignisse gespeichert</string>
<string name="an_error_occurred_trying_to_get_relay_information">Ein Fehler ist beim Abrufen von Relay-Informationen von %1$s aufgetreten</string>
<string name="owner">Inhaber</string>
<string name="used_by">Verwendet von</string>
@@ -1558,4 +1559,8 @@ anz der Bedingungen ist erforderlich</string>
<string name="uptime">%1$d%% Verfügbarkeit</string>
<string name="namecoin_settings">Namecoin-Einstellungen</string>
<string name="ots_explorer_settings">Bitcoin Explorer (OTS)</string>
<string name="events">ereignisse</string>
<string name="dms">DMs</string>
<string name="profiles">profile</string>
<string name="relay_settings_lower">relaiseinstellungen</string>
</resources>
@@ -673,6 +673,7 @@
<string name="write_to_relay">Írás az átjátszóra</string>
<string name="write_to_relay_description">Az átjátszónak küldött bájt-mennyiség, beleértve a szűrőket és eseményeket is</string>
<string name="read_from_relay_description">Az átjátszótól kapott bájt-mennyiség, beleértve a szűrőket és eseményeket is</string>
<string name="relay_event_count">Tárolt események</string>
<string name="an_error_occurred_trying_to_get_relay_information">Hiba lépett fel, amikor megpróbálta lekérni az átjátszó-információt innen: %1$s</string>
<string name="owner">Tulajdonos</string>
<string name="used_by">Használat a következővel:</string>
@@ -1558,4 +1559,8 @@
<string name="uptime">Üzemidő: %1$d%%</string>
<string name="namecoin_settings">Namecoin-beállítások</string>
<string name="ots_explorer_settings">Bitcoin felfedező (OTS)</string>
<string name="events">események</string>
<string name="dms">Közvetlen üzenetek</string>
<string name="profiles">profilok</string>
<string name="relay_settings_lower">átjátszóbeállítások</string>
</resources>
@@ -670,6 +670,7 @@
<string name="write_to_relay">Zapisz do Transmitera</string>
<string name="write_to_relay_description">Ilość w bajtach, która została wysłana do tego transmitera, w tym filtry i wydarzenia</string>
<string name="read_from_relay_description">Ilość w bajtach, która została otrzymana z tego transmitera, w tym filtry i wydarzenia</string>
<string name="relay_event_count">Zapisane zdarzenia</string>
<string name="an_error_occurred_trying_to_get_relay_information">Wystąpił błąd podczas próby uzyskania informacji o transmiterze z %1$s</string>
<string name="owner">Operator</string>
<string name="used_by">Używany przez</string>
@@ -925,6 +926,8 @@
<string name="sign_request_rejected_description">Upewnij się, że aplikacja logującego autoryzuje tę operację</string>
<string name="no_wallet_found_with_error">Nie znaleziono portfeli do zapłacenia faktury z Lightning (Error: %1$s). Proszę zainstalować Lightning wallet, aby używać zapów</string>
<string name="no_wallet_found">Nie znaleziono portfeli do zapłacenia faktury z Lightning. Proszę zainstalować Lightning wallet, aby używać zapów</string>
<string name="no_blossom_apps_found_title">Nie można otworzyć linków Blossom</string>
<string name="no_blossom_apps_found_description">Nie znaleziono aplikacji Blossom. Zainstaluj lokalną aplikację Blossom, aby wyświetlić ten plik</string>
<string name="hidden_words">Ukryte słowa</string>
<string name="hide_new_word_label">Ukryj nowe słowo lub wyrażenie</string>
<string name="automatically_show_profile_picture">Zdjęcie profilowe</string>
@@ -1047,6 +1050,29 @@
<string name="route_global">Wszystkie</string>
<string name="route_video">Filmiki</string>
<string name="route_chess">Szachy</string>
<string name="wallet">Portfel</string>
<string name="wallet_balance">Saldo</string>
<string name="wallet_send">Wyślij</string>
<string name="wallet_receive">Odbierz</string>
<string name="wallet_transactions">Transakcje</string>
<string name="wallet_no_connection">Nie podłączono portfela</string>
<string name="wallet_no_connection_description">Aby korzystać z portfela, skonfiguruj połączenie Nostr Wallet Connect (NWC) w ustawieniach zap.</string>
<string name="wallet_setup">Skonfiguruj portfel</string>
<string name="wallet_sats">satosze</string>
<string name="wallet_paste_invoice">Wstaw fakturę BOLT-11</string>
<string name="wallet_pay">Zapłać</string>
<string name="wallet_payment_success">Płatność udana</string>
<string name="wallet_payment_sending">Wysyłanie zapłaty…</string>
<string name="wallet_amount_sats">Kwota (satoszy)</string>
<string name="wallet_description">Opis (opcjonalnie)</string>
<string name="wallet_create_invoice">Utwórz fakturę</string>
<string name="wallet_creating_invoice">Tworzenie faktury…</string>
<string name="wallet_copy_invoice">Kopiuj fakturę</string>
<string name="wallet_no_transactions">Brak dostępnych transakcji</string>
<string name="wallet_loading">Wczytywanie…</string>
<string name="wallet_incoming">Otrzymano</string>
<string name="wallet_outgoing">Wysłano</string>
<string name="wallet_refresh">Odśwież</string>
<string name="route_security_filters">Filtry bezpieczeństwa</string>
<string name="route_import_follows">Importuj Obserwujących</string>
<string name="new_post">Nowy post</string>
@@ -1282,6 +1308,7 @@
<string name="translate_to_description">Wybierz język, na który chcesz przetłumaczyć treść.</string>
<string name="language_preferences">Ustawienia językowe</string>
<string name="language_preferences_description">Dla każdej pary językowej wybierz, który język ma być wyświetlany jako pierwszy.</string>
<string name="language_preference_pair">%1$s - %2$s</string>
<string name="search_languages">Wyszukiwanie Języków</string>
<string name="add_language">Dodaj język</string>
<string name="add_language_pair">Dodaj pary językowe</string>
@@ -1299,6 +1326,7 @@
<string name="crashreport_found">Znaleziono raport o błędzie</string>
<string name="would_you_like_to_send_the_recent_crash_report_to_amethyst_in_a_dm_no_personal_information_will_be_shared">Czy chcesz wysłać ostatni raport o awarii do Amethyst w DM? Żadne dane osobowe nie będą udostępnione</string>
<string name="crashreport_found_send">Prześlij</string>
<string name="this_message_will_disappear_in">Ta wiadomość zniknie za %1$s</string>
<string name="this_message_will_disappear_in_days">Ta wiadomość zniknie za %1$d dni</string>
<string name="select_signer">Wybierz Sygnatariusza</string>
<string name="in_the_list">Już jest na liście</string>
@@ -669,6 +669,7 @@
<string name="write_to_relay">Enviar para o Relay</string>
<string name="write_to_relay_description">A quantidade em bytes que foi enviada para este relé, incluindo filtros e eventos</string>
<string name="read_from_relay_description">A quantidade em bytes que foi recebida deste relé, incluindo filtros e eventos</string>
<string name="relay_event_count">Eventos armazenados</string>
<string name="an_error_occurred_trying_to_get_relay_information">Ocorreu um erro ao tentar obter informações do relay de %1$s</string>
<string name="owner">Proprietário</string>
<string name="used_by">Usado por</string>
@@ -1553,4 +1554,8 @@
<string name="uptime">%1$d%% de disponibilidade</string>
<string name="namecoin_settings">Configurações do Namecoin</string>
<string name="ots_explorer_settings">Explorador Bitcoin (OTS)</string>
<string name="events">eventos</string>
<string name="dms">DMs</string>
<string name="profiles">perfils</string>
<string name="relay_settings_lower">configurações de Relay</string>
</resources>
@@ -668,6 +668,7 @@
<string name="write_to_relay">Skriv till Relay</string>
<string name="write_to_relay_description">Mängden data i byte som skickades till detta relä, inklusive filter och händelser</string>
<string name="read_from_relay_description">Mängden data i byte som mottogs från detta relä, inklusive filter och händelser</string>
<string name="relay_event_count">Lagrade händelser</string>
<string name="an_error_occurred_trying_to_get_relay_information">Ett fel inträffade vid försök att hämta information från Relay %1$s</string>
<string name="owner">Ägare</string>
<string name="used_by">Används av</string>
@@ -1552,4 +1553,8 @@
<string name="uptime">%1$d%% drifttid</string>
<string name="namecoin_settings">Namecoin-inställningar</string>
<string name="ots_explorer_settings">Bitcoin Explorer (OTS)</string>
<string name="events">händelser</string>
<string name="dms">DMs</string>
<string name="profiles">profiler</string>
<string name="relay_settings_lower">relä inställningar</string>
</resources>
@@ -237,7 +237,7 @@ fun LiveChessGameScreen(
) {
// Game info - use currentPosition.activeColor for turn display
GameInfoHeader(
gameId = gameState.gameId,
gameId = gameState.startEventId,
opponentName = opponentName,
playerColor = gameState.playerColor,
currentTurn = currentPosition.activeColor,
@@ -463,7 +463,7 @@ private fun GameInfoHeader(
// Show turn or game result
when (gameStatus) {
is GameStatus.Finished -> {
val result = (gameStatus as GameStatus.Finished).result
val result = gameStatus.result
val resultText =
when {
result == GameResult.DRAW -> "Draw"
@@ -25,8 +25,8 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowLeft
import androidx.compose.material.icons.filled.KeyboardArrowRight
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
import androidx.compose.material.icons.filled.SkipNext
import androidx.compose.material.icons.filled.SkipPrevious
import androidx.compose.material3.Icon
@@ -82,7 +82,7 @@ fun MoveNavigator(
enabled = currentMove > 0,
) {
Icon(
Icons.Default.KeyboardArrowLeft,
Icons.AutoMirrored.Filled.KeyboardArrowLeft,
contentDescription = "Previous move",
tint =
if (currentMove > 0) {
@@ -106,7 +106,7 @@ fun MoveNavigator(
enabled = currentMove < totalMoves,
) {
Icon(
Icons.Default.KeyboardArrowRight,
Icons.AutoMirrored.Filled.KeyboardArrowRight,
contentDescription = "Next move",
tint =
if (currentMove < totalMoves) {
@@ -1027,7 +1027,7 @@ public inline fun <T> Iterable<Note>.filterEvents(predicate: (T) -> Boolean): Li
return dest
}
public inline fun <T> Iterable<Note>.filterAuthoredEvents(pubkey: HexKey): List<T> {
public fun <T> Iterable<Note>.filterAuthoredEvents(pubkey: HexKey): List<T> {
if (this is Collection && isEmpty()) return emptyList()
val dest = ArrayList<T>()