Merge pull request #4248 from vitorpamplona/claude/intelligent-babbage-23komv

refactor: one UI, waves 4–5 — Account, AccountViewModel, NoteCompose and RichTextViewer into commons/commonsUI
This commit is contained in:
Vitor Pamplona
2026-09-28 21:27:01 -04:00
committed by GitHub
1646 changed files with 15996 additions and 8771 deletions
+28 -10
View File
@@ -11,9 +11,11 @@ contain implementations of Nostr specifications and utilities to help implement
`concord/` (`cordXX`), `buzz/`, plus the binding-agnostic RFC 9420 engine in `mls/`. A new protocol
over Nostr belongs here as a package, not as a Gradle module; `quic`/`nestsClient`/`marmotQuic` are
modules because they are transports with no Nostr in them. Commons stores
shared code between Amethyst Android (`amethyst`) and Amethyst Desktop (`desktopApp`). The Desktop
App is designed to be mouse first and so uses a completely different screen and navigation
architecture while sharing the back end components with the android counterpart. `cli` ships `amy`,
shared code between Amethyst Android (`amethyst`) and Amethyst Desktop (`desktopApp`). Android now
also ships on laptops, so the direction is **one UI**: every screen and the navigation shell at every
window size move to `commonsUI`, `amethyst` shrinks to an Android shim, and a new `desktopApp`
becomes a JVM shim that renders the same UI (see `commons/plans/2026-09-27-one-ui-android-desktop.md`).
Until it lands, today's `desktopApp` still has its own mouse-first screens and navigation. `cli` ships `amy`,
a non-interactive JVM command-line client that drives the same `quartz` + `commons` code — used by
humans, agents, and interop tests. `quic` is a from-scratch pure-Kotlin QUIC v1 + HTTP/3 +
WebTransport client (no JNI, no BouncyCastle), built because no Android-compatible Java QUIC library
@@ -105,7 +107,9 @@ amethyst/
codecs. Not WebTransport — the binding has its own ALPNs and writes frames
straight onto QUIC streams, so it deliberately does not reuse
`nestsClient`'s `WebTransportSession`.
- `amethyst/` & `desktopApp/` = Platform-native layouts and navigation
- `amethyst/` & `desktopApp/` = Platform shims: process/window entry points, services, system
integrations and the actuals of shared ports. Screens and navigation are shared UI and are
moving to `commonsUI` (today's `desktopApp` screens are legacy, to be replaced).
- `cli/` = Thin assembly layer over `quartz/` + `commons/` (no new logic
allowed). May also depend on `:geode` (for `amy serve`, which embeds the
standalone relay); never on `:commonsUI`, `:amethyst` or `:desktopApp`.
@@ -207,10 +211,21 @@ etc. instead of re-implementing them.
(StateFlow/SharedFlow), so they belong in `commons`; anything that imports
`androidx.compose.ui`/`foundation`/`material3`, Coil, or `Res` belongs in
`commonsUI`.
- **Keep native** → screen composables/scaffolding (Desktop `Window` vs Android
`Activity`), navigation (sidebar vs bottom nav), platform interactions
(gestures, keyboard shortcuts), system integrations (notifications, file
pickers).
**Screens and navigation are shared too**: screen composables, the nav host,
and the navigation chrome for every window size (bottom bar, rail, permanent
drawer) belong in `commonsUI`, because Android runs on laptops and the new
Desktop app renders the same UI. Adapt to the window with `ScreenLayoutSpec`
/ `LocalScreenLayout`, not with a per-platform screen.
- **Keep native** → only what is the platform itself: the process/window entry
point (Android `Activity`/`Service`, Desktop `Window`/tray/menu bar),
system integrations (notifications, file pickers, share sheets, camera,
media3, WebView, keyring/Keystore), and the platform `actual`s or port
implementations the shared UI calls. A new screen goes in `commonsUI` when
its dependencies allow — `AccountViewModel` itself is in `commonsUI`
(`commons.viewmodels`), reaching Android through `AccountViewModelHost`. A
screen that still needs an app-only helper may live in `amethyst/`, but keep
Android APIs out of it (behind a port or slot) so it can move later. Don't add screens to today's `desktopApp`
that the shared UI will have to re-create.
When extracting a composable: move it to `commonsUI/commonMain/` (see
`/compose-expert`), add expect/actual for any platform behavior (see
@@ -429,8 +444,11 @@ Do this before considering the task complete.
at byte level (`perl -CSD -pe 's/\x{202E}/\\u202E/g'`).
### Navigation Shell
- **Desktop**: Sidebar + main content area
- **Android**: Bottom navigation
One shell, picked by window size rather than by platform: `ScreenLayoutSpec`
chooses the bottom bar (compact), the rail, or the permanent drawer (wide,
landscape, tall enough), and docks the notification panel on very wide windows.
It is moving to `commonsUI` with `AppNavigation`. Today's `desktopApp` still has
its own sidebar shell, which will be replaced.
## Git Workflow
+17 -10
View File
@@ -1,6 +1,6 @@
---
name: compose-expert
description: Advanced Compose Multiplatform UI patterns for shared composables. Use when working with visual UI components, state management patterns (remember, derivedStateOf, produceState), recomposition optimization (@Stable/@Immutable visual usage), Material3 theming, custom ImageVector icons, or determining whether to share UI in commonMain vs keep platform-specific. Delegates navigation to android-expert/desktop-expert. Complements kotlin-expert (handles Kotlin language aspects of state/annotations).
description: Advanced Compose Multiplatform UI patterns for shared composables. Use when working with visual UI components, state management patterns (remember, derivedStateOf, produceState), recomposition optimization (@Stable/@Immutable visual usage), Material3 theming, custom ImageVector icons, or determining whether to share UI in commonMain vs keep platform-specific. Delegates platform entry points (Activity, Window, tray) to android-expert/desktop-expert; the navigation shell itself is shared UI. Complements kotlin-expert (handles Kotlin language aspects of state/annotations).
---
# Compose Multiplatform Expert
@@ -34,12 +34,17 @@ Visual UI patterns for sharing composables across Android and Desktop.
- **Theme utilities**: Color calculations, style helpers
- **Material3 components**: Any UI using Material primitives
**Screens and navigation are shared too.** Android also ships on laptops and the new
Desktop app renders the same UI, so screens, the nav host and the bottom bar / rail /
permanent drawer all belong in `commonsUI`. They adapt to the window with
`ScreenLayoutSpec` / `LocalScreenLayout`, not per platform. See
`commons/plans/2026-09-27-one-ui-android-desktop.md`.
### Keep Platform-Specific
- **Navigation structure**: Bottom nav (Android) vs Sidebar (Desktop)
- **Screen layouts**: Platform-specific scaffolding
- **System integrations**: File pickers, notifications, share sheets
- **Platform UX**: Gestures, keyboard shortcuts, window management
- **Entry points**: Android `Activity`/`Service`, Desktop `Window`, tray, menu bar
- **System integrations**: File pickers, notifications, share sheets, camera, media3, WebView
- **Platform leaves inside shared UI**: behind expect/actual or a slot the shim fills
### Decision Framework
@@ -563,12 +568,14 @@ fun FeedList(items: List<Item>) {
4. Add caching if generated dynamically
5. Wrap in @Composable for easy use
### Navigation (Delegate)
### Navigation
For navigation patterns:
- Android bottom nav → `android-expert`
- Desktop sidebar → `desktop-expert`
- Multi-window → `desktop-expert`
The navigation shell is shared (`ScreenLayoutSpec` picks bottom bar, rail or permanent
drawer by window size) and is moving to `commonsUI` with `AppNavigation`. Delegate only
the platform leaves:
- Activity / intent plumbing → `android-expert`
- Window, tray, menu bar, multi-window → `desktop-expert`
- Today's legacy `desktopApp` sidebar shell → `desktop-expert` (to be replaced)
## Related Skills
+9 -2
View File
@@ -248,9 +248,16 @@ application {
## 5. Desktop Navigation Patterns
### NavigationRail (Current Pattern)
### NavigationRail (Current Pattern — legacy)
Desktop uses **NavigationRail** (vertical sidebar) instead of Android's bottom navigation.
> **Being replaced.** Android now also ships on laptops, and the plan is one UI: the
> shared navigation shell (bottom bar / rail / permanent drawer picked by window size
> via `ScreenLayoutSpec`) moves to `commonsUI`, and a new `desktopApp` becomes a JVM
> shim that renders it. Don't grow this sidebar shell or add Desktop-only screens; see
> `commons/plans/2026-09-27-one-ui-android-desktop.md`. What stays Desktop-specific is
> the window, tray, menu bar, keyboard shortcuts and file system integration.
Today's desktop app uses **NavigationRail** (vertical sidebar) instead of Android's bottom navigation.
```kotlin
Row(Modifier.fillMaxSize()) {
+19 -14
View File
@@ -52,8 +52,9 @@ Q: Does it vary by platform or by JVM vs non-JVM?
│ Example: Jackson JSON parsing (JVM library)
│
└─ Complex/UI-related
→ Keep platform-specific
Example: Navigation (Activity vs Window too different)
→ Share it in commonsUI; expect/actual or a slot only for the platform leaf
Example: Navigation. One shell in commonsUI, picked by window size
(ScreenLayoutSpec); only the Activity / Window entry point is native
Final check:
Q: Maintenance cost of abstraction < duplication cost?
@@ -85,10 +86,13 @@ val jvmAndroid = create("jvmAndroid") {
```
**Why:** Jackson is JVM-only, works on Android + Desktop, not iOS/web.
**Navigation → platform-specific:**
- Android: `MainActivity` (Activity + Compose Navigation)
- Desktop: `Window` + sidebar + MenuBar
**Why:** UI paradigms fundamentally different.
**Navigation → shared, entry points → platform-specific:**
- Shared (`commonsUI`, moving there): the nav host, bottom bar / rail /
permanent drawer, chosen by window size via `ScreenLayoutSpec`
- Android: `MainActivity` hosts it; Desktop: a `Window` (+ MenuBar, tray) hosts it
**Why:** Android runs on laptops too, and the new Desktop app renders the same
UI. See `commons/plans/2026-09-27-one-ui-android-desktop.md`. Today's
`desktopApp` sidebar shell is legacy.
## Mental Model: Source Sets as Dependency Graph
@@ -183,15 +187,15 @@ Quick decision guidelines based on codebase patterns:
### Sometimes Abstract
- **Business logic:** YES - state machines, data processing
- **ViewModels:** YES - state + business logic shareable (StateFlow/SharedFlow)
- **Screen layouts:** NO - platform-native (Window vs Activity)
- **Why:** ViewModels contain platform-agnostic state; Screens render differently per platform
- **Screen layouts:** YES - shared in `commonsUI`; they adapt to window size, not platform
- **Why:** One UI for Android (phones to laptops) and Desktop; only entry points differ
### Rarely Abstract
- **Complex UI components** (composables with heavy platform dependencies)
- **Why:** Platform paradigms can differ significantly
### Never Abstract
- **Navigation** (Activity vs Window fundamentally different)
- **Process / window entry points** (Activity, Service, Window, tray)
- **Permissions** (Android vs iOS APIs incompatible)
- **Platform UX patterns**
- **Why:** Too platform-specific, abstraction creates leaky APIs
@@ -203,7 +207,7 @@ Quick decision guidelines based on codebase patterns:
| PubKeyFormatter, ZapFormatter | ✅ YES | Pure Kotlin, no platform APIs |
| TimeAgoFormatter | ⚠️ ABSTRACTED | Needs StringProvider for localized strings |
| ViewModels (state + logic) | ✅ YES | StateFlow/SharedFlow platform-agnostic, Compose Multiplatform lifecycle compatible |
| Screen layouts (Scaffold, nav) | ❌ NO | Window vs Activity, sidebar vs bottom nav fundamentally different |
| Screen layouts (Scaffold, nav) | ✅ YES (moving) | One UI; `ScreenLayoutSpec` picks bottom bar / rail / drawer by window size |
| Image loading (Coil) | ⚠️ ABSTRACTED | Coil 3.x supports KMP, needs expect/actual wrapper |
## expect/actual Mechanics
@@ -333,8 +337,9 @@ fun validateSignature(...) { ... } // Duplicated!
// ❌ BAD
expect fun NavigationComponent(...)
```
**Why:** Navigation paradigms too different (Activity vs Window)
**Fix:** Keep platform-specific, accept duplication
**Why:** It forks the whole navigation UI per platform.
**Fix:** Write the navigation once in `commonsUI`; put only the platform leaf
(an Activity result, a file picker) behind expect/actual or a slot
### 2. Under-Sharing
**Problem:** Duplicating business logic across platforms
@@ -384,8 +389,8 @@ import com.fasterxml.jackson.databind.ObjectMapper
| **ViewModels** | **commons/commonMain/viewmodels/** | **StateFlow/SharedFlow + logic shareable, Compose MP lifecycle compatible** |
| UI formatters (pure) | commons/commonMain | Reusable, no dependencies |
| UI components (simple) | commonsUI/commonMain | Cards, buttons, dialogs (Compose UI never goes in `commons`) |
| **Screen layouts** | **Platform-specific** | **Window vs Activity, sidebar vs bottom nav** |
| Navigation | Platform-specific only | Activity vs Window too different |
| **Screen layouts** | **commonsUI** | **One UI; adapt to window size with `ScreenLayoutSpec`** |
| Navigation | commonsUI (entry point native) | Nav host + chrome shared; Activity / Window hosts it |
| Permissions | Platform-specific only | APIs incompatible |
| Platform UX (menus, etc.) | Platform-specific only | Native feel required |
@@ -25,18 +25,19 @@ import android.graphics.Color
import androidx.core.graphics.createBitmap
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.vitorpamplona.amethyst.commons.model.AccountSettings
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.model.mediaServers.DEFAULT_MEDIA_SERVERS
import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName
import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerType
import com.vitorpamplona.amethyst.commons.model.nipB7Blossom.BlossomServerListState
import com.vitorpamplona.amethyst.commons.service.http.DefaultContentTypeInterceptor
import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.model.nipB7Blossom.BlossomServerListState
import com.vitorpamplona.amethyst.service.uploads.FileHeader
import com.vitorpamplona.amethyst.commons.service.upload.FileHeader
import com.vitorpamplona.amethyst.service.uploads.ImageDownloader
import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader
import com.vitorpamplona.amethyst.service.uploads.nip96.Nip96Uploader
import com.vitorpamplona.amethyst.service.uploads.nip96.ServerInfoRetriever
import com.vitorpamplona.amethyst.service.uploads.prepare
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
@@ -21,19 +21,25 @@
package com.vitorpamplona.amethyst
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.model.AccountSettings
import com.vitorpamplona.amethyst.commons.model.InMemoryGeohashIdentityStore
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.model.location.LocationResult
import com.vitorpamplona.amethyst.commons.model.marmot.MarmotGroupNotifier
import com.vitorpamplona.amethyst.commons.model.nip46Signer.Nip46ConsentPrompter
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter
import com.vitorpamplona.amethyst.commons.notifications.dal.NotificationFeedFilter
import com.vitorpamplona.amethyst.commons.relayClient.assemblers.CashuMintDirectoryFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.nip47WalletConnect.NWCPaymentFilterAssembler
import com.vitorpamplona.amethyst.commons.service.http.EncryptionKeyCache
import com.vitorpamplona.amethyst.commons.service.http.OkHttpWebSocket
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.service.location.LocationState
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.dal.NotificationFeedFilter
import com.vitorpamplona.amethyst.model.accountsCache.defaultMarmotStreamTransport
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import com.vitorpamplona.quartz.nip03Timestamp.EmptyOtsResolverBuilder
import com.vitorpamplona.quartz.nip60Cashu.mintApi.OkHttpMintTransport
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
@@ -83,14 +89,21 @@ class NotificationFeedFilterModeOverrideTest {
Account(
settings = AccountSettings(keyPair = keyPair),
signer = NostrSignerInternal(keyPair),
geolocationFlow = { MutableStateFlow<LocationState.LocationResult>(LocationState.LocationResult.Loading) },
geolocationFlow = { MutableStateFlow<LocationResult>(LocationResult.Loading) },
nwcFilterAssembler = { NWCPaymentFilterAssembler(client) },
cashuMintDirectoryFilterAssembler = { CashuMintDirectoryFilterAssembler(client) },
okHttpClientForMoney = { OkHttpClient() },
cashuMintTransport = OkHttpMintTransport { OkHttpClient() },
otsResolverBuilder = { EmptyOtsResolverBuilder.build() },
cache = LocalCache,
client = client,
scope = scope,
appVersion = "test",
encryptionKeyCache = EncryptionKeyCache(),
saveSettings = {},
marmotNotifier = { MarmotGroupNotifier.None },
nip46Consent = Nip46ConsentPrompter.Unanswered,
geohashIdentityStore = InMemoryGeohashIdentityStore(),
marmotStreamTransportFactory = ::defaultMarmotStreamTransport,
)
}
@@ -21,14 +21,19 @@
package com.vitorpamplona.amethyst
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.model.AccountSettings
import com.vitorpamplona.amethyst.commons.model.InMemoryGeohashIdentityStore
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.model.location.LocationResult
import com.vitorpamplona.amethyst.commons.model.marmot.MarmotGroupNotifier
import com.vitorpamplona.amethyst.commons.model.nip46Signer.Nip46ConsentPrompter
import com.vitorpamplona.amethyst.commons.relayClient.assemblers.CashuMintDirectoryFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.nip47WalletConnect.NWCPaymentFilterAssembler
import com.vitorpamplona.amethyst.commons.service.http.EncryptionKeyCache
import com.vitorpamplona.amethyst.commons.service.http.OkHttpWebSocket
import com.vitorpamplona.amethyst.commons.viewmodels.thread.ThreadFeedFilter
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.service.location.LocationState
import com.vitorpamplona.amethyst.model.accountsCache.defaultMarmotStreamTransport
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.crypto.verify
@@ -37,6 +42,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
import com.vitorpamplona.quartz.nip03Timestamp.EmptyOtsResolverBuilder
import com.vitorpamplona.quartz.nip60Cashu.mintApi.OkHttpMintTransport
import junit.framework.TestCase
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -73,14 +79,21 @@ class ThreadDualAxisChartAssemblerTest {
Account(
settings = AccountSettings(keyPair = keyPair),
signer = NostrSignerInternal(keyPair),
geolocationFlow = { MutableStateFlow<LocationState.LocationResult>(LocationState.LocationResult.Loading) },
geolocationFlow = { MutableStateFlow<LocationResult>(LocationResult.Loading) },
nwcFilterAssembler = { NWCPaymentFilterAssembler(client) },
cashuMintDirectoryFilterAssembler = { CashuMintDirectoryFilterAssembler(client) },
okHttpClientForMoney = { OkHttpClient() },
cashuMintTransport = OkHttpMintTransport { OkHttpClient() },
otsResolverBuilder = { EmptyOtsResolverBuilder.build() },
cache = LocalCache,
client = client,
scope = scope,
appVersion = "test",
encryptionKeyCache = EncryptionKeyCache(),
saveSettings = {},
marmotNotifier = { MarmotGroupNotifier.None },
nip46Consent = Nip46ConsentPrompter.Unanswered,
geohashIdentityStore = InMemoryGeohashIdentityStore(),
marmotStreamTransportFactory = ::defaultMarmotStreamTransport,
)
val db =
@@ -30,6 +30,7 @@ import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.test.junit4.v2.createComposeRule
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.amethyst.commons.ui.components.mediaSizingModifier
import com.vitorpamplona.amethyst.service.playback.composable.audioSquare
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
@@ -20,13 +20,7 @@
*/
package com.vitorpamplona.amethyst.ui.components
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.commons.ui.components.TranslationPlatform
/**
* No translation service in this flavor, so no note is ever translated and the copy-text
* menus never need to offer a "Copy Translated" option.
*/
fun cachedTranslation(
content: String,
accountViewModel: AccountViewModel,
): String? = null
/** No translation service in this flavor, so the content is always its own "translation". */
val FlavorTranslationPlatform: TranslationPlatform = TranslationPlatform.None
@@ -23113,8 +23113,8 @@ SPLcom/vitorpamplona/amethyst/commons/model/AccentColorType;-><clinit>()V
SPLcom/vitorpamplona/amethyst/commons/model/AccentColorType;-><init>(Ljava/lang/String;III)V
SPLcom/vitorpamplona/amethyst/commons/model/AccentColorType;->valueOf(Ljava/lang/String;)Lcom/vitorpamplona/amethyst/commons/model/AccentColorType;
SPLcom/vitorpamplona/amethyst/commons/model/AccentColorType;->values()[Lcom/vitorpamplona/amethyst/commons/model/AccentColorType;
Lcom/vitorpamplona/amethyst/model/Account;
Lcom/vitorpamplona/amethyst/model/AccountSettings;
Lcom/vitorpamplona/amethyst/commons/model/Account;
Lcom/vitorpamplona/amethyst/commons/model/AccountSettings;
Lcom/vitorpamplona/amethyst/model/AntiSpamFilter;
SPLcom/vitorpamplona/amethyst/model/AntiSpamFilter;-><clinit>()V
SPLcom/vitorpamplona/amethyst/model/AntiSpamFilter;-><init>()V
@@ -23944,7 +23944,7 @@ SPLcom/vitorpamplona/amethyst/service/notifications/NwcPaymentNotificationWatche
Lcom/vitorpamplona/amethyst/service/notifications/NwcPaymentNotificationWatcher$start$1;
SPLcom/vitorpamplona/amethyst/service/notifications/NwcPaymentNotificationWatcher$start$1;-><init>(Lcom/vitorpamplona/amethyst/service/notifications/NwcPaymentNotificationWatcher;Lkotlin/coroutines/Continuation;)V
SPLcom/vitorpamplona/amethyst/service/notifications/NwcPaymentNotificationWatcher$start$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation;
SPLcom/vitorpamplona/amethyst/service/notifications/NwcPaymentNotificationWatcher$start$1;->invokeSuspend$lambda$0(Lcom/vitorpamplona/amethyst/model/Account;Lcom/vitorpamplona/amethyst/model/Account;)Z
SPLcom/vitorpamplona/amethyst/service/notifications/NwcPaymentNotificationWatcher$start$1;->invokeSuspend$lambda$0(Lcom/vitorpamplona/amethyst/commons/model/Account;Lcom/vitorpamplona/amethyst/commons/model/Account;)Z
SPLcom/vitorpamplona/amethyst/service/notifications/NwcPaymentNotificationWatcher$start$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
Lcom/vitorpamplona/amethyst/service/notifications/NwcPaymentNotificationWatcher$start$1$$ExternalSyntheticLambda0;
SPLcom/vitorpamplona/amethyst/service/notifications/NwcPaymentNotificationWatcher$start$1$$ExternalSyntheticLambda0;-><init>()V
@@ -23952,7 +23952,7 @@ SPLcom/vitorpamplona/amethyst/service/notifications/NwcPaymentNotificationWatche
Lcom/vitorpamplona/amethyst/service/notifications/NwcPaymentNotificationWatcher$start$1$2;
SPLcom/vitorpamplona/amethyst/service/notifications/NwcPaymentNotificationWatcher$start$1$2;-><init>(Lcom/vitorpamplona/amethyst/service/notifications/NwcPaymentNotificationWatcher;Lkotlin/coroutines/Continuation;)V
SPLcom/vitorpamplona/amethyst/service/notifications/NwcPaymentNotificationWatcher$start$1$2;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation;
SPLcom/vitorpamplona/amethyst/service/notifications/NwcPaymentNotificationWatcher$start$1$2;->invoke(Lcom/vitorpamplona/amethyst/model/Account;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
SPLcom/vitorpamplona/amethyst/service/notifications/NwcPaymentNotificationWatcher$start$1$2;->invoke(Lcom/vitorpamplona/amethyst/commons/model/Account;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
SPLcom/vitorpamplona/amethyst/service/notifications/NwcPaymentNotificationWatcher$start$1$2;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
SPLcom/vitorpamplona/amethyst/service/notifications/NwcPaymentNotificationWatcher$start$1$2;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
Lcom/vitorpamplona/amethyst/service/notifications/PokeyReceiver;
@@ -24802,7 +24802,7 @@ Lcom/vitorpamplona/amethyst/ui/screen/AccountSessionManager;
SPLcom/vitorpamplona/amethyst/ui/screen/AccountSessionManager;-><clinit>()V
SPLcom/vitorpamplona/amethyst/ui/screen/AccountSessionManager;-><init>(Lcom/vitorpamplona/amethyst/model/accountsCache/AccountCacheState;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lcom/vitorpamplona/amethyst/LocalPreferences;Lkotlinx/coroutines/CoroutineScope;)V
SPLcom/vitorpamplona/amethyst/ui/screen/AccountSessionManager;->getAccountContent()Lkotlinx/coroutines/flow/StateFlow;
SPLcom/vitorpamplona/amethyst/ui/screen/AccountSessionManager;->loggedInAccount()Lcom/vitorpamplona/amethyst/model/Account;
SPLcom/vitorpamplona/amethyst/ui/screen/AccountSessionManager;->loggedInAccount()Lcom/vitorpamplona/amethyst/commons/model/Account;
SPLcom/vitorpamplona/amethyst/ui/screen/AccountSessionManager;->loginWithDefaultAccount$default(Lcom/vitorpamplona/amethyst/ui/screen/AccountSessionManager;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
SPLcom/vitorpamplona/amethyst/ui/screen/AccountSessionManager;->loginWithDefaultAccount(Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
SPLcom/vitorpamplona/amethyst/ui/screen/AccountSessionManager;->loginWithDefaultAccountIfLoggedOff()V
@@ -26,6 +26,7 @@ import android.os.Build
import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp
import com.vitorpamplona.amethyst.commons.service.http.HttpClientEnvironment
import com.vitorpamplona.amethyst.commons.service.http.MediaCallEventListener
import com.vitorpamplona.amethyst.commons.util.AndroidDebugFlag
import com.vitorpamplona.amethyst.favorites.WebShortcuts
import com.vitorpamplona.amethyst.napplet.WebAppNetworkRegistry
import com.vitorpamplona.amethyst.napplet.WebSitePermissionRegistry
@@ -114,6 +115,9 @@ class Amethyst : Application() {
override fun onCreate() {
super.onCreate()
// Before anything logs: shared code's debug-only logging (commons `logTime`) reads this
// instead of the app's BuildConfig, in both processes.
AndroidDebugFlag.enabled = isDebug
Log.d("AmethystApp") { "onCreate $this" }
// Application.onCreate runs in every process. The sandboxed napplet host
@@ -28,6 +28,7 @@ import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStats
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation
import com.vitorpamplona.quartz.nip57Zaps.validate.LnurlEndpointCache
import com.vitorpamplona.quartz.nip57Zaps.validate.LnurlEndpointInfo
import kotlinx.coroutines.CoroutineScope
@@ -56,8 +57,11 @@ class AmethystLocalCacheHost(
override fun relaySelfPubKey(relay: NormalizedRelayUrl): HexKey? = modules.nip11Cache.getFromCache(relay).self
// The zap path's LNURL cache is a quartz-side singleton the outbound-zap resolver
// fills; it is jvmAndroid-only, which is why the cache reads it through here.
override fun relayInfo(relay: NormalizedRelayUrl): Nip11RelayInformation = modules.nip11Cache.getFromCache(relay)
override val nip11Cache get() = modules.nip11Cache
// The zap path's LNURL cache is a quartz-side singleton the outbound-zap resolver fills.
override fun lnurlEndpoint(lnurlpUrl: String): LnurlEndpointInfo? = LnurlEndpointCache.get(lnurlpUrl)
override fun assertNotMainThread() = checkNotInMainThread()
@@ -0,0 +1,81 @@
/*
* 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
import androidx.core.content.edit
import com.vitorpamplona.amethyst.commons.model.GeohashIdentityStore
import com.vitorpamplona.amethyst.commons.model.preferences.GeohashIdentitySecrets
import com.vitorpamplona.amethyst.commons.model.preferences.readLegacyGeohashIdentity
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
import com.vitorpamplona.quartz.nip19Bech32.toNpub
/**
* The account's geohash identity in [accountSecretsStore], copied on first read out of the
* encrypted SharedPreferences file older versions wrote, and mirrored back there until
* [LocalPreferences.LEGACY_WRITES_RETIRED] (so a rollback keeps the seed and the nickname).
*/
class AndroidGeohashIdentityStore(
private val pubKeyHex: HexKey,
) : GeohashIdentityStore {
/**
* The npub the current store is keyed by.
*
* The legacy file is keyed by the pubkey *hex* — the old code passed
* `signer.pubKey` where every other caller passes an npub, so the identity
* lived in `secret_keeper_<hex>`, a different file from the account's own
* `secret_keeper_<npub>`. The copy below reads that file and writes the
* npub-keyed store, which is what folds this orphan back in with the rest.
*/
private val npub by lazy { pubKeyHex.hexToByteArray().toNpub() }
/**
* What `secret_keeper_<pubkey hex>` holds.
*
* Only called when the store has nothing yet: opening this file creates it,
* so reading it unconditionally would resurrect it after the cleanup has
* deleted it. Touches disk; callers are off the main thread.
*/
private fun legacy(): GeohashIdentitySecrets = readLegacyGeohashIdentity(LegacySharedPreferences(Amethyst.instance.encryptedStorage(pubKeyHex)))
override suspend fun read(): GeohashIdentitySecrets = accountSecretsStore.readGeohashIdentity(npub) { legacy() }
override suspend fun write(value: GeohashIdentitySecrets) = accountSecretsStore.mirrorGeohashIdentity(npub, value)
// Mirrored, not moved: the legacy file stays readable until the legacy writes are retired
// app-wide. Gated on the same switch as every other mirror.
override fun mirrorLegacyNickname(nickname: String) {
if (!LocalPreferences.LEGACY_WRITES_RETIRED) {
Amethyst.instance.encryptedStorage(pubKeyHex).edit { putString(PREF_NICKNAME, nickname) }
}
}
override fun mirrorLegacyDeviceSeed(seedHex: String) {
if (!LocalPreferences.LEGACY_WRITES_RETIRED) {
Amethyst.instance.encryptedStorage(pubKeyHex).edit { putString(PREF_KEY, seedHex) }
}
}
companion object {
private const val PREF_KEY = "geohash_chat_device_seed"
private const val PREF_NICKNAME = "geohash_chat_nickname"
}
}
@@ -32,6 +32,7 @@ import com.vitorpamplona.amethyst.commons.browser.BrowserIconRegistry
import com.vitorpamplona.amethyst.commons.connectedApps.DataStoreNostrSignerPermissionStore
import com.vitorpamplona.amethyst.commons.connectedApps.nip46.DataStoreNip46ClientStore
import com.vitorpamplona.amethyst.commons.favorites.FavoriteAppsRegistry
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.model.NoteState
import com.vitorpamplona.amethyst.commons.model.UiSettings
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
@@ -53,6 +54,7 @@ import com.vitorpamplona.amethyst.commons.napplet.permissions.NappletPermissionL
import com.vitorpamplona.amethyst.commons.relayClient.BlockedRelayFilteringClient
import com.vitorpamplona.amethyst.commons.relayClient.diagnostics.BootRelayDiagnostics
import com.vitorpamplona.amethyst.commons.relayClient.event.EventFinderQueryState
import com.vitorpamplona.amethyst.commons.relayClient.reqCommand.RelaySubscriptionsCoordinator
import com.vitorpamplona.amethyst.commons.relayClient.speedLogger.RelaySpeedLogger
import com.vitorpamplona.amethyst.commons.relayClient.user.UserFinderQueryState
import com.vitorpamplona.amethyst.commons.relays.health.TorCircuitHealthTracker
@@ -63,6 +65,8 @@ import com.vitorpamplona.amethyst.commons.robohash.CachedRobohash
import com.vitorpamplona.amethyst.commons.scheduledposts.ScheduledPostStore
import com.vitorpamplona.amethyst.commons.scheduledposts.ScheduledPostWorkGate
import com.vitorpamplona.amethyst.commons.service.connectivity.ConnectivityStatus
import com.vitorpamplona.amethyst.commons.service.georelay.GeoRelayCsvLoader
import com.vitorpamplona.amethyst.commons.service.georelay.GeohashRelays
import com.vitorpamplona.amethyst.commons.service.http.BlossomReadAuthInterceptor
import com.vitorpamplona.amethyst.commons.service.http.BlossomReadAuthTokenProvider
import com.vitorpamplona.amethyst.commons.service.http.DualHttpClientManager
@@ -78,7 +82,7 @@ import com.vitorpamplona.amethyst.commons.service.pow.PoWPublishQueue
import com.vitorpamplona.amethyst.commons.state.UiSettingsState
import com.vitorpamplona.amethyst.commons.tor.TorRelayState
import com.vitorpamplona.amethyst.commons.tor.TorSettings
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.connectedApps.consent.Nip46ConsentBridge
import com.vitorpamplona.amethyst.model.accountsCache.AccountCacheState
import com.vitorpamplona.amethyst.model.nip60Cashu.CashuPreferences
import com.vitorpamplona.amethyst.model.preferences.UiSharedPreferences
@@ -115,7 +119,6 @@ import com.vitorpamplona.amethyst.service.relayClient.CacheClientConnector
import com.vitorpamplona.amethyst.service.relayClient.RelayProxyClientConnector
import com.vitorpamplona.amethyst.service.relayClient.authCommand.model.AuthCoordinator
import com.vitorpamplona.amethyst.service.relayClient.notifyCommand.model.NotifyCoordinator
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.RelaySubscriptionsCoordinator
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountSubscriptionRegistry
import com.vitorpamplona.amethyst.service.resourceusage.BatteryDrainSampler
import com.vitorpamplona.amethyst.service.resourceusage.ForegroundTimeIntegrator
@@ -199,6 +202,7 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
@@ -240,6 +244,13 @@ class AppModules(
private val _trimLevelEvents = MutableSharedFlow<Int>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
val trimLevelEvents = _trimLevelEvents.asSharedFlow()
/**
* Fires when memory is tight enough that feeds should drop the strong Note references they
* hold: the process is on the system LRU list, the strongest level the OS still delivers
* since API 34, or the heap watchdog raised the same level itself.
*/
val memoryPressureEvents: Flow<Unit> = trimLevelEvents.filter { it >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND }.map { }
/**
* The app-wide DataStore files — the ones that belong to the install rather
* than to an account. [AccountPreferenceStores] is the same idea keyed by
@@ -769,6 +780,8 @@ class AppModules(
// event, which is why this sits next to the cache rather than in a later init block.
init {
cache.appHost = AmethystLocalCacheHost(this, isDebug)
// Geohash chats route through the live georelays directory, fetched over this app's clients.
GeohashRelays.liveRelayLoader = { GeoRelayCsvLoader { okHttpClients.getHttpClient(false) }.fetch() }
}
// NIP-BC onchain zap verification backend. Wired up once at app init so
@@ -1035,6 +1048,13 @@ class AppModules(
otsResolverBuilder = { otsResolverBuilder.build() },
cache = cache,
client = client,
appVersion = BuildConfig.VERSION_NAME,
encryptionKeyCache = keyCache,
saveSettings = { LocalPreferences.saveToEncryptedStorage(it) },
// A provider: notificationDispatcher is declared further down this class.
marmotNotifier = { notificationDispatcher },
nip46Consent = Nip46ConsentBridge,
geohashIdentityStore = { AndroidGeohashIdentityStore(it) },
rootFilesDir = { appContext.filesDir },
powQueue = { powPublishQueue },
meterSigner = { MeteringNostrSigner(it, resourceUsage) },
@@ -32,8 +32,6 @@ import com.vitorpamplona.quartz.utils.Log
import com.vitorpamplona.quartz.utils.LogLevel
import com.vitorpamplona.quartz.utils.bytesUsedInMemory
import com.vitorpamplona.quartz.utils.pointerSizeInBytes
import kotlin.time.DurationUnit
import kotlin.time.measureTimedValue
@Suppress("SENSELESS_COMPARISON")
val isDebug = BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "benchmark"
@@ -236,36 +234,6 @@ fun debugState(context: Context) {
}
}
inline fun <T> logTime(
debugMessage: String,
minToReportMs: Int = 1,
block: () -> T,
): T =
if (isDebug) {
val (result, elapsed) = measureTimedValue(block)
if (elapsed.inWholeMilliseconds > minToReportMs) {
Log.d("DEBUG-TIME") { "${elapsed.toString(DurationUnit.MILLISECONDS, 3).padStart(12)}: $debugMessage" }
}
result
} else {
block()
}
inline fun <T> logTime(
debugMessage: (T) -> String,
minToReportMs: Int = 1,
block: () -> T,
): T =
if (isDebug) {
val (result, elapsed) = measureTimedValue(block)
if (elapsed.inWholeMilliseconds > minToReportMs) {
Log.d("DEBUG-TIME") { "${elapsed.toString(DurationUnit.MILLISECONDS, 3).padStart(12)}: ${debugMessage(result)}" }
}
result
} else {
block()
}
fun debug(
tag: String,
debugMessage: String,
@@ -25,6 +25,7 @@ import android.content.Context
import android.content.SharedPreferences
import androidx.compose.runtime.Immutable
import androidx.core.content.edit
import com.vitorpamplona.amethyst.commons.model.AccountSettings
import com.vitorpamplona.amethyst.commons.model.HomeFeedType
import com.vitorpamplona.amethyst.commons.model.UiSettings
import com.vitorpamplona.amethyst.commons.model.chats.ChatFeedType
@@ -60,7 +61,6 @@ import com.vitorpamplona.amethyst.commons.model.preferences.readLegacyAccountSec
import com.vitorpamplona.amethyst.commons.model.preferences.readLegacyGeohashIdentity
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter
import com.vitorpamplona.amethyst.commons.relayauth.RelayAuthPolicy
import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.model.backups.BackupConflictStorage
import com.vitorpamplona.amethyst.model.nip60Cashu.CashuPreferences
import com.vitorpamplona.amethyst.model.preferences.UiSharedPreferences
@@ -329,7 +329,7 @@ object LocalPreferences {
* `amethyst/plans/2026-09-23-encrypted-storage-retirement.md`.
*
* `internal` rather than private because the mirror is not all in this
* file: [com.vitorpamplona.amethyst.model.GeohashChatIdentityState] writes
* file: [com.vitorpamplona.amethyst.commons.model.GeohashChatIdentityState] writes
* the location-chat identity into its own legacy file and reads this to
* know when to stop. Private, it would have kept writing after the flip
* and the switch would only half work.
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.model.nip46Signer
package com.vitorpamplona.amethyst.connectedApps.consent
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.commons.connectedApps.nip46.Nip46PermissionAuthorizer
@@ -26,14 +26,12 @@ import com.vitorpamplona.amethyst.commons.connectedApps.signers.AppConnectResult
import com.vitorpamplona.amethyst.commons.connectedApps.signers.NostrSignerOp
import com.vitorpamplona.amethyst.commons.connectedApps.signers.SignerOpGrant
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.model.nip46Signer.Nip46ConsentPrompter
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.nip46_signer_allow_always_for
import com.vitorpamplona.amethyst.commons.resources.nip46_signer_decrypt_failed
import com.vitorpamplona.amethyst.commons.resources.nip46_signer_remote_app
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
import com.vitorpamplona.amethyst.connectedApps.consent.SignerConnectCoordinator
import com.vitorpamplona.amethyst.connectedApps.consent.SignerConnectInfo
import com.vitorpamplona.amethyst.connectedApps.consent.SignerConsentCoordinator
import com.vitorpamplona.amethyst.napplet.label
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
@@ -53,7 +51,7 @@ import kotlinx.coroutines.withTimeoutOrNull
* Runs only in the main process (the signer never runs in `:napplet`), so [Amethyst.instance] is set;
* the coordinators launch their Activity from the application context.
*/
object Nip46ConsentBridge {
object Nip46ConsentBridge : Nip46ConsentPrompter {
/**
* Upper bound on how long a consent prompt may block the signer's single-consumer loop. A user who
* ignores the dialog eventually fails the request closed (deny / declined) instead of wedging the
@@ -62,7 +60,7 @@ object Nip46ConsentBridge {
private const val CONSENT_TIMEOUT_MS = 120_000L
/** First-connect consent: show the app's self-declared identity and let the user pick a trust level. */
suspend fun requestConnect(
override suspend fun requestConnect(
coordinate: String,
clientPubKey: HexKey,
request: BunkerRequestConnect,
@@ -96,7 +94,7 @@ object Nip46ConsentBridge {
* [requestedOps] so the user gives informed consent before those ops are pre-granted. Returns the
* user's [AppConnectResult] (or [AppConnectResult.Cancelled] if the prompt is never answered).
*/
suspend fun requestNostrConnectConsent(
override suspend fun requestNostrConnectConsent(
coordinate: String,
name: String?,
url: String?,
@@ -133,7 +131,7 @@ object Nip46ConsentBridge {
* user approves — and it is bounded by [Nip46ConsentInfoBuilder.DECRYPT_PREVIEW_TIMEOUT_MS] so a slow or failing signer
* degrades to an explanatory message instead of hanging or blanking the prompt.
*/
suspend fun requestOp(
override suspend fun requestOp(
coordinate: String,
clientPubKey: HexKey,
op: NostrSignerOp,
@@ -18,12 +18,11 @@
* 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.model.nip46Signer
package com.vitorpamplona.amethyst.connectedApps.consent
import com.vitorpamplona.amethyst.commons.connectedApps.nip46.Nip46PermissionAuthorizer.Companion.decryptCounterparty
import com.vitorpamplona.amethyst.commons.connectedApps.nip46.Nip46PermissionAuthorizer.Companion.toNarrowSignerOp
import com.vitorpamplona.amethyst.commons.connectedApps.signers.NostrSignerOp
import com.vitorpamplona.amethyst.connectedApps.consent.SignerConsentInfo
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.jackson.JacksonMapper
import com.vitorpamplona.quartz.nip19Bech32.entities.NPub
@@ -95,12 +95,12 @@ import com.vitorpamplona.amethyst.commons.resources.nip46_signer_batch_select_no
import com.vitorpamplona.amethyst.commons.resources.nip46_signer_batch_signing_as
import com.vitorpamplona.amethyst.commons.resources.nip46_signer_batch_title
import com.vitorpamplona.amethyst.commons.resources.nip46_signer_messages_with
import com.vitorpamplona.amethyst.commons.service.call.CallSessionBridge
import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav
import com.vitorpamplona.amethyst.commons.ui.note.NoteCompose
import com.vitorpamplona.amethyst.commons.ui.pluralStringRes
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.service.call.CallSessionBridge
import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.theme.AmethystTheme
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip59Giftwrap.rumors.RumorAssembler
@@ -26,7 +26,7 @@ import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.relayClient.event.EventFinderQueryState
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
/**
* Pre-fetches the addressable events behind the user's favorited [FavoriteApp.NostrApp]s (nSites /
@@ -41,7 +41,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
*
* This subscribes each favorited coordinate to the shared
* [EventFinder][com.vitorpamplona.amethyst.commons.relayClient.event.EventFinderFilterAssembler]
* — the same lifecycle-aware loader [observeNote][com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote]
* — the same lifecycle-aware loader [observeNote][com.vitorpamplona.amethyst.commons.relayClient.event.observeNote]
* uses — so the manifests fetch (via the author's outbox relays) as soon as the launcher opens and are
* already in [LocalCache] by the time the user taps. The loader drops each coordinate from its filter
* once the event arrives, so this is a one-shot fetch, not a standing feed.
@@ -26,21 +26,28 @@ import com.vitorpamplona.amethyst.commons.connectedApps.nip46.InMemoryNip46Clien
import com.vitorpamplona.amethyst.commons.connectedApps.nip46.Nip46ClientStore
import com.vitorpamplona.amethyst.commons.connectedApps.signers.InMemoryNostrSignerPermissionStore
import com.vitorpamplona.amethyst.commons.connectedApps.signers.NostrSignerPermissionStore
import com.vitorpamplona.amethyst.commons.cordn.KeyStoreCordnBlobCipher
import com.vitorpamplona.amethyst.commons.marmot.EncryptedKeyPackageBundleStore
import com.vitorpamplona.amethyst.commons.marmot.EncryptedMarmotMessageStore
import com.vitorpamplona.amethyst.commons.marmot.EncryptedMlsGroupStateStore
import com.vitorpamplona.amethyst.commons.marmot.EncryptedPublishObligationStore
import com.vitorpamplona.amethyst.commons.marmot.InMemoryMlsGroupStateStore
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.model.AccountSettings
import com.vitorpamplona.amethyst.commons.model.GeohashIdentityStore
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.model.location.LocationResult
import com.vitorpamplona.amethyst.commons.model.marmot.AndroidIngestDedupStore
import com.vitorpamplona.amethyst.commons.model.marmot.AndroidPushStateStore
import com.vitorpamplona.amethyst.commons.model.marmot.MarmotGroupNotifier
import com.vitorpamplona.amethyst.commons.model.nip46Signer.Nip46ConsentPrompter
import com.vitorpamplona.amethyst.commons.model.preferences.AppPreferenceStores
import com.vitorpamplona.amethyst.commons.relayClient.nip47WalletConnect.NWCPaymentFilterAssembler
import com.vitorpamplona.amethyst.commons.relayauth.DataStoreRelayAuthPermissionStore
import com.vitorpamplona.amethyst.commons.service.http.EncryptionKeyCache
import com.vitorpamplona.amethyst.commons.service.pow.PoWPublishQueue
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.service.location.LocationState
import com.vitorpamplona.marmotquic.QuicAgentTextStreamTransport
import com.vitorpamplona.quartz.marmot.appComponents.agentTextStream.transport.MarmotQuicTransport
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
@@ -48,9 +55,11 @@ import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver
import com.vitorpamplona.quartz.nip55AndroidSigner.client.NostrSignerExternal
import com.vitorpamplona.quartz.nip60Cashu.mintApi.OkHttpMintTransport
import com.vitorpamplona.quartz.nip89AppHandlers.clientTag.NostrSignerWithClientTag
import com.vitorpamplona.quartz.utils.Log
import com.vitorpamplona.quartz.utils.cache.LargeCache
import com.vitorpamplona.quic.tls.JdkCertificateValidator
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -59,18 +68,31 @@ import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import okhttp3.OkHttpClient
import okio.Path.Companion.toOkioPath
import java.io.File
class AccountCacheState(
val geolocationFlow: () -> StateFlow<LocationState.LocationResult>,
val geolocationFlow: () -> StateFlow<LocationResult>,
val nwcFilterAssembler: () -> NWCPaymentFilterAssembler,
val cashuMintDirectoryFilterAssembler: () -> com.vitorpamplona.amethyst.commons.relayClient.assemblers.CashuMintDirectoryFilterAssembler,
val okHttpClientForMoney: (String) -> okhttp3.OkHttpClient,
val okHttpClientForMoney: (String) -> OkHttpClient,
val contentResolverFn: () -> ContentResolver,
val otsResolverBuilder: () -> OtsResolver,
val cache: LocalCache,
val client: INostrClient,
/** The running app's version name, handed to every [Account] it builds. */
val appVersion: String,
/** App-wide media decryption keys, shared by every [Account]. */
val encryptionKeyCache: EncryptionKeyCache,
/** Persists an account's settings (the app's encrypted storage). */
val saveSettings: suspend (AccountSettings) -> Unit,
/** Announces Marmot Welcomes and group messages; shared by every [Account]. */
val marmotNotifier: () -> MarmotGroupNotifier,
/** The NIP-46 consent dialogs, shared by every [Account]. */
val nip46Consent: Nip46ConsentPrompter,
/** Builds the store for one account's geohash-chat identity, keyed by its pubkey. */
val geohashIdentityStore: (HexKey) -> GeohashIdentityStore,
val rootFilesDir: () -> File = { File("") },
val powQueue: () -> PoWPublishQueue? = { null },
/** Optional resource-ledger wrapper applied to every account signer (see MeteringNostrSigner). */
@@ -341,10 +363,18 @@ class AccountCacheState(
geolocationFlow = geolocationFlow,
nwcFilterAssembler = nwcFilterAssembler,
cashuMintDirectoryFilterAssembler = cashuMintDirectoryFilterAssembler,
okHttpClientForMoney = okHttpClientForMoney,
cashuMintTransport = OkHttpMintTransport(okHttpClientForMoney),
otsResolverBuilder = otsResolverBuilder,
cache = cache,
client = client,
appVersion = appVersion,
encryptionKeyCache = encryptionKeyCache,
saveSettings = saveSettings,
marmotNotifier = marmotNotifier,
nip46Consent = nip46Consent,
geohashIdentityStore = geohashIdentityStore(signer.pubKey),
marmotStreamTransportFactory = ::defaultMarmotStreamTransport,
cordnBlobCipher = { KeyStoreCordnBlobCipher() },
scope =
CoroutineScope(
Dispatchers.IO +
@@ -356,7 +386,7 @@ class AccountCacheState(
// The same per-account directory the Marmot stores use. cordn
// scopes itself further by coordinator underneath it, because a
// gid is unique only within one (spec/00.md §4).
cordnFilesDir = accountDir,
cordnFilesDir = accountDir.toOkioPath(),
mlsGroupStateStore = mlsStore,
marmotMessageStore = marmotMessageStore,
marmotKeyPackageStore = marmotKeyPackageStore,
@@ -392,3 +422,14 @@ class AccountCacheState(
const val CLIENT_TAG_NAME = "Amethyst"
}
}
/**
* The app's raw-QUIC transport for Marmot agent text stream previews (`transports/quic.md`).
* Preview brokers are commonly self-signed and the binding expects that; the platform trust
* store is still the default answer, and a deployment that pins does it here.
*/
fun defaultMarmotStreamTransport(scope: CoroutineScope): MarmotQuicTransport =
QuicAgentTextStreamTransport(
parentScope = scope,
certificateValidator = JdkCertificateValidator(),
)
@@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.model.cordn
import android.content.Context
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader
import com.vitorpamplona.quartz.cordn.appEncryptedMedia.CordnBlobUpload
import com.vitorpamplona.quartz.cordn.appEncryptedMedia.CordnEncryptedMedia
@@ -1,47 +0,0 @@
/*
* 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.model.nip11RelayInfo
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel
import com.vitorpamplona.amethyst.commons.relays.nip11RelayInfo.isRelaySignedRelayGroup
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
/**
* Whether [relay]'s cached NIP-11 document advertises support for [nip] (as a decimal string, e.g.
* "29"). Reads only the in-memory cache — it never blocks on a network fetch — so it returns false
* for a relay whose NIP-11 hasn't been loaded yet. Callers that need the answer to become true must
* warm the document first (e.g. `loadRelayInfo`), then re-evaluate once it resolves.
*/
fun relayAdvertisesNip(
relay: NormalizedRelayUrl,
nip: String,
): Boolean =
Amethyst.instance.nip11Cache
.getFromCache(relay)
.supported_nips
?.any { it == nip } == true
/** NIP-29 (relay-based groups): the relay must run it for its groups to be real. */
fun relayAdvertisesNip29(relay: NormalizedRelayUrl): Boolean = relayAdvertisesNip(relay, "29")
/** [isRelaySignedRelayGroup] reading the host relay's cached NIP-11 doc (for non-Compose callers). */
fun isRelaySignedRelayGroup(channel: RelayGroupChannel): Boolean = isRelaySignedRelayGroup(channel, Amethyst.instance.nip11Cache.getFromCache(channel.groupId.relayUrl))
@@ -1,165 +0,0 @@
/*
* 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.model.nipB7Blossom
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.NoteState
import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider
import com.vitorpamplona.amethyst.commons.model.mediaServers.DEFAULT_MEDIA_SERVERS
import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName
import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerType
import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nipB7Blossom.BlossomAuthorizationEvent
import com.vitorpamplona.quartz.nipB7Blossom.BlossomServersEvent
import com.vitorpamplona.quartz.utils.Rfc3986
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
class BlossomServerListState(
val signer: NostrSigner,
val cache: ICacheProvider,
val scope: CoroutineScope,
val settings: AccountSettings,
) {
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
val blossomListNote = cache.getOrCreateAddressableNote(getBlossomServersAddress())
fun getBlossomServersAddress() = BlossomServersEvent.createAddress(signer.pubKey)
fun getBlossomServersListFlow(): StateFlow<NoteState> = blossomListNote.flow().metadata.stateFlow
fun getBlossomServersList(): BlossomServersEvent? = blossomListNote.event as? BlossomServersEvent
fun normalizeServers(note: Note): List<String> {
val event = note.event as? BlossomServersEvent
return event?.servers() ?: emptyList()
}
fun host(url: String): String =
try {
Rfc3986.host(url).removePrefix("cdn.").removePrefix("blossom.")
} catch (e: Exception) {
url.removePrefix("cdn.").removePrefix("blossom.")
}
val flow =
getBlossomServersListFlow()
.map {
normalizeServers(it.note)
}.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
emptyList(),
)
fun mergeServerList(blossom: List<String>?): List<ServerName> = blossom?.map { ServerName(host(it), it, ServerType.Blossom) }?.ifEmpty { DEFAULT_MEDIA_SERVERS } ?: DEFAULT_MEDIA_SERVERS
val hostNameFlow: StateFlow<List<ServerName>> =
flow
.map { blossoms ->
mergeServerList(blossoms)
}.onStart {
emit(mergeServerList(flow.value))
}.onEach { servers ->
resetTargetOrNull(flow.value, servers, settings.defaultFileServer)?.let {
settings.changeDefaultFileServer(it)
}
}.flowOn(Dispatchers.IO)
.stateIn(
scope,
SharingStarted.Eagerly,
DEFAULT_MEDIA_SERVERS,
)
suspend fun saveBlossomServersList(servers: List<String>): BlossomServersEvent {
val serverList = getBlossomServersList()
return if (serverList != null && serverList.tags.isNotEmpty()) {
BlossomServersEvent.updateRelayList(
earlierVersion = serverList,
servers = servers,
signer = signer,
)
} else {
BlossomServersEvent.createFromScratch(
relays = servers,
signer = signer,
)
}
}
suspend fun createBlossomUploadAuth(
hash: HexKey,
size: Long,
alt: String,
servers: List<String> = emptyList(),
): BlossomAuthorizationEvent = BlossomAuthorizationEvent.createUploadAuth(hash, size, alt, signer, servers)
suspend fun createBlossomMediaAuth(
hash: HexKey,
size: Long,
alt: String,
servers: List<String> = emptyList(),
): BlossomAuthorizationEvent = BlossomAuthorizationEvent.createMediaAuth(hash, size, alt, signer, servers)
suspend fun createBlossomDeleteAuth(
hash: HexKey,
alt: String,
servers: List<String> = emptyList(),
): BlossomAuthorizationEvent = BlossomAuthorizationEvent.createDeleteAuth(hash, alt, signer, servers)
suspend fun createBlossomListAuth(
alt: String,
servers: List<String> = emptyList(),
): BlossomAuthorizationEvent = BlossomAuthorizationEvent.createListAuth(signer, alt, servers)
}
/**
* Decides whether the persisted default file server must be reset, and to what.
*
* Returns the new default server, or `null` when no change should happen.
*
* The guard on [rawList] being non-empty is what prevents the startup race: before the user's
* [BlossomServersEvent] (kind 10063) loads from cache/relay, [rawList] is empty and [merged] is the
* transient [DEFAULT_MEDIA_SERVERS] fallback. Resetting against that fallback would clobber the
* locally-saved pick on every launch. Only reset once a real, loaded list is in hand and it no
* longer contains the current pick (e.g. the user removed it from their list).
*/
fun resetTargetOrNull(
rawList: List<String>,
merged: List<ServerName>,
current: ServerName,
): ServerName? =
if (rawList.isNotEmpty() && merged.none { it == current }) {
merged.firstOrNull() ?: DEFAULT_MEDIA_SERVERS[0]
} else {
null
}
@@ -20,8 +20,8 @@
*/
package com.vitorpamplona.amethyst.model.torState
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.tor.TorRelayState
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.accountsCache.AccountCacheState
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.utils.Log
@@ -21,12 +21,12 @@
package com.vitorpamplona.amethyst.model.zap
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.commons.model.MIN_ONCHAIN_ZAP_SATS
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.model.nip60Cashu.CashuWalletState
import com.vitorpamplona.amethyst.commons.model.payments.PayToRailMatcher
import com.vitorpamplona.amethyst.commons.model.payments.PaymentTargetTypes
import com.vitorpamplona.amethyst.model.MIN_ONCHAIN_ZAP_SATS
import com.vitorpamplona.amethyst.model.nip60Cashu.CashuWalletState
import com.vitorpamplona.amethyst.service.payments.PayToAppAvailability
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip57Zaps.splits.BaseZapSplitSetup
@@ -36,6 +36,7 @@ import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.commons.browser.BrowserSitePermission
import com.vitorpamplona.amethyst.commons.connectedApps.signers.NostrSignerPermissionLedger
import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.napplet.NappletBroker
import com.vitorpamplona.amethyst.commons.napplet.NappletCapability
import com.vitorpamplona.amethyst.commons.napplet.NappletIdentity
@@ -44,7 +45,6 @@ import com.vitorpamplona.amethyst.commons.napplet.NappletRequestRouter
import com.vitorpamplona.amethyst.commons.napplet.protocol.NappletProtocolJson
import com.vitorpamplona.amethyst.commons.napplet.protocol.NappletResponse
import com.vitorpamplona.amethyst.favorites.WebShortcuts
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.napplet.gateways.AccountNappletGateways
import com.vitorpamplona.amethyst.napplethost.NappletIpc
import com.vitorpamplona.amethyst.ui.MainActivity
@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.napplet
import android.content.Context
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.commons.browser.OmniboxInput
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.napplet.NappletCapability
import com.vitorpamplona.amethyst.commons.napplet.NappletIdentity
import com.vitorpamplona.amethyst.commons.napplet.protocol.NappletRequest
@@ -63,7 +64,6 @@ import com.vitorpamplona.amethyst.commons.resources.napplet_consent_upload
import com.vitorpamplona.amethyst.commons.resources.napplet_fallback_title
import com.vitorpamplona.amethyst.commons.ui.loadPluralStringRes
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.quartz.lightning.LnInvoiceUtil
import com.vitorpamplona.quartz.nip01Core.core.fastForEach
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
@@ -20,9 +20,9 @@
*/
package com.vitorpamplona.amethyst.napplet
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.napplet.NappletRelayCleartext
import com.vitorpamplona.amethyst.commons.napplet.protocol.NappletProtocolJson
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.SubscriptionListener
@@ -38,9 +38,9 @@ import com.vitorpamplona.amethyst.commons.resources.napplet_op_encrypt
import com.vitorpamplona.amethyst.commons.resources.napplet_op_sign_kind_named
import com.vitorpamplona.amethyst.commons.resources.nip46_signer_allow_always_for
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.kindNameFor
import com.vitorpamplona.amethyst.connectedApps.consent.SignerConnectInfo
import com.vitorpamplona.amethyst.connectedApps.consent.SignerConsentInfo
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.kindNameFor
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.jackson.JacksonMapper
@@ -20,7 +20,7 @@
*/
package com.vitorpamplona.amethyst.napplet.gateways
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag
import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent
@@ -30,6 +30,7 @@ import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.connectedApps.signers.NostrConnectPrompt
import com.vitorpamplona.amethyst.commons.connectedApps.signers.NostrSignerConsentPrompt
import com.vitorpamplona.amethyst.commons.connectedApps.signers.NostrSignerPermissionLedger
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.napplet.NappletBroker
import com.vitorpamplona.amethyst.commons.napplet.NappletConsentPrompt
import com.vitorpamplona.amethyst.commons.napplet.NappletIdentityGateway
@@ -51,7 +52,6 @@ import com.vitorpamplona.amethyst.commons.resources.napplet_cap_notify
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
import com.vitorpamplona.amethyst.connectedApps.consent.SignerConnectCoordinator
import com.vitorpamplona.amethyst.connectedApps.consent.SignerConsentCoordinator
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.napplet.NappletConsentCoordinator
import com.vitorpamplona.amethyst.napplet.NappletConsentSummary
import com.vitorpamplona.amethyst.napplet.buildConnectInfo
@@ -21,9 +21,9 @@
package com.vitorpamplona.amethyst.napplet.gateways
import android.util.Base64
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.napplet.NappletResource
import com.vitorpamplona.amethyst.commons.napplet.NappletResourceResult
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.napplet.NappletNetworkRegistry
import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip01Core.core.Event
@@ -21,7 +21,7 @@
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.quartz.experimental.clink.client.OfferClient
import com.vitorpamplona.quartz.experimental.clink.offers.OfferEvent
import com.vitorpamplona.quartz.experimental.clink.offers.OfferResponse
@@ -37,6 +37,7 @@ import androidx.core.content.ContextCompat
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.nipACWebRtcCalls.CallState
import com.vitorpamplona.amethyst.commons.service.call.CallSessionBridge
import com.vitorpamplona.amethyst.ui.call.CallActivity
import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.runBlocking
@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.service.call
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.vitorpamplona.amethyst.commons.service.call.CallSessionBridge
import com.vitorpamplona.amethyst.service.call.notification.CallNotifier
import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.CoroutineScope
@@ -20,7 +20,7 @@
*/
package com.vitorpamplona.amethyst.service.call
import com.vitorpamplona.amethyst.model.CallTurnServer
import com.vitorpamplona.amethyst.commons.model.CallTurnServer
import org.webrtc.PeerConnection
object IceServerConfig {
@@ -44,11 +44,11 @@ import com.vitorpamplona.amethyst.commons.resources.crashreport_found_send
import com.vitorpamplona.amethyst.commons.resources.would_you_like_to_send_the_recent_crash_report_to_amethyst_in_a_dm_no_personal_information_will_be_shared
import com.vitorpamplona.amethyst.commons.service.crashreports.DEV_REPORT_PUBKEY
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeToMessage
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.Size16dp
import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.navigation.routes.routeToMessage
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.service.eventCache
import android.content.ComponentCallbacks2
import com.vitorpamplona.amethyst.AccountInfo
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.quartz.nip19Bech32.decodePublicKeyAsHexOrNull
import com.vitorpamplona.quartz.utils.Log
import java.util.concurrent.atomic.AtomicBoolean
@@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst.service.location
import android.content.Context
import android.location.Location
import android.location.LocationManager
import com.vitorpamplona.amethyst.commons.model.location.LocationResult
import com.vitorpamplona.quartz.experimental.bitchat.geohash.GeohashChannelLevel
import com.vitorpamplona.quartz.nip01Core.tags.geohash.GeoHash
import com.vitorpamplona.quartz.nip01Core.tags.geohash.GeohashPrecision
import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.CoroutineScope
@@ -100,16 +100,6 @@ class LocationState(
const val SUBSCRIPTION_STOP_TIMEOUT_MS: Long = 5_000L
}
sealed class LocationResult {
data class Success(
val geoHash: GeoHash,
) : LocationResult()
object LackPermission : LocationResult()
object Loading : LocationResult()
}
private enum class Gate { NoPermission, Paused, Listen }
private var hasLocationPermission = MutableStateFlow(false)
@@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.service.notifications
import android.content.Context
import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.model.accountsCache.AccountCacheState
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountSubscriptionRegistry
import com.vitorpamplona.quartz.nip01Core.core.HexKey
@@ -58,9 +58,9 @@ import kotlinx.coroutines.launch
* and expects the one they land on to be current, so all of them keep their own notifications,
* DMs and gift wraps live. This costs nothing once the app is away — it ends with the screen.
* - **While the app is away: only the accounts that opted in**, via
* [com.vitorpamplona.amethyst.model.AccountSettings.alwaysOnNotificationService] ("Keep this
* [com.vitorpamplona.amethyst.commons.model.AccountSettings.alwaysOnNotificationService] ("Keep this
* account active in the background") or their NIP-46 signer toggle
* ([com.vitorpamplona.amethyst.model.AccountSettings.nip46SignerEnabled]).
* ([com.vitorpamplona.amethyst.commons.model.AccountSettings.nip46SignerEnabled]).
*
* That is what the setting's name promises, and for a while it did not hold: participation gated
* subscriptions everywhere, so an account you had not opted in for showed no notifications even
@@ -30,17 +30,18 @@ import coil3.request.ImageRequest
import coil3.request.allowHardware
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.model.isMutedPublicChatMessage
import com.vitorpamplona.amethyst.commons.nipACWebRtcCalls.CallManager
import com.vitorpamplona.amethyst.commons.notifications.dal.NotificationFeedFilter
import com.vitorpamplona.amethyst.commons.relayClient.event.EventFinderQueryState
import com.vitorpamplona.amethyst.commons.relayClient.user.UserFinderQueryState
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.app_notification_chess_challenge_accepted
import com.vitorpamplona.amethyst.commons.resources.app_notification_chess_your_turn
import com.vitorpamplona.amethyst.commons.resources.app_notification_poll_channel_message
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.call.notification.CallNotifier
import com.vitorpamplona.amethyst.service.notifications.renderers.ArticleNotification
import com.vitorpamplona.amethyst.service.notifications.renderers.BadgeNotification
@@ -57,7 +58,6 @@ import com.vitorpamplona.amethyst.service.notifications.renderers.RepostNotifica
import com.vitorpamplona.amethyst.service.notifications.renderers.ZapNotification
import com.vitorpamplona.amethyst.service.relayClient.authCommand.model.ScreenAuthAccount
import com.vitorpamplona.amethyst.ui.MainActivity
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.dal.NotificationFeedFilter
import com.vitorpamplona.quartz.buzz.stream.StreamMessageV2Event
import com.vitorpamplona.quartz.experimental.notifications.wake.WakeUpEvent
import com.vitorpamplona.quartz.marmot.mip02Welcome.WelcomeEvent
@@ -22,10 +22,11 @@ package com.vitorpamplona.amethyst.service.notifications
import android.content.Context
import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.commons.model.marmot.MarmotGroupNotifier
import com.vitorpamplona.amethyst.commons.notifications.dal.NotificationFeedFilter
import com.vitorpamplona.amethyst.service.notifications.renderers.BuzzDmNotification
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.dal.NotificationFeedFilter
import com.vitorpamplona.quartz.buzz.stream.StreamMessageV2Event
import com.vitorpamplona.quartz.experimental.notifications.wake.WakeUpEvent
import com.vitorpamplona.quartz.marmot.mip02Welcome.WelcomeEvent
@@ -96,7 +97,7 @@ class NotificationDispatcher(
private val scope: CoroutineScope,
/** Forwarded to [EventNotificationConsumer]: reports wakelock held-time to the resource-usage ledger. */
onWakeLockHeld: ((heldMs: Long) -> Unit)? = null,
) {
) : MarmotGroupNotifier {
companion object {
private const val TAG = "NotificationDispatcher"
@@ -306,7 +307,7 @@ class NotificationDispatcher(
* routing. Called from processMarmotWelcomeFlow once MLS group join
* succeeds — at which point we know which account the invite was for.
*/
suspend fun notifyWelcome(
override suspend fun notifyWelcome(
event: WelcomeEvent,
account: Account,
) {
@@ -322,10 +323,10 @@ class NotificationDispatcher(
* Direct-invocation entry point for Marmot kind:445 group messages.
* Bypasses the cache-observer path because GroupEvents are routed by
* the `h` tag (nostr_group_id), not by `p` tag. Called from
* [com.vitorpamplona.amethyst.ui.screen.loggedIn.GroupEventHandler]
* [com.vitorpamplona.amethyst.commons.model.GroupEventHandler]
* once the MLS-decrypted inner event has been parsed and indexed.
*/
suspend fun notifyGroupMessage(
override suspend fun notifyGroupMessage(
innerEvent: ChatEvent,
nostrGroupId: String,
account: Account,
@@ -23,11 +23,11 @@ package com.vitorpamplona.amethyst.service.notifications
import android.content.Context
import android.os.PowerManager
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.relayClient.event.EventFinderQueryState
import com.vitorpamplona.amethyst.commons.relayClient.user.UserFinderQueryState
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.relayClient.authCommand.model.ScreenAuthAccount
import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.CancellationException
@@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst.service.notifications
import android.app.NotificationManager
import android.content.Context
import androidx.core.content.ContextCompat
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
import com.vitorpamplona.quartz.nip19Bech32.toNpub
@@ -21,7 +21,7 @@
package com.vitorpamplona.amethyst.service.notifications
import android.content.Context
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.service.notifications.renderers.NwcPaymentNotifier
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -36,7 +36,7 @@ import kotlinx.coroutines.launch
* account's connected NWC wallets.
*
* The relay subscription that receives these events is NOT here — it lives in
* [com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.nip47WalletConnect.NwcNotificationsEoseManager],
* [com.vitorpamplona.amethyst.commons.relayClient.reqCommand.account.nip47WalletConnect.NwcNotificationsEoseManager],
* grouped with the account's always-on zap/notification inbox subscriptions so it
* shares their lifecycle (open while logged in, warm in the background). That
* manager decrypts each notification and publishes non-zap payments to
@@ -24,8 +24,8 @@ import com.vitorpamplona.amethyst.AccountInfo
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.commons.model.AccountSettings
import com.vitorpamplona.amethyst.isDebug
import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent
import com.vitorpamplona.quartz.utils.Log
@@ -21,12 +21,12 @@
package com.vitorpamplona.amethyst.service.notifications.renderers
import android.content.Context
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.app_notification_articles_channel_message
import com.vitorpamplona.amethyst.commons.resources.app_notification_articles_channel_message_highlight
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.notifications.NotificationCategory
import com.vitorpamplona.amethyst.service.notifications.NotificationContent
import com.vitorpamplona.amethyst.service.notifications.NotificationEnricher
@@ -21,12 +21,12 @@
package com.vitorpamplona.amethyst.service.notifications.renderers
import android.content.Context
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.app_notification_badges_channel_message
import com.vitorpamplona.amethyst.commons.resources.app_notification_badges_channel_message_from
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.notifications.NotificationCategory
import com.vitorpamplona.amethyst.service.notifications.NotificationEnricher
import com.vitorpamplona.amethyst.service.notifications.NotificationRoutes
@@ -21,13 +21,13 @@
package com.vitorpamplona.amethyst.service.notifications.renderers
import android.content.Context
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.app_notification_new_message
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.notifications.NotificationCategory
import com.vitorpamplona.amethyst.service.notifications.NotificationEnricher
import com.vitorpamplona.amethyst.service.notifications.NotificationRoutes
@@ -21,12 +21,12 @@
package com.vitorpamplona.amethyst.service.notifications.renderers
import android.content.Context
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.app_notification_chess_channel_name
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
import com.vitorpamplona.amethyst.isDebug
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.notifications.NotificationCategory
import com.vitorpamplona.amethyst.service.notifications.NotificationEnricher
import com.vitorpamplona.amethyst.service.notifications.NotificationRoutes
@@ -21,6 +21,7 @@
package com.vitorpamplona.amethyst.service.notifications.renderers
import android.content.Context
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.app_notification_code_channel_message_issue
@@ -39,7 +40,6 @@ import com.vitorpamplona.amethyst.commons.resources.app_notification_code_channe
import com.vitorpamplona.amethyst.commons.resources.app_notification_code_channel_message_status_draft
import com.vitorpamplona.amethyst.commons.resources.app_notification_code_channel_message_status_open
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.notifications.NotificationCategory
import com.vitorpamplona.amethyst.service.notifications.NotificationContent
import com.vitorpamplona.amethyst.service.notifications.NotificationEnricher
@@ -21,9 +21,9 @@
package com.vitorpamplona.amethyst.service.notifications.renderers
import android.content.Context
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.notifications.NotificationCategory
import com.vitorpamplona.amethyst.service.notifications.NotificationContent
import com.vitorpamplona.amethyst.service.notifications.NotificationEnricher
@@ -21,12 +21,12 @@
package com.vitorpamplona.amethyst.service.notifications.renderers
import android.content.Context
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.app_notification_added_to_group
import com.vitorpamplona.amethyst.commons.resources.app_notification_new_message
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.notifications.NotificationCategory
import com.vitorpamplona.amethyst.service.notifications.NotificationEnricher
import com.vitorpamplona.amethyst.service.notifications.NotificationRoutes
@@ -21,12 +21,12 @@
package com.vitorpamplona.amethyst.service.notifications.renderers
import android.content.Context
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.app_notification_media_channel_message_photo
import com.vitorpamplona.amethyst.commons.resources.app_notification_media_channel_message_video
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.notifications.NotificationCategory
import com.vitorpamplona.amethyst.service.notifications.NotificationContent
import com.vitorpamplona.amethyst.service.notifications.NotificationEnricher
@@ -21,11 +21,11 @@
package com.vitorpamplona.amethyst.service.notifications.renderers
import android.content.Context
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.app_notification_mentions_channel_message
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.notifications.NotificationCategory
import com.vitorpamplona.amethyst.service.notifications.NotificationContent
import com.vitorpamplona.amethyst.service.notifications.NotificationEnricher
@@ -21,11 +21,11 @@
package com.vitorpamplona.amethyst.service.notifications.renderers
import android.content.Context
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.app_notification_payments_channel_message
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
import com.vitorpamplona.amethyst.commons.util.showAmount
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.notifications.NotificationCategory
import com.vitorpamplona.amethyst.service.notifications.NotificationRoutes
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.postStandard
@@ -21,12 +21,12 @@
package com.vitorpamplona.amethyst.service.notifications.renderers
import android.content.Context
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.app_notification_reactions_channel_message
import com.vitorpamplona.amethyst.commons.resources.app_notification_reactions_channel_message_for
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.notifications.NotificationCategory
import com.vitorpamplona.amethyst.service.notifications.NotificationContent
import com.vitorpamplona.amethyst.service.notifications.NotificationEnricher
@@ -21,12 +21,12 @@
package com.vitorpamplona.amethyst.service.notifications.renderers
import android.content.Context
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.app_notification_me
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.notifications.NotificationCategory
import com.vitorpamplona.amethyst.service.notifications.NotificationContent
import com.vitorpamplona.amethyst.service.notifications.NotificationEnricher
@@ -21,11 +21,11 @@
package com.vitorpamplona.amethyst.service.notifications.renderers
import android.content.Context
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.app_notification_reposts_channel_message
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.notifications.NotificationCategory
import com.vitorpamplona.amethyst.service.notifications.NotificationContent
import com.vitorpamplona.amethyst.service.notifications.NotificationEnricher
@@ -21,6 +21,7 @@
package com.vitorpamplona.amethyst.service.notifications.renderers
import android.content.Context
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
@@ -32,7 +33,6 @@ import com.vitorpamplona.amethyst.commons.resources.app_notification_zaps_channe
import com.vitorpamplona.amethyst.commons.resources.app_notification_zaps_channel_message_from
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
import com.vitorpamplona.amethyst.commons.util.showAmount
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.notifications.NotificationCategory
import com.vitorpamplona.amethyst.service.notifications.NotificationContent
import com.vitorpamplona.amethyst.service.notifications.NotificationEnricher
@@ -32,7 +32,7 @@ import androidx.compose.ui.platform.LocalContext
import coil3.asDrawable
import coil3.imageLoader
import coil3.request.ImageRequest
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -46,6 +46,7 @@ import androidx.media3.common.util.UnstableApi
import androidx.media3.ui.compose.ContentFrame
import androidx.media3.ui.compose.SURFACE_TYPE_TEXTURE_VIEW
import com.vitorpamplona.amethyst.commons.ui.components.getDialogWindow
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.service.playback.composable.controls.BottomGradientOverlay
import com.vitorpamplona.amethyst.service.playback.composable.controls.FullscreenSwipeControlsState
import com.vitorpamplona.amethyst.service.playback.composable.controls.FullscreenSwipeLevelIndicator
@@ -63,7 +64,6 @@ import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.LoadedMe
import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.isHlsMedia
import com.vitorpamplona.amethyst.service.playback.composable.wavefront.AudioPlayingAnimation
import com.vitorpamplona.amethyst.service.playback.composable.wavefront.rememberIsAudioTrack
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
internal const val SKIP_SECONDS = 10
internal const val SKIP_MILLIS = SKIP_SECONDS * 1000L
@@ -30,31 +30,26 @@ import androidx.compose.foundation.layout.size
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import com.vitorpamplona.amethyst.commons.audio.WaveformData
import com.vitorpamplona.amethyst.commons.model.MediaAspectRatioCache
import com.vitorpamplona.amethyst.commons.model.nip71Video.CaptionTrack
import com.vitorpamplona.amethyst.commons.ui.components.DisplayBlurHash
import com.vitorpamplona.amethyst.commons.ui.components.ImageUrlWithDownloadButton
import com.vitorpamplona.amethyst.commons.ui.note.DownloadForOfflineIcon
import com.vitorpamplona.amethyst.commons.ui.theme.Size75dp
import com.vitorpamplona.amethyst.commons.ui.theme.imageModifier
import com.vitorpamplona.amethyst.commons.ui.theme.videoGalleryModifier
import com.vitorpamplona.amethyst.model.MediaAspectRatioCache
import com.vitorpamplona.amethyst.ui.components.DisplayBlurHash
import com.vitorpamplona.amethyst.ui.components.ImageUrlWithDownloadButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@Immutable
class WaveformData(
val wave: List<Float>,
)
@Composable
fun VideoView(
videoUri: String,
@@ -26,10 +26,11 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import com.vitorpamplona.amethyst.commons.audio.WaveformData
import com.vitorpamplona.amethyst.commons.model.nip71Video.CaptionTrack
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.service.playback.composable.mainVideo.VideoPlayerActiveMutex
import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.GetMediaItem
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@@ -40,6 +40,7 @@ import androidx.compose.ui.tooling.preview.Preview
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
import com.vitorpamplona.amethyst.commons.model.VideoPlayerAction
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.call_settings_video_quality
import com.vitorpamplona.amethyst.commons.resources.download_to_phone
@@ -59,7 +60,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size20Modifier
import com.vitorpamplona.amethyst.commons.ui.theme.Size50Modifier
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
import com.vitorpamplona.amethyst.commons.ui.theme.VolumeBottomIconSize
import com.vitorpamplona.amethyst.model.VideoPlayerAction
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@@ -55,6 +55,8 @@ import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
import com.vitorpamplona.amethyst.commons.model.VideoButtonLocation
import com.vitorpamplona.amethyst.commons.model.VideoPlayerAction
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.captions_turn_off
import com.vitorpamplona.amethyst.commons.resources.captions_turn_on
@@ -70,8 +72,8 @@ import com.vitorpamplona.amethyst.commons.ui.theme.PinBottomIconSize
import com.vitorpamplona.amethyst.commons.ui.theme.Size20Modifier
import com.vitorpamplona.amethyst.commons.ui.theme.Size50Modifier
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
import com.vitorpamplona.amethyst.model.VideoButtonLocation
import com.vitorpamplona.amethyst.model.VideoPlayerAction
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel
import com.vitorpamplona.amethyst.service.cast.CastRequest
import com.vitorpamplona.amethyst.service.cast.CastSessionState
import com.vitorpamplona.amethyst.service.cast.resolveCastLiveness
@@ -85,8 +87,7 @@ import com.vitorpamplona.amethyst.ui.cast.CastDevicePickerDialog
import com.vitorpamplona.amethyst.ui.cast.rememberCastWithLocalNetworkPermission
import com.vitorpamplona.amethyst.ui.components.ShareMediaAction
import com.vitorpamplona.amethyst.ui.components.getActivity
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.saveMediaToGallery
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.launch
@@ -25,9 +25,9 @@ import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import com.vitorpamplona.amethyst.commons.audio.WaveformData
import com.vitorpamplona.amethyst.commons.model.nip71Video.CaptionTrack
import com.vitorpamplona.amethyst.commons.ui.state.produceCachedState
import com.vitorpamplona.amethyst.service.playback.composable.WaveformData
import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.service.playback.composable.mediaitem
import androidx.compose.runtime.Immutable
import androidx.media3.common.MediaItem
import com.vitorpamplona.amethyst.commons.audio.WaveformData
import com.vitorpamplona.amethyst.commons.model.nip71Video.CaptionTrack
import com.vitorpamplona.amethyst.service.playback.composable.WaveformData
import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@@ -44,9 +44,9 @@ import androidx.media3.common.Tracks
import com.vitorpamplona.amethyst.commons.audio.AudioVisualizer
import com.vitorpamplona.amethyst.commons.audio.Spectrum
import com.vitorpamplona.amethyst.commons.audio.VisualizerStyle
import com.vitorpamplona.amethyst.commons.audio.WaveformData
import com.vitorpamplona.amethyst.commons.audio.delayedByFrames
import com.vitorpamplona.amethyst.service.playback.composable.MediaControllerState
import com.vitorpamplona.amethyst.service.playback.composable.WaveformData
import com.vitorpamplona.amethyst.service.playback.playerPool.PcmTapRegistry
import kotlinx.coroutines.flow.emptyFlow
@@ -20,12 +20,13 @@
*/
package com.vitorpamplona.amethyst.service.playback.composable.wavefront
import com.vitorpamplona.amethyst.service.playback.composable.WaveformData
import com.vitorpamplona.amethyst.commons.audio.WaveformData
import kotlin.math.PI
import kotlin.math.sin
import kotlin.random.Random
private const val SYNTHETIC_WAVEFORM_SAMPLES = 96
private const val TWO_PI = (Math.PI * 2).toFloat()
private const val TWO_PI = (PI * 2).toFloat()
/**
* Builds a stable, decorative waveform from a [seed] (usually an event id). ExoPlayer drives
@@ -57,7 +58,7 @@ fun syntheticWaveformFor(seed: String): WaveformData {
List(SYNTHETIC_WAVEFORM_SAMPLES) { index ->
val phase = index.toFloat() / SYNTHETIC_WAVEFORM_SAMPLES
// Optional gentle fade so some taper at the ends, others stay even.
val envelope = 1f - envelopeStrength * (1f - sin(phase * Math.PI).toFloat())
val envelope = 1f - envelopeStrength * (1f - sin(phase * PI).toFloat())
val carrier = sin(phase * TWO_PI * carrierCycles + phaseOffset) * carrierWeight
val noise = (rng.nextFloat() - 0.5f) * 2f * noiseStrength
((baseline + carrier + noise) * envelope).coerceIn(0.05f, 1.0f)
@@ -36,9 +36,9 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.media3.common.Player
import com.linc.audiowaveform.infiniteLinearGradient
import com.vitorpamplona.amethyst.commons.audio.WaveformData
import com.vitorpamplona.amethyst.commons.ui.components.AudioWaveformReadOnly
import com.vitorpamplona.amethyst.service.playback.composable.MediaControllerState
import com.vitorpamplona.amethyst.service.playback.composable.WaveformData
@Composable
fun Waveform(
@@ -34,7 +34,7 @@ import androidx.collection.LruCache
*
* A URL is permanently one or the other, and the recorder overwrites with the latest verdict on
* every timeline change, so a transient early value self-corrects. Backed by a bounded, thread-safe
* [LruCache] (same convention as [com.vitorpamplona.amethyst.model.MediaAspectRatioCache]) so a long
* [LruCache] (same convention as [com.vitorpamplona.amethyst.commons.model.MediaAspectRatioCache]) so a long
* feed session over many distinct URLs can't grow it without bound — an evicted entry just relearns
* on its next play, which is one uncached play, the same negligible cost as a process restart.
*/
@@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.service.playback.pip
import android.graphics.Rect
import android.os.Bundle
import com.vitorpamplona.amethyst.service.playback.composable.WaveformData
import com.vitorpamplona.amethyst.commons.audio.WaveformData
import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.MediaItemData
import com.vitorpamplona.quartz.utils.ensure
@@ -30,7 +30,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalContext
import androidx.core.util.Consumer
import com.vitorpamplona.amethyst.model.MediaAspectRatioCache
import com.vitorpamplona.amethyst.commons.model.MediaAspectRatioCache
import com.vitorpamplona.amethyst.service.playback.composable.DEFAULT_MUTED_SETTING
import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.MediaItemData
import com.vitorpamplona.amethyst.ui.components.getActivity
@@ -40,10 +40,10 @@ import androidx.media3.common.util.UnstableApi
import androidx.media3.ui.compose.ContentFrame
import androidx.media3.ui.compose.state.rememberMuteButtonState
import androidx.media3.ui.compose.state.rememberPlayPauseButtonState
import com.vitorpamplona.amethyst.commons.audio.WaveformData
import com.vitorpamplona.amethyst.commons.model.MediaAspectRatioCache
import com.vitorpamplona.amethyst.commons.ui.theme.VoiceHeightModifier
import com.vitorpamplona.amethyst.model.MediaAspectRatioCache
import com.vitorpamplona.amethyst.service.playback.composable.MediaControllerState
import com.vitorpamplona.amethyst.service.playback.composable.WaveformData
import com.vitorpamplona.amethyst.service.playback.composable.controls.PIP_PRESHRINK_MAX_SHORT_SIDE_PX
import com.vitorpamplona.amethyst.service.playback.composable.controls.constrainVideoQualityToViewport
import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.MediaItemData
@@ -32,7 +32,7 @@ import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.audio.AudioSink
import androidx.media3.exoplayer.audio.DefaultAudioSink
import androidx.media3.exoplayer.audio.TeeAudioProcessor
import com.vitorpamplona.amethyst.model.MediaAspectRatioCache
import com.vitorpamplona.amethyst.commons.model.MediaAspectRatioCache
import com.vitorpamplona.amethyst.service.playback.diskCache.VideoCache
import com.vitorpamplona.amethyst.service.playback.playerPool.aspectRatio.AspectRatioCacher
import com.vitorpamplona.amethyst.service.playback.playerPool.positions.CurrentPlayPositionCacher
@@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.service.playback.playerPool.aspectRatio
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.common.VideoSize
import com.vitorpamplona.amethyst.model.MutableMediaAspectRatioCache
import com.vitorpamplona.amethyst.commons.model.MutableMediaAspectRatioCache
class AspectRatioCacher(
val cache: MutableMediaAspectRatioCache,
@@ -20,12 +20,12 @@
*/
package com.vitorpamplona.amethyst.service.pow
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.scheduledposts.ScheduledPost
import com.vitorpamplona.amethyst.commons.scheduledposts.ScheduledPostStore
import com.vitorpamplona.amethyst.commons.service.pow.PersistedPoWJob
import com.vitorpamplona.amethyst.commons.service.pow.PoWJobStore
import com.vitorpamplona.amethyst.commons.service.pow.PoWPublishQueue
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
@@ -21,7 +21,6 @@
package com.vitorpamplona.amethyst.service.pow
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.boost
import com.vitorpamplona.amethyst.commons.resources.post
import com.vitorpamplona.amethyst.commons.resources.pow_kind_chat_message
@@ -30,6 +29,7 @@ import com.vitorpamplona.amethyst.commons.resources.private_message
import com.vitorpamplona.amethyst.commons.resources.reaction
import com.vitorpamplona.amethyst.commons.resources.voice_post
import com.vitorpamplona.amethyst.commons.resources.voice_reply
import com.vitorpamplona.amethyst.commons.service.pow.powKindLabelRes
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
@@ -39,24 +39,6 @@ import com.vitorpamplona.quartz.nip56Reports.ReportEvent
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceEvent
import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent
import org.jetbrains.compose.resources.StringResource
/**
* The one user-facing label for "what is being mined": shared by the
* broadcast banner, the mining foreground notification, and failure toasts
* so a job is described the same way everywhere it appears.
*/
fun powKindLabelRes(kind: Int): StringResource =
when (kind) {
ReactionEvent.KIND -> Res.string.reaction
RepostEvent.KIND, GenericRepostEvent.KIND -> Res.string.boost
VoiceEvent.KIND -> Res.string.voice_post
VoiceReplyEvent.KIND -> Res.string.voice_reply
ReportEvent.KIND -> Res.string.pow_kind_report
GiftWrapEvent.KIND -> Res.string.private_message
ChannelMessageEvent.KIND, LiveActivitiesChatMessageEvent.KIND -> Res.string.pow_kind_chat_message
else -> Res.string.post
}
/**
* Android-resource twin of [powKindLabelRes], for [com.vitorpamplona.amethyst.service.pow.PowMiningForegroundService].
@@ -24,9 +24,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.service.relayClient.authCommand.model.AuthCoordinator
import com.vitorpamplona.amethyst.service.relayClient.authCommand.model.ScreenAuthAccount
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable
fun RelayAuthSubscription(accountViewModel: AccountViewModel) = RelayAuthSubscription(accountViewModel, Amethyst.instance.authCoordinator)
@@ -64,8 +64,11 @@ import androidx.compose.ui.unit.sp
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.commons.model.Channel
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.model.nip11RelayInfo.loadRelayInfo
import com.vitorpamplona.amethyst.commons.relayClient.auth.RelayAuthPrompt
import com.vitorpamplona.amethyst.commons.relayClient.auth.UserAuthChoice
import com.vitorpamplona.amethyst.commons.relayClient.reqCommand.channel.observeChannel
import com.vitorpamplona.amethyst.commons.relayClient.user.observeUserInfo
import com.vitorpamplona.amethyst.commons.relayauth.AuthPurpose
import com.vitorpamplona.amethyst.commons.relayauth.AuthPurposeKind
import com.vitorpamplona.amethyst.commons.relayauth.RelayAuthPolicy
@@ -98,15 +101,12 @@ import com.vitorpamplona.amethyst.commons.resources.relay_auth_why_send_dm
import com.vitorpamplona.amethyst.commons.resources.relay_auth_why_thread
import com.vitorpamplona.amethyst.commons.resources.relay_auth_why_thread_with
import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage
import com.vitorpamplona.amethyst.commons.ui.note.ClickableUserPicture
import com.vitorpamplona.amethyst.commons.ui.pluralStringRes
import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.RelayIconFilter
import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
@@ -21,6 +21,7 @@
package com.vitorpamplona.amethyst.service.relayClient.authCommand.model
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.model.buzz.BuzzRelayDialect
import com.vitorpamplona.amethyst.commons.relayClient.auth.ListWithUniqueSetCache
import com.vitorpamplona.amethyst.commons.relayClient.auth.RelayAuthFirstParty
@@ -32,7 +33,6 @@ import com.vitorpamplona.amethyst.commons.relayauth.RelayAuthDecision
import com.vitorpamplona.amethyst.commons.relayauth.RelayAuthPolicy
import com.vitorpamplona.amethyst.commons.relayauth.RelayAuthVerdict
import com.vitorpamplona.amethyst.isDebug
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
@@ -27,7 +27,7 @@ import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.payment_required_title
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl
@Composable
@@ -42,13 +42,13 @@ import com.vitorpamplona.amethyst.commons.model.EmptyTagList
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.error_dialog_button_ok
import com.vitorpamplona.amethyst.commons.resources.notify_block_relay
import com.vitorpamplona.amethyst.commons.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.Size16dp
import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable
fun NotifyRequestDialog(
@@ -21,8 +21,8 @@
package com.vitorpamplona.amethyst.service.relayClient.notifyCommand.model
import android.util.LruCache
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.relayClient.notify.NotifyRequestsCache
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.RelayConnectionListener
@@ -22,8 +22,10 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.vitorpamplona.amethyst.commons.relayClient.reqCommand.account.AccountFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.reqCommand.account.AccountUiQueryState
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
@Composable
fun AccountFilterAssemblerSubscription(accountViewModel: AccountViewModel) =
@@ -22,8 +22,10 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.vitorpamplona.amethyst.commons.relayClient.reqCommand.account.AccountForegroundFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.reqCommand.account.AccountUiQueryState
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
@Composable
fun AccountForegroundFilterAssemblerSubscription(accountViewModel: AccountViewModel) =
@@ -20,7 +20,9 @@
*/
package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.relayClient.reqCommand.account.AccountFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.reqCommand.account.AccountQueryState
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.utils.Log
@@ -24,8 +24,8 @@ import com.vitorpamplona.amethyst.commons.model.Channel
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.commons.relayClient.channel.nip28PublicChats.filterChannelMetadataUpdatesById
import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.PerUniqueIdEoseManager
import com.vitorpamplona.amethyst.commons.relayClient.reqCommand.channel.ChannelFinderQueryState
import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.ChannelFinderQueryState
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
@@ -36,14 +36,16 @@ class ChannelMetadataWatcherSubAssembler(
override fun updateFilter(
key: ChannelFinderQueryState,
since: SincePerRelayMap?,
): List<RelayBasedFilter> =
if (key.channel is PublicChatChannel) {
key.channel.relays().flatMap {
filterChannelMetadataUpdatesById(it, listOf(key.channel), since?.get(it)?.time)
): List<RelayBasedFilter> {
val channel = key.channel
return if (channel is PublicChatChannel) {
channel.relays().flatMap {
filterChannelMetadataUpdatesById(it, listOf(channel), since?.get(it)?.time)
}
} else {
emptyList()
}
}
/**
* Only one key per channel.
@@ -24,8 +24,8 @@ import com.vitorpamplona.amethyst.commons.model.Channel
import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel
import com.vitorpamplona.amethyst.commons.relayClient.channel.nip53LiveActivities.filterLiveStreamUpdatesByAddress
import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.PerUniqueIdEoseManager
import com.vitorpamplona.amethyst.commons.relayClient.reqCommand.channel.ChannelFinderQueryState
import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.ChannelFinderQueryState
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
@@ -40,14 +40,16 @@ class LiveActivityWatcherSubAssembly(
override fun updateFilter(
key: ChannelFinderQueryState,
since: SincePerRelayMap?,
): List<RelayBasedFilter> =
if (key.channel is LiveActivitiesChannel) {
key.channel.relays().flatMap {
filterLiveStreamUpdatesByAddress(it, listOf(key.channel), since?.get(it)?.time)
): List<RelayBasedFilter> {
val channel = key.channel
return if (channel is LiveActivitiesChannel) {
channel.relays().flatMap {
filterLiveStreamUpdatesByAddress(it, listOf(channel), since?.get(it)?.time)
}
} else {
emptyList()
}
}
/**
* Only one key per channel.
@@ -27,7 +27,7 @@ import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.relayClient.nip47WalletConnect.NWCPaymentFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.nip47WalletConnect.NWCPaymentQueryState
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcRequestEvent
@SuppressLint("StateFlowValueCalledInComposition")
@@ -21,8 +21,9 @@
package com.vitorpamplona.amethyst.service.relayClient.searchCommand
import androidx.compose.runtime.Composable
import com.vitorpamplona.amethyst.commons.relayClient.searchCommand.SearchFilterAssembler
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.search.SearchBarViewModel
@Composable
@@ -46,10 +46,10 @@ import com.vitorpamplona.amethyst.commons.resources.resource_usage_reason_reconn
import com.vitorpamplona.amethyst.commons.resources.resource_usage_reason_wakelock
import com.vitorpamplona.amethyst.commons.service.crashreports.DEV_REPORT_PUBKEY
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeToMessage
import com.vitorpamplona.amethyst.commons.ui.pluralStringRes
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.ui.navigation.routes.routeToMessage
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -21,8 +21,7 @@
package com.vitorpamplona.amethyst.service.uploads
import android.media.MediaDataSource
import com.vitorpamplona.amethyst.commons.service.image.BlurhashWrapper
import com.vitorpamplona.amethyst.commons.service.image.ThumbhashWrapper
import com.vitorpamplona.amethyst.commons.service.upload.FileHeader
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag
import com.vitorpamplona.quartz.utils.Log
@@ -31,58 +30,45 @@ import kotlinx.coroutines.CancellationException
import okhttp3.OkHttpClient
import java.io.IOException
class FileHeader(
val mimeType: String?,
val hash: String,
val size: Int,
val dim: DimensionTag?,
val blurHash: BlurhashWrapper?,
val thumbHash: ThumbhashWrapper? = null,
) {
class UnableToDownload(
val fileUrl: String,
) : Exception()
/** Downloads [fileUrl] and computes its [FileHeader] with [prepare]. */
suspend fun FileHeader.Companion.prepare(
fileUrl: String,
mimeType: String?,
dimPrecomputed: DimensionTag?,
okHttpClient: (String) -> OkHttpClient,
): Result<FileHeader> =
try {
val imageData: ImageDownloader.Blob? = ImageDownloader().waitAndGetImage(fileUrl, okHttpClient)
companion object {
suspend fun prepare(
fileUrl: String,
mimeType: String?,
dimPrecomputed: DimensionTag?,
okHttpClient: (String) -> OkHttpClient,
): Result<FileHeader> =
try {
val imageData: ImageDownloader.Blob? = ImageDownloader().waitAndGetImage(fileUrl, okHttpClient)
if (imageData != null) {
prepare(imageData.bytes, mimeType ?: imageData.contentType, dimPrecomputed)
} else {
Result.failure(UnableToDownload(fileUrl))
}
} catch (e: Exception) {
if (e is CancellationException) throw e
Log.e("ImageDownload") { "Couldn't download image from server: ${e.message}" }
Result.failure(e)
}
fun prepare(
data: ByteArray,
mimeType: String?,
dimPrecomputed: DimensionTag?,
): Result<FileHeader> =
try {
val hash = sha256(data).toHexKey()
val size = data.size
val preview = PreviewMetadataCalculator.computeFromBytes(data, mimeType, dimPrecomputed)
Result.success(FileHeader(mimeType, hash, size, preview.dim, preview.blurhash, preview.thumbhash))
} catch (e: Exception) {
if (e is CancellationException) throw e
Log.e("ImageDownload") { "Couldn't convert image in to File Header: ${e.message}" }
Result.failure(e)
}
if (imageData != null) {
prepare(imageData.bytes, mimeType ?: imageData.contentType, dimPrecomputed)
} else {
Result.failure(FileHeader.UnableToDownload(fileUrl))
}
} catch (e: Exception) {
if (e is CancellationException) throw e
Log.e("ImageDownload") { "Couldn't download image from server: ${e.message}" }
Result.failure(e)
}
/** Hashes [data] and decodes its dimensions and preview hashes (Android media APIs). */
fun FileHeader.Companion.prepare(
data: ByteArray,
mimeType: String?,
dimPrecomputed: DimensionTag?,
): Result<FileHeader> =
try {
val hash = sha256(data).toHexKey()
val size = data.size
val preview = PreviewMetadataCalculator.computeFromBytes(data, mimeType, dimPrecomputed)
Result.success(FileHeader(mimeType, hash, size, preview.dim, preview.blurhash, preview.thumbhash))
} catch (e: Exception) {
if (e is CancellationException) throw e
Log.e("ImageDownload") { "Couldn't convert image in to File Header: ${e.message}" }
Result.failure(e)
}
}
class ByteArrayMediaDataSource(
var imageData: ByteArray,
@@ -25,8 +25,8 @@ import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.service.uploads
import android.content.Context
import android.net.Uri
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName
import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerType
import com.vitorpamplona.amethyst.commons.model.mediaServers.blossomUploadOrder
@@ -39,7 +40,7 @@ import com.vitorpamplona.amethyst.commons.resources.server_did_not_provide_a_url
import com.vitorpamplona.amethyst.commons.resources.upload_cancelled
import com.vitorpamplona.amethyst.commons.service.upload.BlossomClient
import com.vitorpamplona.amethyst.commons.service.upload.BlossomPaymentException
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.commons.service.upload.FileHeader
import com.vitorpamplona.amethyst.service.uploads.UploadingState.UploadingFinalState
import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader
import com.vitorpamplona.amethyst.service.uploads.nip96.Nip96Uploader
@@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.service.uploads.blossom
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.service.upload.BlossomClient
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.CoroutineScope
@@ -20,7 +20,7 @@
*/
package com.vitorpamplona.amethyst.service.uploads.blossom
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.quartz.lightning.LnInvoiceUtil
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceSuccessResponse
import com.vitorpamplona.quartz.nipB7Blossom.BlossomPaymentProof
@@ -28,9 +28,9 @@ import android.webkit.MimeTypeMap
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.failed_to_delete_with_message
import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_to_server_with_message
import com.vitorpamplona.amethyst.commons.service.HttpStatusMessages
import com.vitorpamplona.amethyst.commons.service.upload.BlossomPaymentException
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
import com.vitorpamplona.amethyst.service.HttpStatusMessages
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.uploads.MediaUploadResult
import com.vitorpamplona.amethyst.service.uploads.PreviewMetadataCalculator

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