Merge remote-tracking branch 'origin/main' into claude/fix-nest-audio-display-3chAG

This commit is contained in:
Claude
2026-05-04 20:07:25 +00:00
42 changed files with 447 additions and 110 deletions
@@ -89,9 +89,22 @@ class NotificationRelayService : Service() {
try {
ContextCompat.startForegroundService(context, intent)
} catch (e: Exception) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
e is ForegroundServiceStartNotAllowedException
) {
// Android 12+ blocks startForegroundService() from the background unless
// the caller has a temporary FGS exemption (e.g. broadcast receiver,
// exact alarm, high-priority FCM). Cold-start triggered by a flow emission
// from AlwaysOnNotificationServiceManager hits this path. The other
// notification layers (BootCompletedReceiver, ServiceWatchdogManager,
// NotificationCatchUpWorker) plus MainActivity.onResume retry from
// contexts that are allowed.
Log.w(TAG) { "Foreground service start not allowed from background; will retry from another layer" }
} else {
Log.e(TAG, "Failed to start foreground service", e)
}
}
}
fun stop(context: Context) {
context.stopService(Intent(context, NotificationRelayService::class.java))
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountFilterAssembler
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountForegroundFilterAssembler
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.ChannelFinderFilterAssemblyGroup
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssembler
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentFilterAssembler
@@ -70,8 +71,11 @@ class RelaySubscriptionsCoordinator(
failureTracker: RelayOfflineTracker,
scope: CoroutineScope,
) {
// main one: notifications, dms and account settings
val account = AccountFilterAssembler(client, cache, authenticator, failureTracker, scope)
// main one: notifications, dms and account settings (always-on while logged in)
val account = AccountFilterAssembler(client)
// foreground-only account loaders (follows-outbox finder, random-relay notifications)
val accountForeground = AccountForegroundFilterAssembler(client, cache, authenticator, failureTracker, scope)
// always running, feed assemblers.
val home = HomeFilterAssembler(client)
@@ -124,6 +128,7 @@ class RelaySubscriptionsCoordinator(
val all =
listOf(
account,
accountForeground,
home,
chatroomList,
video,
@@ -23,20 +23,14 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.drafts.AccountDraftsEoseManager
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.follows.AccountFollowsLoaderSubAssembler
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.marmot.MarmotGroupEventsEoseManager
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.metadata.AccountMetadataEoseManager
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.nip01Notifications.AccountNotificationsEoseFromInboxRelaysManager
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.nip01Notifications.AccountNotificationsEoseFromRandomRelaysManager
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.nip59GiftWraps.AccountGiftWrapsEoseManager
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountFeedContentStates
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayOfflineTracker
import com.vitorpamplona.quartz.nip01Core.relay.client.auth.IAuthStatus
import kotlinx.coroutines.CoroutineScope
// This allows multiple screen to be listening to logged-in accounts.
@Stable
@@ -47,24 +41,20 @@ class AccountQueryState(
)
/**
* This assembler loads everything eech account needs.
* Always-on account loaders: metadata, gift wraps, drafts, inbox-relay
* notifications, marmot group events. Foreground-only loaders live in
* [AccountForegroundFilterAssembler].
*/
@Stable
class AccountFilterAssembler(
client: INostrClient,
cache: LocalCache,
authenticator: IAuthStatus,
failureTracker: RelayOfflineTracker,
scope: CoroutineScope,
) : ComposeSubscriptionManager<AccountQueryState>() {
val group =
listOf(
AccountMetadataEoseManager(client, ::allKeys),
AccountFollowsLoaderSubAssembler(client, cache, scope, authenticator, failureTracker, ::allKeys),
AccountGiftWrapsEoseManager(client, ::allKeys),
AccountDraftsEoseManager(client, ::allKeys),
AccountNotificationsEoseFromInboxRelaysManager(client, ::allKeys),
AccountNotificationsEoseFromRandomRelaysManager(client, ::allKeys),
MarmotGroupEventsEoseManager(client, ::allKeys),
)
@@ -0,0 +1,60 @@
/*
* 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.service.relayClient.reqCommand.account
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.follows.AccountFollowsLoaderSubAssembler
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.nip01Notifications.AccountNotificationsEoseFromRandomRelaysManager
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayOfflineTracker
import com.vitorpamplona.quartz.nip01Core.relay.client.auth.IAuthStatus
import kotlinx.coroutines.CoroutineScope
/**
* Account-scoped loaders that only need to run while the app has a screen
* in the foreground. Mounted by a lifecycle-aware subscription so they pause
* on ON_STOP and resume on ON_START.
*
* Lightweight always-on account work (metadata, gift wraps, drafts, inbox
* notifications, marmot groups) stays in [AccountFilterAssembler].
*/
@Stable
class AccountForegroundFilterAssembler(
client: INostrClient,
cache: LocalCache,
authenticator: IAuthStatus,
failureTracker: RelayOfflineTracker,
scope: CoroutineScope,
) : ComposeSubscriptionManager<AccountQueryState>() {
val group =
listOf(
AccountFollowsLoaderSubAssembler(client, cache, scope, authenticator, failureTracker, ::allKeys),
AccountNotificationsEoseFromRandomRelaysManager(client, ::allKeys),
)
override fun invalidateKeys() = invalidateFilters()
override fun invalidateFilters() = group.forEach { it.invalidateFilters() }
override fun destroy() = group.forEach { it.destroy() }
}
@@ -0,0 +1,46 @@
/*
* 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.service.relayClient.reqCommand.account
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable
fun AccountForegroundFilterAssemblerSubscription(accountViewModel: AccountViewModel) =
AccountForegroundFilterAssemblerSubscription(
accountViewModel,
accountViewModel.dataSources().accountForeground,
)
@Composable
fun AccountForegroundFilterAssemblerSubscription(
accountViewModel: AccountViewModel,
dataSource: AccountForegroundFilterAssembler,
) {
val state =
remember(accountViewModel) {
AccountQueryState(accountViewModel.account, accountViewModel.feedStates, accountViewModel.trustedAccounts.value)
}
LifecycleAwareKeyDataSourceSubscription(state, dataSource)
}
@@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.vitorpamplona.amethyst.commons.model.Channel
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable
@@ -44,5 +44,5 @@ fun ChannelFinderFilterAssemblerSubscription(
ChannelFinderQueryState(channel)
}
KeyDataSourceSubscription(state, dataSource)
LifecycleAwareKeyDataSourceSubscription(state, dataSource)
}
@@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -46,5 +46,5 @@ fun EventFinderFilterAssemblerSubscription(
EventFinderQueryState(note, account)
}
KeyDataSourceSubscription(state, dataSource)
LifecycleAwareKeyDataSourceSubscription(state, dataSource)
}
@@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user
import android.annotation.SuppressLint
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -52,5 +52,5 @@ fun UserFinderFilterAssemblerSubscription(
UserFinderQueryState(user, forAccount)
}
KeyDataSourceSubscription(state, dataSource)
LifecycleAwareKeyDataSourceSubscription(state, dataSource)
}
@@ -21,7 +21,7 @@
package com.vitorpamplona.amethyst.service.relayClient.searchCommand
import androidx.compose.runtime.Composable
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.search.SearchBarViewModel
@@ -36,5 +36,5 @@ fun TextSearchDataSourceSubscription(
searchBarViewModel: SearchBarViewModel,
dataSource: SearchFilterAssembler,
) {
KeyDataSourceSubscription(searchBarViewModel.searchDataSourceState, dataSource)
LifecycleAwareKeyDataSourceSubscription(searchBarViewModel.searchDataSourceState, dataSource)
}
@@ -21,7 +21,7 @@
package com.vitorpamplona.amethyst.service.relayClient.searchCommand
import androidx.compose.runtime.Composable
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -36,5 +36,5 @@ fun UserSearchDataSourceSubscription(
userSuggestions: UserSuggestionState,
dataSource: SearchFilterAssembler,
) {
KeyDataSourceSubscription(userSuggestions.searchDataSourceState, dataSource)
LifecycleAwareKeyDataSourceSubscription(userSuggestions.searchDataSourceState, dataSource)
}
@@ -31,6 +31,7 @@ import com.vitorpamplona.amethyst.debugState
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.lang.LanguageTranslatorService
import com.vitorpamplona.amethyst.service.notifications.NotificationRelayService
import com.vitorpamplona.amethyst.service.playback.composable.DEFAULT_MUTED_SETTING
import com.vitorpamplona.amethyst.service.playback.pip.BackgroundMedia
import com.vitorpamplona.amethyst.ui.navigation.findParameterValue
@@ -89,6 +90,13 @@ class MainActivity : AppCompatActivity() {
// starts muted every time
DEFAULT_MUTED_SETTING.value = true
// If always-on notifications are enabled but the foreground service couldn't be
// started from the background during cold-start (Android 12+ restriction), retry
// now that the activity is in the foreground.
if (NotificationRelayService.isEnabled(this)) {
NotificationRelayService.start(this)
}
}
override fun onPause() {
@@ -45,6 +45,7 @@ import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.notifications.PushNotificationUtils
import com.vitorpamplona.amethyst.service.relayClient.authCommand.compose.RelayAuthSubscription
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountFilterAssemblerSubscription
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountForegroundFilterAssemblerSubscription
import com.vitorpamplona.amethyst.ui.navigation.AppNavigation
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
import com.vitorpamplona.amethyst.ui.screen.AccountSessionManager
@@ -82,6 +83,10 @@ fun LoggedInPage(
// Loads account information + DMs and Notifications from Relays.
AccountFilterAssemblerSubscription(accountViewModel)
// Foreground-only loaders: follows-outbox finder + random-relay notifications.
// Pauses on ON_STOP, resumes on ON_START.
AccountForegroundFilterAssemblerSubscription(accountViewModel)
// Pre-loads the feed for every icon the user has pinned to the bottom bar.
// Subscriptions follow the user's chosen list reactively, not the default 5.
BottomBarFeedPreloaders(accountViewModel)
@@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.articles.datasource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable
@@ -44,5 +44,5 @@ fun ArticlesFilterAssemblerSubscription(
ArticlesQueryState(accountViewModel.account, accountViewModel.feedStates, accountViewModel.viewModelScope)
}
KeyDataSourceSubscription(state, dataSource)
LifecycleAwareKeyDataSourceSubscription(state, dataSource)
}
@@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.badges.datasource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable
@@ -44,5 +44,5 @@ fun BadgesFilterAssemblerSubscription(
BadgesQueryState(accountViewModel.account, accountViewModel.feedStates, accountViewModel.viewModelScope)
}
KeyDataSourceSubscription(state, dataSource)
LifecycleAwareKeyDataSourceSubscription(state, dataSource)
}
@@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.badges.profile.datasource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable
@@ -32,5 +32,5 @@ fun ProfileBadgesFilterAssemblerSubscription(accountViewModel: AccountViewModel)
ProfileBadgesQueryState(accountViewModel.account)
}
KeyDataSourceSubscription(state, accountViewModel.dataSources().profileBadges)
LifecycleAwareKeyDataSourceSubscription(state, accountViewModel.dataSources().profileBadges)
}
@@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.datasource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
@@ -39,5 +39,5 @@ fun ChatroomFilterAssemblerSubscription(
ChatroomQueryState(room, accountViewModel.account)
}
KeyDataSourceSubscription(state, dataSource)
LifecycleAwareKeyDataSourceSubscription(state, dataSource)
}
@@ -27,7 +27,7 @@ import androidx.compose.runtime.remember
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.model.Channel
import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable
@@ -43,7 +43,7 @@ fun ChannelFilterAssemblerSubscription(
ChannelQueryState(channel, accountViewModel.account)
}
KeyDataSourceSubscription(state, dataSource)
LifecycleAwareKeyDataSourceSubscription(state, dataSource)
// Live streams need the 30311 event to populate `channel.info` before we can read its
// `goal` tag and add the goal+zap subscriptions. Re-invalidate when metadata changes so
@@ -25,7 +25,7 @@ import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.ChessViewModelNew
@@ -78,14 +78,14 @@ fun ChessSubscription(
}
// Register subscription with Amethyst's subscription system
KeyDataSourceSubscription(state, chessAssembler)
LifecycleAwareKeyDataSourceSubscription(state, chessAssembler)
// Trigger ViewModel refresh when subscription state changes
// This fetches challenges from LocalCache after events arrive
DisposableEffect(state) {
chessViewModel.forceRefresh()
onDispose {
// Subscription cleanup handled by KeyDataSourceSubscription
// Subscription cleanup handled by LifecycleAwareKeyDataSourceSubscription
}
}
}
@@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.datasource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.model.AddressableNote
@Composable
@@ -37,5 +37,5 @@ fun CommunityFilterAssemblerSubscription(
CommunityQueryState(channel)
}
KeyDataSourceSubscription(state, filterAssembler)
LifecycleAwareKeyDataSourceSubscription(state, filterAssembler)
}
@@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.list.datasourc
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable
@@ -44,5 +44,5 @@ fun CommunitiesListFilterAssemblerSubscription(
CommunitiesListQueryState(accountViewModel.account, accountViewModel.feedStates, accountViewModel.viewModelScope)
}
KeyDataSourceSubscription(state, dataSource)
LifecycleAwareKeyDataSourceSubscription(state, dataSource)
}
@@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.emojipacks.browse.datasour
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable
@@ -44,5 +44,5 @@ fun BrowseEmojiSetsFilterAssemblerSubscription(
BrowseEmojiSetsQueryState(accountViewModel.account, accountViewModel.feedStates, accountViewModel.viewModelScope)
}
KeyDataSourceSubscription(state, dataSource)
LifecycleAwareKeyDataSourceSubscription(state, dataSource)
}
@@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.datasourc
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -38,5 +38,5 @@ fun FollowPackFeedFilterAssemblerSubscription(
FollowPackFeedQueryState(pack, accountViewModel.account)
}
KeyDataSourceSubscription(state, accountViewModel.dataSources().followPacks)
LifecycleAwareKeyDataSourceSubscription(state, accountViewModel.dataSources().followPacks)
}
@@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.list.datasourc
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable
@@ -44,5 +44,5 @@ fun FollowPacksFilterAssemblerSubscription(
FollowPacksQueryState(accountViewModel.account, accountViewModel.feedStates, accountViewModel.viewModelScope)
}
KeyDataSourceSubscription(state, dataSource)
LifecycleAwareKeyDataSourceSubscription(state, dataSource)
}
@@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.datasource
import android.annotation.SuppressLint
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -40,5 +40,5 @@ fun GeoHashFilterAssemblerSubscription(
GeohashQueryState(tag.geohash, accountViewModel.account.followOutboxesOrProxy.flow.value)
}
KeyDataSourceSubscription(state, accountViewModel.dataSources().geohashes)
LifecycleAwareKeyDataSourceSubscription(state, accountViewModel.dataSources().geohashes)
}
@@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.datasource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -38,5 +38,5 @@ fun HashtagFilterAssemblerSubscription(
HashtagQueryState(tag.hashtag, accountViewModel.account.followOutboxesOrProxy.flow.value)
}
KeyDataSourceSubscription(state, accountViewModel.dataSources().hashtags)
LifecycleAwareKeyDataSourceSubscription(state, accountViewModel.dataSources().hashtags)
}
@@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.livestreams.datasource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable
@@ -44,5 +44,5 @@ fun LiveStreamsFilterAssemblerSubscription(
LiveStreamsQueryState(accountViewModel.account, accountViewModel.feedStates, accountViewModel.viewModelScope)
}
KeyDataSourceSubscription(state, dataSource)
LifecycleAwareKeyDataSourceSubscription(state, dataSource)
}
@@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.longs.datasource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable
@@ -44,5 +44,5 @@ fun LongsFilterAssemblerSubscription(
LongsQueryState(accountViewModel.account, accountViewModel.feedStates, accountViewModel.viewModelScope)
}
KeyDataSourceSubscription(state, dataSource)
LifecycleAwareKeyDataSourceSubscription(state, dataSource)
}
@@ -24,7 +24,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager
@@ -156,5 +156,5 @@ fun NestRoomFilterAssemblerSubscription(
remember(note, account.pubKey) {
NestRoomQueryState(note, account)
}
KeyDataSourceSubscription(state, filterAssembler)
LifecycleAwareKeyDataSourceSubscription(state, filterAssembler)
}
@@ -24,7 +24,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager
@@ -137,5 +137,5 @@ fun NestRoomLivenessProbeSubscription(
remember(note, account.pubKey) {
NestRoomLivenessQueryState(note, account)
}
KeyDataSourceSubscription(state, filterAssembler)
LifecycleAwareKeyDataSourceSubscription(state, filterAssembler)
}
@@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable
@@ -44,5 +44,5 @@ fun NestsFilterAssemblerSubscription(
NestsQueryState(accountViewModel.account, accountViewModel.feedStates, accountViewModel.viewModelScope)
}
KeyDataSourceSubscription(state, dataSource)
LifecycleAwareKeyDataSourceSubscription(state, dataSource)
}
@@ -58,6 +58,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
@@ -93,6 +94,16 @@ private val MAX_RING_WIDTH = 7.dp
// fades to 0 when not speaking.
private val MAX_GLOW_RADIUS = 12.dp
// Detached green ring drawn around a speaking avatar with a clear
// gap to the profile picture, so the "is talking" signal reads from
// across the room without overpowering the face. Only rendered while
// `isSpeaking` is true; pulses with the live audio level. Drawn via
// drawBehind to avoid affecting layout bounds — the gap + ring fit
// inside the GRID_SPACING between cells.
private val OUTER_RING_GAP = 5.dp
private val OUTER_RING_WIDTH = 2.5.dp
private val OUTER_RING_MAX_WIDTH = 4.dp
// Cap badge sizes so they stay legible without dominating the avatar
// at 100.dp. The 0.42 ratio was tuned for ~48.dp avatars (giving
// ~20.dp badges); without a cap, scaling to 100.dp produces 42.dp
@@ -392,16 +403,44 @@ private fun MemberCell(
targetValue = if (isSpeaking) (clampedLevel * 0.45f) else 0f,
label = "speaker-ring-glow-alpha",
)
// Detached outer ring: stays at full opacity while speaking so the
// signal is unmistakable, fades out when speech stops. Stroke width
// tracks audio level for an extra liveness cue on top of the inner
// border ring.
val animatedOuterRingAlpha by animateFloatAsState(
targetValue = if (isSpeaking) 1f else 0f,
label = "speaker-outer-ring-alpha",
)
val animatedOuterRingWidth by animateDpAsState(
targetValue =
if (isSpeaking) {
OUTER_RING_WIDTH + (clampedLevel * (OUTER_RING_MAX_WIDTH - OUTER_RING_WIDTH).value).dp
} else {
0.dp
},
label = "speaker-outer-ring-width",
)
val avatarModifier =
Modifier
.drawBehind {
if (animatedGlowAlpha <= 0.001f) return@drawBehind
if (animatedGlowAlpha > 0.001f) {
val baseRadius = size.minDimension / 2f
val extra = MAX_GLOW_RADIUS.toPx() * clampedLevel
drawCircle(
color = NEST_SPEAKING_COLOR.copy(alpha = animatedGlowAlpha),
radius = baseRadius + extra,
)
}
if (animatedOuterRingAlpha > 0.001f && animatedOuterRingWidth > 0.dp) {
val baseRadius = size.minDimension / 2f
val strokePx = animatedOuterRingWidth.toPx()
val ringRadius = baseRadius + OUTER_RING_GAP.toPx() + strokePx / 2f
drawCircle(
color = NEST_SPEAKING_COLOR.copy(alpha = animatedOuterRingAlpha),
radius = ringRadius,
style = Stroke(width = strokePx),
)
}
}.border(animatedRingWidth, animatedRingColor, CircleShape)
.let { if (member.absent) it.alpha(0.5f) else it }
val user =
@@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.pictures.datasource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable
@@ -44,5 +44,5 @@ fun PicturesFilterAssemblerSubscription(
PicturesQueryState(accountViewModel.account, accountViewModel.feedStates, accountViewModel.viewModelScope)
}
KeyDataSourceSubscription(state, dataSource)
LifecycleAwareKeyDataSourceSubscription(state, dataSource)
}
@@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.datasource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable
@@ -44,5 +44,5 @@ fun PollsFilterAssemblerSubscription(
PollsQueryState(accountViewModel.account, accountViewModel.feedStates, accountViewModel.viewModelScope)
}
KeyDataSourceSubscription(state, dataSource)
LifecycleAwareKeyDataSourceSubscription(state, dataSource)
}
@@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.products.datasource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable
@@ -44,5 +44,5 @@ fun ProductsFilterAssemblerSubscription(
ProductsQueryState(accountViewModel.account, accountViewModel.feedStates, accountViewModel.viewModelScope)
}
KeyDataSourceSubscription(state, dataSource)
LifecycleAwareKeyDataSourceSubscription(state, dataSource)
}
@@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.model.User
@Composable
@@ -37,5 +37,5 @@ fun UserProfileFilterAssemblerSubscription(
UserProfileQueryState(user)
}
KeyDataSourceSubscription(state, assembler)
LifecycleAwareKeyDataSourceSubscription(state, assembler)
}
@@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.publicChats.datasource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable
@@ -44,5 +44,5 @@ fun PublicChatsFilterAssemblerSubscription(
PublicChatsQueryState(accountViewModel.account, accountViewModel.feedStates, accountViewModel.viewModelScope)
}
KeyDataSourceSubscription(state, dataSource)
LifecycleAwareKeyDataSourceSubscription(state, dataSource)
}
@@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relay.datasource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
@@ -38,5 +38,5 @@ fun RelayFeedFilterAssemblerSubscription(
RelayFeedQueryState(normalizedUrl)
} ?: return
KeyDataSourceSubscription(state, accountViewModel.dataSources().relayFeed)
LifecycleAwareKeyDataSourceSubscription(state, accountViewModel.dataSources().relayFeed)
}
@@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.datasource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
@@ -36,5 +36,5 @@ fun RelayInfoNip66FilterAssemblerSubscription(
RelayInfoNip66QueryState(relayUrl, accountViewModel.account.followOutboxesOrProxy.flow.value)
}
KeyDataSourceSubscription(state, accountViewModel.dataSources().relayInfoNip66)
LifecycleAwareKeyDataSourceSubscription(state, accountViewModel.dataSources().relayInfoNip66)
}
@@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.shorts.datasource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable
@@ -44,5 +44,5 @@ fun ShortsFilterAssemblerSubscription(
ShortsQueryState(accountViewModel.account, accountViewModel.feedStates, accountViewModel.viewModelScope)
}
KeyDataSourceSubscription(state, dataSource)
LifecycleAwareKeyDataSourceSubscription(state, dataSource)
}
@@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.nip01Core.core.HexKey
@@ -50,5 +50,5 @@ fun ThreadFilterAssemblerSubscription(
ThreadQueryState(eventId, account)
}
KeyDataSourceSubscription(state, filterAssembler)
LifecycleAwareKeyDataSourceSubscription(state, filterAssembler)
}
@@ -176,6 +176,8 @@ Za podpisovanje se je potrebno prijaviti s privatnim ključem</string>
<string name="failed_to_save_the_image">Neuspešno shranjevanje slike</string>
<string name="video_saved_to_the_gallery">Video shranjen v video galerijo telefona</string>
<string name="failed_to_save_the_video">Neuspešno shranjevanje videa</string>
<string name="pdf_saved_to_the_gallery">PDF shranjen v Prenosi/Amethyst</string>
<string name="failed_to_save_the_pdf">PDF ni bil shranjen</string>
<string name="upload_image">Naloži sliko</string>
<string name="upload_file">Naloži datoteko</string>
<string name="take_a_picture">Zajemi sliko</string>
@@ -502,6 +504,7 @@ Za podpisovanje se je potrebno prijaviti s privatnim ključem</string>
<string name="nest_mic_unmute">Vklopi mikrofon</string>
<string name="nest_broadcast_connecting">Grem v živo…</string>
<string name="nest_broadcast_failed">Oddajanje neuspešno: %1$s</string>
<string name="nest_mute_failed">Utišanje neuspešno: %1$s</string>
<string name="nest_record_permission_required">Za govor v tej sobi je potreben dostop do mikrofona.</string>
<string name="nest_open_settings">Odpri nastavitve</string>
<string name="nest_notification_channel">Gnezda</string>
@@ -512,6 +515,10 @@ Za podpisovanje se je potrebno prijaviti s privatnim ključem</string>
<string name="nest_notification_stop">Stop</string>
<string name="nest_join">Pridruži se gnezdu</string>
<string name="nest_lobby_host_label">Gostitelj</string>
<string name="nest_lobby_open_action">Odpri</string>
<string name="nest_lobby_recent_chat">Nedavni klepeti</string>
<string name="nest_lobby_no_listeners">Trenutno v sobi ni nikogar</string>
<string name="nest_lobby_listeners_count">%1$d jih posluša</string>
<string name="nest_role_host">Gostitelj</string>
<string name="nest_role_moderator">Moderator</string>
<string name="nest_state_muted">Mikrofon je utišan</string>
@@ -522,11 +529,18 @@ Za podpisovanje se je potrebno prijaviti s privatnim ključem</string>
<string name="nest_presence_speaking">govori</string>
<string name="nest_presence_listening">pridružen kot občinstvo</string>
<string name="nest_presence_left">zapusti gnezdo</string>
<string name="nest_leave">Zapusti</string>
<string name="nest_leave_host_title">Prekini to gnezdo</string>
<string name="nest_leave_host_body">Ste gostitelj. Če zaprete sobo, boste prekinili povezavo vsem. Izberite »Zapusti«, če se želite vrniti pozneje soba se samodejno zapre po 8 urah neaktivnosti.</string>
<string name="nest_leave_host_close">Zapri sobo</string>
<string name="nest_leave_host_just_leave">Zapusti</string>
<string name="nest_leave_host_close_failed">Sobe ni bilo mogoče zapreti. Vseeno zapuščam soba se bo zaprla samodejno.</string>
<plurals name="nest_listener_count">
<item quantity="one">%1$d poslušalec</item>
<item quantity="two">%1$d poslušalcev</item>
<item quantity="few">%1$d poslušalcev</item>
<item quantity="other">%1$d poslušalci</item>
</plurals>
<string name="nest_live_chip">V ŽIVO</string>
<string name="nest_tab_chat">Pogovor</string>
<string name="nest_tab_audience">Občinstvo</string>
@@ -561,7 +575,15 @@ Za podpisovanje se je potrebno prijaviti s privatnim ključem</string>
<string name="nest_minimize">Pomanjšaj</string>
<string name="nest_minimize_description">Pomanjšaj za nadaljnje poslušanje</string>
<string name="nest_audio_dropped">Izpad zvoka</string>
<string name="nest_loading_room">Nalagam sobo…</string>
<string name="nest_unjoinable_title">Te sobe ne morem odpreti</string>
<string name="nest_unjoinable_body">V prostoru manjka avdio storitev ali končna točka. Gostitelj mora posodobiti podatke, preden se poslušalci lahko povežejo.</string>
<string name="nest_unjoinable_back">Nazaj</string>
<string name="nest_create_fab">Začni sobo za klepet</string>
<string name="nests_section_live_now">V živo zdaj</string>
<string name="nests_section_scheduled">Na urniku</string>
<string name="nests_section_recently_ended">Nedavno dodano</string>
<string name="nests_ended_ago">Končano %1$s nazaj</string>
<string name="nest_no_server_title">Vzpostavi strežnik gnezda</string>
<string name="nest_no_server_body">Niste še izbrali strežnika gnezda. Želite dodati %1$s na seznam strežnikov in nadaljevati?\n\nTo lahko kasneje spremenite v nastavitvah.</string>
<string name="nest_no_server_use_default">Uporabi privzeto</string>
@@ -571,6 +593,7 @@ Za podpisovanje se je potrebno prijaviti s privatnim ključem</string>
<string name="nest_create_field_room">Ime sobe</string>
<string name="nest_create_field_summary">O čem teče beseda?</string>
<string name="nest_create_field_service">URL naslov storitve MoQ</string>
<string name="nest_create_field_service_hint">Stranska storitev za avtentikacijo — privzeto nostrnests.com</string>
<string name="nest_create_field_endpoint">Končna točka releja MoQ</string>
<string name="nest_create_field_endpoint_hint">URL za WebTransport običajno isti gostitelj</string>
<string name="nest_create_field_image">URL naslovne slike (neobvezno)</string>
@@ -583,6 +606,11 @@ Za podpisovanje se je potrebno prijaviti s privatnim ključem</string>
<string name="nests_servers_my_section">Vaši strežniki</string>
<string name="nests_servers_my_explainer">Shranjeno kot zamenljiv dogodek kind-10112, da lahko vaše nastavitve berejo tudi drugi odjemalci.</string>
<string name="nests_servers_add_field">Dodaj strežnik gnezda</string>
<string name="nests_servers_add_relay_field">Rele (WebTransport) URL</string>
<string name="nests_servers_add_auth_field">URL za avtentikacijo (JWT mint)</string>
<string name="nests_servers_add_pair_button">Dodaj</string>
<string name="nests_servers_relay_label">Rele</string>
<string name="nests_servers_auth_label">Avtentikacija</string>
<string name="nests_servers_recommended_section">Priporočeni strežniki</string>
<string name="nests_servers_recommended_explainer">Vgrajeni predlogi, ki jih lahko na seznam dodate z enim dotikom.</string>
<string name="nests_servers_use_defaults">uporabi Amethyst-ove privzete nastavitve</string>
@@ -1313,6 +1341,10 @@ Za podpisovanje se je potrebno prijaviti s privatnim ključem</string>
<string name="error_unable_to_fetch_invoice">Ni mogoče pridobiti fakture s strežnikov prejemnika</string>
<string name="wallet_connect_pay_invoice_error_error">Vaš \"Wallet Connect\" ponudnik je vrnil naslednjo napako: %1$s</string>
<string name="could_not_connect_to_tor">Ni bilo mogoče povezati s Tor</string>
<string name="tor_connection_failed_title">Tor se ne povezuje</string>
<string name="tor_connection_failed_body">Amethystu ni uspelo zagnati povezave Tor. Želite uporabiti običajno povezavo za nalaganje vsebine? To izbiro bomo ohranili eno uro, nato bomo ponovno poskusili s Torom.</string>
<string name="tor_continue_without_for_session">Uporabi običajno povezavo</string>
<string name="tor_keep_waiting">Nadaljuj s čakanjem</string>
<string name="unable_to_download_relay_document">Prenos rele dokumentacije ni na voljo</string>
<string name="could_not_assemble_lnurl_from_lightning_address_check_the_user_s_setup">Ni bilo mogoče sestaviti LNUrl iz \"lightning\" naslova \'%1$s\'. Preverite uporabnikove nastavitve</string>
<string name="the_receiver_s_lightning_service_at_is_not_available_it_was_calculated_from_the_lightning_address_error_check_if_the_server_is_up_and_if_the_lightning_address_is_correct">Prejemnikova \"lightning\" storitev na %1$s ni na voljo. Izračunana je bila iz \"lightning\" naslova \'%2$s\'. Napaka: %3$s. Preverite, ali strežnik deluje in ali je \"lightning\" naslov pravilen</string>
@@ -1510,6 +1542,7 @@ Za podpisovanje se je potrebno prijaviti s privatnim ključem</string>
<string name="open_all_reactions_to_this_post">Odpri vse reakcije na to objavo</string>
<string name="close_all_reactions_to_this_post">Zapri vse reakcije na to objavo</string>
<string name="reply_description">Odgovori</string>
<string name="jump_to_parent_reply">Skoči na izvorno sporočilo</string>
<string name="boost_or_quote_description">Posreduj ali citiraj</string>
<string name="like_description">Všečkaj</string>
<string name="zap_description">Zap</string>
@@ -1518,6 +1551,10 @@ Za podpisovanje se je potrebno prijaviti s privatnim ključem</string>
<string name="bottom_bar_settings_description">Povlecite za spremembo vrstnega reda. Preklopite za dodajanje ali odstranjevanje elementov s spodnje vrstice. Če ni izbran noben element, je spodnja vrstica skrita.</string>
<string name="bottom_bar_settings_available">Dostopno</string>
<string name="bottom_bar_settings_reorder">Razporedi</string>
<string name="bottom_bar_settings_restore_default">Obnovi prevzete</string>
<string name="home_tabs_settings">Domači zavihki</string>
<string name="home_tabs_settings_description">Izberite zavihke, ki se prikažejo na začetnem zaslonu. Če je aktiven le en zavihek, je vrstica z zavihki skrita.</string>
<string name="home_tab_everything">Vse</string>
<string name="reactions_settings">Vrstica odzivnih ikon</string>
<string name="reactions_settings_description">Nastavite prikaza gumbov za odzive, njihov vrstni red in prikaz števcev.</string>
<string name="reactions_settings_enabled">Omogočeno</string>
@@ -2123,6 +2160,42 @@ Za podpisovanje se je potrebno prijaviti s privatnim ključem</string>
<string name="last_seen_on_date">Nazadje viden na %1$s (%2$s nazaj)</string>
<string name="last_seen_just_now">Nazadnje viden prav zdaj</string>
<string name="last_seen_never">Nikoli viden</string>
<plurals name="duration_minutes">
<item quantity="one">%1$d minuta</item>
<item quantity="two">%1$dminuti</item>
<item quantity="few">%1$d minut</item>
<item quantity="other">%1$d minute</item>
</plurals>
<plurals name="duration_hours">
<item quantity="one">%1$d Ura</item>
<item quantity="two">%1$d uri</item>
<item quantity="few">%1$d ure</item>
<item quantity="other">%1$d ur</item>
</plurals>
<plurals name="duration_days">
<item quantity="one">%1$d dan</item>
<item quantity="two">%1$d dni</item>
<item quantity="few">%1$d dni</item>
<item quantity="other">%1$d dni</item>
</plurals>
<plurals name="duration_weeks">
<item quantity="one">%1$d teden</item>
<item quantity="two">%1$d tedna</item>
<item quantity="few">%1$d tedni</item>
<item quantity="other">%1$d tednov</item>
</plurals>
<plurals name="duration_months">
<item quantity="one">%1$d mesec</item>
<item quantity="two">%1$d meseca</item>
<item quantity="few">%1$d meseci</item>
<item quantity="other">%1$d mesecev</item>
</plurals>
<plurals name="duration_years">
<item quantity="one">%1$d leto</item>
<item quantity="two">%1$d leti</item>
<item quantity="few">%1$d let</item>
<item quantity="other">%1$d let</item>
</plurals>
<string name="event_sync_less_than_until">&lt;%1$s</string>
<string name="event_sync_status_connecting">Povezujem</string>
<string name="event_sync_status_downloading">Prenašam</string>
@@ -22,23 +22,35 @@ package com.vitorpamplona.amethyst.commons.relayClient.subscriptions
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.MutableComposeSubscriptionManager
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.MutableQueryState
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
private const val UNSUBSCRIBE_GRACE_MILLIS = 30_000L
/**
* A lifecycle-aware version of [KeyDataSourceSubscription] that subscribes
* when the lifecycle reaches STARTED and unsubscribes when it reaches STOPPED.
* when the lifecycle reaches STARTED and unsubscribes 30 seconds after it
* reaches STOPPED. If the lifecycle returns to STARTED before the grace
* period elapses, the pending unsubscribe is cancelled and the subscription
* keeps running uninterrupted.
*
* The grace window absorbs short app switches (copy a snippet from another
* app, dismiss a notification, glance at recents) without tearing down and
* rebuilding the relay REQ which would otherwise lose EOSE state and
* trigger a refetch on return.
*
* Use this for heavy feed subscriptions (home, video, discovery, chatroom list)
* that should NOT run when the app is in the background. When an always-on
* notification service keeps the relay client connected, these subscriptions
* would otherwise leak bandwidth on feeds nobody is viewing.
* that should NOT run when the app is truly in the background. When an
* always-on notification service keeps the relay client connected, these
* subscriptions would otherwise leak bandwidth on feeds nobody is viewing.
*
* Lightweight subscriptions that should always run (account metadata, notifications,
* gift wraps) should continue using the regular [KeyDataSourceSubscription].
@@ -49,24 +61,43 @@ fun <T> LifecycleAwareKeyDataSourceSubscription(
dataSource: ComposeSubscriptionManager<T>,
) {
val lifecycleOwner = LocalLifecycleOwner.current
var isStarted by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
DisposableEffect(state, lifecycleOwner) {
var isSubscribed = false
var pendingUnsubscribe: Job? = null
fun subscribeNow() {
pendingUnsubscribe?.cancel()
pendingUnsubscribe = null
if (!isSubscribed) {
dataSource.subscribe(state)
isSubscribed = true
}
}
fun scheduleUnsubscribe() {
if (!isSubscribed || pendingUnsubscribe != null) return
pendingUnsubscribe =
scope.launch {
delay(UNSUBSCRIBE_GRACE_MILLIS)
if (isSubscribed) {
dataSource.unsubscribe(state)
isSubscribed = false
}
pendingUnsubscribe = null
}
}
val observer =
LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_START -> {
if (!isStarted) {
dataSource.subscribe(state)
isStarted = true
}
subscribeNow()
}
Lifecycle.Event.ON_STOP -> {
if (isStarted) {
dataSource.unsubscribe(state)
isStarted = false
}
scheduleUnsubscribe()
}
else -> {}
@@ -75,17 +106,84 @@ fun <T> LifecycleAwareKeyDataSourceSubscription(
lifecycleOwner.lifecycle.addObserver(observer)
// If already started (e.g., recomposition while visible), subscribe immediately
if (lifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
dataSource.subscribe(state)
isStarted = true
subscribeNow()
}
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
if (isStarted) {
pendingUnsubscribe?.cancel()
pendingUnsubscribe = null
if (isSubscribed) {
dataSource.unsubscribe(state)
isStarted = false
isSubscribed = false
}
}
}
}
@Composable
fun <T : MutableQueryState> LifecycleAwareKeyDataSourceSubscription(
state: T,
dataSource: MutableComposeSubscriptionManager<T>,
) {
val lifecycleOwner = LocalLifecycleOwner.current
val scope = rememberCoroutineScope()
DisposableEffect(state, lifecycleOwner) {
var isSubscribed = false
var pendingUnsubscribe: Job? = null
fun subscribeNow() {
pendingUnsubscribe?.cancel()
pendingUnsubscribe = null
if (!isSubscribed) {
dataSource.subscribe(state)
isSubscribed = true
}
}
fun scheduleUnsubscribe() {
if (!isSubscribed || pendingUnsubscribe != null) return
pendingUnsubscribe =
scope.launch {
delay(UNSUBSCRIBE_GRACE_MILLIS)
if (isSubscribed) {
dataSource.unsubscribe(state)
isSubscribed = false
}
pendingUnsubscribe = null
}
}
val observer =
LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_START -> {
subscribeNow()
}
Lifecycle.Event.ON_STOP -> {
scheduleUnsubscribe()
}
else -> {}
}
}
lifecycleOwner.lifecycle.addObserver(observer)
if (lifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
subscribeNow()
}
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
pendingUnsubscribe?.cancel()
pendingUnsubscribe = null
if (isSubscribed) {
dataSource.unsubscribe(state)
isSubscribed = false
}
}
}