mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 19:28:25 +00:00
Merge remote-tracking branch 'origin/main' into claude/kind-keller-itzy63
# Conflicts: # quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/mls/interop/TreeOperationsInteropTest.kt
This commit is contained in:
@@ -58,10 +58,10 @@ amethyst/ # Android app module
|
||||
│ │ │ │ ├── MainActivity.kt # Entry point
|
||||
│ │ │ │ ├── navigation/
|
||||
│ │ │ │ │ ├── AppNavigation.kt # NavHost
|
||||
│ │ │ │ │ ├── routes/Routes.kt # @Serializable routes
|
||||
│ │ │ │ │ ├── routes/RouteNavController.kt # NavHost-bound route helpers
|
||||
│ │ │ │ │ └── bottombars/AppBottomBar.kt
|
||||
│ │ │ │ ├── screen/ # 80+ screens
|
||||
│ │ │ │ └── theme/Theme.kt # Material3 theme
|
||||
│ │ │ │ └── theme/Theme.kt # AmethystTheme (accent, prefs, window insets)
|
||||
│ │ │ └── Amethyst.kt # Application class
|
||||
│ │ └── AndroidManifest.xml # Permissions, intent filters
|
||||
│ └── androidMain/ # KMP Android source set
|
||||
@@ -69,6 +69,12 @@ amethyst/ # Android app module
|
||||
└── build.gradle # Android config
|
||||
```
|
||||
|
||||
The `@Serializable` `Route` catalog is headless and lives in
|
||||
`commons/.../commons/model/navigation/Routes.kt`; `INav`/`EmptyNav` and the top bars are
|
||||
in `commonsUI/.../commons/ui/navigation/`. The palettes, `ColorScheme.*` tokens, sizes and
|
||||
typography are in `commonsUI/.../commons/ui/theme/` (markdown styles in its `jvmAndroid`).
|
||||
Add a route to the commons `Routes.kt`; wire its destination in the app's `AppNavigation.kt`.
|
||||
|
||||
## 1. Type-Safe Navigation
|
||||
|
||||
### Pattern: @Serializable Routes
|
||||
@@ -953,9 +959,11 @@ fun testPermissionRequest() {
|
||||
|
||||
**Key Android Files:**
|
||||
- `amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt`
|
||||
- `amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt`
|
||||
- `commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/navigation/Routes.kt` (route catalog)
|
||||
- `commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/navigation/navs/INav.kt`
|
||||
- `amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt`
|
||||
- `amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt`
|
||||
- `amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt` (`AmethystTheme`)
|
||||
- `commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/theme/AmethystColorScheme.kt` (palettes + `ColorScheme.*` tokens)
|
||||
- `amethyst/src/main/AndroidManifest.xml`
|
||||
- `amethyst/build.gradle`
|
||||
|
||||
|
||||
@@ -608,7 +608,9 @@ fun testNavigationToProfile() {
|
||||
|
||||
## File Locations
|
||||
|
||||
- `amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt`
|
||||
- `commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/navigation/Routes.kt` (route catalog)
|
||||
- `commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/navigation/navs/INav.kt`
|
||||
- `amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteNavController.kt` (NavHost helpers)
|
||||
- `amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt`
|
||||
- `amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Nav.kt`
|
||||
- `amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/AppBottomBar.kt`
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
# Replaceable-event backup conflicts
|
||||
|
||||
## Problem
|
||||
|
||||
`AccountSettings` keeps an on-disk backup of ~26 of the account's replaceable events
|
||||
(profile, follow list, mute list, relay lists, NIP-51 lists, wallet events, app settings).
|
||||
Each `*State` collector copies whatever is newest in `LocalCache` into that backup. When
|
||||
another client rewrites one of these events without reading the previous version (a fresh
|
||||
follow list, a mute list without the private items, a profile missing fields), the newer
|
||||
event wins on relays, lands in `LocalCache`, and the backup is overwritten with it: the
|
||||
only surviving copy of the user's data is gone.
|
||||
|
||||
## Design
|
||||
|
||||
1. **Know what we signed.** `LocallySignedEvents` (commons, `model/backups/`) records the
|
||||
ids of replaceable/addressable events passing through `justConsumeMyOwnEvent`, the
|
||||
choke point for every local publish and for the backup restore at startup. It is a
|
||||
bounded LRU (500 ids): only the latest versions of each list matter.
|
||||
2. **Events diff themselves.** Every backed-up event implements
|
||||
`DiffableEvent<D>` (quartz, `nip01Core/diff/`): `diffFrom(older)` returns its own diff
|
||||
class built from its own parsed objects, e.g. `ContactListDiff(follows:
|
||||
ListDiff<ContactTag>)`, `MuteListDiff(publicMutes: ListDiff<MuteTag>, privateItems)`,
|
||||
`AdvertisedRelayListDiff(relays: ListDiff<AdvertisedRelayInfo>)`, `NutzapInfoDiff(mints:
|
||||
ListDiff<NutzapMintTag>, relays, p2pkPubkey: ValueChange<HexKey>)`, `MetadataDiff` with
|
||||
one `ValueChange` per `UserMetadata` field plus NIP-39 claims and unmodeled JSON fields.
|
||||
The relay-tag lists (DM, key package, search, indexer, feeds, blocked, trusted, private
|
||||
outbox) share `RelayListDiff`; encrypted-only events (Cashu wallet, Concord list, NIP-78
|
||||
data) diff their content as a whole.
|
||||
|
||||
Tags an event's parser doesn't read never reach its diff, so bookkeeping other clients
|
||||
rewrite on every save (`client`, NIP-13 `nonce`, NIP-40 `expiration`) is ignored by
|
||||
construction; `EventDiffTest.clientPowAndExpirationTagsAreIgnored` pins it.
|
||||
|
||||
`nip01Core/diff/` only holds the containers: `EventDiff` (`removesData()`),
|
||||
`ListDiff<T>` (items matched by an identity key; a matched pair with different details
|
||||
is a change, not a removal plus an addition), `ValueChange<T>`, and `ContentChange` for
|
||||
NIP-44 private items, which can't be compared item by item without decrypting.
|
||||
3. **Only question lossy external rewrites.** `BackupConflictGuard` (commons,
|
||||
`model/backups/`, tested in `BackupConflictGuardTest`) guards every `update*` backup
|
||||
method of `AccountSettings`, which owns one as `backupGuard`. A newer version that was not signed here goes through
|
||||
`ReplaceableBackupDiff.detectLoss` (commons), which asks the event for its diff and
|
||||
keeps it only when `removesData()`.
|
||||
|
||||
If nothing was dropped, the other app evidently built on the previous version, and the
|
||||
backup is updated silently as before.
|
||||
|
||||
Wiped versions (no tags, empty app data, a mints-less nutzap info) reach the guard too,
|
||||
so wiping everything is questioned like any other loss. An empty version only becomes
|
||||
the backup when this app signed it or the user kept it. The guard runs under a lock
|
||||
shared with the resolutions, and a version older than an open conflict's incoming can
|
||||
never replace or clear it (e.g. the retry of an accept that a newer change overtook).
|
||||
4. **Freeze and ask.** On a loss, the backup keeps the saved version and a
|
||||
`ReplaceableBackupConflict` is published on `BackupConflictGuard.conflicts`.
|
||||
Home shows a card per open conflict as the first item of each tab's feed
|
||||
(`BackupConflictCards`), so they scroll away with the posts instead of covering them;
|
||||
an empty, loading or failed feed shows them above its placeholder: "You changed your mute list in another app" with a
|
||||
"12 removed · 3 added" summary. Cards can't be dismissed; they stay until the user
|
||||
decides. The most recent conflict gets a lead card with an event-specific headline
|
||||
("You unblocked 3 relays", "You unfollowed 12 people" with its bar); the others collapse
|
||||
into slim pills. List headlines lead with what was removed and add
|
||||
what was added to the same list, worded per list ("You unfollowed 12 people and
|
||||
followed 40", "You left 2 groups and joined 1", "You unblocked 3 relays and blocked 1").
|
||||
All copy assumes the change was on purpose: the user did it in another client, and
|
||||
Amethyst is only confirming before its backup moves on. So it says what "you" did, in
|
||||
the past tense ("You left 1 public chat", not "You'd leave"), and never calls it a loss.
|
||||
Tapping one opens `Route.BackupConflictReview(slot)`
|
||||
(`BackupConflictReviewScreen`): a `LazyColumn` of every entry, so lists with hundreds of
|
||||
items scroll lazily. Entries are typed (`ReviewItem`, built per diff class in
|
||||
`BackupConflictPresentation.kt`) and rendered with the app's loaders, which subscribe to
|
||||
relays and recompose when data arrives: people (`LoadUser` + `UsernameDisplay`, tap →
|
||||
profile), muted threads (`LoadNote` + `NoteCompose`), communities and feeds
|
||||
(`LoadAddressableNote` + `NoteCompose`), public chats (`observeChannel`, tap → chat),
|
||||
ephemeral rooms (tap → room) and relays (tap → relay info). Leaving the screen without
|
||||
deciding keeps the card and the frozen backup.
|
||||
|
||||
The list is laid out per event (`BackupConflictEventViews.kt`, `eventDiffItems`), each
|
||||
leading with one picture of the change (`BackupConflictVisuals.kt`; red lost, green
|
||||
gained, amber changed, all from the theme): the follow list shows saved → new counts, a
|
||||
kept/dropped/new split bar, Dropped/New/Edited tabs and a searchable grid of faces; the
|
||||
mute list a shield with the unmuted count, per-kind tiles, a face pile, struck-through
|
||||
words and the private-items card; the profile both versions side by side as mini
|
||||
cards, then inline field diffs with images as thumbnails; NIP-65 outbox and inbox
|
||||
lanes; nutzap info a key swap
|
||||
drawn as color fingerprints with a warning, then mints; groups a tile per group, faded
|
||||
when you left it. The other lists (`BackupConflictListViews.kt`) get their own too:
|
||||
each relay list as per-relay rows tagged by what the list is for (DM, key-package,
|
||||
search, indexer, relay-feed, private-outbox and trusted relays; no "N of M" summary card,
|
||||
since the change is the tagged rows, not a reach number); blocked relays
|
||||
as an "N unblocked" warning with allowed / newly blocked / still blocked), public
|
||||
chats, communities, favorite feeds and ephemeral rooms as tiles loaded from relays and
|
||||
faded when you left them, hashtags and places as a pill cloud (places as "City · #geohash" through `LoadCityName`, each pill opening its feed), trust providers as a
|
||||
per-service before/after table, payment targets and BOLT12 offers as cards, and the
|
||||
encrypted-only Cashu wallet and Concord list as one explained panel. Only NIP-78 app
|
||||
settings still uses the generic removed / added / changed sections.
|
||||
Each card has an Accept button that keeps the new version right from Home (the change
|
||||
was made on purpose; reverting needs the review screen, a tap on the card away). The
|
||||
review screen is titled with the event and what happened to it ("Community list
|
||||
updated", "Mute list cleared").
|
||||
Buttons are worded per event ("Keep 120" / "Restore 523", "Re-mute 37", "Rejoin 3").
|
||||
Keeping the new version is the default (the filled button, on the right); restoring
|
||||
always reverts another app's change, so it is an outlined red button on every screen.
|
||||
The screen offers:
|
||||
- **Restore saved version** — `Account.restoreBackupOver` re-signs the saved kind, tags
|
||||
and content (NIP-44 self-encrypted items stay valid) with
|
||||
`created_at = max(now, incoming + 1)` (capped at 15 minutes ahead; a far-future
|
||||
external version is answered at "now"), dropping the old `client` tag (so the signer
|
||||
adds the current one), the `nonce` (its PoW was for the old id) and an already-past
|
||||
`expiration` (`BackupRestore.tagsToResign`), and publishes it through
|
||||
`EventBroadcaster.sendRestoredVersion`: where a normal save of that kind goes, and for
|
||||
the user's own relay lists (NIP-65, DM, key package) and nutzap info also to every
|
||||
relay the restored list names, since the lossy version may have dropped them from the
|
||||
outbox set; profiles, like their normal saves, go everywhere.
|
||||
- **Use new version** — `Account.acceptExternalVersion` whitelists that id and re-runs
|
||||
the original update, so the backup moves forward.
|
||||
|
||||
Both resolutions first *claim* the conflict: they act only if it is still the open
|
||||
conflict of its slot, so a stale screen, a double tap, or a conflict dropped because the
|
||||
wallet/nutzap info was deleted does nothing. While a restore is signing, its incoming
|
||||
version can't re-raise the conflict; a failed restore reopens it. The same incoming
|
||||
version re-emitted by the cache is ignored without re-diffing.
|
||||
|
||||
Public items removed while the private section went from empty to filled are treated as
|
||||
made private, not lost (`ContentChange.publicRemovalsAreLoss`).
|
||||
|
||||
While a conflict is open, edits made in Amethyst are compared against the frozen
|
||||
saved version too (they are built on top of the external one), so they cannot
|
||||
silently clear the conflict. They move the conflict's `incoming` forward but keep the
|
||||
external version as its `cause` (the screen shows that date).
|
||||
|
||||
Open conflicts are persisted with the backups (`BackupConflictStorage`, as the saved,
|
||||
incoming and cause events) and re-seeded on load through `BackupConflictGuard.restore`. The
|
||||
diff is recomputed, so a version the other app has since fixed stops being a conflict. A
|
||||
restored conflict has no retry closure until its version arrives again; keeping it rebuilds
|
||||
the update from the event's type (`AccountSettings.reapplyBackup`), except NIP-78, which
|
||||
lands on the next delivery.
|
||||
|
||||
## Known limits / follow-ups
|
||||
|
||||
- Private (encrypted) list items are only compared as "all gone"; a rewrite that drops
|
||||
*some* private items isn't detected. Decrypting both sides would fix it but needs the
|
||||
signer (an Amber round-trip), so it's left out of the passive path.
|
||||
- The ids of locally signed events are in memory only. If the app dies inside the 1s
|
||||
settings-save debounce right after a local removal, the next launch may ask once about
|
||||
our own event. Harmless (either answer keeps the data the user chose).
|
||||
- Edits from the same user on another Amethyst install count as "external" when they
|
||||
remove items. That's intended: the device can't tell them apart from a careless app.
|
||||
- Desktop marks its own events too but has no backup store or conflict UI yet.
|
||||
- Moving public items into a private section that already existed is still questioned: the
|
||||
private section re-encrypts on every save, so it only reads as "changed", and telling
|
||||
"made private" from "deleted" would need decrypting it.
|
||||
- A version dated more than 15 minutes in the future can't be outranked safely, so Restore
|
||||
refuses it (the conflict reopens) and only keeping it works.
|
||||
- Read-only (npub) accounts can't re-sign the saved version, so their review offers only
|
||||
keeping the new one.
|
||||
Vendored
+3
-3
@@ -240,6 +240,6 @@
|
||||
# The enum FIELDS are already pinned by the blanket `-keepclassmembers enum *`
|
||||
# above; that rule deliberately lets the CLASS be renamed, which is exactly what
|
||||
# breaks here. Every enum used as a route argument needs its name too.
|
||||
-keep class com.vitorpamplona.amethyst.ui.navigation.routes.DiscoverTab { *; }
|
||||
-keep class com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.BookmarkType { *; }
|
||||
-keep class com.vitorpamplona.amethyst.ui.navigation.routes.GeocacheTab { *; }
|
||||
-keep class com.vitorpamplona.amethyst.commons.model.navigation.DiscoverTab { *; }
|
||||
-keep class com.vitorpamplona.amethyst.commons.model.nip51Lists.BookmarkType { *; }
|
||||
-keep class com.vitorpamplona.amethyst.commons.model.navigation.GeocacheTab { *; }
|
||||
|
||||
@@ -26,6 +26,9 @@ import androidx.core.graphics.createBitmap
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
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.service.http.DefaultContentTypeInterceptor
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.nipB7Blossom.BlossomServerListState
|
||||
@@ -34,9 +37,6 @@ 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.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@ import com.vitorpamplona.amethyst.commons.resources.error_video_open_in_browser
|
||||
import com.vitorpamplona.amethyst.commons.resources.error_video_playback_failed
|
||||
import com.vitorpamplona.amethyst.commons.resources.error_video_playback_failed_description
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Assert.assertTrue
|
||||
|
||||
+4
-1
@@ -67,10 +67,13 @@ import com.vitorpamplona.amethyst.commons.resources.push_server_title
|
||||
import com.vitorpamplona.amethyst.commons.resources.push_server_uses_app_explainer
|
||||
import com.vitorpamplona.amethyst.commons.resources.quick_action_dont_show_again_button
|
||||
import com.vitorpamplona.amethyst.commons.resources.select_push_server
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.SpinnerSelectionDialog
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.TextSpinner
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.TitleExplainer
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.model.UiSettingsFlow
|
||||
import com.vitorpamplona.amethyst.service.notifications.PushDistributorHandler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsBlockTile
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
+2
-2
@@ -27,9 +27,9 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.MaxWidthPaddingTop5dp
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.MaxWidthPaddingTop5dp
|
||||
|
||||
@Composable
|
||||
fun TranslatableRichTextViewer(
|
||||
|
||||
@@ -24765,9 +24765,9 @@ Lcom/vitorpamplona/amethyst/ui/StringResourceCacheKt$StringResSetup$lambda$0$0$$
|
||||
SPLcom/vitorpamplona/amethyst/ui/StringResourceCacheKt$StringResSetup$lambda$0$0$$inlined$onPauseOrDispose$1;-><init>(Landroidx/lifecycle/compose/LifecycleResumePauseEffectScope;)V
|
||||
Lcom/vitorpamplona/amethyst/ui/components/WindowUtilsKt;
|
||||
SPLcom/vitorpamplona/amethyst/ui/components/WindowUtilsKt;->getActivityOrNull(Landroid/content/Context;)Landroidx/activity/ComponentActivity;
|
||||
Lcom/vitorpamplona/amethyst/ui/navigation/topbars/ShorterTopAppBarKt;
|
||||
SPLcom/vitorpamplona/amethyst/ui/navigation/topbars/ShorterTopAppBarKt;-><clinit>()V
|
||||
SPLcom/vitorpamplona/amethyst/ui/navigation/topbars/ShorterTopAppBarKt;->getTopBarSize()F
|
||||
Lcom/vitorpamplona/amethyst/commons/ui/navigation/topbars/ShorterTopAppBarKt;
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/navigation/topbars/ShorterTopAppBarKt;-><clinit>()V
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/navigation/topbars/ShorterTopAppBarKt;->getTopBarSize()F
|
||||
Lcom/vitorpamplona/amethyst/ui/note/elements/NowProviderKt;
|
||||
SPLcom/vitorpamplona/amethyst/ui/note/elements/NowProviderKt;-><clinit>()V
|
||||
SPLcom/vitorpamplona/amethyst/ui/note/elements/NowProviderKt;->NowProvider(Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;I)V
|
||||
@@ -25489,14 +25489,14 @@ SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/ComposableSingletons$Key
|
||||
Lcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/ComposableSingletons$KeyTextFieldKt$$ExternalSyntheticLambda1;
|
||||
SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/ComposableSingletons$KeyTextFieldKt$$ExternalSyntheticLambda1;-><init>()V
|
||||
SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/ComposableSingletons$KeyTextFieldKt$$ExternalSyntheticLambda1;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
|
||||
Lcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/ComposableSingletons$SignUpButtonKt;
|
||||
SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/ComposableSingletons$SignUpButtonKt;-><clinit>()V
|
||||
SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/ComposableSingletons$SignUpButtonKt;-><init>()V
|
||||
SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/ComposableSingletons$SignUpButtonKt;->getLambda$-1905508794$amethyst()Lkotlin/jvm/functions/Function3;
|
||||
SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/ComposableSingletons$SignUpButtonKt;->lambda__1905508794$lambda$0(Landroidx/compose/foundation/layout/RowScope;Landroidx/compose/runtime/Composer;I)Lkotlin/Unit;
|
||||
Lcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/ComposableSingletons$SignUpButtonKt$$ExternalSyntheticLambda0;
|
||||
SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/ComposableSingletons$SignUpButtonKt$$ExternalSyntheticLambda0;-><init>()V
|
||||
SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/ComposableSingletons$SignUpButtonKt$$ExternalSyntheticLambda0;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
|
||||
Lcom/vitorpamplona/amethyst/commons/account/ui/login/ComposableSingletons$SignUpButtonKt;
|
||||
SPLcom/vitorpamplona/amethyst/commons/account/ui/login/ComposableSingletons$SignUpButtonKt;-><clinit>()V
|
||||
SPLcom/vitorpamplona/amethyst/commons/account/ui/login/ComposableSingletons$SignUpButtonKt;-><init>()V
|
||||
SPLcom/vitorpamplona/amethyst/commons/account/ui/login/ComposableSingletons$SignUpButtonKt;->getLambda$-1905508794$amethyst()Lkotlin/jvm/functions/Function3;
|
||||
SPLcom/vitorpamplona/amethyst/commons/account/ui/login/ComposableSingletons$SignUpButtonKt;->lambda__1905508794$lambda$0(Landroidx/compose/foundation/layout/RowScope;Landroidx/compose/runtime/Composer;I)Lkotlin/Unit;
|
||||
Lcom/vitorpamplona/amethyst/commons/account/ui/login/ComposableSingletons$SignUpButtonKt$$ExternalSyntheticLambda0;
|
||||
SPLcom/vitorpamplona/amethyst/commons/account/ui/login/ComposableSingletons$SignUpButtonKt$$ExternalSyntheticLambda0;-><init>()V
|
||||
SPLcom/vitorpamplona/amethyst/commons/account/ui/login/ComposableSingletons$SignUpButtonKt$$ExternalSyntheticLambda0;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
|
||||
Lcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/KeyTextFieldKt;
|
||||
SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/KeyTextFieldKt;->$r8$lambda$EB2Pyswic2JGUT6jyNtb4e7r5z8(Landroidx/compose/runtime/MutableState;Landroidx/compose/runtime/Composer;I)Lkotlin/Unit;
|
||||
SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/KeyTextFieldKt;->$r8$lambda$raCAC-FTNT20_jVOcW9Lmx1V5zI(Landroidx/compose/ui/semantics/SemanticsPropertyReceiver;)Lkotlin/Unit;
|
||||
@@ -25529,12 +25529,12 @@ SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/KeyTextFieldKt$$External
|
||||
Lcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/KeyTextFieldKt$$ExternalSyntheticLambda9;
|
||||
SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/KeyTextFieldKt$$ExternalSyntheticLambda9;-><init>(Landroidx/compose/runtime/MutableState;)V
|
||||
SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/KeyTextFieldKt$$ExternalSyntheticLambda9;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
|
||||
Lcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginButtonKt;
|
||||
SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginButtonKt;->LoginButton$lambda$0(ZLandroidx/compose/foundation/layout/RowScope;Landroidx/compose/runtime/Composer;I)Lkotlin/Unit;
|
||||
SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginButtonKt;->LoginButton(ZZLkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)V
|
||||
Lcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginButtonKt$$ExternalSyntheticLambda0;
|
||||
SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginButtonKt$$ExternalSyntheticLambda0;-><init>(Z)V
|
||||
SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginButtonKt$$ExternalSyntheticLambda0;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
|
||||
Lcom/vitorpamplona/amethyst/commons/account/ui/login/LoginButtonKt;
|
||||
SPLcom/vitorpamplona/amethyst/commons/account/ui/login/LoginButtonKt;->LoginButton$lambda$0(ZLandroidx/compose/foundation/layout/RowScope;Landroidx/compose/runtime/Composer;I)Lkotlin/Unit;
|
||||
SPLcom/vitorpamplona/amethyst/commons/account/ui/login/LoginButtonKt;->LoginButton(ZZLkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)V
|
||||
Lcom/vitorpamplona/amethyst/commons/account/ui/login/LoginButtonKt$$ExternalSyntheticLambda0;
|
||||
SPLcom/vitorpamplona/amethyst/commons/account/ui/login/LoginButtonKt$$ExternalSyntheticLambda0;-><init>(Z)V
|
||||
SPLcom/vitorpamplona/amethyst/commons/account/ui/login/LoginButtonKt$$ExternalSyntheticLambda0;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
|
||||
Lcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginErrorManager;
|
||||
SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginErrorManager;-><clinit>()V
|
||||
SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginErrorManager;-><init>()V
|
||||
@@ -25613,26 +25613,28 @@ Lcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/OpenURIIfNotLoggedInKt$$Ex
|
||||
SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/OpenURIIfNotLoggedInKt$$ExternalSyntheticLambda3;-><init>(Lkotlin/jvm/functions/Function2;I)V
|
||||
Lcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/OpenURIIfNotLoggedInKt$OpenURIIfNotLoggedIn$lambda$5$0$$inlined$onDispose$1;
|
||||
SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/OpenURIIfNotLoggedInKt$OpenURIIfNotLoggedIn$lambda$5$0$$inlined$onDispose$1;-><init>(Landroidx/activity/ComponentActivity;Landroidx/core/util/Consumer;)V
|
||||
Lcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/SignUpButtonKt;
|
||||
SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/SignUpButtonKt;->SignUpButton(Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)V
|
||||
Lcom/vitorpamplona/amethyst/ui/theme/ColorKt;
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ColorKt;-><clinit>()V
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ColorKt;->getPurple200()J
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ColorKt;->getPurple500()J
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ColorKt;->getTeal200()J
|
||||
Lcom/vitorpamplona/amethyst/ui/theme/ShapeKt;
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ShapeKt;-><clinit>()V
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ShapeKt;->getQuoteBorder()Landroidx/compose/foundation/shape/RoundedCornerShape;
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ShapeKt;->getShapes()Landroidx/compose/material3/Shapes;
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ShapeKt;->getSize10dp()F
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ShapeKt;->getSize13dp()F
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ShapeKt;->getSize20dp()F
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ShapeKt;->getSize35dp()F
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ShapeKt;->getSize40dp()F
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ShapeKt;->getSize50dp()F
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ShapeKt;->getSmallBorder()Landroidx/compose/foundation/shape/RoundedCornerShape;
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ShapeKt;->getSmallestBorder()Landroidx/compose/foundation/shape/RoundedCornerShape;
|
||||
Lcom/vitorpamplona/amethyst/commons/account/ui/login/SignUpButtonKt;
|
||||
SPLcom/vitorpamplona/amethyst/commons/account/ui/login/SignUpButtonKt;->SignUpButton(Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)V
|
||||
Lcom/vitorpamplona/amethyst/commons/ui/theme/ColorKt;
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/ColorKt;-><clinit>()V
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/ColorKt;->getPurple200()J
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/ColorKt;->getPurple500()J
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/ColorKt;->getTeal200()J
|
||||
Lcom/vitorpamplona/amethyst/commons/ui/theme/ShapeKt;
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/ShapeKt;-><clinit>()V
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/ShapeKt;->getQuoteBorder()Landroidx/compose/foundation/shape/RoundedCornerShape;
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/ShapeKt;->getShapes()Landroidx/compose/material3/Shapes;
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/ShapeKt;->getSize10dp()F
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/ShapeKt;->getSize13dp()F
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/ShapeKt;->getSize20dp()F
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/ShapeKt;->getSize35dp()F
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/ShapeKt;->getSize40dp()F
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/ShapeKt;->getSize50dp()F
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/ShapeKt;->getSmallBorder()Landroidx/compose/foundation/shape/RoundedCornerShape;
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/ShapeKt;->getSmallestBorder()Landroidx/compose/foundation/shape/RoundedCornerShape;
|
||||
Lcom/vitorpamplona/amethyst/ui/theme/ThemeKt;
|
||||
Lcom/vitorpamplona/amethyst/commons/ui/theme/MarkdownStyleKt;
|
||||
Lcom/vitorpamplona/amethyst/commons/ui/theme/AmethystColorSchemeKt;
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->$r8$lambda$7JAdMf4levt4F3mr6nvsppu__xY(Landroidx/compose/ui/unit/Density;Landroidx/compose/ui/text/font/FontFamily;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;I)Lkotlin/Unit;
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->$r8$lambda$GsT95FSXMVwriWjqAt3KW-oAPpo(Landroid/view/View;ZLandroidx/compose/material3/ColorScheme;)Lkotlin/Unit;
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;-><clinit>()V
|
||||
@@ -25645,16 +25647,16 @@ SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->AmethystTheme$lambda$9$0(Landro
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->AmethystTheme$lambda$9(Landroidx/compose/ui/unit/Density;Landroidx/compose/ui/text/font/FontFamily;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;I)Lkotlin/Unit;
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->AmethystTheme(Lcom/vitorpamplona/amethyst/model/ThemeType;Lcom/vitorpamplona/amethyst/model/AccentColorType;Lcom/vitorpamplona/amethyst/model/FontFamilyType;Lcom/vitorpamplona/amethyst/model/FontSizeType;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->AmethystTheme(Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;I)V
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->accentContainer-DxMtmZc(JZ)J
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/AmethystColorSchemeKt;->accentContainer-DxMtmZc(JZ)J
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->accentPrimary(Lcom/vitorpamplona/amethyst/model/AccentColorType;Z)J
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->accentSecondary(Lcom/vitorpamplona/amethyst/model/AccentColorType;Z)J
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->darkColors(Lcom/vitorpamplona/amethyst/model/AccentColorType;)Landroidx/compose/material3/ColorScheme;
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->getPlaceholderText(Landroidx/compose/material3/ColorScheme;)J
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->getTransparentBackground(Landroidx/compose/material3/ColorScheme;)J
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->isLight(Landroidx/compose/material3/ColorScheme;)Z
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/AmethystColorSchemeKt;->getPlaceholderText(Landroidx/compose/material3/ColorScheme;)J
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/AmethystColorSchemeKt;->getTransparentBackground(Landroidx/compose/material3/ColorScheme;)J
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/AmethystColorSchemeKt;->isLight(Landroidx/compose/material3/ColorScheme;)Z
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->lightColors(Lcom/vitorpamplona/amethyst/model/AccentColorType;)Landroidx/compose/material3/ColorScheme;
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->onAccent-8_81llA(J)J
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->onAccentContainer-DxMtmZc(JZ)J
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/AmethystColorSchemeKt;->onAccent-8_81llA(J)J
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/AmethystColorSchemeKt;->onAccentContainer-DxMtmZc(JZ)J
|
||||
Lcom/vitorpamplona/amethyst/ui/theme/ThemeKt$$ExternalSyntheticLambda0;
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt$$ExternalSyntheticLambda0;-><init>(Landroidx/compose/ui/unit/Density;Landroidx/compose/ui/text/font/FontFamily;Lkotlin/jvm/functions/Function2;)V
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt$$ExternalSyntheticLambda0;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
|
||||
@@ -25676,19 +25678,19 @@ SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt$MarkDownStyleOnLight$1;-><clinit>
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt$MarkDownStyleOnLight$1;-><init>()V
|
||||
Lcom/vitorpamplona/amethyst/ui/theme/ThemeKt$WhenMappings;
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt$WhenMappings;-><clinit>()V
|
||||
Lcom/vitorpamplona/amethyst/ui/theme/TypeKt;
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/TypeKt;-><clinit>()V
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/TypeKt;->getDefaultHeadingStyle()Lkotlin/jvm/functions/Function2;
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/TypeKt;->getDefaultParagraphSpacing()J
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/TypeKt;->getFont14SP()J
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/TypeKt;->getFont17SP()J
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/TypeKt;->getTypography()Landroidx/compose/material3/Typography;
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/TypeKt;->toFontFamily(Lcom/vitorpamplona/amethyst/model/FontFamilyType;)Landroidx/compose/ui/text/font/FontFamily;
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/TypeKt;->withFontFamily(Landroidx/compose/material3/Typography;Landroidx/compose/ui/text/font/FontFamily;)Landroidx/compose/material3/Typography;
|
||||
Lcom/vitorpamplona/amethyst/ui/theme/TypeKt$$ExternalSyntheticLambda0;
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/TypeKt$$ExternalSyntheticLambda0;-><init>()V
|
||||
Lcom/vitorpamplona/amethyst/ui/theme/TypeKt$WhenMappings;
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/TypeKt$WhenMappings;-><clinit>()V
|
||||
Lcom/vitorpamplona/amethyst/commons/ui/theme/TypeKt;
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/TypeKt;-><clinit>()V
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/MarkdownStyleKt;->getDefaultHeadingStyle()Lkotlin/jvm/functions/Function2;
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/TypeKt;->getDefaultParagraphSpacing()J
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/TypeKt;->getFont14SP()J
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/TypeKt;->getFont17SP()J
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/TypeKt;->getTypography()Landroidx/compose/material3/Typography;
|
||||
SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->toFontFamily(Lcom/vitorpamplona/amethyst/model/FontFamilyType;)Landroidx/compose/ui/text/font/FontFamily;
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/TypeKt;->withFontFamily(Landroidx/compose/material3/Typography;Landroidx/compose/ui/text/font/FontFamily;)Landroidx/compose/material3/Typography;
|
||||
Lcom/vitorpamplona/amethyst/commons/ui/theme/TypeKt$$ExternalSyntheticLambda0;
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/TypeKt$$ExternalSyntheticLambda0;-><init>()V
|
||||
Lcom/vitorpamplona/amethyst/commons/ui/theme/TypeKt$WhenMappings;
|
||||
SPLcom/vitorpamplona/amethyst/commons/ui/theme/TypeKt$WhenMappings;-><clinit>()V
|
||||
Lcom/vitorpamplona/amethyst/ui/tor/ArtiGuardState;
|
||||
SPLcom/vitorpamplona/amethyst/ui/tor/ArtiGuardState;-><clinit>()V
|
||||
SPLcom/vitorpamplona/amethyst/ui/tor/ArtiGuardState;-><init>()V
|
||||
|
||||
@@ -30,6 +30,7 @@ import coil3.memory.MemoryCache
|
||||
import com.vitorpamplona.amethyst.commons.model.NoteState
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
||||
import com.vitorpamplona.amethyst.commons.model.nip03Timestamp.BitcoinExplorerEndpoint
|
||||
import com.vitorpamplona.amethyst.commons.model.nip03Timestamp.IncomingOtsEventVerifier
|
||||
import com.vitorpamplona.amethyst.commons.model.nip03Timestamp.TorAwareOkHttpOtsResolverBuilder
|
||||
import com.vitorpamplona.amethyst.commons.napplet.permissions.NappletPermissionLedger
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.BlockedRelayFilteringClient
|
||||
@@ -49,6 +50,7 @@ import com.vitorpamplona.amethyst.commons.service.http.BlossomReadAuthTokenProvi
|
||||
import com.vitorpamplona.amethyst.commons.service.http.DualHttpClientManager
|
||||
import com.vitorpamplona.amethyst.commons.service.http.DualHttpClientManagerForRelays
|
||||
import com.vitorpamplona.amethyst.commons.service.http.EncryptionKeyCache
|
||||
import com.vitorpamplona.amethyst.commons.service.http.LocalBlossomMediaCallFactory
|
||||
import com.vitorpamplona.amethyst.commons.service.http.OnionLocationCache
|
||||
import com.vitorpamplona.amethyst.commons.service.lnurl.OkHttpLnurlEndpointResolver
|
||||
import com.vitorpamplona.amethyst.commons.service.pow.PoWPolicy
|
||||
@@ -59,7 +61,6 @@ import com.vitorpamplona.amethyst.connectedApps.nip46.DataStoreNip46ClientStore
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.UiSettings
|
||||
import com.vitorpamplona.amethyst.model.accountsCache.AccountCacheState
|
||||
import com.vitorpamplona.amethyst.model.nip03Timestamp.IncomingOtsEventVerifier
|
||||
import com.vitorpamplona.amethyst.model.nip11RelayInfo.Nip11CachedRetriever
|
||||
import com.vitorpamplona.amethyst.model.preferences.BuzzAttestationPreferences
|
||||
import com.vitorpamplona.amethyst.model.preferences.BuzzChannelStarPreferences
|
||||
@@ -458,13 +459,14 @@ class AppModules(
|
||||
scope = applicationIOScope,
|
||||
dns = surgeDns,
|
||||
// Transparently rewrites sha256-keyed HTTP requests to the local
|
||||
// Blossom cache when the master toggle is on, the profile-pictures-only
|
||||
// restriction is off, and the probe sees 127.0.0.1:24242 as available.
|
||||
shouldBridgeBlossomCache = {
|
||||
// Blossom cache when the master toggle is on, the probe sees
|
||||
// 127.0.0.1:24242 as available, and either the profile-pictures-only
|
||||
// restriction is off or the request is a profile picture.
|
||||
shouldBridgeBlossomCache = { profilePicture ->
|
||||
val settings = sessionManager.loggedInAccount()?.settings
|
||||
val master = settings?.useLocalBlossomCache?.value ?: false
|
||||
val profileOnly = settings?.localBlossomCacheProfilePicturesOnly?.value ?: false
|
||||
master && !profileOnly && localBlossomCacheProbe.available.value
|
||||
master && (profilePicture || !profileOnly) && localBlossomCacheProbe.available.value
|
||||
},
|
||||
onionCache = onionLocationCache,
|
||||
usageInterceptor = httpUsageInterceptor,
|
||||
@@ -477,6 +479,7 @@ class AppModules(
|
||||
cachedHeaderProvider = blossomReadAuthTokens::cachedHeader,
|
||||
onAuthRequired = blossomReadAuthTokens::warm,
|
||||
),
|
||||
onLocalBlossomCacheUnreachable = { localBlossomCacheProbe.markUnavailable() },
|
||||
)
|
||||
|
||||
// Offers easy methods to know when connections are happening through Tor or not
|
||||
@@ -1035,14 +1038,16 @@ class AppModules(
|
||||
}
|
||||
},
|
||||
httpClientBuilder = roleBasedHttpClientBuilder,
|
||||
// Same gate as the interceptor: the profile-pictures-only restriction keeps note
|
||||
// media (including native `blossom:` URIs) off the local cache.
|
||||
useLocalBlossomCache = {
|
||||
sessionManager
|
||||
.loggedInAccount()
|
||||
?.settings
|
||||
?.useLocalBlossomCache
|
||||
?.value ?: false
|
||||
val settings = sessionManager.loggedInAccount()?.settings
|
||||
val master = settings?.useLocalBlossomCache?.value ?: false
|
||||
val profileOnly = settings?.localBlossomCacheProfilePicturesOnly?.value ?: false
|
||||
master && !profileOnly
|
||||
},
|
||||
localCacheProbe = localBlossomCacheProbe,
|
||||
scope = applicationIOScope,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1148,7 +1153,8 @@ class AppModules(
|
||||
blossomServerResolver = { blossomResolver },
|
||||
// Through the role builder (not raw getHttpClient) so Coil's image
|
||||
// traffic carries the "image" ledger tag. Same Tor decision inside.
|
||||
callFactory = { roleBasedHttpClientBuilder.okHttpClientForImage(it) },
|
||||
// Marked as media so the local Blossom cache may serve these downloads.
|
||||
callFactory = { LocalBlossomMediaCallFactory(roleBasedHttpClientBuilder.okHttpClientForImage(it)) },
|
||||
thumbnailCache = thumbnailDiskCache,
|
||||
backgroundScope = applicationIOScope,
|
||||
readAuth = blossomReadAuthTokens,
|
||||
@@ -1341,8 +1347,7 @@ class AppModules(
|
||||
state.account.settings.localBlossomCacheProfilePicturesOnly
|
||||
.drop(1),
|
||||
).collect {
|
||||
blossomResolver.uriToUrlCache.evictAll()
|
||||
blossomResolver.blossomHitCache.cache.evictAll()
|
||||
blossomResolver.clearCaches()
|
||||
localBlossomCacheProbe.invalidate()
|
||||
// Re-probe immediately so enabling the feature activates it
|
||||
// this session. Otherwise `available` only advances when a
|
||||
@@ -1358,8 +1363,14 @@ class AppModules(
|
||||
}
|
||||
applicationIOScope.launch {
|
||||
localBlossomCacheProbe.available.drop(1).collect {
|
||||
blossomResolver.uriToUrlCache.evictAll()
|
||||
blossomResolver.blossomHitCache.cache.evictAll()
|
||||
blossomResolver.clearCaches()
|
||||
}
|
||||
}
|
||||
// A resolution to the local cache made while a media type was not Tor-routed must not
|
||||
// survive the user switching it to Tor: the cache would keep fetching it outside Tor.
|
||||
applicationIOScope.launch {
|
||||
torPrefs.value.propertyWatchFlow.drop(1).collect {
|
||||
blossomResolver.clearCaches()
|
||||
}
|
||||
}
|
||||
// Warm the local-cache probe so the very first image load doesn't pay
|
||||
@@ -1367,6 +1378,22 @@ class AppModules(
|
||||
applicationIOScope.launch {
|
||||
localBlossomCacheProbe.isAvailable()
|
||||
}
|
||||
// Re-checks the local cache about once a minute while the feature is on, so the bridge
|
||||
// comes back when the cache app is restarted and turns off when it is closed, even while
|
||||
// no `blossom:` URI is being resolved. A loopback HEAD never wakes the radio.
|
||||
applicationIOScope.launch {
|
||||
while (true) {
|
||||
delay(LOCAL_BLOSSOM_CACHE_RECHECK_MS)
|
||||
if (sessionManager
|
||||
.loggedInAccount()
|
||||
?.settings
|
||||
?.useLocalBlossomCache
|
||||
?.value == true
|
||||
) {
|
||||
localBlossomCacheProbe.isAvailable()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Warms the video cache off the main thread. SimpleCache's constructor opens a SQLite
|
||||
// index over StandaloneDatabaseProvider and walks every cached span on disk — up to a
|
||||
@@ -1505,5 +1532,8 @@ class AppModules(
|
||||
* check and burn CPU on a heap that has nothing left to give.
|
||||
*/
|
||||
private const val MIN_RECLAIM_INTERVAL_MS = 120_000L
|
||||
|
||||
/** How often the local Blossom cache is re-probed while the feature is enabled. */
|
||||
private const val LOCAL_BLOSSOM_CACHE_RECHECK_MS = 60_000L
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@ import com.vitorpamplona.amethyst.commons.model.HomeFeedType
|
||||
import com.vitorpamplona.amethyst.commons.model.chats.ChatFeedType
|
||||
import com.vitorpamplona.amethyst.commons.model.clink.ClinkDebitWalletEntry
|
||||
import com.vitorpamplona.amethyst.commons.model.concord.ConcordViewMode
|
||||
import com.vitorpamplona.amethyst.commons.model.mediaServers.DEFAULT_MEDIA_SERVERS
|
||||
import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName
|
||||
import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupViewMode
|
||||
import com.vitorpamplona.amethyst.commons.model.nip47WalletConnect.NwcWalletEntry
|
||||
import com.vitorpamplona.amethyst.commons.model.nip47WalletConnect.NwcWalletEntryNorm
|
||||
@@ -36,9 +38,9 @@ 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.UiSettings
|
||||
import com.vitorpamplona.amethyst.model.backups.BackupConflictStorage
|
||||
import com.vitorpamplona.amethyst.model.nip60Cashu.CashuPreferences
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
|
||||
import com.vitorpamplona.quartz.concord.cord02Community.ConcordCommunityListEvent
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent
|
||||
import com.vitorpamplona.quartz.experimental.nipA3.PaymentTargetsEvent
|
||||
@@ -158,6 +160,7 @@ private object PrefKeys {
|
||||
const val DEFAULT_NWC_WALLET_ID = "defaultNwcWalletId" // legacy, migrated into DEFAULT_PAYMENT_SOURCE_ID
|
||||
const val CLINK_DEBIT_WALLETS = "clinkDebitWallets"
|
||||
const val DEFAULT_PAYMENT_SOURCE_ID = "defaultPaymentSourceId"
|
||||
const val OPEN_BACKUP_CONFLICTS = "openBackupConflicts"
|
||||
const val LATEST_USER_METADATA = "latestUserMetadata"
|
||||
const val LATEST_CONTACT_LIST = "latestContactList"
|
||||
const val LATEST_DM_RELAY_LIST = "latestDMRelayList"
|
||||
@@ -581,6 +584,16 @@ object LocalPreferences {
|
||||
|
||||
putOrRemove(PrefKeys.LATEST_CONTACT_LIST, settings.backupContactList)
|
||||
|
||||
// The undecided conflicts themselves, not just the backups they hold back.
|
||||
// Without these the card vanishes on the next launch and the user never
|
||||
// answers the question the backup is still waiting on.
|
||||
val openConflicts = settings.openBackupConflicts()
|
||||
if (openConflicts.isEmpty()) {
|
||||
remove(PrefKeys.OPEN_BACKUP_CONFLICTS)
|
||||
} else {
|
||||
putString(PrefKeys.OPEN_BACKUP_CONFLICTS, BackupConflictStorage.encode(openConflicts))
|
||||
}
|
||||
|
||||
putOrRemove(PrefKeys.LATEST_USER_METADATA, settings.backupUserMetadata)
|
||||
putOrRemove(PrefKeys.LATEST_DM_RELAY_LIST, settings.backupDMRelayList)
|
||||
putOrRemove(PrefKeys.LATEST_NIP65_RELAY_LIST, settings.backupNIP65RelayList)
|
||||
@@ -807,6 +820,7 @@ object LocalPreferences {
|
||||
val defaultFileServerStr = getString(PrefKeys.DEFAULT_FILE_SERVER, null)
|
||||
|
||||
val pendingAttestationsStr = getString(PrefKeys.PENDING_ATTESTATIONS, null)
|
||||
val openBackupConflictsStr = getString(PrefKeys.OPEN_BACKUP_CONFLICTS, null)
|
||||
val latestUserMetadataStr = getString(PrefKeys.LATEST_USER_METADATA, null)
|
||||
val latestContactListStr = getString(PrefKeys.LATEST_CONTACT_LIST, null)
|
||||
val latestDmRelayListStr = getString(PrefKeys.LATEST_DM_RELAY_LIST, null)
|
||||
@@ -959,6 +973,7 @@ object LocalPreferences {
|
||||
return@with AccountSettings(
|
||||
keyPair = keyPair,
|
||||
transientAccount = false,
|
||||
cashuCounters = CashuPreferences.forAccount(keyPair.pubKey.toNpub()),
|
||||
externalSignerPackageName = externalSignerPackageName,
|
||||
localRelayServers = MutableStateFlow(localRelayServers),
|
||||
defaultFileServer = defaultFileServerResolved,
|
||||
@@ -1062,7 +1077,7 @@ object LocalPreferences {
|
||||
backupCashuWallet = latestCashuWalletResolved,
|
||||
backupNutzapInfo = latestNutzapInfoResolved,
|
||||
callsEnabled = MutableStateFlow(callsEnabled),
|
||||
)
|
||||
).also { it.restoreBackupConflicts(BackupConflictStorage.decode(openBackupConflictsStr)) }
|
||||
}
|
||||
}
|
||||
// Milestone with its cost attached. Decrypting and parsing one account's settings is one of
|
||||
|
||||
+1
-1
@@ -76,7 +76,7 @@ import com.vitorpamplona.amethyst.commons.resources.napplet_policy_paranoid_desc
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_policy_reasonable
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_policy_reasonable_desc
|
||||
import com.vitorpamplona.amethyst.commons.resources.nip46_connect_requests_title
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.AmethystTheme
|
||||
|
||||
class SignerConnectActivity : ComponentActivity() {
|
||||
|
||||
+3
-3
@@ -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.ui.navigation.navs.EmptyNav
|
||||
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.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||
import com.vitorpamplona.amethyst.ui.pluralStringRes
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.AmethystTheme
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.rumors.RumorAssembler
|
||||
|
||||
@@ -39,6 +39,8 @@ import com.vitorpamplona.amethyst.commons.model.IAccount
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.model.User
|
||||
import com.vitorpamplona.amethyst.commons.model.VideoPostKind
|
||||
import com.vitorpamplona.amethyst.commons.model.backups.BackupRestore
|
||||
import com.vitorpamplona.amethyst.commons.model.backups.ReplaceableBackupConflict
|
||||
import com.vitorpamplona.amethyst.commons.model.buzz.BuzzChannelStars
|
||||
import com.vitorpamplona.amethyst.commons.model.buzz.BuzzHeldAttestations
|
||||
import com.vitorpamplona.amethyst.commons.model.buzz.BuzzRelayDialect
|
||||
@@ -52,6 +54,8 @@ import com.vitorpamplona.amethyst.commons.model.edits.PrivateStorageRelayListDec
|
||||
import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel
|
||||
import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatListState
|
||||
import com.vitorpamplona.amethyst.commons.model.navigation.BottomBarEntry
|
||||
import com.vitorpamplona.amethyst.commons.model.navigation.NavBarItem
|
||||
import com.vitorpamplona.amethyst.commons.model.nip18Reposts.RepostAction
|
||||
import com.vitorpamplona.amethyst.commons.model.nip25Reactions.ReactionAction
|
||||
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
|
||||
@@ -66,16 +70,27 @@ import com.vitorpamplona.amethyst.commons.model.nip47WalletConnect.NwcInfoCache
|
||||
import com.vitorpamplona.amethyst.commons.model.nip51Lists.BookmarkListState
|
||||
import com.vitorpamplona.amethyst.commons.model.nip51Lists.GitRepositoryListState
|
||||
import com.vitorpamplona.amethyst.commons.model.nip51Lists.OldBookmarkListState
|
||||
import com.vitorpamplona.amethyst.commons.model.nip51Lists.PinListState
|
||||
import com.vitorpamplona.amethyst.commons.model.nip51Lists.blockPeopleList.BlockPeopleListState
|
||||
import com.vitorpamplona.amethyst.commons.model.nip51Lists.blockedRelays.BlockedRelayListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.commons.model.nip51Lists.broadcastRelays.BroadcastRelayListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.commons.model.nip51Lists.favoriteAlgoFeedsLists.FavoriteAlgoFeedsListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.commons.model.nip51Lists.geohashLists.GeohashListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.commons.model.nip51Lists.hashtagLists.HashtagListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.commons.model.nip51Lists.indexerRelays.IndexerRelayListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.commons.model.nip51Lists.muteList.MuteListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.commons.model.nip51Lists.peopleList.PeopleListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.commons.model.nip51Lists.proxyRelays.ProxyRelayListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.commons.model.nip51Lists.relayFeeds.RelayFeedsListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.commons.model.nip51Lists.searchRelays.SearchRelayListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.commons.model.nip51Lists.trustedRelays.TrustedRelayListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.commons.model.nip56Reports.ReportAction
|
||||
import com.vitorpamplona.amethyst.commons.model.nip62Vanish.VanishRequestsState
|
||||
import com.vitorpamplona.amethyst.commons.model.nip72Communities.CommunityListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.commons.model.nip85TrustedAssertions.ContactCardDecryptionCache
|
||||
import com.vitorpamplona.amethyst.commons.model.nip85TrustedAssertions.ContactCardsState
|
||||
import com.vitorpamplona.amethyst.commons.model.nip85TrustedAssertions.TrustProviderListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.commons.model.nipBCOnchainZaps.OnchainWalletState
|
||||
import com.vitorpamplona.amethyst.commons.model.privateChatLastReadRoute
|
||||
import com.vitorpamplona.amethyst.commons.model.privateChats.hasEncryptedContent
|
||||
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.FeedDecryptionCaches
|
||||
@@ -103,9 +118,6 @@ import com.vitorpamplona.amethyst.commons.service.pow.PoWPublishQueue
|
||||
import com.vitorpamplona.amethyst.commons.service.pow.PoWReplay
|
||||
import com.vitorpamplona.amethyst.commons.viewmodels.ReplyMode
|
||||
import com.vitorpamplona.amethyst.logTime
|
||||
import com.vitorpamplona.amethyst.model.AccountMarmotActions
|
||||
import com.vitorpamplona.amethyst.model.AccountRelayGroupActions
|
||||
import com.vitorpamplona.amethyst.model.EventBroadcaster
|
||||
import com.vitorpamplona.amethyst.model.algoFeeds.FavoriteAlgoFeedsOrchestrator
|
||||
import com.vitorpamplona.amethyst.model.bolt12Offers.Bolt12OfferListState
|
||||
import com.vitorpamplona.amethyst.model.buzz.ChannelInvitesState
|
||||
@@ -132,38 +144,27 @@ import com.vitorpamplona.amethyst.model.nip30CustomEmojis.OwnedEmojiPacksState
|
||||
import com.vitorpamplona.amethyst.model.nip46Signer.Nip46SignerState
|
||||
import com.vitorpamplona.amethyst.model.nip47WalletConnect.NwcSignerState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.PinListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.blockPeopleList.BlockPeopleListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.broadcastRelays.BroadcastRelayListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.broadcastRelays.BroadcastRelayListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.favoriteAlgoFeedsLists.FavoriteAlgoFeedsListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.geohashLists.GeohashListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.hashtagLists.HashtagListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.indexerRelays.IndexerRelayListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.indexerRelays.IndexerRelayListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.interestSets.InterestSetsState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.labeledBookmarkLists.LabeledBookmarkListsState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.muteList.MuteListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.FollowListsState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.PeopleListsState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.proxyRelays.ProxyRelayListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.proxyRelays.ProxyRelayListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.relayFeeds.RelayFeedListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.relayFeeds.RelayFeedsListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.searchRelays.SearchRelayListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.searchRelays.SearchRelayListState
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.trustedRelays.TrustedRelayListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.model.nip51Lists.trustedRelays.TrustedRelayListState
|
||||
import com.vitorpamplona.amethyst.model.nip62Vanish.VanishRequestsState
|
||||
import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState
|
||||
import com.vitorpamplona.amethyst.model.nip72Communities.CommunityListState
|
||||
import com.vitorpamplona.amethyst.model.nip78AppSpecific.AppSpecificState
|
||||
import com.vitorpamplona.amethyst.model.nip89AppHandlers.AppRecommendationsState
|
||||
import com.vitorpamplona.amethyst.model.nipA3PaymentTargets.NipA3PaymentTargetsState
|
||||
import com.vitorpamplona.amethyst.model.nipB7Blossom.BlossomServerListState
|
||||
import com.vitorpamplona.amethyst.model.nipBCOnchainZaps.OnchainWalletState
|
||||
import com.vitorpamplona.amethyst.model.serverList.AssumedRelayListsState
|
||||
import com.vitorpamplona.amethyst.model.serverList.MergedFollowListsState
|
||||
import com.vitorpamplona.amethyst.model.serverList.MergedFollowPlusMineRelayListsState
|
||||
@@ -175,8 +176,6 @@ import com.vitorpamplona.amethyst.model.trustedAssertions.TrustProviderListState
|
||||
import com.vitorpamplona.amethyst.service.location.LocationState
|
||||
import com.vitorpamplona.amethyst.service.uploads.FileHeader
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger
|
||||
import com.vitorpamplona.amethyst.ui.navigation.bottombars.BottomBarEntry
|
||||
import com.vitorpamplona.amethyst.ui.navigation.bottombars.NavBarItem
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.EventProcessor
|
||||
import com.vitorpamplona.marmotquic.QuicAgentTextStreamTransport
|
||||
import com.vitorpamplona.quartz.buzz.threading.buzzThread
|
||||
@@ -859,7 +858,7 @@ class Account(
|
||||
* relay subscription only runs while at least one opener is active.
|
||||
*/
|
||||
val cashuMintDirectoryState =
|
||||
com.vitorpamplona.amethyst.model.nip60Cashu.CashuMintDirectoryState(
|
||||
com.vitorpamplona.amethyst.commons.model.nip60Cashu.CashuMintDirectoryState(
|
||||
cache = cache,
|
||||
scope = scope,
|
||||
assembler = cashuMintDirectoryFilterAssembler(),
|
||||
@@ -1457,6 +1456,9 @@ class Account(
|
||||
// wraps still register with the delivery-ticks tracker at publish time.
|
||||
// Null for restart-restored jobs, whose rumor id wasn't persisted.
|
||||
displayedNoteId: HexKey? = null,
|
||||
// Runs once the mined wraps are handed to the relays (e.g. to drop the
|
||||
// composer's draft). Not persisted: a restart-restored job skips it.
|
||||
onPublished: suspend () -> Unit = {},
|
||||
): Boolean {
|
||||
val queue = powQueue() ?: return false
|
||||
if (seals.isEmpty()) return true
|
||||
@@ -1491,7 +1493,10 @@ class Account(
|
||||
}
|
||||
seals.map { NIP17Factory().wrapSeal(it, expirationDelta, templateConversion = mineWrap) }
|
||||
},
|
||||
publish = { wraps -> broadcastPrivately(wraps, displayedNoteId) },
|
||||
publish = { wraps ->
|
||||
broadcastPrivately(wraps, displayedNoteId)
|
||||
onPublished()
|
||||
},
|
||||
)
|
||||
return true
|
||||
}
|
||||
@@ -1917,6 +1922,40 @@ class Account(
|
||||
|
||||
fun sendLiterallyEverywhere(event: Event) = broadcaster.sendLiterallyEverywhere(event)
|
||||
|
||||
/**
|
||||
* Resolves a [ReplaceableBackupConflict] in favor of this device: re-signs the saved
|
||||
* version (same kind, tags and content, so NIP-44 private items stay readable, minus the
|
||||
* old client, proof-of-work and past expiration tags; see [BackupRestore.tagsToResign])
|
||||
* with a timestamp newer than the current one, and publishes it so it replaces that version.
|
||||
*
|
||||
* Does nothing if the conflict is stale (already resolved, replaced by a newer one, or
|
||||
* dropped because the event was deleted), which also makes a double tap harmless.
|
||||
*/
|
||||
suspend fun restoreBackupOver(conflict: ReplaceableBackupConflict) {
|
||||
val token = settings.backupGuard.startRestoring(conflict) ?: return
|
||||
var restored = false
|
||||
try {
|
||||
val saved = conflict.saved
|
||||
val now = TimeUtils.now()
|
||||
// Newer than the version it replaces, but never far in the future: relays reject
|
||||
// those, and later edits signed at "now" would lose to it. A far-future external
|
||||
// version can't be outranked safely, and a restore signed at "now" would lose to it
|
||||
// everywhere, so it isn't sent: the conflict reopens and only keeping it works.
|
||||
val createdAt = (conflict.incoming.createdAt + 1).coerceAtLeast(now)
|
||||
if (createdAt > now + maxRestoreFutureSeconds) return
|
||||
val resigned = signer.sign<Event>(createdAt, saved.kind, BackupRestore.tagsToResign(saved, now), saved.content)
|
||||
broadcaster.sendRestoredVersion(resigned)
|
||||
restored = true
|
||||
} finally {
|
||||
settings.backupGuard.finishRestoring(conflict, token, restored)
|
||||
}
|
||||
}
|
||||
|
||||
private val maxRestoreFutureSeconds = 15 * 60L
|
||||
|
||||
/** Resolves a [ReplaceableBackupConflict] in favor of the current version. No-op when stale. */
|
||||
fun acceptExternalVersion(conflict: ReplaceableBackupConflict) = settings.backupGuard.keepIncoming(conflict)
|
||||
|
||||
suspend fun <T : Event> signAndSendPrivately(
|
||||
template: EventTemplate<T>,
|
||||
relayList: Set<NormalizedRelayUrl>,
|
||||
@@ -2939,13 +2978,24 @@ class Account(
|
||||
broadcastPrivately(NIP17Factory().createEncryptedFileNIP17(template, signer))
|
||||
}
|
||||
|
||||
override suspend fun sendNip17PrivateMessage(template: EventTemplate<ChatMessageEvent>) {
|
||||
override suspend fun sendNip17PrivateMessage(template: EventTemplate<ChatMessageEvent>) = sendNip17PrivateMessage(template) {}
|
||||
|
||||
/**
|
||||
* [onSent] runs once the wraps are handed to the relays — right away when
|
||||
* the message is not mined, after the nonce search when it is — so the
|
||||
* composer can keep its draft for as long as the message only exists in
|
||||
* the mining queue.
|
||||
*/
|
||||
suspend fun sendNip17PrivateMessage(
|
||||
template: EventTemplate<ChatMessageEvent>,
|
||||
onSent: suspend () -> Unit,
|
||||
) {
|
||||
val powDifficulty = powDifficultyFor(GiftWrapEvent.KIND)
|
||||
if (powDifficulty != null) {
|
||||
// See sendNip17EncryptedFile: sign inline, queue only wrap mining.
|
||||
val senderMessage = signer.sign(template)
|
||||
val seals = NIP17Factory().createSeals(senderMessage, senderMessage.groupMembers(), signer)
|
||||
if (mineWrapsInBackground(seals.seals, seals.expirationDelta, powDifficulty, displayedNoteId = senderMessage.id)) {
|
||||
if (mineWrapsInBackground(seals.seals, seals.expirationDelta, powDifficulty, displayedNoteId = senderMessage.id, onPublished = onSent)) {
|
||||
// The wraps publish only after mining, but the user has already
|
||||
// replied — advance the read marker now.
|
||||
markDmRoomAsRead(senderMessage)
|
||||
@@ -2954,6 +3004,7 @@ class Account(
|
||||
}
|
||||
|
||||
broadcastPrivately(NIP17Factory().createMessageNIP17(template, signer))
|
||||
onSent()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2965,10 +3016,13 @@ class Account(
|
||||
*
|
||||
* [powOverrideDifficulty] is the composer chip's per-post override:
|
||||
* null follows the account's gift-wrap setting, 0 disables mining.
|
||||
* [onSent] runs once the wraps are handed to the relays (after mining,
|
||||
* when mined).
|
||||
*/
|
||||
suspend fun sendPrivateNote(
|
||||
template: EventTemplate<TextNoteEvent>,
|
||||
powOverrideDifficulty: Int? = null,
|
||||
onSent: suspend () -> Unit = {},
|
||||
) {
|
||||
if (!isWriteable()) return
|
||||
|
||||
@@ -2978,10 +3032,11 @@ class Account(
|
||||
val senderNote = signer.sign(template)
|
||||
val recipients = senderNote.taggedUserIds().plus(signer.pubKey).toSet()
|
||||
val seals = NIP17Factory().createSeals(senderNote, recipients, signer)
|
||||
if (mineWrapsInBackground(seals.seals, seals.expirationDelta, powDifficulty, displayedNoteId = senderNote.id)) return
|
||||
if (mineWrapsInBackground(seals.seals, seals.expirationDelta, powDifficulty, displayedNoteId = senderNote.id, onPublished = onSent)) return
|
||||
}
|
||||
|
||||
broadcastPrivately(NIP17Factory().createNoteNIP17(template, signer))
|
||||
onSent()
|
||||
}
|
||||
|
||||
override suspend fun sendGiftWraps(wraps: List<GiftWrapEvent>) {
|
||||
|
||||
@@ -22,14 +22,23 @@ package com.vitorpamplona.amethyst.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.commons.audio.VisualizerStyle
|
||||
import com.vitorpamplona.amethyst.commons.cashu.CashuKeysetCounterStore
|
||||
import com.vitorpamplona.amethyst.commons.cashu.UnavailableCashuKeysetCounterStore
|
||||
import com.vitorpamplona.amethyst.commons.model.HomeFeedType
|
||||
import com.vitorpamplona.amethyst.commons.model.backups.BackupConflictGuard
|
||||
import com.vitorpamplona.amethyst.commons.model.backups.backupSlotOf
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.filter
|
||||
import com.vitorpamplona.amethyst.commons.model.chats.ChatFeedType
|
||||
import com.vitorpamplona.amethyst.commons.model.clink.ClinkDebitWalletEntryNorm
|
||||
import com.vitorpamplona.amethyst.commons.model.concord.ConcordListRepository
|
||||
import com.vitorpamplona.amethyst.commons.model.concord.ConcordViewMode
|
||||
import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatRepository
|
||||
import com.vitorpamplona.amethyst.commons.model.mediaServers.DEFAULT_MEDIA_SERVERS
|
||||
import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName
|
||||
import com.vitorpamplona.amethyst.commons.model.mergeMutedPublicChats
|
||||
import com.vitorpamplona.amethyst.commons.model.navigation.BottomBarEntry
|
||||
import com.vitorpamplona.amethyst.commons.model.navigation.DrawerItemVisibility
|
||||
import com.vitorpamplona.amethyst.commons.model.navigation.NavBarItem
|
||||
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatListRepository
|
||||
import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupRepository
|
||||
import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupViewMode
|
||||
@@ -39,25 +48,18 @@ import com.vitorpamplona.amethyst.commons.model.payments.PaymentSourceResolver
|
||||
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter
|
||||
import com.vitorpamplona.amethyst.commons.relayauth.RelayAuthPolicy
|
||||
import com.vitorpamplona.amethyst.commons.service.pow.PoWCategory
|
||||
import com.vitorpamplona.amethyst.model.nip60Cashu.CashuPreferences
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
|
||||
import com.vitorpamplona.amethyst.ui.navigation.bottombars.BottomBarEntry
|
||||
import com.vitorpamplona.amethyst.ui.navigation.bottombars.NavBarItem
|
||||
import com.vitorpamplona.amethyst.ui.navigation.drawer.DrawerItemVisibility
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedDefinition
|
||||
import com.vitorpamplona.quartz.concord.cord02Community.ConcordCommunityListEvent
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent
|
||||
import com.vitorpamplona.quartz.experimental.nipA3.PaymentTargetsEvent
|
||||
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
|
||||
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
|
||||
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip19Bech32.toNpub
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
||||
import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent
|
||||
@@ -236,6 +238,14 @@ class AccountSettings(
|
||||
* keysets is fine because the derivation includes the keyset id.
|
||||
*/
|
||||
var cashuKeysetCounters: MutableMap<String, Long> = mutableMapOf(),
|
||||
/**
|
||||
* Durable NUT-13 counter store for this account, supplied by the host.
|
||||
*
|
||||
* Defaulted to the failing stand-in rather than an in-memory map: the preview
|
||||
* and test factories below never mint, and a host that does mint must wire a
|
||||
* real store or hear about it on the first reservation.
|
||||
*/
|
||||
val cashuCounters: CashuKeysetCounterStore = UnavailableCashuKeysetCounterStore,
|
||||
val lastReadPerRoute: MutableStateFlow<Map<String, MutableStateFlow<Long>>> = MutableStateFlow(mapOf()),
|
||||
val hasDonatedInVersion: MutableStateFlow<Set<String>> = MutableStateFlow(setOf()),
|
||||
val dismissedPollNoteIds: MutableStateFlow<Set<String>> = MutableStateFlow(setOf()),
|
||||
@@ -769,10 +779,6 @@ class AccountSettings(
|
||||
if (changed) saveAccountSettings()
|
||||
}
|
||||
|
||||
fun changeDefaultHomeFollowList(name: FeedDefinition) {
|
||||
changeDefaultHomeFollowList(name.code)
|
||||
}
|
||||
|
||||
fun changeDefaultHomeFollowList(name: TopFilter) {
|
||||
if (defaultHomeFollowList.value != name) {
|
||||
defaultHomeFollowList.tryEmit(name)
|
||||
@@ -780,10 +786,6 @@ class AccountSettings(
|
||||
}
|
||||
}
|
||||
|
||||
fun changeDefaultStoriesFollowList(name: FeedDefinition) {
|
||||
changeDefaultStoriesFollowList(name.code)
|
||||
}
|
||||
|
||||
fun changeDefaultStoriesFollowList(name: TopFilter) {
|
||||
if (defaultStoriesFollowList.value != name) {
|
||||
defaultStoriesFollowList.tryEmit(name)
|
||||
@@ -791,10 +793,6 @@ class AccountSettings(
|
||||
}
|
||||
}
|
||||
|
||||
fun changeDefaultNotificationFollowList(name: FeedDefinition) {
|
||||
changeDefaultNotificationFollowList(name.code)
|
||||
}
|
||||
|
||||
fun changeDefaultNotificationFollowList(name: TopFilter) {
|
||||
if (defaultNotificationFollowList.value != name) {
|
||||
defaultNotificationFollowList.tryEmit(name)
|
||||
@@ -802,10 +800,6 @@ class AccountSettings(
|
||||
}
|
||||
}
|
||||
|
||||
fun changeDefaultDiscoveryFollowList(name: FeedDefinition) {
|
||||
changeDefaultDiscoveryFollowList(name.code)
|
||||
}
|
||||
|
||||
fun changeDefaultDiscoveryFollowList(name: TopFilter) {
|
||||
if (defaultDiscoveryFollowList.value != name) {
|
||||
defaultDiscoveryFollowList.tryEmit(name)
|
||||
@@ -813,10 +807,6 @@ class AccountSettings(
|
||||
}
|
||||
}
|
||||
|
||||
fun changeDefaultPollsFollowList(name: FeedDefinition) {
|
||||
changeDefaultPollsFollowList(name.code)
|
||||
}
|
||||
|
||||
fun changeDefaultPollsFollowList(name: TopFilter) {
|
||||
if (defaultPollsFollowList.value != name) {
|
||||
defaultPollsFollowList.tryEmit(name)
|
||||
@@ -824,10 +814,6 @@ class AccountSettings(
|
||||
}
|
||||
}
|
||||
|
||||
fun changeDefaultCommunitiesFollowList(name: FeedDefinition) {
|
||||
changeDefaultCommunitiesFollowList(name.code)
|
||||
}
|
||||
|
||||
fun changeDefaultCommunitiesFollowList(name: TopFilter) {
|
||||
if (defaultCommunitiesFollowList.value != name) {
|
||||
defaultCommunitiesFollowList.tryEmit(name)
|
||||
@@ -835,10 +821,6 @@ class AccountSettings(
|
||||
}
|
||||
}
|
||||
|
||||
fun changeDefaultPicturesFollowList(name: FeedDefinition) {
|
||||
changeDefaultPicturesFollowList(name.code)
|
||||
}
|
||||
|
||||
fun changeDefaultPicturesFollowList(name: TopFilter) {
|
||||
if (defaultPicturesFollowList.value != name) {
|
||||
defaultPicturesFollowList.tryEmit(name)
|
||||
@@ -846,10 +828,6 @@ class AccountSettings(
|
||||
}
|
||||
}
|
||||
|
||||
fun changeDefaultRelayGroupsDiscoveryFollowList(name: FeedDefinition) {
|
||||
changeDefaultRelayGroupsDiscoveryFollowList(name.code)
|
||||
}
|
||||
|
||||
fun changeDefaultRelayGroupsDiscoveryFollowList(name: TopFilter) {
|
||||
if (defaultRelayGroupsDiscoveryFollowList.value != name) {
|
||||
defaultRelayGroupsDiscoveryFollowList.tryEmit(name)
|
||||
@@ -857,10 +835,6 @@ class AccountSettings(
|
||||
}
|
||||
}
|
||||
|
||||
fun changeDefaultNappletsFollowList(name: FeedDefinition) {
|
||||
changeDefaultNappletsFollowList(name.code)
|
||||
}
|
||||
|
||||
fun changeDefaultNappletsFollowList(name: TopFilter) {
|
||||
if (defaultNappletsFollowList.value != name) {
|
||||
defaultNappletsFollowList.tryEmit(name)
|
||||
@@ -868,10 +842,6 @@ class AccountSettings(
|
||||
}
|
||||
}
|
||||
|
||||
fun changeDefaultNsitesFollowList(name: FeedDefinition) {
|
||||
changeDefaultNsitesFollowList(name.code)
|
||||
}
|
||||
|
||||
fun changeDefaultNsitesFollowList(name: TopFilter) {
|
||||
if (defaultNsitesFollowList.value != name) {
|
||||
defaultNsitesFollowList.tryEmit(name)
|
||||
@@ -879,10 +849,6 @@ class AccountSettings(
|
||||
}
|
||||
}
|
||||
|
||||
fun changeDefaultWorkoutsFollowList(name: FeedDefinition) {
|
||||
changeDefaultWorkoutsFollowList(name.code)
|
||||
}
|
||||
|
||||
fun changeDefaultWorkoutsFollowList(name: TopFilter) {
|
||||
if (defaultWorkoutsFollowList.value != name) {
|
||||
defaultWorkoutsFollowList.tryEmit(name)
|
||||
@@ -890,10 +856,6 @@ class AccountSettings(
|
||||
}
|
||||
}
|
||||
|
||||
fun changeDefaultGitRepositoriesFollowList(name: FeedDefinition) {
|
||||
changeDefaultGitRepositoriesFollowList(name.code)
|
||||
}
|
||||
|
||||
fun changeDefaultGitRepositoriesFollowList(name: TopFilter) {
|
||||
if (defaultGitRepositoriesFollowList.value != name) {
|
||||
defaultGitRepositoriesFollowList.tryEmit(name)
|
||||
@@ -901,10 +863,6 @@ class AccountSettings(
|
||||
}
|
||||
}
|
||||
|
||||
fun changeDefaultHighlightsFollowList(name: FeedDefinition) {
|
||||
changeDefaultHighlightsFollowList(name.code)
|
||||
}
|
||||
|
||||
fun changeDefaultHighlightsFollowList(name: TopFilter) {
|
||||
if (defaultHighlightsFollowList.value != name) {
|
||||
defaultHighlightsFollowList.tryEmit(name)
|
||||
@@ -912,10 +870,6 @@ class AccountSettings(
|
||||
}
|
||||
}
|
||||
|
||||
fun changeDefaultCalendarsFollowList(name: FeedDefinition) {
|
||||
changeDefaultCalendarsFollowList(name.code)
|
||||
}
|
||||
|
||||
fun changeDefaultCalendarsFollowList(name: TopFilter) {
|
||||
if (defaultCalendarsFollowList.value != name) {
|
||||
defaultCalendarsFollowList.tryEmit(name)
|
||||
@@ -923,10 +877,6 @@ class AccountSettings(
|
||||
}
|
||||
}
|
||||
|
||||
fun changeDefaultProductsFollowList(name: FeedDefinition) {
|
||||
changeDefaultProductsFollowList(name.code)
|
||||
}
|
||||
|
||||
fun changeDefaultProductsFollowList(name: TopFilter) {
|
||||
if (defaultProductsFollowList.value != name) {
|
||||
defaultProductsFollowList.tryEmit(name)
|
||||
@@ -934,10 +884,6 @@ class AccountSettings(
|
||||
}
|
||||
}
|
||||
|
||||
fun changeDefaultGeocachesFollowList(name: FeedDefinition) {
|
||||
changeDefaultGeocachesFollowList(name.code)
|
||||
}
|
||||
|
||||
fun changeDefaultGeocachesFollowList(name: TopFilter) {
|
||||
if (defaultGeocachesFollowList.value != name) {
|
||||
defaultGeocachesFollowList.tryEmit(name)
|
||||
@@ -945,10 +891,6 @@ class AccountSettings(
|
||||
}
|
||||
}
|
||||
|
||||
fun changeDefaultShortsFollowList(name: FeedDefinition) {
|
||||
changeDefaultShortsFollowList(name.code)
|
||||
}
|
||||
|
||||
fun changeDefaultShortsFollowList(name: TopFilter) {
|
||||
if (defaultShortsFollowList.value != name) {
|
||||
defaultShortsFollowList.tryEmit(name)
|
||||
@@ -956,10 +898,6 @@ class AccountSettings(
|
||||
}
|
||||
}
|
||||
|
||||
fun changeDefaultPublicChatsFollowList(name: FeedDefinition) {
|
||||
changeDefaultPublicChatsFollowList(name.code)
|
||||
}
|
||||
|
||||
fun changeDefaultPublicChatsFollowList(name: TopFilter) {
|
||||
if (defaultPublicChatsFollowList.value != name) {
|
||||
defaultPublicChatsFollowList.tryEmit(name)
|
||||
@@ -967,10 +905,6 @@ class AccountSettings(
|
||||
}
|
||||
}
|
||||
|
||||
fun changeDefaultLiveStreamsFollowList(name: FeedDefinition) {
|
||||
changeDefaultLiveStreamsFollowList(name.code)
|
||||
}
|
||||
|
||||
fun changeDefaultLiveStreamsFollowList(name: TopFilter) {
|
||||
if (defaultLiveStreamsFollowList.value != name) {
|
||||
defaultLiveStreamsFollowList.tryEmit(name)
|
||||
@@ -978,10 +912,6 @@ class AccountSettings(
|
||||
}
|
||||
}
|
||||
|
||||
fun changeDefaultNestsFollowList(name: FeedDefinition) {
|
||||
changeDefaultNestsFollowList(name.code)
|
||||
}
|
||||
|
||||
fun changeDefaultNestsFollowList(name: TopFilter) {
|
||||
if (defaultNestsFollowList.value != name) {
|
||||
defaultNestsFollowList.tryEmit(name)
|
||||
@@ -989,10 +919,6 @@ class AccountSettings(
|
||||
}
|
||||
}
|
||||
|
||||
fun changeDefaultLongsFollowList(name: FeedDefinition) {
|
||||
changeDefaultLongsFollowList(name.code)
|
||||
}
|
||||
|
||||
fun changeDefaultLongsFollowList(name: TopFilter) {
|
||||
if (defaultLongsFollowList.value != name) {
|
||||
defaultLongsFollowList.tryEmit(name)
|
||||
@@ -1000,10 +926,6 @@ class AccountSettings(
|
||||
}
|
||||
}
|
||||
|
||||
fun changeDefaultArticlesFollowList(name: FeedDefinition) {
|
||||
changeDefaultArticlesFollowList(name.code)
|
||||
}
|
||||
|
||||
fun changeDefaultArticlesFollowList(name: TopFilter) {
|
||||
if (defaultArticlesFollowList.value != name) {
|
||||
defaultArticlesFollowList.tryEmit(name)
|
||||
@@ -1011,10 +933,6 @@ class AccountSettings(
|
||||
}
|
||||
}
|
||||
|
||||
fun changeDefaultMusicTracksFollowList(name: FeedDefinition) {
|
||||
changeDefaultMusicTracksFollowList(name.code)
|
||||
}
|
||||
|
||||
fun changeDefaultMusicTracksFollowList(name: TopFilter) {
|
||||
if (defaultMusicTracksFollowList.value != name) {
|
||||
defaultMusicTracksFollowList.tryEmit(name)
|
||||
@@ -1022,10 +940,6 @@ class AccountSettings(
|
||||
}
|
||||
}
|
||||
|
||||
fun changeDefaultMusicPlaylistsFollowList(name: FeedDefinition) {
|
||||
changeDefaultMusicPlaylistsFollowList(name.code)
|
||||
}
|
||||
|
||||
fun changeDefaultMusicPlaylistsFollowList(name: TopFilter) {
|
||||
if (defaultMusicPlaylistsFollowList.value != name) {
|
||||
defaultMusicPlaylistsFollowList.tryEmit(name)
|
||||
@@ -1033,10 +947,6 @@ class AccountSettings(
|
||||
}
|
||||
}
|
||||
|
||||
fun changeDefaultPodcastEpisodesFollowList(name: FeedDefinition) {
|
||||
changeDefaultPodcastEpisodesFollowList(name.code)
|
||||
}
|
||||
|
||||
fun changeDefaultPodcastEpisodesFollowList(name: TopFilter) {
|
||||
if (defaultPodcastEpisodesFollowList.value != name) {
|
||||
defaultPodcastEpisodesFollowList.tryEmit(name)
|
||||
@@ -1044,10 +954,6 @@ class AccountSettings(
|
||||
}
|
||||
}
|
||||
|
||||
fun changeDefaultPodcastsFollowList(name: FeedDefinition) {
|
||||
changeDefaultPodcastsFollowList(name.code)
|
||||
}
|
||||
|
||||
fun changeDefaultPodcastsFollowList(name: TopFilter) {
|
||||
if (defaultPodcastsFollowList.value != name) {
|
||||
defaultPodcastsFollowList.tryEmit(name)
|
||||
@@ -1055,10 +961,6 @@ class AccountSettings(
|
||||
}
|
||||
}
|
||||
|
||||
fun changeDefaultSoftwareAppsFollowList(name: FeedDefinition) {
|
||||
changeDefaultSoftwareAppsFollowList(name.code)
|
||||
}
|
||||
|
||||
fun changeDefaultSoftwareAppsFollowList(name: TopFilter) {
|
||||
if (defaultSoftwareAppsFollowList.value != name) {
|
||||
defaultSoftwareAppsFollowList.tryEmit(name)
|
||||
@@ -1066,10 +968,6 @@ class AccountSettings(
|
||||
}
|
||||
}
|
||||
|
||||
fun changeDefaultBadgesFollowList(name: FeedDefinition) {
|
||||
changeDefaultBadgesFollowList(name.code)
|
||||
}
|
||||
|
||||
fun changeDefaultBadgesFollowList(name: TopFilter) {
|
||||
if (defaultBadgesFollowList.value != name) {
|
||||
defaultBadgesFollowList.tryEmit(name)
|
||||
@@ -1077,10 +975,6 @@ class AccountSettings(
|
||||
}
|
||||
}
|
||||
|
||||
fun changeDefaultBrowseEmojiSetsFollowList(name: FeedDefinition) {
|
||||
changeDefaultBrowseEmojiSetsFollowList(name.code)
|
||||
}
|
||||
|
||||
fun changeDefaultBrowseEmojiSetsFollowList(name: TopFilter) {
|
||||
if (defaultBrowseEmojiSetsFollowList.value != name) {
|
||||
defaultBrowseEmojiSetsFollowList.tryEmit(name)
|
||||
@@ -1088,10 +982,6 @@ class AccountSettings(
|
||||
}
|
||||
}
|
||||
|
||||
fun changeDefaultFollowPacksFollowList(name: FeedDefinition) {
|
||||
changeDefaultFollowPacksFollowList(name.code)
|
||||
}
|
||||
|
||||
fun changeDefaultFollowPacksFollowList(name: TopFilter) {
|
||||
if (defaultFollowPacksFollowList.value != name) {
|
||||
defaultFollowPacksFollowList.tryEmit(name)
|
||||
@@ -1099,10 +989,6 @@ class AccountSettings(
|
||||
}
|
||||
}
|
||||
|
||||
fun changeDefaultAppRecommendationsFollowList(name: FeedDefinition) {
|
||||
changeDefaultAppRecommendationsFollowList(name.code)
|
||||
}
|
||||
|
||||
fun changeDefaultAppRecommendationsFollowList(name: TopFilter) {
|
||||
if (defaultAppRecommendationsFollowList.value != name) {
|
||||
defaultAppRecommendationsFollowList.tryEmit(name)
|
||||
@@ -1158,6 +1044,69 @@ class AccountSettings(
|
||||
// Backup Lists
|
||||
// ----
|
||||
|
||||
/**
|
||||
* Holds back external rewrites that dropped data from a backup, see [BackupConflictGuard].
|
||||
* Home shows a card for each open conflict until the user keeps the new version or
|
||||
* restores the saved one. Open conflicts are saved with the backups they hold back.
|
||||
*/
|
||||
val backupGuard =
|
||||
BackupConflictGuard(
|
||||
onChange = { saveAccountSettings() },
|
||||
reapply = { reapplyBackup(it) },
|
||||
)
|
||||
|
||||
val backupConflicts get() = backupGuard.conflicts
|
||||
|
||||
/** Re-seeds the open conflicts read back from storage, see [BackupConflictGuard.restore]. */
|
||||
fun restoreBackupConflicts(saved: List<Triple<Event, Event, Event>>) = backupGuard.restore(saved)
|
||||
|
||||
/** The open conflicts, as the three events storage needs to rebuild each one. */
|
||||
fun openBackupConflicts(): List<Triple<Event, Event, Event>> = backupGuard.open()
|
||||
|
||||
/**
|
||||
* Re-runs the update that a restored conflict no longer carries.
|
||||
*
|
||||
* The guard's retries are closures, so they cannot be written to disk; a conflict read
|
||||
* back after a restart has none until its version happens to arrive again. Keeping the new
|
||||
* version must work regardless, and every update is the same shape — hand the event to the
|
||||
* function that owns its slot — so the slot can be recovered from the event's own type.
|
||||
*/
|
||||
private fun reapplyBackup(incoming: Event) {
|
||||
when (incoming) {
|
||||
is MetadataEvent -> updateUserMetadata(incoming)
|
||||
is ContactListEvent -> updateContactListTo(incoming)
|
||||
is ChatMessageRelayListEvent -> updateDMRelayList(incoming)
|
||||
is KeyPackageRelayListEvent -> updateKeyPackageRelayList(incoming)
|
||||
is AdvertisedRelayListEvent -> updateNIP65RelayList(incoming)
|
||||
is CashuWalletEvent -> updateCashuWallet(incoming)
|
||||
is NutzapInfoEvent -> updateNutzapInfo(incoming)
|
||||
is PaymentTargetsEvent -> updateNIPA3PaymentTargets(incoming)
|
||||
is Bolt12OfferListEvent -> updateBolt12Offers(incoming)
|
||||
is SearchRelayListEvent -> updateSearchRelayList(incoming)
|
||||
is IndexerRelayListEvent -> updateIndexRelayList(incoming)
|
||||
is RelayFeedsListEvent -> updateRelayFeedList(incoming)
|
||||
is BlockedRelayListEvent -> updateBlockedRelayList(incoming)
|
||||
is TrustedRelayListEvent -> updateTrustedRelayList(incoming)
|
||||
is PrivateOutboxRelayListEvent -> updatePrivateHomeRelayList(incoming)
|
||||
is ChannelListEvent -> updateChannelListTo(incoming)
|
||||
is GeohashListEvent -> updateGeohashListTo(incoming)
|
||||
is HashtagListEvent -> updateHashtagListTo(incoming)
|
||||
is FavoriteAlgoFeedsListEvent -> updateFavoriteAlgoFeedsListTo(incoming)
|
||||
is CommunityListEvent -> updateCommunityListTo(incoming)
|
||||
is EphemeralChatListEvent -> updateEphemeralChatListTo(incoming)
|
||||
is SimpleGroupListEvent -> updateRelayGroupListTo(incoming)
|
||||
is ConcordCommunityListEvent -> updateConcordListTo(incoming)
|
||||
is TrustProviderListEvent -> updateTrustProviderListTo(incoming)
|
||||
is MuteListEvent -> updateMuteList(incoming)
|
||||
// NIP-78 only raises a conflict when its blob was emptied, and an empty blob is
|
||||
// stored without merging the settings handed with it, so defaults are safe here.
|
||||
// A non-empty one needs the decrypted settings only the caller has; it lands when
|
||||
// the event next arrives, already recorded as accepted.
|
||||
is AppSpecificDataEvent -> if (incoming.content.isBlank()) updateAppSpecificData(incoming, AccountSyncedSettingsInternal())
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
fun updateLocalRelayServers(servers: Set<String>) {
|
||||
if (localRelayServers.value != servers) {
|
||||
localRelayServers.update { servers }
|
||||
@@ -1176,48 +1125,43 @@ class AccountSettings(
|
||||
fun updateUserMetadata(newMetadata: MetadataEvent?) {
|
||||
if (newMetadata == null) return
|
||||
|
||||
// Events might be different objects, we have to compare their ids.
|
||||
if (backupUserMetadata?.id != newMetadata.id) {
|
||||
if (backupGuard.accept(backupUserMetadata, newMetadata) { updateUserMetadata(newMetadata) }) {
|
||||
backupUserMetadata = newMetadata
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateContactListTo(newContactList: ContactListEvent?) {
|
||||
if (newContactList == null || newContactList.tags.isEmpty()) return
|
||||
if (newContactList == null) return
|
||||
|
||||
// Events might be different objects, we have to compare their ids.
|
||||
if (backupContactList?.id != newContactList.id) {
|
||||
if (backupGuard.accept(backupContactList, newContactList, isEmpty = newContactList.tags.isEmpty()) { updateContactListTo(newContactList) }) {
|
||||
backupContactList = newContactList
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateDMRelayList(newDMRelayList: ChatMessageRelayListEvent?) {
|
||||
if (newDMRelayList == null || newDMRelayList.tags.isEmpty()) return
|
||||
if (newDMRelayList == null) return
|
||||
|
||||
// Events might be different objects, we have to compare their ids.
|
||||
if (backupDMRelayList?.id != newDMRelayList.id) {
|
||||
if (backupGuard.accept(backupDMRelayList, newDMRelayList, isEmpty = newDMRelayList.tags.isEmpty()) { updateDMRelayList(newDMRelayList) }) {
|
||||
backupDMRelayList = newDMRelayList
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateKeyPackageRelayList(newKeyPackageRelayList: KeyPackageRelayListEvent?) {
|
||||
if (newKeyPackageRelayList == null || newKeyPackageRelayList.tags.isEmpty()) return
|
||||
if (newKeyPackageRelayList == null) return
|
||||
|
||||
// Events might be different objects, we have to compare their ids.
|
||||
if (backupKeyPackageRelayList?.id != newKeyPackageRelayList.id) {
|
||||
if (backupGuard.accept(backupKeyPackageRelayList, newKeyPackageRelayList, isEmpty = newKeyPackageRelayList.tags.isEmpty()) { updateKeyPackageRelayList(newKeyPackageRelayList) }) {
|
||||
backupKeyPackageRelayList = newKeyPackageRelayList
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateNIP65RelayList(newNIP65RelayList: AdvertisedRelayListEvent?) {
|
||||
if (newNIP65RelayList == null || newNIP65RelayList.tags.isEmpty()) return
|
||||
if (newNIP65RelayList == null) return
|
||||
|
||||
// Events might be different objects, we have to compare their ids.
|
||||
if (backupNIP65RelayList?.id != newNIP65RelayList.id) {
|
||||
if (backupGuard.accept(backupNIP65RelayList, newNIP65RelayList, isEmpty = newNIP65RelayList.tags.isEmpty()) { updateNIP65RelayList(newNIP65RelayList) }) {
|
||||
backupNIP65RelayList = newNIP65RelayList
|
||||
saveAccountSettings()
|
||||
}
|
||||
@@ -1226,14 +1170,17 @@ class AccountSettings(
|
||||
fun updateCashuWallet(newWallet: CashuWalletEvent?) {
|
||||
if (newWallet == null) return
|
||||
// Replaceable: keep the latest by id (id changes on each re-sign).
|
||||
if (backupCashuWallet?.id != newWallet.id) {
|
||||
if (backupGuard.accept(backupCashuWallet, newWallet) { updateCashuWallet(newWallet) }) {
|
||||
backupCashuWallet = newWallet
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateNutzapInfo(newNutzapInfo: NutzapInfoEvent?) {
|
||||
if (newNutzapInfo == null || newNutzapInfo.tags.isEmpty()) return
|
||||
if (newNutzapInfo == null) return
|
||||
// The guard runs first, so another app wiping the nutzap info is questioned like any
|
||||
// other lossy rewrite instead of silently clearing the backup.
|
||||
if (!backupGuard.accept(backupNutzapInfo, newNutzapInfo) { updateNutzapInfo(newNutzapInfo) }) return
|
||||
// A mints-less kind:10019 is the "stop receiving nutzaps" tombstone
|
||||
// (an empty replacement carrying only an `alt` tag). Don't restore it
|
||||
// on next launch — backing it up would undo clearNutzapInfo() once the
|
||||
@@ -1242,10 +1189,8 @@ class AccountSettings(
|
||||
clearNutzapInfo()
|
||||
return
|
||||
}
|
||||
if (backupNutzapInfo?.id != newNutzapInfo.id) {
|
||||
backupNutzapInfo = newNutzapInfo
|
||||
saveAccountSettings()
|
||||
}
|
||||
backupNutzapInfo = newNutzapInfo
|
||||
saveAccountSettings()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1255,6 +1200,8 @@ class AccountSettings(
|
||||
* LocalCache on next launch and resurrect the deleted wallet.
|
||||
*/
|
||||
fun clearCashuWallet() {
|
||||
// A deleted wallet must not be restorable from an open conflict.
|
||||
backupGuard.drop(backupSlotOf(CashuWalletEvent.KIND))
|
||||
if (backupCashuWallet != null) {
|
||||
backupCashuWallet = null
|
||||
saveAccountSettings()
|
||||
@@ -1263,30 +1210,18 @@ class AccountSettings(
|
||||
|
||||
/** Drop the cached kind:10019. Mirror of [clearCashuWallet] for the nutzap info. */
|
||||
fun clearNutzapInfo() {
|
||||
backupGuard.drop(backupSlotOf(NutzapInfoEvent.KIND))
|
||||
if (backupNutzapInfo != null) {
|
||||
backupNutzapInfo = null
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NUT-13 keyset counters live in [CashuPreferences], a dedicated
|
||||
* SharedPreferences file with synchronous (`commit = true`) writes.
|
||||
* AccountSettings goes through a 1-second debounce on its own save
|
||||
* path; the cashu counter cannot tolerate that window because the
|
||||
* mint persists signed (keyset, blind_message) pairs the moment it
|
||||
* sees them, so any local lag → "outputs already signed" on retry.
|
||||
* See [CashuPreferences] for the full rationale.
|
||||
*/
|
||||
private val cashuPrefs: CashuPreferences by lazy {
|
||||
CashuPreferences.forAccount(keyPair.pubKey.toNpub())
|
||||
}
|
||||
|
||||
/**
|
||||
* Reserve [count] consecutive NUT-13 counters for [keysetId],
|
||||
* returning the first one. Caller derives `(secret, r)` from
|
||||
* `(seed, keysetId, i)` for `i in [returned .. returned+count-1]`.
|
||||
* Persisted synchronously before returning — see [CashuPreferences].
|
||||
* Persisted synchronously before returning — see [CashuKeysetCounterStore].
|
||||
*
|
||||
* One-time migration: when this keyset has a non-zero value in the
|
||||
* legacy [cashuKeysetCounters] map (from a build that persisted
|
||||
@@ -1299,95 +1234,87 @@ class AccountSettings(
|
||||
count: Int,
|
||||
): Long {
|
||||
migrateLegacyCashuCounter(keysetId)
|
||||
return cashuPrefs.reserveCounters(keysetId, count)
|
||||
return cashuCounters.reserve(keysetId, count)
|
||||
}
|
||||
|
||||
/** Inspect the next counter for [keysetId] without consuming any. */
|
||||
fun peekCashuCounter(keysetId: String): Long {
|
||||
migrateLegacyCashuCounter(keysetId)
|
||||
return cashuPrefs.peekCounter(keysetId)
|
||||
return cashuCounters.peek(keysetId)
|
||||
}
|
||||
|
||||
private fun migrateLegacyCashuCounter(keysetId: String) {
|
||||
val legacy = cashuKeysetCounters[keysetId] ?: return
|
||||
cashuPrefs.seedCounterIfMissing(keysetId, legacy)
|
||||
cashuCounters.seedIfMissing(keysetId, legacy)
|
||||
}
|
||||
|
||||
fun updateNIPA3PaymentTargets(newNIPA3PaymentTargets: PaymentTargetsEvent?) {
|
||||
if (newNIPA3PaymentTargets == null || newNIPA3PaymentTargets.tags.isEmpty()) return
|
||||
if (newNIPA3PaymentTargets == null) return
|
||||
|
||||
// Events might be different objects, we have to compare their ids.
|
||||
if (backupNipA3PaymentTargets?.id != newNIPA3PaymentTargets.id) {
|
||||
if (backupGuard.accept(backupNipA3PaymentTargets, newNIPA3PaymentTargets, isEmpty = newNIPA3PaymentTargets.tags.isEmpty()) { updateNIPA3PaymentTargets(newNIPA3PaymentTargets) }) {
|
||||
backupNipA3PaymentTargets = newNIPA3PaymentTargets
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateBolt12Offers(newBolt12Offers: Bolt12OfferListEvent?) {
|
||||
if (newBolt12Offers == null || newBolt12Offers.tags.isEmpty()) return
|
||||
if (newBolt12Offers == null) return
|
||||
|
||||
// Events might be different objects, we have to compare their ids.
|
||||
if (backupBolt12Offers?.id != newBolt12Offers.id) {
|
||||
if (backupGuard.accept(backupBolt12Offers, newBolt12Offers, isEmpty = newBolt12Offers.tags.isEmpty()) { updateBolt12Offers(newBolt12Offers) }) {
|
||||
backupBolt12Offers = newBolt12Offers
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateSearchRelayList(newSearchRelayList: SearchRelayListEvent?) {
|
||||
if (newSearchRelayList == null || newSearchRelayList.tags.isEmpty()) return
|
||||
if (newSearchRelayList == null) return
|
||||
|
||||
// Events might be different objects, we have to compare their ids.
|
||||
if (backupSearchRelayList?.id != newSearchRelayList.id) {
|
||||
if (backupGuard.accept(backupSearchRelayList, newSearchRelayList, isEmpty = newSearchRelayList.tags.isEmpty()) { updateSearchRelayList(newSearchRelayList) }) {
|
||||
backupSearchRelayList = newSearchRelayList
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateIndexRelayList(newIndexRelayList: IndexerRelayListEvent?) {
|
||||
if (newIndexRelayList == null || newIndexRelayList.tags.isEmpty()) return
|
||||
if (newIndexRelayList == null) return
|
||||
|
||||
// Events might be different objects, we have to compare their ids.
|
||||
if (backupIndexRelayList?.id != newIndexRelayList.id) {
|
||||
if (backupGuard.accept(backupIndexRelayList, newIndexRelayList, isEmpty = newIndexRelayList.tags.isEmpty()) { updateIndexRelayList(newIndexRelayList) }) {
|
||||
backupIndexRelayList = newIndexRelayList
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateRelayFeedList(newRelayFeedList: RelayFeedsListEvent?) {
|
||||
if (newRelayFeedList == null || newRelayFeedList.tags.isEmpty()) return
|
||||
if (newRelayFeedList == null) return
|
||||
|
||||
// Events might be different objects, we have to compare their ids.
|
||||
if (backupRelayFeedsList?.id != newRelayFeedList.id) {
|
||||
if (backupGuard.accept(backupRelayFeedsList, newRelayFeedList, isEmpty = newRelayFeedList.tags.isEmpty()) { updateRelayFeedList(newRelayFeedList) }) {
|
||||
backupRelayFeedsList = newRelayFeedList
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateBlockedRelayList(newBlockedRelayList: BlockedRelayListEvent?) {
|
||||
if (newBlockedRelayList == null || newBlockedRelayList.tags.isEmpty()) return
|
||||
if (newBlockedRelayList == null) return
|
||||
|
||||
// Events might be different objects, we have to compare their ids.
|
||||
if (backupBlockedRelayList?.id != newBlockedRelayList.id) {
|
||||
if (backupGuard.accept(backupBlockedRelayList, newBlockedRelayList, isEmpty = newBlockedRelayList.tags.isEmpty()) { updateBlockedRelayList(newBlockedRelayList) }) {
|
||||
backupBlockedRelayList = newBlockedRelayList
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateTrustedRelayList(newTrustedRelayList: TrustedRelayListEvent?) {
|
||||
if (newTrustedRelayList == null || newTrustedRelayList.tags.isEmpty()) return
|
||||
if (newTrustedRelayList == null) return
|
||||
|
||||
// Events might be different objects, we have to compare their ids.
|
||||
if (backupTrustedRelayList?.id != newTrustedRelayList.id) {
|
||||
if (backupGuard.accept(backupTrustedRelayList, newTrustedRelayList, isEmpty = newTrustedRelayList.tags.isEmpty()) { updateTrustedRelayList(newTrustedRelayList) }) {
|
||||
backupTrustedRelayList = newTrustedRelayList
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
fun updatePrivateHomeRelayList(newPrivateHomeRelayList: PrivateOutboxRelayListEvent?) {
|
||||
if (newPrivateHomeRelayList == null || newPrivateHomeRelayList.tags.isEmpty()) return
|
||||
if (newPrivateHomeRelayList == null) return
|
||||
|
||||
// Events might be different objects, we have to compare their ids.
|
||||
if (backupPrivateHomeRelayList?.id != newPrivateHomeRelayList.id) {
|
||||
if (backupGuard.accept(backupPrivateHomeRelayList, newPrivateHomeRelayList, isEmpty = newPrivateHomeRelayList.tags.isEmpty()) { updatePrivateHomeRelayList(newPrivateHomeRelayList) }) {
|
||||
backupPrivateHomeRelayList = newPrivateHomeRelayList
|
||||
saveAccountSettings()
|
||||
}
|
||||
@@ -1396,50 +1323,45 @@ class AccountSettings(
|
||||
override fun channelList() = backupChannelList
|
||||
|
||||
override fun updateChannelListTo(newChannelList: ChannelListEvent?) {
|
||||
if (newChannelList == null || newChannelList.tags.isEmpty()) return
|
||||
if (newChannelList == null) return
|
||||
|
||||
// Events might be different objects, we have to compare their ids.
|
||||
if (backupChannelList?.id != newChannelList.id) {
|
||||
if (backupGuard.accept(backupChannelList, newChannelList, isEmpty = newChannelList.tags.isEmpty()) { updateChannelListTo(newChannelList) }) {
|
||||
backupChannelList = newChannelList
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateGeohashListTo(newGeohashList: GeohashListEvent?) {
|
||||
if (newGeohashList == null || newGeohashList.tags.isEmpty()) return
|
||||
if (newGeohashList == null) return
|
||||
|
||||
// Events might be different objects, we have to compare their ids.
|
||||
if (backupGeohashList?.id != newGeohashList.id) {
|
||||
if (backupGuard.accept(backupGeohashList, newGeohashList, isEmpty = newGeohashList.tags.isEmpty()) { updateGeohashListTo(newGeohashList) }) {
|
||||
backupGeohashList = newGeohashList
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateHashtagListTo(newHashtagList: HashtagListEvent?) {
|
||||
if (newHashtagList == null || newHashtagList.tags.isEmpty()) return
|
||||
if (newHashtagList == null) return
|
||||
|
||||
// Events might be different objects, we have to compare their ids.
|
||||
if (backupHashtagList?.id != newHashtagList.id) {
|
||||
if (backupGuard.accept(backupHashtagList, newHashtagList, isEmpty = newHashtagList.tags.isEmpty()) { updateHashtagListTo(newHashtagList) }) {
|
||||
backupHashtagList = newHashtagList
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateFavoriteAlgoFeedsListTo(newFavoriteDvmList: FavoriteAlgoFeedsListEvent?) {
|
||||
if (newFavoriteDvmList == null || newFavoriteDvmList.tags.isEmpty()) return
|
||||
if (newFavoriteDvmList == null) return
|
||||
|
||||
// Events might be different objects, we have to compare their ids.
|
||||
if (backupFavoriteAlgoFeedsList?.id != newFavoriteDvmList.id) {
|
||||
if (backupGuard.accept(backupFavoriteAlgoFeedsList, newFavoriteDvmList, isEmpty = newFavoriteDvmList.tags.isEmpty()) { updateFavoriteAlgoFeedsListTo(newFavoriteDvmList) }) {
|
||||
backupFavoriteAlgoFeedsList = newFavoriteDvmList
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateCommunityListTo(newCommunityList: CommunityListEvent?) {
|
||||
if (newCommunityList == null || newCommunityList.tags.isEmpty()) return
|
||||
if (newCommunityList == null) return
|
||||
|
||||
// Events might be different objects, we have to compare their ids.
|
||||
if (backupCommunityList?.id != newCommunityList.id) {
|
||||
if (backupGuard.accept(backupCommunityList, newCommunityList, isEmpty = newCommunityList.tags.isEmpty()) { updateCommunityListTo(newCommunityList) }) {
|
||||
backupCommunityList = newCommunityList
|
||||
saveAccountSettings()
|
||||
}
|
||||
@@ -1448,10 +1370,9 @@ class AccountSettings(
|
||||
override fun ephemeralChatList() = backupEphemeralChatList
|
||||
|
||||
override fun updateEphemeralChatListTo(newEphemeralChatList: EphemeralChatListEvent?) {
|
||||
if (newEphemeralChatList == null || newEphemeralChatList.tags.isEmpty()) return
|
||||
if (newEphemeralChatList == null) return
|
||||
|
||||
// Events might be different objects, we have to compare their ids.
|
||||
if (backupEphemeralChatList?.id != newEphemeralChatList.id) {
|
||||
if (backupGuard.accept(backupEphemeralChatList, newEphemeralChatList, isEmpty = newEphemeralChatList.tags.isEmpty()) { updateEphemeralChatListTo(newEphemeralChatList) }) {
|
||||
backupEphemeralChatList = newEphemeralChatList
|
||||
saveAccountSettings()
|
||||
}
|
||||
@@ -1464,8 +1385,7 @@ class AccountSettings(
|
||||
// so an empty `tags` is NOT an empty list — guard only on null.
|
||||
if (newRelayGroupList == null) return
|
||||
|
||||
// Events might be different objects, we have to compare their ids.
|
||||
if (backupRelayGroupList?.id != newRelayGroupList.id) {
|
||||
if (backupGuard.accept(backupRelayGroupList, newRelayGroupList) { updateRelayGroupListTo(newRelayGroupList) }) {
|
||||
backupRelayGroupList = newRelayGroupList
|
||||
saveAccountSettings()
|
||||
}
|
||||
@@ -1478,27 +1398,25 @@ class AccountSettings(
|
||||
// so an empty `tags` is NOT an empty list — guard only on null.
|
||||
if (newConcordList == null) return
|
||||
|
||||
if (backupConcordList?.id != newConcordList.id) {
|
||||
if (backupGuard.accept(backupConcordList, newConcordList) { updateConcordListTo(newConcordList) }) {
|
||||
backupConcordList = newConcordList
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateTrustProviderListTo(trustProviderList: TrustProviderListEvent?) {
|
||||
if (trustProviderList == null || trustProviderList.tags.isEmpty()) return
|
||||
if (trustProviderList == null) return
|
||||
|
||||
// Events might be different objects, we have to compare their ids.
|
||||
if (backupTrustProviderList?.id != trustProviderList.id) {
|
||||
if (backupGuard.accept(backupTrustProviderList, trustProviderList, isEmpty = trustProviderList.tags.isEmpty()) { updateTrustProviderListTo(trustProviderList) }) {
|
||||
backupTrustProviderList = trustProviderList
|
||||
saveAccountSettings()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateMuteList(newMuteList: MuteListEvent?) {
|
||||
if (newMuteList == null || newMuteList.tags.isEmpty()) return
|
||||
if (newMuteList == null) return
|
||||
|
||||
// Events might be different objects, we have to compare their ids.
|
||||
if (backupMuteList?.id != newMuteList.id) {
|
||||
if (backupGuard.accept(backupMuteList, newMuteList, isEmpty = newMuteList.tags.isEmpty()) { updateMuteList(newMuteList) }) {
|
||||
backupMuteList = newMuteList
|
||||
saveAccountSettings()
|
||||
}
|
||||
@@ -1508,11 +1426,15 @@ class AccountSettings(
|
||||
appSettings: AppSpecificDataEvent?,
|
||||
newSyncedSettings: AccountSyncedSettingsInternal,
|
||||
) {
|
||||
if (appSettings == null || appSettings.content.isEmpty()) return
|
||||
if (appSettings == null) return
|
||||
|
||||
// Events might be different objects, we have to compare their ids.
|
||||
if (backupAppSpecificData?.id != appSettings.id) {
|
||||
if (backupGuard.accept(backupAppSpecificData, appSettings, isEmpty = appSettings.content.isBlank()) { updateAppSpecificData(appSettings, newSyncedSettings) }) {
|
||||
backupAppSpecificData = appSettings
|
||||
// An emptied blob (signed here or accepted by the user) has no settings to merge.
|
||||
if (appSettings.content.isBlank()) {
|
||||
saveAccountSettings()
|
||||
return
|
||||
}
|
||||
syncedSettings.updateFrom(newSyncedSettings)
|
||||
|
||||
// Null means an older client rewrote the blob without this key — leave the
|
||||
|
||||
@@ -22,13 +22,13 @@ package com.vitorpamplona.amethyst.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.commons.audio.VisualizerStyle
|
||||
import com.vitorpamplona.amethyst.commons.model.navigation.BottomBarEntry
|
||||
import com.vitorpamplona.amethyst.commons.model.navigation.DrawerItemVisibility
|
||||
import com.vitorpamplona.amethyst.commons.model.navigation.NavBarItem
|
||||
import com.vitorpamplona.amethyst.commons.model.navigation.navBarItemsFromNames
|
||||
import com.vitorpamplona.amethyst.commons.model.navigation.toNames
|
||||
import com.vitorpamplona.amethyst.commons.service.pow.PoWCategory
|
||||
import com.vitorpamplona.amethyst.commons.service.pow.PoWPolicy
|
||||
import com.vitorpamplona.amethyst.ui.navigation.bottombars.BottomBarEntry
|
||||
import com.vitorpamplona.amethyst.ui.navigation.bottombars.NavBarItem
|
||||
import com.vitorpamplona.amethyst.ui.navigation.bottombars.navBarItemsFromNames
|
||||
import com.vitorpamplona.amethyst.ui.navigation.bottombars.toNames
|
||||
import com.vitorpamplona.amethyst.ui.navigation.drawer.DrawerItemVisibility
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists
|
||||
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
|
||||
|
||||
+1
-1
@@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.model
|
||||
|
||||
import android.content.res.Resources
|
||||
import androidx.core.os.ConfigurationCompat
|
||||
import com.vitorpamplona.amethyst.commons.model.navigation.BottomBarEntry
|
||||
import com.vitorpamplona.amethyst.commons.service.pow.PoWCategory
|
||||
import com.vitorpamplona.amethyst.ui.navigation.bottombars.BottomBarEntry
|
||||
import com.vitorpamplona.amethyst.ui.navigation.bottombars.DefaultBottomBarEntries
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.model
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.model.User
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.filter
|
||||
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
@@ -39,6 +40,7 @@ import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
import com.vitorpamplona.quartz.nip17Dm.base.BaseDMGroupEvent
|
||||
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip29RelayGroups.isGroupScoped
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent
|
||||
@@ -49,6 +51,8 @@ import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEv
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
|
||||
import com.vitorpamplona.quartz.nip60Cashu.wallet.CashuWalletEvent
|
||||
import com.vitorpamplona.quartz.nip61Nutzaps.info.NutzapInfoEvent
|
||||
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent
|
||||
import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent
|
||||
@@ -369,6 +373,35 @@ class EventBroadcaster(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Publishes a re-signed backup that replaces a lossy version from another app. It goes
|
||||
* where a normal save of that event goes; lists of the user's own relays (NIP-65, DM,
|
||||
* key package) and the wallet go everywhere, and also to every relay the restored list
|
||||
* names, since the lossy version may have removed them from the outbox set. Profiles and
|
||||
* nutzap info go everywhere too, like their normal saves.
|
||||
*/
|
||||
fun sendRestoredVersion(event: Event) {
|
||||
when (event) {
|
||||
is AdvertisedRelayListEvent -> sendEverywhereAnd(event, event.relays().mapTo(mutableSetOf()) { it.relayUrl })
|
||||
is ChatMessageRelayListEvent -> sendEverywhereAnd(event, event.relays().toSet())
|
||||
is KeyPackageRelayListEvent -> sendEverywhereAnd(event, event.relays().toSet())
|
||||
is CashuWalletEvent -> sendLiterallyEverywhere(event)
|
||||
// Profiles and nutzap info are normally saved everywhere so others can find them;
|
||||
// the restore must reach the same relays or they keep serving the lossy version.
|
||||
is MetadataEvent -> sendLiterallyEverywhere(event)
|
||||
is NutzapInfoEvent -> sendEverywhereAnd(event, event.relays().toSet())
|
||||
else -> sendMyPublicAndPrivateOutbox(event)
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendEverywhereAnd(
|
||||
event: Event,
|
||||
extraRelays: Set<NormalizedRelayUrl>,
|
||||
) {
|
||||
account.client.publish(event, account.followPlusAllMineWithIndex.flow.value + account.client.availableRelaysFlow().value + extraRelays)
|
||||
account.cache.justConsumeMyOwnEvent(event)
|
||||
}
|
||||
|
||||
fun sendLiterallyEverywhere(event: Event) {
|
||||
account.client.publish(event, account.followPlusAllMineWithIndex.flow.value + account.client.availableRelaysFlow().value)
|
||||
account.cache.justConsumeMyOwnEvent(event)
|
||||
|
||||
+2
-2
@@ -28,16 +28,16 @@ import com.vitorpamplona.amethyst.commons.connectedApps.signers.InMemoryNostrSig
|
||||
import com.vitorpamplona.amethyst.commons.connectedApps.signers.NostrSignerPermissionStore
|
||||
import com.vitorpamplona.amethyst.commons.marmot.InMemoryMlsGroupStateStore
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
||||
import com.vitorpamplona.amethyst.commons.model.marmot.AndroidIngestDedupStore
|
||||
import com.vitorpamplona.amethyst.commons.model.marmot.AndroidPushStateStore
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.nip47WalletConnect.NWCPaymentFilterAssembler
|
||||
import com.vitorpamplona.amethyst.commons.service.pow.PoWPublishQueue
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.marmot.AndroidIngestDedupStore
|
||||
import com.vitorpamplona.amethyst.model.marmot.AndroidKeyPackageBundleStore
|
||||
import com.vitorpamplona.amethyst.model.marmot.AndroidMarmotMessageStore
|
||||
import com.vitorpamplona.amethyst.model.marmot.AndroidMlsGroupStateStore
|
||||
import com.vitorpamplona.amethyst.model.marmot.AndroidPublishObligationStore
|
||||
import com.vitorpamplona.amethyst.model.marmot.AndroidPushStateStore
|
||||
import com.vitorpamplona.amethyst.service.location.LocationState
|
||||
import com.vitorpamplona.amethyst.service.relayClient.authCommand.model.DataStoreRelayAuthPermissionStore
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* 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.backups
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.JsonMapper
|
||||
import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
|
||||
/**
|
||||
* How an undecided backup conflict is written to storage.
|
||||
*
|
||||
* A conflict is a question put to the user, and the backup it holds back stays held back until
|
||||
* they answer. That outlives the process it was raised in, so the three events it is made of —
|
||||
* the saved version, the incoming one, and the change that caused it — are stored and read back.
|
||||
*
|
||||
* The diff is **not** stored. It is recomputed from the two versions on load, so a conflict whose
|
||||
* incoming version no longer removes anything (the other app put it back) simply stops being one
|
||||
* instead of being resurrected from a stale description of itself.
|
||||
*
|
||||
* Kept out of `LocalPreferences` so it is reachable from a plain JVM test.
|
||||
*/
|
||||
object BackupConflictStorage {
|
||||
// Every settings save writes the open conflicts, and each can hold a follow list with
|
||||
// thousands of entries: reuse the last encoding while the conflicts themselves are the same.
|
||||
@Volatile
|
||||
private var lastEncoded: Pair<List<String>, String>? = null
|
||||
|
||||
fun encode(conflicts: List<Triple<Event, Event, Event>>): String {
|
||||
val ids = conflicts.flatMap { (saved, incoming, cause) -> listOf(saved.id, incoming.id, cause.id) }
|
||||
lastEncoded?.let { (cachedIds, encoded) -> if (cachedIds == ids) return encoded }
|
||||
return encodeUncached(conflicts).also { lastEncoded = ids to it }
|
||||
}
|
||||
|
||||
private fun encodeUncached(conflicts: List<Triple<Event, Event, Event>>): String =
|
||||
JsonMapper.toJson(
|
||||
conflicts.map { (saved, incoming, cause) ->
|
||||
listOf(
|
||||
OptimizedJsonMapper.toJson(saved),
|
||||
OptimizedJsonMapper.toJson(incoming),
|
||||
OptimizedJsonMapper.toJson(cause),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
/**
|
||||
* Unreadable entries are dropped one at a time: a conflict that cannot be parsed is not one
|
||||
* the user can be asked about, and it must not take the readable ones down with it.
|
||||
*/
|
||||
fun decode(stored: String?): List<Triple<Event, Event, Event>> {
|
||||
if (stored.isNullOrBlank()) return emptyList()
|
||||
val rows =
|
||||
try {
|
||||
JsonMapper.fromJson<List<List<String>>>(stored)
|
||||
} catch (e: Exception) {
|
||||
Log.w("BackupConflictStorage", "Could not read the stored backup conflicts", e)
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
return rows.mapNotNull { row ->
|
||||
if (row.size != 3) return@mapNotNull null
|
||||
try {
|
||||
Triple(
|
||||
OptimizedJsonMapper.fromJson(row[0]),
|
||||
OptimizedJsonMapper.fromJson(row[1]),
|
||||
OptimizedJsonMapper.fromJson(row[2]),
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.w("BackupConflictStorage", "Could not read a stored backup conflict", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
@@ -27,6 +27,8 @@ import com.vitorpamplona.amethyst.commons.connectedApps.signers.AppConnectResult
|
||||
import com.vitorpamplona.amethyst.commons.connectedApps.signers.AppSignerPolicy
|
||||
import com.vitorpamplona.amethyst.commons.connectedApps.signers.NostrOpDecision
|
||||
import com.vitorpamplona.amethyst.commons.connectedApps.signers.NostrSignerPermissionLedger
|
||||
import com.vitorpamplona.amethyst.commons.model.nip46Signer.Nip46ActivityEntry
|
||||
import com.vitorpamplona.amethyst.commons.model.nip46Signer.Nip46ActivityLog
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
|
||||
|
||||
+1
@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.model.NoteState
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
||||
import com.vitorpamplona.amethyst.commons.model.nip51Lists.blockedRelays.BlockedRelayListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
|
||||
+1
@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.model.nip51Lists.broadcastRelays
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.model.NoteState
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
||||
import com.vitorpamplona.amethyst.commons.model.nip51Lists.broadcastRelays.BroadcastRelayListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
|
||||
+1
@@ -24,6 +24,7 @@ import com.vitorpamplona.amethyst.commons.defaults.DefaultIndexerRelayList
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.model.NoteState
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
||||
import com.vitorpamplona.amethyst.commons.model.nip51Lists.indexerRelays.IndexerRelayListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
|
||||
+1
@@ -28,6 +28,7 @@ import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.filter
|
||||
import com.vitorpamplona.amethyst.commons.model.eventIdSet
|
||||
import com.vitorpamplona.amethyst.commons.model.events
|
||||
import com.vitorpamplona.amethyst.commons.model.nip51Lists.peopleList.PeopleList
|
||||
import com.vitorpamplona.amethyst.commons.model.updateFlow
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
|
||||
+1
@@ -28,6 +28,7 @@ import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.filter
|
||||
import com.vitorpamplona.amethyst.commons.model.eventIdSet
|
||||
import com.vitorpamplona.amethyst.commons.model.events
|
||||
import com.vitorpamplona.amethyst.commons.model.nip51Lists.peopleList.PeopleList
|
||||
import com.vitorpamplona.amethyst.commons.model.nip51Lists.peopleList.PeopleListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.commons.model.updateFlow
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
|
||||
+1
@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.model.nip51Lists.proxyRelays
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.model.NoteState
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
||||
import com.vitorpamplona.amethyst.commons.model.nip51Lists.proxyRelays.ProxyRelayListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
|
||||
+1
@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.model.nip51Lists.relayFeeds
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.model.NoteState
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
||||
import com.vitorpamplona.amethyst.commons.model.nip51Lists.relayFeeds.RelayFeedsListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
|
||||
+1
@@ -24,6 +24,7 @@ import com.vitorpamplona.amethyst.commons.defaults.DefaultSearchRelayList
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.model.NoteState
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
||||
import com.vitorpamplona.amethyst.commons.model.nip51Lists.searchRelays.SearchRelayListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
|
||||
+1
@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.model.nip51Lists.trustedRelays
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.model.NoteState
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
||||
import com.vitorpamplona.amethyst.commons.model.nip51Lists.trustedRelays.TrustedRelayListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
|
||||
+7
-6
@@ -25,6 +25,7 @@ import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.core.content.edit
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.commons.cashu.CashuKeysetCounterStore
|
||||
|
||||
/**
|
||||
* Per-account Cashu state that needs durable, synchronous persistence —
|
||||
@@ -70,10 +71,10 @@ import com.vitorpamplona.amethyst.Amethyst
|
||||
*/
|
||||
class CashuPreferences(
|
||||
private val prefs: SharedPreferences,
|
||||
) {
|
||||
) : CashuKeysetCounterStore {
|
||||
/** Inspect the next free counter for [keysetId] without advancing it. */
|
||||
@Synchronized
|
||||
fun peekCounter(keysetId: String): Long = prefs.getLong(counterKey(keysetId), 0L)
|
||||
override fun peek(keysetId: String): Long = prefs.getLong(counterKey(keysetId), 0L)
|
||||
|
||||
/**
|
||||
* Atomically reserve [count] consecutive NUT-13 counters for
|
||||
@@ -83,12 +84,12 @@ class CashuPreferences(
|
||||
*/
|
||||
@Synchronized
|
||||
@SuppressLint("ApplySharedPref")
|
||||
fun reserveCounters(
|
||||
override fun reserve(
|
||||
keysetId: String,
|
||||
count: Int,
|
||||
): Long {
|
||||
require(count > 0) { "Counter reservation must be positive" }
|
||||
val current = peekCounter(keysetId)
|
||||
val current = peek(keysetId)
|
||||
val next = current + count.toLong()
|
||||
prefs.edit(commit = true) { putLong(counterKey(keysetId), next) }
|
||||
return current
|
||||
@@ -103,12 +104,12 @@ class CashuPreferences(
|
||||
*/
|
||||
@Synchronized
|
||||
@SuppressLint("ApplySharedPref")
|
||||
fun seedCounterIfMissing(
|
||||
override fun seedIfMissing(
|
||||
keysetId: String,
|
||||
legacyValue: Long,
|
||||
) {
|
||||
if (legacyValue <= 0L) return
|
||||
val current = peekCounter(keysetId)
|
||||
val current = peek(keysetId)
|
||||
if (current >= legacyValue) return
|
||||
prefs.edit(commit = true) { putLong(counterKey(keysetId), legacyValue) }
|
||||
}
|
||||
|
||||
+18
-3
@@ -111,11 +111,26 @@ class AppSpecificState(
|
||||
getAppSpecificDataFlow().collect {
|
||||
try {
|
||||
Log.d("AccountRegisterObservers") { "Updating AppSpecificData for ${signer.pubKey}" }
|
||||
(it.note.event as? AppSpecificDataEvent)?.let {
|
||||
val decrypted = signer.decrypt(it.content, it.pubKey)
|
||||
(it.note.event as? AppSpecificDataEvent)?.let { event ->
|
||||
// A blob another app emptied has nothing to decrypt, so it used to die
|
||||
// in the catch below — yet an empty version is exactly the wipe the
|
||||
// backup guard exists to question, and [AccountSettings] already
|
||||
// accepts one (`isEmpty`). Without this it never got there: the guard
|
||||
// was never called, no conflict was raised, and the NIP-78 card could
|
||||
// not appear for the case it was written for.
|
||||
//
|
||||
// The settings handed over are never merged — updateAppSpecificData
|
||||
// returns before that whenever the content is empty — so defaults
|
||||
// here cannot overwrite the real ones.
|
||||
if (event.content.isBlank()) {
|
||||
settings.updateAppSpecificData(event, AccountSyncedSettingsInternal())
|
||||
return@let
|
||||
}
|
||||
|
||||
val decrypted = signer.decrypt(event.content, event.pubKey)
|
||||
try {
|
||||
val syncedSettings = JsonMapper.fromJson<AccountSyncedSettingsInternal>(decrypted)
|
||||
settings.updateAppSpecificData(it, syncedSettings)
|
||||
settings.updateAppSpecificData(event, syncedSettings)
|
||||
} catch (e: Throwable) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.w("LocalPreferences", "Error Decoding latestAppSpecificData from Preferences", e)
|
||||
|
||||
+3
-3
@@ -23,10 +23,10 @@ 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.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nipB7Blossom.BlossomAuthorizationEvent
|
||||
|
||||
+1
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.model.preferences
|
||||
|
||||
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import com.vitorpamplona.amethyst.commons.model.preferences.UpdatablePropertyFlow
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect
|
||||
import com.vitorpamplona.quartz.utils.cache.LargeCache
|
||||
|
||||
@@ -24,6 +24,7 @@ import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.emptyPreferences
|
||||
import com.vitorpamplona.amethyst.commons.model.preferences.UpdatablePropertyFlow
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
+1
@@ -24,6 +24,7 @@ import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.emptyPreferences
|
||||
import com.vitorpamplona.amethyst.commons.model.preferences.UpdatablePropertyFlow
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
|
||||
@@ -70,8 +70,8 @@ import com.vitorpamplona.amethyst.commons.resources.napplet_consent_deny_always
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_consent_hide_event
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_consent_not_now
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_consent_show_event
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.AmethystTheme
|
||||
|
||||
/**
|
||||
|
||||
+4
-4
@@ -43,12 +43,12 @@ import com.vitorpamplona.amethyst.commons.resources.crashreport_found
|
||||
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.ui.navigation.navs.INav
|
||||
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.navigation.routes.routeToMessage
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size16dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
|
||||
+26
-6
@@ -42,6 +42,7 @@ import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
@@ -94,6 +95,18 @@ abstract class FlowProgressForegroundService<T> : Service() {
|
||||
/** When non-null, re-render the card on this cadence (for clock-driven text like "time left"). */
|
||||
protected open val refreshMs: Long? = null
|
||||
|
||||
/**
|
||||
* How long to keep the service (and whatever it holds up) alive after [isActive] turns
|
||||
* false, before stopping. Lets work that was just handed off asynchronously — e.g. a
|
||||
* broadcast still leaving through the relay pool — finish before the process loses its
|
||||
* foreground protection and gets frozen. A new active emission during the grace keeps
|
||||
* the service running. Asked each time the work drains, so it can depend on how it did.
|
||||
*/
|
||||
protected open fun stopGraceMs(): Long = 0L
|
||||
|
||||
/** The card shown during [stopGraceMs]; null keeps the last one. */
|
||||
protected open fun renderDraining(): Content? = null
|
||||
|
||||
protected abstract fun state(): StateFlow<T>
|
||||
|
||||
/** Keep the service (and notification) alive while this is true; stop once it goes false. */
|
||||
@@ -222,9 +235,15 @@ abstract class FlowProgressForegroundService<T> : Service() {
|
||||
onStarted()
|
||||
watchJob =
|
||||
scope.launch {
|
||||
state().collect { value ->
|
||||
state().collectLatest { value ->
|
||||
onEmission(value)
|
||||
if (!isActive(value)) {
|
||||
// collectLatest: a new active emission cancels this pending stop.
|
||||
val grace = stopGraceMs()
|
||||
if (grace > 0) {
|
||||
renderDraining()?.let { notify(buildNotification(it)) }
|
||||
delay(grace)
|
||||
}
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
stopSelf()
|
||||
} else {
|
||||
@@ -246,7 +265,7 @@ abstract class FlowProgressForegroundService<T> : Service() {
|
||||
|
||||
private fun startForegroundCompat(value: T) {
|
||||
ensureChannel()
|
||||
val notification = buildNotification(value)
|
||||
val notification = buildNotification(render(value))
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
startForeground(notificationId, notification, fgsType)
|
||||
} else {
|
||||
@@ -254,18 +273,19 @@ abstract class FlowProgressForegroundService<T> : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateNotification(value: T) {
|
||||
private fun updateNotification(value: T) = notify(buildNotification(render(value)))
|
||||
|
||||
private fun notify(notification: Notification) {
|
||||
val manager = NotificationManagerCompat.from(this)
|
||||
if (!manager.areNotificationsEnabled()) return
|
||||
try {
|
||||
manager.notify(notificationId, buildNotification(value))
|
||||
manager.notify(notificationId, notification)
|
||||
} catch (_: SecurityException) {
|
||||
// POST_NOTIFICATIONS revoked mid-flight; the FGS keeps running.
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildNotification(value: T): Notification {
|
||||
val content = render(value)
|
||||
private fun buildNotification(content: Content): Notification {
|
||||
val style =
|
||||
when (val bar = content.bar) {
|
||||
is Bar.Indeterminate -> NotificationCompat.ProgressStyle().setProgressIndeterminate(true)
|
||||
|
||||
@@ -36,6 +36,7 @@ import com.vitorpamplona.amethyst.commons.service.http.BlossomReadAuthTokenProvi
|
||||
import com.vitorpamplona.amethyst.commons.service.image.readAuthAware
|
||||
import com.vitorpamplona.amethyst.commons.service.image.withAuthHeader
|
||||
import com.vitorpamplona.amethyst.service.uploads.blossom.bud10.BlossomServerResolver
|
||||
import com.vitorpamplona.quartz.nipB7Blossom.BlossomUri
|
||||
import com.vitorpamplona.quartz.utils.startsWithIgnoreCase
|
||||
import okhttp3.Call
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
@@ -44,18 +45,49 @@ import kotlin.coroutines.cancellation.CancellationException
|
||||
class BlossomFetcher(
|
||||
private val options: Options,
|
||||
private val data: Uri,
|
||||
private val imageLoader: ImageLoader,
|
||||
private val blossomServerResolver: () -> BlossomServerResolver,
|
||||
private val networkFetcher: (url: String) -> Fetcher,
|
||||
private val networkFetcher: (url: String, diskCacheKey: String) -> Fetcher,
|
||||
) : Fetcher {
|
||||
override suspend fun fetch(): FetchResult? =
|
||||
try {
|
||||
val urlResult = blossomServerResolver().findServers(data.toString())
|
||||
networkFetcher(urlResult?.serverUrl ?: data.toString()).fetch()
|
||||
val uri = data.toString()
|
||||
val key = options.diskCacheKey ?: diskCacheKey(uri)
|
||||
|
||||
// A blob already on disk is served without resolving: resolution may HEAD
|
||||
// servers and wait on relays, and fails outright offline, where the bytes are
|
||||
// still sitting in the cache. NetworkFetcher reads the snapshot under [key]
|
||||
// before it ever looks at the url.
|
||||
val onDisk = options.diskCachePolicy.readEnabled && imageLoader.diskCache?.openSnapshot(key)?.use { true } ?: false
|
||||
val fromDisk =
|
||||
if (onDisk) {
|
||||
// Evicted between the check and the read: the fetcher would try the
|
||||
// network with the unresolvable `blossom:` uri, so resolve after all.
|
||||
try {
|
||||
networkFetcher(uri, key).fetch()
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
fromDisk ?: networkFetcher(blossomServerResolver().findServers(uri)?.serverUrl ?: uri, key).fetch()
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
null
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Blossom blobs are content-addressed, so the sha256 names the bytes wherever they
|
||||
* were served from. Keying on it (instead of the resolved server URL) finds the
|
||||
* blob again after a restart, a probe flip or a different server answering.
|
||||
*/
|
||||
fun diskCacheKey(uri: String): String = BlossomUri.parse(uri)?.let { "blossom:${it.sha256}" } ?: uri
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoilApi::class)
|
||||
class Factory(
|
||||
val blossomServerResolver: () -> BlossomServerResolver,
|
||||
@@ -78,20 +110,19 @@ class BlossomFetcher(
|
||||
if (!isApplicable(data)) return null
|
||||
// Wrapped per resolved url (not per Factory) because the server the
|
||||
// blob actually lives on is only known once the resolver has run.
|
||||
return BlossomFetcher(options, data, blossomServerResolver) { url ->
|
||||
return BlossomFetcher(options, data, imageLoader, blossomServerResolver) { url, diskCacheKey ->
|
||||
val keyedOptions = options.copy(diskCacheKey = diskCacheKey)
|
||||
readAuthAware(url, readAuth) { authHeader ->
|
||||
NetworkFetcher(
|
||||
url = url,
|
||||
options = options.withAuthHeader(authHeader),
|
||||
options = keyedOptions.withAuthHeader(authHeader),
|
||||
networkClient = lazy { networkClient(url).asNetworkClient() },
|
||||
diskCache = lazy { imageLoader.diskCache },
|
||||
cacheStrategy = cacheStrategyLazy,
|
||||
connectivityChecker = lazy { connectivityCheckerLazy.get(options.context) },
|
||||
concurrentRequestStrategy = concurrentRequestStrategyLazy,
|
||||
)
|
||||
// Keyed on the resolved server url, which is what the NetworkFetcher above
|
||||
// caches under -- not on the `blossom:` uri the request came in as.
|
||||
}.onSystemFileSystem(options.diskCacheKey ?: url)
|
||||
}.onSystemFileSystem(diskCacheKey)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-5
@@ -49,8 +49,6 @@ import com.vitorpamplona.amethyst.commons.service.image.ThumbHashFetcher
|
||||
import com.vitorpamplona.amethyst.commons.service.image.readAuthAware
|
||||
import com.vitorpamplona.amethyst.commons.service.image.withAuthHeader
|
||||
import com.vitorpamplona.amethyst.isDebug
|
||||
import com.vitorpamplona.amethyst.service.images.BlossomFetcher
|
||||
import com.vitorpamplona.amethyst.service.images.ProfilePictureFetcher
|
||||
import com.vitorpamplona.amethyst.service.uploads.blossom.bud10.BlossomServerResolver
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -87,9 +85,10 @@ class ImageLoaderSetup {
|
||||
// coordinates through a map of in-flight fetches that it owns, so it only
|
||||
// works when every fetcher shares the same instance -- a fresh one per
|
||||
// request can never see anybody else's fetch and the de-dupe silently
|
||||
// no-ops. Shared across all three network-backed factories so a feed
|
||||
// image and the same blob reached through `blossom:` (or a profile
|
||||
// picture) still collapse onto one download.
|
||||
// no-ops. Shared across all three network-backed factories so the same
|
||||
// key requested through any of them (e.g. a feed image and a profile
|
||||
// picture with the same URL) collapses onto one download. `blossom:`
|
||||
// blobs are keyed by their sha256 instead (see BlossomFetcher.diskCacheKey).
|
||||
val concurrentRequests = DeDupeConcurrentRequestStrategy()
|
||||
|
||||
SingletonImageLoader.setUnsafe(
|
||||
|
||||
+4
-1
@@ -36,6 +36,8 @@ import coil3.network.NetworkFetcher
|
||||
import coil3.network.okhttp.asNetworkClient
|
||||
import coil3.request.Options
|
||||
import com.vitorpamplona.amethyst.commons.service.http.BlossomReadAuthTokenProvider
|
||||
import com.vitorpamplona.amethyst.commons.service.http.LocalBlossomCacheRedirectInterceptor.Companion.PROFILE_PICTURE
|
||||
import com.vitorpamplona.amethyst.commons.service.http.LocalBlossomMediaCallFactory
|
||||
import com.vitorpamplona.amethyst.commons.service.image.readAuthAware
|
||||
import com.vitorpamplona.amethyst.commons.service.image.withAuthHeader
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.ProfilePictureUrl
|
||||
@@ -121,7 +123,8 @@ class ProfilePictureFetcher(
|
||||
NetworkFetcher(
|
||||
url = data.url,
|
||||
options = options.withAuthHeader(authHeader),
|
||||
networkClient = lazy { networkClient(data.url).asNetworkClient() },
|
||||
// Marked so the local Blossom cache bridge can honour "profile pictures only".
|
||||
networkClient = lazy { LocalBlossomMediaCallFactory(networkClient(data.url), PROFILE_PICTURE).asNetworkClient() },
|
||||
diskCache = diskCacheLazy,
|
||||
cacheStrategy = cacheStrategyLazy,
|
||||
connectivityChecker = lazy { connectivityCheckerLazy.get(options.context) },
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.error_video_open_in_browser
|
||||
import com.vitorpamplona.amethyst.commons.resources.error_video_playback_failed
|
||||
import com.vitorpamplona.amethyst.commons.resources.error_video_playback_failed_description
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
|
||||
/**
|
||||
|
||||
+4
-4
@@ -38,14 +38,14 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import com.vitorpamplona.amethyst.commons.model.nip71Video.CaptionTrack
|
||||
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.note.DownloadForOfflineIcon
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size75dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.imageModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.videoGalleryModifier
|
||||
import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
+1
-1
@@ -43,7 +43,7 @@ import androidx.media3.common.util.UnstableApi
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.captions_off
|
||||
import com.vitorpamplona.amethyst.commons.resources.captions_unnamed_track
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import java.util.Locale
|
||||
|
||||
+5
-5
@@ -41,11 +41,11 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.enter_picture_in_picture
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.PinBottomIconSize
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size50Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.PinBottomIconSize
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size50Modifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
|
||||
+2
-2
@@ -48,8 +48,8 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.ui.compose.state.rememberProgressStateWithTickCount
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
|
||||
+6
-6
@@ -43,12 +43,12 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.mute_button
|
||||
import com.vitorpamplona.amethyst.commons.resources.muted_button
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size30Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size50Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.ui.theme.VolumeBottomIconSize
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size30Modifier
|
||||
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 kotlinx.coroutines.delay
|
||||
|
||||
@Preview
|
||||
|
||||
+9
-9
@@ -50,16 +50,16 @@ import com.vitorpamplona.amethyst.commons.resources.picture_in_picture
|
||||
import com.vitorpamplona.amethyst.commons.resources.playback_actions_dialog_title
|
||||
import com.vitorpamplona.amethyst.commons.resources.share_or_save
|
||||
import com.vitorpamplona.amethyst.commons.resources.video_player_settings_action_fullscreen
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.M3ActionDialog
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.M3ActionRow
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.M3ActionSection
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.BitcoinOrange
|
||||
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 com.vitorpamplona.amethyst.ui.components.M3ActionDialog
|
||||
import com.vitorpamplona.amethyst.ui.components.M3ActionRow
|
||||
import com.vitorpamplona.amethyst.ui.components.M3ActionSection
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size50Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.ui.theme.VolumeBottomIconSize
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
|
||||
+6
-6
@@ -40,12 +40,12 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.vitorpamplona.amethyst.ui.note.EnablePiP
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.PinBottomIconSize
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size22Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size50Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.EnablePiP
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.PinBottomIconSize
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size22Modifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size50Modifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
|
||||
+5
-5
@@ -44,11 +44,11 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.pause
|
||||
import com.vitorpamplona.amethyst.commons.resources.play
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.PlayIconSize
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size50Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.PlayIconSize
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size50Modifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
|
||||
+3
-3
@@ -45,9 +45,9 @@ import androidx.compose.ui.window.PopupProperties
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.ui.compose.state.rememberPlaybackSpeedState
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonRow
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
|
||||
+2
-2
@@ -33,8 +33,8 @@ import androidx.media3.common.Player
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.common.util.Util
|
||||
import androidx.media3.ui.compose.state.rememberProgressStateWithTickInterval
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
@Preview
|
||||
|
||||
+6
-6
@@ -64,6 +64,12 @@ import com.vitorpamplona.amethyst.commons.resources.download_to_phone
|
||||
import com.vitorpamplona.amethyst.commons.resources.picture_in_picture
|
||||
import com.vitorpamplona.amethyst.commons.resources.share_or_save
|
||||
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.BitcoinOrange
|
||||
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.service.cast.CastRequest
|
||||
@@ -78,12 +84,6 @@ 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.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.PinBottomIconSize
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size50Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
+6
-6
@@ -52,12 +52,12 @@ import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.download_to_phone
|
||||
import com.vitorpamplona.amethyst.commons.resources.video_download_has_started_toast
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.PinBottomIconSize
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size50Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.BitcoinOrange
|
||||
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 kotlinx.coroutines.launch
|
||||
|
||||
@Preview
|
||||
|
||||
+6
-6
@@ -42,12 +42,12 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.share_or_save
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.PinBottomIconSize
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size50Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.BitcoinOrange
|
||||
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
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
|
||||
+3
-3
@@ -38,10 +38,10 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.skip_back
|
||||
import com.vitorpamplona.amethyst.commons.resources.skip_forward
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.service.playback.composable.SKIP_SECONDS
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
|
||||
private val FadeIn = fadeIn()
|
||||
private val FadeOut = fadeOut()
|
||||
|
||||
+4
-4
@@ -57,10 +57,10 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.call_settings_video_quality
|
||||
import com.vitorpamplona.amethyst.commons.resources.video_quality_auto
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.PinBottomIconSize
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size50Modifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
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 kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import java.util.Locale
|
||||
|
||||
+1
-1
@@ -42,8 +42,8 @@ import androidx.compose.ui.graphics.drawscope.Fill
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.media3.common.Player
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.service.playback.composable.MediaControllerState
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.conflate
|
||||
|
||||
+1
-1
@@ -40,6 +40,7 @@ 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.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
|
||||
@@ -48,7 +49,6 @@ import com.vitorpamplona.amethyst.service.playback.composable.controls.constrain
|
||||
import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.MediaItemData
|
||||
import com.vitorpamplona.amethyst.service.playback.composable.wavefront.Waveform
|
||||
import com.vitorpamplona.amethyst.ui.components.getActivity
|
||||
import com.vitorpamplona.amethyst.ui.theme.VoiceHeightModifier
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
@Composable
|
||||
|
||||
+6
-1
@@ -39,6 +39,7 @@ import androidx.media3.session.MediaSession
|
||||
import androidx.media3.session.MediaSessionService
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.commons.service.http.DynamicCallFactory
|
||||
import com.vitorpamplona.amethyst.commons.service.http.LocalBlossomCacheRedirectInterceptor
|
||||
import com.vitorpamplona.amethyst.service.playback.diskCache.VideoCache
|
||||
import com.vitorpamplona.amethyst.service.playback.pip.BackgroundMedia
|
||||
import com.vitorpamplona.amethyst.service.playback.playerPool.ExoPlayerBuilder
|
||||
@@ -59,7 +60,11 @@ class PlaybackService : MediaSessionService() {
|
||||
okHttpClient: DynamicCallFactory,
|
||||
blossomServerResolver: BlossomServerResolver,
|
||||
): MediaSessionPool {
|
||||
val dataSourceFactory = OkHttpDataSource.Factory(okHttpClient)
|
||||
// Marked as media so the local Blossom cache may serve these downloads.
|
||||
val dataSourceFactory =
|
||||
OkHttpDataSource
|
||||
.Factory(okHttpClient)
|
||||
.setDefaultRequestProperties(mapOf(LocalBlossomCacheRedirectInterceptor.MEDIA_HEADER to LocalBlossomCacheRedirectInterceptor.MEDIA))
|
||||
|
||||
val resolvingDataSourceFactory: DataSource.Factory =
|
||||
ResolvingDataSource.Factory(
|
||||
|
||||
+67
@@ -22,15 +22,20 @@ package com.vitorpamplona.amethyst.service.pow
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.os.PowerManager
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.service.pow.PoWEstimator
|
||||
import com.vitorpamplona.amethyst.commons.service.pow.PoWJobPhase
|
||||
import com.vitorpamplona.amethyst.commons.service.pow.PoWJobState
|
||||
import com.vitorpamplona.amethyst.service.foreground.FlowProgressForegroundService
|
||||
import com.vitorpamplona.amethyst.ui.pluralStringRes
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
@@ -44,6 +49,17 @@ import kotlinx.coroutines.launch
|
||||
* unmined resumes on the next app launch. Started on every enqueue (the app
|
||||
* is necessarily in the foreground then), stops itself when the queue drains.
|
||||
*
|
||||
* Staying schedulable is not enough on its own once the user also locks the
|
||||
* screen or leaves the app, so while it runs the service also:
|
||||
* - holds a partial wake lock, or the CPU suspends with the screen off and the
|
||||
* nonce search stalls until the phone is unlocked;
|
||||
* - keeps the relay pool (and Tor) up. The UI stops holding them ~30 s after
|
||||
* the app is backgrounded, so a post mined after that would only reach the
|
||||
* in-memory outbox — while its checkpoint and draft were already deleted —
|
||||
* and die with the process;
|
||||
* - lingers [PUBLISH_GRACE_MS] after the queue drains, so the event that was
|
||||
* just handed to the pool actually leaves before the process is frozen.
|
||||
*
|
||||
* The notification card (a live [androidx.core.app.NotificationCompat.ProgressStyle]) and all the
|
||||
* service lifecycle live in [FlowProgressForegroundService]; this subclass only maps mining state
|
||||
* to that card.
|
||||
@@ -68,17 +84,60 @@ class PowMiningForegroundService : FlowProgressForegroundService<ImmutableList<P
|
||||
private var sessionTotal = 0
|
||||
private var lastQueueSize = 0
|
||||
|
||||
// Whether the last non-empty queue still had a job publishing: a queue that drains by
|
||||
// publishing needs the grace for the event to leave; one drained by cancel does not.
|
||||
private var drainedByPublishing = false
|
||||
|
||||
override fun stopGraceMs() = if (drainedByPublishing) PUBLISH_GRACE_MS else 0L
|
||||
|
||||
override fun renderDraining() = Content(stringRes(this, R.string.pow_notification_sending), null, Bar.Indeterminate)
|
||||
|
||||
// Benchmarked once per service run (~250 ms, cached by the estimator).
|
||||
@Volatile
|
||||
private var hashRate: Double? = null
|
||||
|
||||
private var wakeLock: PowerManager.WakeLock? = null
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
running = true
|
||||
|
||||
wakeLock =
|
||||
runCatching {
|
||||
(getSystemService(Context.POWER_SERVICE) as PowerManager)
|
||||
.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "amethyst:pow-mining")
|
||||
.apply {
|
||||
setReferenceCounted(false)
|
||||
acquire(WAKE_LOCK_TIMEOUT_MS)
|
||||
}
|
||||
}.onFailure { Log.w(TAG, "Could not acquire the mining wake lock", it) }
|
||||
.getOrNull()
|
||||
|
||||
// Re-arm the timeout while the service lives: before Android 14 there is no FGS budget
|
||||
// and a hard nonce can mine for hours, but a lock with no timeout could outlive a
|
||||
// service that died without onDestroy.
|
||||
scope.launch {
|
||||
while (true) {
|
||||
delay(WAKE_LOCK_TIMEOUT_MS / 2)
|
||||
wakeLock?.let { if (it.isHeld) it.acquire(WAKE_LOCK_TIMEOUT_MS) }
|
||||
}
|
||||
}
|
||||
|
||||
// Mirrors what the resumed UI collects (AccountScreen's ManageRelayServices +
|
||||
// ManageWebOkHttp): subscribed, they keep the relay pool connected and, through
|
||||
// the connector's combine and the proxy-port provider, Tor up.
|
||||
val app = Amethyst.instance
|
||||
scope.launch(Dispatchers.IO) {
|
||||
launch { app.relayProxyClientConnector.relayServices.collect {} }
|
||||
launch { app.okHttpClients.defaultHttpClient.collect {} }
|
||||
launch { app.okHttpClients.defaultHttpClientWithoutProxy.collect {} }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
running = false
|
||||
wakeLock?.let { if (it.isHeld) it.release() }
|
||||
wakeLock = null
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
@@ -97,6 +156,7 @@ class PowMiningForegroundService : FlowProgressForegroundService<ImmutableList<P
|
||||
}
|
||||
|
||||
override fun onEmission(value: ImmutableList<PoWJobState>) {
|
||||
if (value.isNotEmpty()) drainedByPublishing = value.any { it.phase == PoWJobPhase.PUBLISHING }
|
||||
if (value.size > lastQueueSize) sessionTotal += value.size - lastQueueSize
|
||||
lastQueueSize = value.size
|
||||
}
|
||||
@@ -155,6 +215,13 @@ class PowMiningForegroundService : FlowProgressForegroundService<ImmutableList<P
|
||||
|
||||
private const val PROGRESS_REFRESH_MS = 30_000L
|
||||
|
||||
// Time for a just-mined post to connect, send and get its OK before the service
|
||||
// lets go of the relays and the process becomes freezable.
|
||||
private const val PUBLISH_GRACE_MS = 20_000L
|
||||
|
||||
// Safety net only: onDestroy releases it, and the service re-arms it while alive.
|
||||
private const val WAKE_LOCK_TIMEOUT_MS = 10 * 60 * 1000L
|
||||
|
||||
// Best-effort de-dup for start(): the queue calls it on EVERY enqueue.
|
||||
@Volatile
|
||||
private var running = false
|
||||
|
||||
+3
-3
@@ -97,15 +97,15 @@ import com.vitorpamplona.amethyst.commons.resources.relay_auth_why_read_venue
|
||||
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.pluralStringRes
|
||||
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.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
||||
import com.vitorpamplona.amethyst.ui.pluralStringRes
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.RelayIconFilter
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
|
||||
+2
-2
@@ -25,9 +25,9 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.notify.NotifyRequestsCache
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.payment_required_title
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
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.ui.stringRes
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl
|
||||
|
||||
@Composable
|
||||
|
||||
+4
-4
@@ -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.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.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size16dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
|
||||
@Composable
|
||||
fun NotifyRequestDialog(
|
||||
|
||||
+1
-1
@@ -21,6 +21,7 @@
|
||||
package com.vitorpamplona.amethyst.service.relayClient.reqCommand
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
||||
import com.vitorpamplona.amethyst.commons.model.cachedDvmAnnouncements
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.apps.recommendations.ProfileAppRecommendationsFilterAssembler
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.articles.ArticlesFilterAssembler
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.assemblers.CashuMintDirectoryFilterAssembler
|
||||
@@ -71,7 +72,6 @@ import com.vitorpamplona.amethyst.commons.relayClient.user.UserFinderFilterAssem
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.video.VideoFilterAssembler
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.wallet.OnchainZapsFilterAssembler
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.workouts.WorkoutsFilterAssembler
|
||||
import com.vitorpamplona.amethyst.model.cachedDvmAnnouncements
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountFilterAssembler
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountForegroundFilterAssembler
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.ChannelFinderFilterAssemblyGroup
|
||||
|
||||
+54
-1
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.metadata
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.defaults.Constants
|
||||
import com.vitorpamplona.amethyst.commons.model.User
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.account.metadata.filterBasicAccountInfoFromKeys
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.account.metadata.filterBookmarksAndReportsFromKey
|
||||
@@ -38,6 +39,24 @@ import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Which relays to ask for an account's own events, in order of what we actually know.
|
||||
*
|
||||
* Split out from the manager so the precedence can be tested without standing up an account:
|
||||
* the rule is the whole fix, and the case that broke — every set but [defaults] empty — is the
|
||||
* one no live account reproduces on demand.
|
||||
*/
|
||||
fun pickAccountDataRelays(
|
||||
home: Set<NormalizedRelayUrl>,
|
||||
inbox: Set<NormalizedRelayUrl>,
|
||||
defaults: Set<NormalizedRelayUrl>,
|
||||
): Set<NormalizedRelayUrl> =
|
||||
when {
|
||||
home.isNotEmpty() -> home
|
||||
inbox.isNotEmpty() -> inbox
|
||||
else -> defaults
|
||||
}
|
||||
|
||||
/**
|
||||
* Each account's own profile, lists and recent posts — for **every** logged-in account, in one
|
||||
* subscription.
|
||||
@@ -59,13 +78,40 @@ class AccountMetadataEoseManager(
|
||||
|
||||
fun relayFlow(query: AccountQueryState) = query.account.homeRelays.flow
|
||||
|
||||
/**
|
||||
* Where to look for an account's **own** profile, follow list and lists.
|
||||
*
|
||||
* Normally its home relays: where it publishes is where its own events are. But home relays
|
||||
* are NIP-65 write relays (plus private storage and local ones), and a kind:10002 that lists
|
||||
* none — read-only, or simply wrong — is a statement about publishing, not about where the
|
||||
* account's existing events can be found. Keyed on that set alone, such an account joined no
|
||||
* relay bucket at all and this subscription asked **nothing** on its behalf: its profile,
|
||||
* follows and every backed-up list stayed frozen at whatever was last stored, forever, with
|
||||
* nothing on screen to say why. A follow list replaced from another client never arrived, so
|
||||
* the backup guard could not even raise the conflict it exists to raise.
|
||||
*
|
||||
* The fallbacks only widen where we *read*; they never redirect a publish. Its inbox relays
|
||||
* come first — a client that rewrote these lists most likely also put them where the account
|
||||
* says to reach it — and the app's event finders last, which is what "we don't know" means
|
||||
* everywhere else.
|
||||
*
|
||||
* Deliberately not fixed in [relayListOrDefaultsWhenUnknown]: an empty list there means "they
|
||||
* told us: nothing", and defaulting it would override an explicit choice about publishing.
|
||||
*/
|
||||
fun relaysToQuery(query: AccountQueryState): Set<NormalizedRelayUrl> =
|
||||
pickAccountDataRelays(
|
||||
home = relayFlow(query).value,
|
||||
inbox = query.account.nip65RelayList.inboxFlow.value,
|
||||
defaults = Constants.eventFinderRelays,
|
||||
)
|
||||
|
||||
override fun updateFilter(
|
||||
keys: List<AccountQueryState>,
|
||||
since: SincePerRelayMap?,
|
||||
): List<RelayBasedFilter> {
|
||||
val accountsPerRelay = mutableMapOf<NormalizedRelayUrl, MutableList<AccountQueryState>>()
|
||||
keys.forEach { key ->
|
||||
relayFlow(key).value.forEach { relay ->
|
||||
relaysToQuery(key).forEach { relay ->
|
||||
accountsPerRelay.getOrPut(relay) { mutableListOf() }.add(key)
|
||||
}
|
||||
}
|
||||
@@ -117,6 +163,13 @@ class AccountMetadataEoseManager(
|
||||
key.account.scope.launch(Dispatchers.IO) {
|
||||
relayFlow(key).collectLatest { invalidateFilters() }
|
||||
},
|
||||
// The inbox list is watched too because [relaysToQuery] falls back to it:
|
||||
// an account with no home relays would otherwise keep querying whatever
|
||||
// the fallback resolved to at startup, deaf to its own NIP-65 changing.
|
||||
key.account.scope.launch(Dispatchers.IO) {
|
||||
key.account.nip65RelayList.inboxFlow
|
||||
.collectLatest { invalidateFilters() }
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -45,11 +45,11 @@ import com.vitorpamplona.amethyst.commons.resources.resource_usage_reason_conn_t
|
||||
import com.vitorpamplona.amethyst.commons.resources.resource_usage_reason_reconnects
|
||||
import com.vitorpamplona.amethyst.commons.resources.resource_usage_reason_wakelock
|
||||
import com.vitorpamplona.amethyst.commons.service.crashreports.DEV_REPORT_PUBKEY
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
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.pluralStringRes
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
+33
-13
@@ -23,12 +23,14 @@ package com.vitorpamplona.amethyst.service.resourceusage
|
||||
import android.os.SystemClock
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.ResponseBody
|
||||
import okio.Buffer
|
||||
import okio.ForwardingSource
|
||||
import okio.Source
|
||||
import okio.buffer
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
@@ -97,24 +99,29 @@ class UsageCountingInterceptor(
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
|
||||
// Loopback traffic (LocalBlossomCacheRedirectInterceptor rewrites cache
|
||||
// hits to 127.0.0.1) never touches the radio: counting it would inflate
|
||||
// the network numbers — and could trip the background-data alert — for
|
||||
// exactly the users who cache aggressively to SAVE data.
|
||||
// Loopback traffic never touches the radio: counting it would inflate the
|
||||
// network numbers — and could trip the background-data alert — for exactly
|
||||
// the users who cache aggressively to SAVE data. This interceptor is the
|
||||
// outermost one, so it sees URLs BEFORE LocalBlossomCacheRedirectInterceptor
|
||||
// rewrites them to 127.0.0.1; the host that was actually contacted is only
|
||||
// known from the response's request.
|
||||
if (isLoopback(request.url.host)) return chain.proceed(request)
|
||||
|
||||
val role = request.tag(UsageRoleTag::class.java)?.role ?: defaultRole
|
||||
|
||||
accountant.add(UsageKeys.netReqs(role, isMobile(), isForeground()), 1)
|
||||
bursts?.onHttpActivity()
|
||||
|
||||
val requestBytes = request.body?.contentLength()?.coerceAtLeast(0L) ?: 0L
|
||||
if (requestBytes > 0) {
|
||||
accountant.add(UsageKeys.net(role, isMobile(), isForeground(), received = false), requestBytes)
|
||||
}
|
||||
|
||||
val startedAtMs = nowMs()
|
||||
val response = chain.proceed(request)
|
||||
val response =
|
||||
try {
|
||||
chain.proceed(request)
|
||||
} catch (e: IOException) {
|
||||
countRequest(request, role)
|
||||
throw e
|
||||
}
|
||||
|
||||
if (isLoopback(response.request.url.host)) return response
|
||||
|
||||
countRequest(request, role)
|
||||
|
||||
return response
|
||||
.newBuilder()
|
||||
.body(
|
||||
@@ -131,6 +138,19 @@ class UsageCountingInterceptor(
|
||||
).build()
|
||||
}
|
||||
|
||||
private fun countRequest(
|
||||
request: Request,
|
||||
role: String,
|
||||
) {
|
||||
accountant.add(UsageKeys.netReqs(role, isMobile(), isForeground()), 1)
|
||||
bursts?.onHttpActivity()
|
||||
|
||||
val requestBytes = request.body?.contentLength()?.coerceAtLeast(0L) ?: 0L
|
||||
if (requestBytes > 0) {
|
||||
accountant.add(UsageKeys.net(role, isMobile(), isForeground(), received = false), requestBytes)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
/** OkHttp reports IPv6 hosts unbracketed ("::1"); keep the bracketed form defensively. */
|
||||
fun isLoopback(host: String): Boolean = host.startsWith("127.") || host == "localhost" || host == "::1" || host == "[::1]"
|
||||
|
||||
+1
-1
@@ -37,9 +37,9 @@ import com.vitorpamplona.amethyst.commons.resources.scheduled_posts_notification
|
||||
import com.vitorpamplona.amethyst.commons.resources.scheduled_posts_notification_sent_title
|
||||
import com.vitorpamplona.amethyst.commons.scheduledposts.ScheduledPost
|
||||
import com.vitorpamplona.amethyst.commons.scheduledposts.ScheduledPostNotifier
|
||||
import com.vitorpamplona.amethyst.commons.scheduledposts.extractContentPreview
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
import com.vitorpamplona.amethyst.ui.MainActivity
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.scheduledposts.extractContentPreview
|
||||
|
||||
/**
|
||||
* Android [ScheduledPostNotifier]: posts a user-visible system notification when a
|
||||
|
||||
@@ -27,7 +27,6 @@ import androidx.core.net.toUri
|
||||
import androidx.media3.common.MimeTypes
|
||||
import com.davotoula.lightcompressor.video.GifToMp4Converter
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.service.uploads.isAvif
|
||||
import com.vitorpamplona.amethyst.ui.components.util.MediaCompressorFileUtils
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import id.zelory.compressor.Compressor
|
||||
|
||||
+1
-1
@@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.service.uploads
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
|
||||
-1
@@ -32,7 +32,6 @@ import com.vitorpamplona.amethyst.commons.blurhash.toBlurhash
|
||||
import com.vitorpamplona.amethyst.commons.service.image.BlurhashWrapper
|
||||
import com.vitorpamplona.amethyst.commons.service.image.ThumbhashWrapper
|
||||
import com.vitorpamplona.amethyst.commons.thumbhash.toThumbhash
|
||||
import com.vitorpamplona.amethyst.service.uploads.isAvif
|
||||
import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
+2
-2
@@ -23,6 +23,8 @@ 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.mediaServers.ServerName
|
||||
import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerType
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.avif_metadata_strip_failed
|
||||
import com.vitorpamplona.amethyst.commons.resources.blossom_payment_required
|
||||
@@ -40,8 +42,6 @@ import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.uploads.UploadingState.UploadingFinalState
|
||||
import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader
|
||||
import com.vitorpamplona.amethyst.service.uploads.nip96.Nip96Uploader
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
|
||||
import com.vitorpamplona.quartz.nip98HttpAuth.HTTPAuthorizationEvent
|
||||
|
||||
+78
-21
@@ -28,17 +28,23 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.isValid
|
||||
import com.vitorpamplona.quartz.nipB7Blossom.BlossomServersEvent
|
||||
import com.vitorpamplona.quartz.nipB7Blossom.BlossomUri
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import com.vitorpamplona.quartz.utils.firstNotNullOrNullAsync
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.IO
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import okhttp3.OkHttpClient
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
class BlossomServerResolver(
|
||||
val loggedInUsers: () -> List<HexKey>,
|
||||
@@ -46,10 +52,20 @@ class BlossomServerResolver(
|
||||
val httpClientBuilder: IRoleBasedHttpClientBuilder,
|
||||
val useLocalBlossomCache: () -> Boolean = { false },
|
||||
val localCacheProbe: LocalBlossomCacheProbe? = null,
|
||||
private val scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO),
|
||||
) {
|
||||
val blossomHitCache: ServerHeadCache = ServerHeadCache()
|
||||
val uriToUrlCache = LruCache<String, BlossomUriServer>(200)
|
||||
|
||||
// Unresolvable URIs, with when they failed. Without this every retry — each ExoPlayer
|
||||
// load attempt blocks a loader thread on it — paid the full resolution timeout again.
|
||||
private val missCache = LruCache<String, Long>(200)
|
||||
|
||||
// One resolution per URI at a time: the feed preview, Coil and the player asking for the
|
||||
// same `blossom:` URI together share it instead of each firing its own HEADs and relay
|
||||
// subscriptions. Runs in [scope] so a caller that goes away doesn't cancel it for the rest.
|
||||
private val inFlight = ConcurrentHashMap<String, Deferred<BlossomUriServer?>>()
|
||||
|
||||
class BlossomUriServer(
|
||||
val uri: BlossomUri,
|
||||
val serverUrl: String,
|
||||
@@ -57,41 +73,71 @@ class BlossomServerResolver(
|
||||
|
||||
fun cachedFindServer(uriStr: String): BlossomUriServer? = uriToUrlCache[uriStr]
|
||||
|
||||
// Bumped by [clearCaches]. A resolution that started before a clear (e.g. it picked the
|
||||
// local cache just before the cache went down) must not write its stale answer back.
|
||||
private val generation = AtomicInteger(0)
|
||||
|
||||
/** Forgets every resolution, e.g. when the local cache comes up or goes down. */
|
||||
fun clearCaches() {
|
||||
generation.incrementAndGet()
|
||||
uriToUrlCache.evictAll()
|
||||
missCache.evictAll()
|
||||
blossomHitCache.cache.evictAll()
|
||||
}
|
||||
|
||||
suspend fun findServers(uriStr: String): BlossomUriServer? {
|
||||
uriToUrlCache[uriStr]?.let { return it }
|
||||
missCache[uriStr]?.let { failedAt ->
|
||||
if (TimeUtils.nowMillis() - failedAt < MISS_TTL_MS) return null
|
||||
}
|
||||
|
||||
// Confined to Dispatchers.IO: this is reached from Compose
|
||||
// `produceState`/`LaunchedEffect` (RichTextViewer, MarmotGroupIconDisplay),
|
||||
// which run on the main dispatcher. The pre-suspension work here —
|
||||
// BlossomUri parsing, LruCache lookups, the local-cache probe's client
|
||||
// build, and the server-list flow setup — must stay off the UI thread.
|
||||
val result =
|
||||
withContext(Dispatchers.IO) {
|
||||
withTimeoutOrNull(10000) {
|
||||
findServersInner(uriStr)
|
||||
val resolution =
|
||||
inFlight.computeIfAbsent(uriStr) {
|
||||
// Confined to Dispatchers.IO: this is reached from Compose
|
||||
// `produceState`/`LaunchedEffect` (RichTextViewer, MarmotGroupIconDisplay),
|
||||
// which run on the main dispatcher. The pre-suspension work here —
|
||||
// BlossomUri parsing, LruCache lookups, the local-cache probe's client
|
||||
// build, and the server-list flow setup — must stay off the UI thread.
|
||||
scope.async(Dispatchers.IO) {
|
||||
val startedAt = generation.get()
|
||||
try {
|
||||
val result = withTimeoutOrNull(RESOLVE_TIMEOUT_MS) { findServersInner(uriStr) }
|
||||
// Cleared while resolving: answer the waiting callers, remember nothing.
|
||||
if (startedAt == generation.get()) {
|
||||
if (result != null) {
|
||||
uriToUrlCache.put(uriStr, result)
|
||||
} else {
|
||||
missCache.put(uriStr, TimeUtils.nowMillis())
|
||||
}
|
||||
}
|
||||
result
|
||||
} finally {
|
||||
inFlight.remove(uriStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result != null) {
|
||||
uriToUrlCache.put(uriStr, result)
|
||||
}
|
||||
|
||||
return result
|
||||
return resolution.await()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
suspend fun findServersInner(uriStr: String): BlossomUriServer? {
|
||||
val uri = BlossomUri.parse(uriStr) ?: return null
|
||||
|
||||
if (useLocalBlossomCache() && localCacheProbe?.isAvailable() == true) {
|
||||
val expectedMimeType = mimeTypeMap[uri.extension]
|
||||
|
||||
// Same rule as the HTTP-level bridge, which Tor-routed clients don't carry: media the
|
||||
// user sends through Tor must not be handed to the local cache, which would fetch it
|
||||
// from the origin outside Tor.
|
||||
if (useLocalBlossomCache() && !isTorRouted(uri, expectedMimeType) && localCacheProbe?.isAvailable() == true) {
|
||||
return BlossomUriServer(uri, uri.toLocalCacheUrl(LocalBlossomCacheProbe.LOCAL_CACHE_BASE))
|
||||
}
|
||||
|
||||
val expectedMimeType = mimeTypeMap[uri.extension]
|
||||
val filename = uri.filename()
|
||||
|
||||
if (uri.servers.isNotEmpty()) {
|
||||
val workingUrl = firstWorkingUrl(uri.servers, filename, expectedMimeType, uri.size)
|
||||
// Bounded well under RESOLVE_TIMEOUT_MS so a hint server that drops packets
|
||||
// leaves time for the author's own server list below.
|
||||
val workingUrl = firstWorkingUrl(uri.servers, filename, expectedMimeType, uri.size, XS_TIMEOUT_MS)
|
||||
if (workingUrl != null) {
|
||||
return BlossomUriServer(uri, workingUrl)
|
||||
}
|
||||
@@ -135,13 +181,20 @@ class BlossomServerResolver(
|
||||
filename: String,
|
||||
expectedMimeType: String?,
|
||||
expectedSize: Long?,
|
||||
timeoutMs: Long = RESOLVE_TIMEOUT_MS,
|
||||
): String? =
|
||||
firstNotNullOrNullAsync(servers, 10000) {
|
||||
firstNotNullOrNullAsync(servers, timeoutMs) {
|
||||
blossomHitCache.urlIfServerHasFile(it, filename, expectedMimeType, expectedSize) { url ->
|
||||
client(url, expectedMimeType)
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether this blob's media type would be fetched through Tor from a regular (clearnet) server. */
|
||||
private fun isTorRouted(
|
||||
uri: BlossomUri,
|
||||
mimeType: String?,
|
||||
): Boolean = client(uri.toServerUrl() ?: "https://blossom.invalid/${uri.filename()}", mimeType).proxy != null
|
||||
|
||||
fun client(
|
||||
url: String,
|
||||
mimeType: String?,
|
||||
@@ -153,9 +206,13 @@ class BlossomServerResolver(
|
||||
else -> httpClientBuilder.okHttpClientForPreview(url)
|
||||
}
|
||||
|
||||
fun canResolve(scheme: String) = scheme == SCHEME
|
||||
fun canResolve(scheme: String) = scheme.equals(SCHEME, ignoreCase = true)
|
||||
|
||||
companion object {
|
||||
const val SCHEME = "blossom"
|
||||
|
||||
private const val RESOLVE_TIMEOUT_MS = 10_000L
|
||||
private const val XS_TIMEOUT_MS = 4_000L
|
||||
private const val MISS_TTL_MS = 30_000L
|
||||
}
|
||||
}
|
||||
|
||||
+19
@@ -32,6 +32,7 @@ import kotlinx.coroutines.withContext
|
||||
import okhttp3.Request
|
||||
import okhttp3.coroutines.executeAsync
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
/**
|
||||
* Discovers a local Blossom cache running on `http://127.0.0.1:24242` per
|
||||
@@ -48,6 +49,9 @@ class LocalBlossomCacheProbe(
|
||||
@Volatile
|
||||
private var cachedAtMs: Long = 0L
|
||||
|
||||
// Bumped by [markUnavailable]; lets a probe that raced it discard its stale positive result.
|
||||
private val unavailableMarks = AtomicInteger(0)
|
||||
|
||||
private val _available = MutableStateFlow(false)
|
||||
val available: StateFlow<Boolean> = _available
|
||||
|
||||
@@ -66,7 +70,11 @@ class LocalBlossomCacheProbe(
|
||||
return@withLock _available.value
|
||||
}
|
||||
|
||||
val startedAt = unavailableMarks.get()
|
||||
val newResult = probe()
|
||||
// A refused connection reported while this probe was in flight is newer evidence
|
||||
// than a HEAD that may have succeeded just before the cache went away.
|
||||
if (newResult && startedAt != unavailableMarks.get()) return@withLock false
|
||||
_available.value = newResult
|
||||
cachedAtMs = currentTimeMs()
|
||||
newResult
|
||||
@@ -80,6 +88,17 @@ class LocalBlossomCacheProbe(
|
||||
cachedAtMs = 0L
|
||||
}
|
||||
|
||||
/**
|
||||
* Records that the cache just refused a connection. Flipping [available] right away turns
|
||||
* the bridge off everywhere it is read, instead of letting every sha256 URL keep failing
|
||||
* against the dead loopback port until the positive TTL runs out and someone re-probes.
|
||||
*/
|
||||
fun markUnavailable() {
|
||||
unavailableMarks.incrementAndGet()
|
||||
cachedAtMs = currentTimeMs()
|
||||
_available.value = false
|
||||
}
|
||||
|
||||
// Confined to Dispatchers.IO because callers reach this through suspend
|
||||
// resolvers invoked from Compose `LaunchedEffect`/`produceState`, which run
|
||||
// on the main dispatcher: building the OkHttp client and issuing the HEAD
|
||||
|
||||
+11
-5
@@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.service.uploads.blossom.bud10
|
||||
|
||||
import androidx.collection.LruCache
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.coroutines.executeAsync
|
||||
@@ -56,13 +56,18 @@ class ServerHeadCache {
|
||||
|
||||
client(url).newCall(request).executeAsync().use { response ->
|
||||
if (!response.isSuccessful) {
|
||||
cache.put(url, HasFile.NoFile)
|
||||
// Only a definitive answer is remembered. A 5xx, 429 or auth challenge is
|
||||
// transient: caching it would hide this server for the app's lifetime.
|
||||
if (response.code == 404 || response.code == 410) {
|
||||
cache.put(url, HasFile.NoFile)
|
||||
}
|
||||
return HasFile.NoFile
|
||||
}
|
||||
|
||||
// Retrieve the "Content-Length" header
|
||||
val contentLength = response.header("Content-Length")?.toLongOrNull()
|
||||
val mimeType = response.header("Content-Type")?.toMediaType()?.toString()
|
||||
// type/subtype only: a `; charset=binary` parameter must not fail the match below.
|
||||
val mimeType = response.header("Content-Type")?.toMediaTypeOrNull()?.let { "${it.type}/${it.subtype}" }
|
||||
|
||||
return if (contentLength != null && mimeType != null) {
|
||||
val result = HasFile.TypeAndSize(mimeType, contentLength)
|
||||
@@ -75,7 +80,8 @@ class ServerHeadCache {
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
cache.put(url, HasFile.NoFile)
|
||||
// Offline, DNS failure, timeout: says nothing about the server having the file,
|
||||
// so it is not cached — a brief outage must not hide the blob until eviction.
|
||||
return HasFile.NoFile
|
||||
}
|
||||
}
|
||||
@@ -104,7 +110,7 @@ class ServerHeadCache {
|
||||
if (result.size == expectedSize) {
|
||||
return url
|
||||
}
|
||||
if (expectedSize == null && result.size > 0 && result.mimeType == expectedMimeType) {
|
||||
if (expectedSize == null && result.size > 0 && result.mimeType.equals(expectedMimeType, ignoreCase = true)) {
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -23,11 +23,11 @@ package com.vitorpamplona.amethyst.service.uploads.hls
|
||||
import android.content.Context
|
||||
import androidx.core.net.toUri
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName
|
||||
import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerType
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader
|
||||
import com.vitorpamplona.amethyst.service.uploads.nip96.Nip96Uploader
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
|
||||
import okhttp3.OkHttpClient
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.commons.actions.ConcordActions
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
||||
import com.vitorpamplona.amethyst.commons.model.navigation.Route
|
||||
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
|
||||
import com.vitorpamplona.amethyst.debugState
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
@@ -39,7 +40,6 @@ import com.vitorpamplona.amethyst.service.playback.composable.DEFAULT_MUTED_SETT
|
||||
import com.vitorpamplona.amethyst.service.playback.pip.BackgroundMedia
|
||||
import com.vitorpamplona.amethyst.ui.navigation.findParameterValue
|
||||
import com.vitorpamplona.amethyst.ui.navigation.findQueryParameterValue
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeForPointer
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.NowProvider
|
||||
|
||||
@@ -78,6 +78,19 @@ import com.vitorpamplona.amethyst.commons.resources.lightning_create_and_add_inv
|
||||
import com.vitorpamplona.amethyst.commons.resources.lightning_invoice
|
||||
import com.vitorpamplona.amethyst.commons.resources.what_s_on_your_mind
|
||||
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.creators.invoice.AddLnInvoiceButton
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.QuoteBorder
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightPage
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.replyModifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.subtleBorder
|
||||
import com.vitorpamplona.amethyst.service.playback.composable.VideoView
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromFiles
|
||||
@@ -86,25 +99,12 @@ import com.vitorpamplona.amethyst.ui.components.BechLink
|
||||
import com.vitorpamplona.amethyst.ui.components.LoadUrlPreview
|
||||
import com.vitorpamplona.amethyst.ui.components.OutlinedThinPaddingTextField
|
||||
import com.vitorpamplona.amethyst.ui.insets.imePaddingSafe
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.PostingTopBar
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.invoice.AddLnInvoiceButton
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.invoice.InvoiceRequest
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.uploads.ImageVideoDescription
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.ShowUserSuggestionList
|
||||
import com.vitorpamplona.amethyst.ui.note.types.ReplyRenderType
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size5dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.SuggestionListDefaultHeightPage
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.model.User
|
||||
import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_media_no_details
|
||||
import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_be_able_to_sign_events
|
||||
@@ -46,7 +47,6 @@ import com.vitorpamplona.amethyst.service.uploads.MediaCompressor
|
||||
import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator
|
||||
import com.vitorpamplona.amethyst.service.uploads.SuspendableConfirmation
|
||||
import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.MediaUploadTracker
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing
|
||||
|
||||
@@ -45,12 +45,12 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.copy_stack_to_clipboard
|
||||
import com.vitorpamplona.amethyst.commons.resources.error_dialog_button_ok
|
||||
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.toasts.ThrowableToastMsg
|
||||
import com.vitorpamplona.amethyst.ui.components.toasts.ThrowableToastMsg2
|
||||
import com.vitorpamplona.amethyst.ui.components.util.setText
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size16dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.PrintWriter
|
||||
import java.io.StringWriter
|
||||
|
||||
@@ -29,6 +29,8 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.commons.model.VideoPostKind
|
||||
import com.vitorpamplona.amethyst.commons.model.mediaServers.DEFAULT_MEDIA_SERVERS
|
||||
import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_media_no_details
|
||||
import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_be_able_to_sign_events
|
||||
@@ -40,8 +42,6 @@ import com.vitorpamplona.amethyst.service.uploads.MediaCompressor
|
||||
import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator
|
||||
import com.vitorpamplona.amethyst.service.uploads.SuspendableConfirmation
|
||||
import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
@@ -53,6 +53,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import com.vitorpamplona.amethyst.commons.model.VideoPostKind
|
||||
import com.vitorpamplona.amethyst.commons.model.mediaServers.DEFAULT_MEDIA_SERVERS
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.add_caption
|
||||
import com.vitorpamplona.amethyst.commons.resources.add_caption_example
|
||||
@@ -70,21 +71,20 @@ import com.vitorpamplona.amethyst.commons.resources.strip_metadata_description
|
||||
import com.vitorpamplona.amethyst.commons.resources.strip_metadata_label
|
||||
import com.vitorpamplona.amethyst.commons.resources.video_codec_h265_description
|
||||
import com.vitorpamplona.amethyst.commons.resources.video_codec_h265_label
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.TextSpinner
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.TitleExplainer
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.creators.contentWarning.SettingSwitchItem
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.ShowImageUploadGallery
|
||||
import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge
|
||||
import com.vitorpamplona.amethyst.ui.components.TextSpinner
|
||||
import com.vitorpamplona.amethyst.ui.components.TitleExplainer
|
||||
import com.vitorpamplona.amethyst.ui.insets.imePaddingSafe
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.PostingTopBar
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.SettingSwitchItem
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsRow
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size5dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
|
||||
+4
-4
@@ -75,13 +75,13 @@ import com.vitorpamplona.amethyst.commons.resources.twitter
|
||||
import com.vitorpamplona.amethyst.commons.resources.twitter_proof_url_template
|
||||
import com.vitorpamplona.amethyst.commons.resources.username
|
||||
import com.vitorpamplona.amethyst.commons.resources.website_url
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SavingTopBar
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectSingleFromGallery
|
||||
import com.vitorpamplona.amethyst.ui.insets.imePaddingSafe
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.SavingTopBar
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
|
||||
+1
-1
@@ -26,6 +26,7 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerType
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.avif_metadata_strip_failed
|
||||
import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_media_no_details
|
||||
@@ -40,7 +41,6 @@ import com.vitorpamplona.amethyst.service.uploads.MediaCompressor
|
||||
import com.vitorpamplona.amethyst.service.uploads.MetadataStripper
|
||||
import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader
|
||||
import com.vitorpamplona.amethyst.service.uploads.nip96.Nip96Uploader
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.nip39ExtIdentities.GitHubIdentity
|
||||
|
||||
+1
-1
@@ -29,9 +29,9 @@ import com.vitorpamplona.amethyst.commons.resources.cancel
|
||||
import com.vitorpamplona.amethyst.commons.resources.metadata_strip_failed_body
|
||||
import com.vitorpamplona.amethyst.commons.resources.metadata_strip_failed_title
|
||||
import com.vitorpamplona.amethyst.commons.resources.metadata_strip_failed_upload
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.service.uploads.ConfirmationCallbacks
|
||||
import com.vitorpamplona.amethyst.service.uploads.SuspendableConfirmation
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
|
||||
@Composable
|
||||
fun StrippingFailureDialog(confirmation: SuspendableConfirmation) {
|
||||
|
||||
+10
-10
@@ -63,20 +63,20 @@ import com.vitorpamplona.amethyst.commons.resources.bolt12_offers_section_explai
|
||||
import com.vitorpamplona.amethyst.commons.resources.delete_bolt12_offer
|
||||
import com.vitorpamplona.amethyst.commons.resources.invalid_bolt12_offer
|
||||
import com.vitorpamplona.amethyst.commons.resources.no_bolt12_offers_message
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SavingTopBar
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.SettingsCategoryFirstModifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.grayText
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.insets.imePaddingSafe
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.SavingTopBar
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.abbreviateBolt12Offer
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.SettingsCategory
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.SettingsCategoryFirstModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.grayText
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
|
||||
@Composable
|
||||
fun Bolt12OffersScreen(
|
||||
|
||||
+17
-13
@@ -61,6 +61,15 @@ import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.mediaServers.ui.MediaServerEditField
|
||||
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.navigation.Route
|
||||
import com.vitorpamplona.amethyst.commons.relays.ui.RelayDragState
|
||||
import com.vitorpamplona.amethyst.commons.relays.ui.draggableRelayItem
|
||||
import com.vitorpamplona.amethyst.commons.relays.ui.relayDragHandle
|
||||
import com.vitorpamplona.amethyst.commons.relays.ui.rememberRelayDragState
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.add_a_blossom_server
|
||||
import com.vitorpamplona.amethyst.commons.resources.add_media_server
|
||||
@@ -85,20 +94,15 @@ import com.vitorpamplona.amethyst.commons.resources.media_servers_upload_section
|
||||
import com.vitorpamplona.amethyst.commons.resources.my_blossom_data
|
||||
import com.vitorpamplona.amethyst.commons.resources.no_blossom_server_message
|
||||
import com.vitorpamplona.amethyst.commons.resources.use_default_servers
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.DoubleHorzSpacer
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.DoubleVertPadding
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.allGoodColor
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.grayText
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.warningColor
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayDragState
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.draggableRelayItem
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relayDragHandle
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.rememberRelayDragState
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleVertPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.allGoodColor
|
||||
import com.vitorpamplona.amethyst.ui.theme.grayText
|
||||
import com.vitorpamplona.amethyst.ui.theme.warningColor
|
||||
import com.vitorpamplona.quartz.utils.Rfc3986
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
|
||||
|
||||
+6
-6
@@ -57,13 +57,13 @@ import com.vitorpamplona.amethyst.commons.resources.local_blossom_cache_profile_
|
||||
import com.vitorpamplona.amethyst.commons.resources.media_servers
|
||||
import com.vitorpamplona.amethyst.commons.resources.use_local_blossom_cache
|
||||
import com.vitorpamplona.amethyst.commons.resources.use_local_blossom_cache_caption
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.allGoodColor
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.grayText
|
||||
import com.vitorpamplona.amethyst.ui.insets.imePaddingSafe
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.allGoodColor
|
||||
import com.vitorpamplona.amethyst.ui.theme.grayText
|
||||
|
||||
@Composable
|
||||
fun AllMediaServersScreen(
|
||||
@@ -212,7 +212,7 @@ fun MediaCacheSection(accountViewModel: AccountViewModel) {
|
||||
*/
|
||||
@Composable
|
||||
private fun CacheDetectionChip(accountViewModel: AccountViewModel) {
|
||||
val probeAvailable by accountViewModel.useLocalBlossomBridgeForProfilePics
|
||||
val probeAvailable by accountViewModel.localBlossomCacheDetected
|
||||
.collectAsStateWithLifecycle()
|
||||
|
||||
val color = if (probeAvailable) MaterialTheme.colorScheme.allGoodColor else MaterialTheme.colorScheme.grayText
|
||||
|
||||
+7
-7
@@ -98,6 +98,7 @@ import coil3.compose.SubcomposeAsyncImageContent
|
||||
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.navigation.Route
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.back
|
||||
import com.vitorpamplona.amethyst.commons.resources.blossom_delete_from_host
|
||||
@@ -127,16 +128,15 @@ import com.vitorpamplona.amethyst.commons.resources.manage_stored_files_empty
|
||||
import com.vitorpamplona.amethyst.commons.resources.my_blossom_data
|
||||
import com.vitorpamplona.amethyst.commons.resources.quick_action_share
|
||||
import com.vitorpamplona.amethyst.commons.resources.retry
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarExtensibleWithBackButton
|
||||
import com.vitorpamplona.amethyst.commons.ui.pluralStringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.allGoodColor
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.grayText
|
||||
import com.vitorpamplona.amethyst.service.playback.composable.VideoViewInner
|
||||
import com.vitorpamplona.amethyst.ui.components.util.setText
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarExtensibleWithBackButton
|
||||
import com.vitorpamplona.amethyst.ui.pluralStringRes
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.allGoodColor
|
||||
import com.vitorpamplona.amethyst.ui.theme.grayText
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip56Reports.ReportType
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
+8
-7
@@ -62,6 +62,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.mediaServers.ui.MediaServerEditField
|
||||
import com.vitorpamplona.amethyst.commons.model.navigation.Route
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.add_a_blossom_server
|
||||
import com.vitorpamplona.amethyst.commons.resources.blossom_import_add_url_label
|
||||
@@ -81,14 +83,13 @@ import com.vitorpamplona.amethyst.commons.resources.blossom_import_sources_secti
|
||||
import com.vitorpamplona.amethyst.commons.resources.blossom_import_start_button
|
||||
import com.vitorpamplona.amethyst.commons.resources.blossom_import_title
|
||||
import com.vitorpamplona.amethyst.commons.resources.delete_media_server
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton
|
||||
import com.vitorpamplona.amethyst.ui.pluralStringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton
|
||||
import com.vitorpamplona.amethyst.commons.ui.pluralStringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.allGoodColor
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.grayText
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.allGoodColor
|
||||
import com.vitorpamplona.amethyst.ui.theme.grayText
|
||||
|
||||
/** Vibrant palette for server monograms; picked deterministically from the host name. */
|
||||
private val ImportMonogramColors =
|
||||
|
||||
+2
@@ -25,6 +25,8 @@ import androidx.compose.runtime.Stable
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.commons.model.mediaServers.DEFAULT_MEDIA_SERVERS
|
||||
import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerType
|
||||
import com.vitorpamplona.amethyst.commons.service.upload.BlossomClient
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomMirrorQueue
|
||||
|
||||
+2
@@ -23,6 +23,8 @@ package com.vitorpamplona.amethyst.ui.actions.mediaServers
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName
|
||||
import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerType
|
||||
import com.vitorpamplona.amethyst.commons.service.http.IRoleBasedHttpClientBuilder
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
+3
-3
@@ -57,10 +57,10 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.blossom_sync_done
|
||||
import com.vitorpamplona.amethyst.commons.resources.blossom_syncing
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.allGoodColor
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.grayText
|
||||
import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomSyncState
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.allGoodColor
|
||||
import com.vitorpamplona.amethyst.ui.theme.grayText
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
/** How long the finished "Sync complete" banner lingers before it auto-dismisses. */
|
||||
|
||||
+3
-2
@@ -22,11 +22,12 @@ package com.vitorpamplona.amethyst.ui.actions.mediaServers
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.file_server
|
||||
import com.vitorpamplona.amethyst.commons.resources.file_server_description
|
||||
import com.vitorpamplona.amethyst.ui.components.TextSpinner
|
||||
import com.vitorpamplona.amethyst.ui.components.TitleExplainer
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.TextSpinner
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.TitleExplainer
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsRow
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
|
||||
+12
-12
@@ -69,23 +69,23 @@ import com.vitorpamplona.amethyst.commons.resources.nests_servers_relay_label
|
||||
import com.vitorpamplona.amethyst.commons.resources.nests_servers_remove
|
||||
import com.vitorpamplona.amethyst.commons.resources.nests_servers_title
|
||||
import com.vitorpamplona.amethyst.commons.resources.nests_servers_use_defaults
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SavingTopBar
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.DoubleVertPadding
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.SettingsCategoryFirstModifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.SettingsCategorySpacingModifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.grayText
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.AllMediaServersScreen
|
||||
import com.vitorpamplona.amethyst.ui.insets.imePaddingSafe
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.SavingTopBar
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.SettingsCategory
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.SettingsCategoryWithButton
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleVertPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.SettingsCategoryFirstModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.SettingsCategorySpacingModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.grayText
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.references.HttpUrlFormatter
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.nestsServers.NestsServersEvent
|
||||
|
||||
|
||||
+10
-10
@@ -61,19 +61,19 @@ import com.vitorpamplona.amethyst.commons.resources.payment_target_type
|
||||
import com.vitorpamplona.amethyst.commons.resources.payment_targets
|
||||
import com.vitorpamplona.amethyst.commons.resources.payment_targets_explainer
|
||||
import com.vitorpamplona.amethyst.commons.resources.payment_targets_section_explainer
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SavingTopBar
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.SettingsCategoryFirstModifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.grayText
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.insets.imePaddingSafe
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.SavingTopBar
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.SettingsCategory
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.SettingsCategoryFirstModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.grayText
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.quartz.experimental.nipA3.PaymentTarget
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -38,8 +38,8 @@ import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.record_a_message_description
|
||||
import com.vitorpamplona.amethyst.ui.components.ToggleableBox
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.ToggleableBox
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.record_a_message
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
|
||||
@Composable
|
||||
fun RecordVoiceButton(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user