diff --git a/.claude/skills/searchable-events/references/searchable-kinds.md b/.claude/skills/searchable-events/references/searchable-kinds.md index c8cab211a5..b2e3348127 100644 --- a/.claude/skills/searchable-events/references/searchable-kinds.md +++ b/.claude/skills/searchable-events/references/searchable-kinds.md @@ -4,7 +4,7 @@ Every concrete `SearchableEvent` implementor in Quartz, with the exact `indexabl expression. **Update this file in the same PR as any change to the searchable set or to an `indexableContent()` body** (see SKILL.md). Verified against the code 2026-09-17. -Counts: 133 concrete classes covering 136 kind values (`GitStatusEvent` spans 4 kinds; +Counts: 137 concrete classes covering 140 kind values (`GitStatusEvent` spans 4 kinds; kind 30063 has a collision — see the footnote). File paths are under `quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/`. @@ -65,6 +65,8 @@ Separator legend: **NL** = `joinToString("\n")`, **SP** = `joinToString(" ")`. | 9736 | Bolt12ZapEvent | nipB1Bolt12Zaps/zap | `content` | | 9737 | Bolt12ZapIntentEvent | nipB1Bolt12Zaps/intent | `content` | | 9802 | HighlightEvent | nip84Highlights | `listOfNotNull(comment(), context(), content)` NL | +| 9998 | ListHeaderEvent | experimental/decentralizedLists/header | `tags.searchableListContent()` NL — `names` (singular, plural), `titles` (singular, plural), `name`, `title`, `description`, `comments`, then every `t` value; ids/pubkeys/coordinates are left to tag filters | +| 9999 | ListItemEvent | experimental/decentralizedLists/item | same as 9998 | | 10003 | BookmarkListEvent | nip51Lists/bookmarkList | `listOfNotNull(title())` NL | | 10100 | AgentProfileEvent | buzz/agentProfiles | `profileOrNull()?.let { listOfNotNull(it.name, it.displayName).joinToString("\n") } ?: ""` | | 10154 | PodcastMetadataEvent | nipF4Podcasts/metadata | `listOfNotNull(title(), description())` NL | @@ -140,6 +142,8 @@ Separator legend: **NL** = `joinToString("\n")`, **SP** = `joinToString(" ")`. | 39092 | MediaStarterPackEvent | nip51Lists/mediaStarterPack | `listOfNotNull(title(), description())` NL | | 39307 | TextTrackEvent | nip71Video/textTrack | `content` (the WebVTT cue text) | | 39701 | WebBookmarkEvent | nipB0WebBookmarks | `listOfNotNull(title(), description())` NL | +| 39998 | AddressableListHeaderEvent | experimental/decentralizedLists/header | same as 9998 | +| 39999 | AddressableListItemEvent | experimental/decentralizedLists/item | same as 9998 | | 40002 | StreamMessageV2Event | buzz/stream | `content` | | 40100 | CanvasEvent | buzz/stream | `content` | | 45001 | ForumPostEvent | buzz/forum | `content` | diff --git a/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/ui/components/SelectNotificationProvider.kt b/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/ui/components/SelectNotificationProvider.kt index 78a6fcd112..114e9c5ee7 100644 --- a/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/ui/components/SelectNotificationProvider.kt +++ b/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/ui/components/SelectNotificationProvider.kt @@ -56,6 +56,7 @@ import com.halilibo.richtext.ui.material3.RichText import com.halilibo.richtext.ui.resolveDefaults import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.model.UiSettingsFlow import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.error_dialog_button_ok import com.vitorpamplona.amethyst.commons.resources.push_server_explainer @@ -71,7 +72,6 @@ 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.quartz.utils.Log diff --git a/amethyst/src/main/generated/baselineProfiles/baseline-prof.txt b/amethyst/src/main/generated/baselineProfiles/baseline-prof.txt index 751cfbc406..d760efd883 100644 --- a/amethyst/src/main/generated/baselineProfiles/baseline-prof.txt +++ b/amethyst/src/main/generated/baselineProfiles/baseline-prof.txt @@ -22282,7 +22282,7 @@ SPLcom/vitorpamplona/amethyst/AppModules;->getThumbnailDiskCache()Lcom/vitorpamp SPLcom/vitorpamplona/amethyst/AppModules;->getTorManager()Lcom/vitorpamplona/amethyst/ui/tor/TorManager; SPLcom/vitorpamplona/amethyst/AppModules;->getTorPrefs()Lcom/vitorpamplona/amethyst/model/preferences/TorSharedPreferences; SPLcom/vitorpamplona/amethyst/AppModules;->getUiPrefs()Lcom/vitorpamplona/amethyst/model/preferences/UiSharedPreferences; -SPLcom/vitorpamplona/amethyst/AppModules;->getUiState()Lcom/vitorpamplona/amethyst/ui/screen/UiSettingsState; +SPLcom/vitorpamplona/amethyst/AppModules;->getUiState()Lcom/vitorpamplona/amethyst/commons/state/UiSettingsState; SPLcom/vitorpamplona/amethyst/AppModules;->getVideoCache()Lcom/vitorpamplona/amethyst/service/playback/diskCache/VideoCache; SPLcom/vitorpamplona/amethyst/AppModules;->httpUsageInterceptor$lambda$0(Lcom/vitorpamplona/amethyst/AppModules;)Z SPLcom/vitorpamplona/amethyst/AppModules;->httpUsageInterceptor$lambda$1(Lcom/vitorpamplona/amethyst/AppModules;)Z @@ -22301,7 +22301,7 @@ SPLcom/vitorpamplona/amethyst/AppModules;->signerPermissionStore_delegate$lambda SPLcom/vitorpamplona/amethyst/AppModules;->thumbnailDiskCache_delegate$lambda$0(Lcom/vitorpamplona/amethyst/AppModules;)Lcom/vitorpamplona/amethyst/service/images/ThumbnailDiskCache; SPLcom/vitorpamplona/amethyst/AppModules;->torPrefs_delegate$lambda$0(Lcom/vitorpamplona/amethyst/AppModules;)Lcom/vitorpamplona/amethyst/model/preferences/TorSharedPreferences; SPLcom/vitorpamplona/amethyst/AppModules;->uiPrefs_delegate$lambda$0(Lcom/vitorpamplona/amethyst/AppModules;)Lcom/vitorpamplona/amethyst/model/preferences/UiSharedPreferences; -SPLcom/vitorpamplona/amethyst/AppModules;->uiState_delegate$lambda$0(Lcom/vitorpamplona/amethyst/AppModules;)Lcom/vitorpamplona/amethyst/ui/screen/UiSettingsState; +SPLcom/vitorpamplona/amethyst/AppModules;->uiState_delegate$lambda$0(Lcom/vitorpamplona/amethyst/AppModules;)Lcom/vitorpamplona/amethyst/commons/state/UiSettingsState; SPLcom/vitorpamplona/amethyst/AppModules;->videoCache_delegate$lambda$0(Lcom/vitorpamplona/amethyst/AppModules;)Lcom/vitorpamplona/amethyst/service/playback/diskCache/VideoCache; Lcom/vitorpamplona/amethyst/AppModules$$ExternalSyntheticLambda0; SPLcom/vitorpamplona/amethyst/AppModules$$ExternalSyntheticLambda0;->(Landroid/content/Context;)V @@ -23107,12 +23107,12 @@ Lcom/vitorpamplona/amethyst/favorites/FavoriteAppsRegistryKt; SPLcom/vitorpamplona/amethyst/favorites/FavoriteAppsRegistryKt;->()V SPLcom/vitorpamplona/amethyst/favorites/FavoriteAppsRegistryKt;->access$getFavoriteAppsDataStore(Landroid/content/Context;)Landroidx/datastore/core/DataStore; SPLcom/vitorpamplona/amethyst/favorites/FavoriteAppsRegistryKt;->getFavoriteAppsDataStore(Landroid/content/Context;)Landroidx/datastore/core/DataStore; -Lcom/vitorpamplona/amethyst/model/AccentColorType; -SPLcom/vitorpamplona/amethyst/model/AccentColorType;->$values()[Lcom/vitorpamplona/amethyst/model/AccentColorType; -SPLcom/vitorpamplona/amethyst/model/AccentColorType;->()V -SPLcom/vitorpamplona/amethyst/model/AccentColorType;->(Ljava/lang/String;III)V -SPLcom/vitorpamplona/amethyst/model/AccentColorType;->valueOf(Ljava/lang/String;)Lcom/vitorpamplona/amethyst/model/AccentColorType; -SPLcom/vitorpamplona/amethyst/model/AccentColorType;->values()[Lcom/vitorpamplona/amethyst/model/AccentColorType; +Lcom/vitorpamplona/amethyst/commons/model/AccentColorType; +SPLcom/vitorpamplona/amethyst/commons/model/AccentColorType;->$values()[Lcom/vitorpamplona/amethyst/commons/model/AccentColorType; +SPLcom/vitorpamplona/amethyst/commons/model/AccentColorType;->()V +SPLcom/vitorpamplona/amethyst/commons/model/AccentColorType;->(Ljava/lang/String;III)V +SPLcom/vitorpamplona/amethyst/commons/model/AccentColorType;->valueOf(Ljava/lang/String;)Lcom/vitorpamplona/amethyst/commons/model/AccentColorType; +SPLcom/vitorpamplona/amethyst/commons/model/AccentColorType;->values()[Lcom/vitorpamplona/amethyst/commons/model/AccentColorType; Lcom/vitorpamplona/amethyst/model/Account; Lcom/vitorpamplona/amethyst/model/AccountSettings; Lcom/vitorpamplona/amethyst/model/AntiSpamFilter; @@ -23121,44 +23121,44 @@ SPLcom/vitorpamplona/amethyst/model/AntiSpamFilter;->()V Lcom/vitorpamplona/amethyst/model/AntiSpamState; SPLcom/vitorpamplona/amethyst/model/AntiSpamState;->()V SPLcom/vitorpamplona/amethyst/model/AntiSpamState;->(Lcom/vitorpamplona/amethyst/model/AntiSpamFilter;)V -Lcom/vitorpamplona/amethyst/model/BooleanType; -SPLcom/vitorpamplona/amethyst/model/BooleanType;->$values()[Lcom/vitorpamplona/amethyst/model/BooleanType; -SPLcom/vitorpamplona/amethyst/model/BooleanType;->()V -SPLcom/vitorpamplona/amethyst/model/BooleanType;->(Ljava/lang/String;ILjava/lang/Boolean;II)V -SPLcom/vitorpamplona/amethyst/model/BooleanType;->valueOf(Ljava/lang/String;)Lcom/vitorpamplona/amethyst/model/BooleanType; -SPLcom/vitorpamplona/amethyst/model/BooleanType;->values()[Lcom/vitorpamplona/amethyst/model/BooleanType; +Lcom/vitorpamplona/amethyst/commons/model/BooleanType; +SPLcom/vitorpamplona/amethyst/commons/model/BooleanType;->$values()[Lcom/vitorpamplona/amethyst/commons/model/BooleanType; +SPLcom/vitorpamplona/amethyst/commons/model/BooleanType;->()V +SPLcom/vitorpamplona/amethyst/commons/model/BooleanType;->(Ljava/lang/String;ILjava/lang/Boolean;II)V +SPLcom/vitorpamplona/amethyst/commons/model/BooleanType;->valueOf(Ljava/lang/String;)Lcom/vitorpamplona/amethyst/commons/model/BooleanType; +SPLcom/vitorpamplona/amethyst/commons/model/BooleanType;->values()[Lcom/vitorpamplona/amethyst/commons/model/BooleanType; Lcom/vitorpamplona/amethyst/model/CachePruner; SPLcom/vitorpamplona/amethyst/model/CachePruner;->()V SPLcom/vitorpamplona/amethyst/model/CachePruner;->(Lcom/vitorpamplona/amethyst/model/LocalCache;)V Lcom/vitorpamplona/amethyst/model/CacheSearch; SPLcom/vitorpamplona/amethyst/model/CacheSearch;->()V SPLcom/vitorpamplona/amethyst/model/CacheSearch;->(Lcom/vitorpamplona/amethyst/model/LocalCache;)V -Lcom/vitorpamplona/amethyst/model/ConnectivityType; -SPLcom/vitorpamplona/amethyst/model/ConnectivityType;->$values()[Lcom/vitorpamplona/amethyst/model/ConnectivityType; -SPLcom/vitorpamplona/amethyst/model/ConnectivityType;->()V -SPLcom/vitorpamplona/amethyst/model/ConnectivityType;->(Ljava/lang/String;ILjava/lang/Boolean;II)V -SPLcom/vitorpamplona/amethyst/model/ConnectivityType;->valueOf(Ljava/lang/String;)Lcom/vitorpamplona/amethyst/model/ConnectivityType; -SPLcom/vitorpamplona/amethyst/model/ConnectivityType;->values()[Lcom/vitorpamplona/amethyst/model/ConnectivityType; +Lcom/vitorpamplona/amethyst/commons/model/ConnectivityType; +SPLcom/vitorpamplona/amethyst/commons/model/ConnectivityType;->$values()[Lcom/vitorpamplona/amethyst/commons/model/ConnectivityType; +SPLcom/vitorpamplona/amethyst/commons/model/ConnectivityType;->()V +SPLcom/vitorpamplona/amethyst/commons/model/ConnectivityType;->(Ljava/lang/String;ILjava/lang/Boolean;II)V +SPLcom/vitorpamplona/amethyst/commons/model/ConnectivityType;->valueOf(Ljava/lang/String;)Lcom/vitorpamplona/amethyst/commons/model/ConnectivityType; +SPLcom/vitorpamplona/amethyst/commons/model/ConnectivityType;->values()[Lcom/vitorpamplona/amethyst/commons/model/ConnectivityType; Lcom/vitorpamplona/amethyst/model/Dao; -Lcom/vitorpamplona/amethyst/model/FeatureSetType; -SPLcom/vitorpamplona/amethyst/model/FeatureSetType;->$values()[Lcom/vitorpamplona/amethyst/model/FeatureSetType; -SPLcom/vitorpamplona/amethyst/model/FeatureSetType;->()V -SPLcom/vitorpamplona/amethyst/model/FeatureSetType;->(Ljava/lang/String;III)V -SPLcom/vitorpamplona/amethyst/model/FeatureSetType;->valueOf(Ljava/lang/String;)Lcom/vitorpamplona/amethyst/model/FeatureSetType; -SPLcom/vitorpamplona/amethyst/model/FeatureSetType;->values()[Lcom/vitorpamplona/amethyst/model/FeatureSetType; -Lcom/vitorpamplona/amethyst/model/FontFamilyType; -SPLcom/vitorpamplona/amethyst/model/FontFamilyType;->$values()[Lcom/vitorpamplona/amethyst/model/FontFamilyType; -SPLcom/vitorpamplona/amethyst/model/FontFamilyType;->()V -SPLcom/vitorpamplona/amethyst/model/FontFamilyType;->(Ljava/lang/String;III)V -SPLcom/vitorpamplona/amethyst/model/FontFamilyType;->valueOf(Ljava/lang/String;)Lcom/vitorpamplona/amethyst/model/FontFamilyType; -SPLcom/vitorpamplona/amethyst/model/FontFamilyType;->values()[Lcom/vitorpamplona/amethyst/model/FontFamilyType; -Lcom/vitorpamplona/amethyst/model/FontSizeType; -SPLcom/vitorpamplona/amethyst/model/FontSizeType;->$values()[Lcom/vitorpamplona/amethyst/model/FontSizeType; -SPLcom/vitorpamplona/amethyst/model/FontSizeType;->()V -SPLcom/vitorpamplona/amethyst/model/FontSizeType;->(Ljava/lang/String;IFII)V -SPLcom/vitorpamplona/amethyst/model/FontSizeType;->getScale()F -SPLcom/vitorpamplona/amethyst/model/FontSizeType;->valueOf(Ljava/lang/String;)Lcom/vitorpamplona/amethyst/model/FontSizeType; -SPLcom/vitorpamplona/amethyst/model/FontSizeType;->values()[Lcom/vitorpamplona/amethyst/model/FontSizeType; +Lcom/vitorpamplona/amethyst/commons/model/FeatureSetType; +SPLcom/vitorpamplona/amethyst/commons/model/FeatureSetType;->$values()[Lcom/vitorpamplona/amethyst/commons/model/FeatureSetType; +SPLcom/vitorpamplona/amethyst/commons/model/FeatureSetType;->()V +SPLcom/vitorpamplona/amethyst/commons/model/FeatureSetType;->(Ljava/lang/String;III)V +SPLcom/vitorpamplona/amethyst/commons/model/FeatureSetType;->valueOf(Ljava/lang/String;)Lcom/vitorpamplona/amethyst/commons/model/FeatureSetType; +SPLcom/vitorpamplona/amethyst/commons/model/FeatureSetType;->values()[Lcom/vitorpamplona/amethyst/commons/model/FeatureSetType; +Lcom/vitorpamplona/amethyst/commons/model/FontFamilyType; +SPLcom/vitorpamplona/amethyst/commons/model/FontFamilyType;->$values()[Lcom/vitorpamplona/amethyst/commons/model/FontFamilyType; +SPLcom/vitorpamplona/amethyst/commons/model/FontFamilyType;->()V +SPLcom/vitorpamplona/amethyst/commons/model/FontFamilyType;->(Ljava/lang/String;III)V +SPLcom/vitorpamplona/amethyst/commons/model/FontFamilyType;->valueOf(Ljava/lang/String;)Lcom/vitorpamplona/amethyst/commons/model/FontFamilyType; +SPLcom/vitorpamplona/amethyst/commons/model/FontFamilyType;->values()[Lcom/vitorpamplona/amethyst/commons/model/FontFamilyType; +Lcom/vitorpamplona/amethyst/commons/model/FontSizeType; +SPLcom/vitorpamplona/amethyst/commons/model/FontSizeType;->$values()[Lcom/vitorpamplona/amethyst/commons/model/FontSizeType; +SPLcom/vitorpamplona/amethyst/commons/model/FontSizeType;->()V +SPLcom/vitorpamplona/amethyst/commons/model/FontSizeType;->(Ljava/lang/String;IFII)V +SPLcom/vitorpamplona/amethyst/commons/model/FontSizeType;->getScale()F +SPLcom/vitorpamplona/amethyst/commons/model/FontSizeType;->valueOf(Ljava/lang/String;)Lcom/vitorpamplona/amethyst/commons/model/FontSizeType; +SPLcom/vitorpamplona/amethyst/commons/model/FontSizeType;->values()[Lcom/vitorpamplona/amethyst/commons/model/FontSizeType; Lcom/vitorpamplona/amethyst/model/ILocalCache; Lcom/vitorpamplona/amethyst/model/LocalCache; SPLcom/vitorpamplona/amethyst/model/LocalCache;->()V @@ -23187,119 +23187,119 @@ Lcom/vitorpamplona/amethyst/model/LocalCacheFlow; SPLcom/vitorpamplona/amethyst/model/LocalCacheFlow;->()V SPLcom/vitorpamplona/amethyst/model/LocalCacheFlow;->()V SPLcom/vitorpamplona/amethyst/model/LocalCacheFlow;->getNewEventBundles()Lkotlinx/coroutines/flow/SharedFlow; -Lcom/vitorpamplona/amethyst/model/ProfileGalleryType; -SPLcom/vitorpamplona/amethyst/model/ProfileGalleryType;->$values()[Lcom/vitorpamplona/amethyst/model/ProfileGalleryType; -SPLcom/vitorpamplona/amethyst/model/ProfileGalleryType;->()V -SPLcom/vitorpamplona/amethyst/model/ProfileGalleryType;->(Ljava/lang/String;III)V -SPLcom/vitorpamplona/amethyst/model/ProfileGalleryType;->valueOf(Ljava/lang/String;)Lcom/vitorpamplona/amethyst/model/ProfileGalleryType; -SPLcom/vitorpamplona/amethyst/model/ProfileGalleryType;->values()[Lcom/vitorpamplona/amethyst/model/ProfileGalleryType; -Lcom/vitorpamplona/amethyst/model/ThemeType; -SPLcom/vitorpamplona/amethyst/model/ThemeType;->$values()[Lcom/vitorpamplona/amethyst/model/ThemeType; -SPLcom/vitorpamplona/amethyst/model/ThemeType;->()V -SPLcom/vitorpamplona/amethyst/model/ThemeType;->(Ljava/lang/String;III)V -SPLcom/vitorpamplona/amethyst/model/ThemeType;->valueOf(Ljava/lang/String;)Lcom/vitorpamplona/amethyst/model/ThemeType; -SPLcom/vitorpamplona/amethyst/model/ThemeType;->values()[Lcom/vitorpamplona/amethyst/model/ThemeType; -Lcom/vitorpamplona/amethyst/model/UiSettings; -SPLcom/vitorpamplona/amethyst/model/UiSettings;->()V -SPLcom/vitorpamplona/amethyst/model/UiSettings;->(Lcom/vitorpamplona/amethyst/model/ThemeType;Ljava/lang/String;Lcom/vitorpamplona/amethyst/model/ConnectivityType;Lcom/vitorpamplona/amethyst/model/ConnectivityType;Lcom/vitorpamplona/amethyst/model/BooleanType;Lcom/vitorpamplona/amethyst/model/ConnectivityType;Lcom/vitorpamplona/amethyst/model/BooleanType;Lcom/vitorpamplona/amethyst/model/ConnectivityType;ZZLcom/vitorpamplona/amethyst/model/FeatureSetType;Lcom/vitorpamplona/amethyst/model/ProfileGalleryType;Lcom/vitorpamplona/amethyst/model/BooleanType;Lcom/vitorpamplona/amethyst/model/BooleanType;Lcom/vitorpamplona/amethyst/model/BooleanType;ZZZZZZZZLcom/vitorpamplona/amethyst/model/BooleanType;Lcom/vitorpamplona/amethyst/model/AccentColorType;Lcom/vitorpamplona/amethyst/model/FontFamilyType;Lcom/vitorpamplona/amethyst/model/FontSizeType;Ljava/lang/String;Z)V -SPLcom/vitorpamplona/amethyst/model/UiSettings;->equals(Ljava/lang/Object;)Z -SPLcom/vitorpamplona/amethyst/model/UiSettings;->getAccentColor()Lcom/vitorpamplona/amethyst/model/AccentColorType; -SPLcom/vitorpamplona/amethyst/model/UiSettings;->getAutomaticallyCreateDrafts()Lcom/vitorpamplona/amethyst/model/BooleanType; -SPLcom/vitorpamplona/amethyst/model/UiSettings;->getAutomaticallyHideNavigationBars()Lcom/vitorpamplona/amethyst/model/BooleanType; -SPLcom/vitorpamplona/amethyst/model/UiSettings;->getAutomaticallyPlayVideos()Lcom/vitorpamplona/amethyst/model/BooleanType; -SPLcom/vitorpamplona/amethyst/model/UiSettings;->getAutomaticallyProposeAiImprovements()Lcom/vitorpamplona/amethyst/model/BooleanType; -SPLcom/vitorpamplona/amethyst/model/UiSettings;->getAutomaticallyShowImages()Lcom/vitorpamplona/amethyst/model/ConnectivityType; -SPLcom/vitorpamplona/amethyst/model/UiSettings;->getAutomaticallyShowProfilePictures()Lcom/vitorpamplona/amethyst/model/ConnectivityType; -SPLcom/vitorpamplona/amethyst/model/UiSettings;->getAutomaticallyShowUrlPreview()Lcom/vitorpamplona/amethyst/model/ConnectivityType; -SPLcom/vitorpamplona/amethyst/model/UiSettings;->getAutomaticallyStartPlayback()Lcom/vitorpamplona/amethyst/model/ConnectivityType; -SPLcom/vitorpamplona/amethyst/model/UiSettings;->getComposeSignature()Ljava/lang/String; -SPLcom/vitorpamplona/amethyst/model/UiSettings;->getDontAskForNotificationPermissions()Z -SPLcom/vitorpamplona/amethyst/model/UiSettings;->getDontShowOnchainPublicWarning()Z -SPLcom/vitorpamplona/amethyst/model/UiSettings;->getDontShowPushNotificationSelector()Z -SPLcom/vitorpamplona/amethyst/model/UiSettings;->getFeatureSet()Lcom/vitorpamplona/amethyst/model/FeatureSetType; -SPLcom/vitorpamplona/amethyst/model/UiSettings;->getFontFamily()Lcom/vitorpamplona/amethyst/model/FontFamilyType; -SPLcom/vitorpamplona/amethyst/model/UiSettings;->getFontSize()Lcom/vitorpamplona/amethyst/model/FontSizeType; -SPLcom/vitorpamplona/amethyst/model/UiSettings;->getGallerySet()Lcom/vitorpamplona/amethyst/model/ProfileGalleryType; -SPLcom/vitorpamplona/amethyst/model/UiSettings;->getPreferredLanguage()Ljava/lang/String; -SPLcom/vitorpamplona/amethyst/model/UiSettings;->getShowHomeConversationsTab()Z -SPLcom/vitorpamplona/amethyst/model/UiSettings;->getShowHomeEverythingTab()Z -SPLcom/vitorpamplona/amethyst/model/UiSettings;->getShowHomeNewThreadsTab()Z -SPLcom/vitorpamplona/amethyst/model/UiSettings;->getShowOnchainWallet()Z -SPLcom/vitorpamplona/amethyst/model/UiSettings;->getShowProfileAppRecommendations()Z -SPLcom/vitorpamplona/amethyst/model/UiSettings;->getShowProfileBadges()Z -SPLcom/vitorpamplona/amethyst/model/UiSettings;->getShowProfileFollowersFeed()Z -SPLcom/vitorpamplona/amethyst/model/UiSettings;->getShowProfileZapReceivedFeed()Z -SPLcom/vitorpamplona/amethyst/model/UiSettings;->getSuggestWorkoutsFromHealthConnect()Lcom/vitorpamplona/amethyst/model/BooleanType; -SPLcom/vitorpamplona/amethyst/model/UiSettings;->getTheme()Lcom/vitorpamplona/amethyst/model/ThemeType; -SPLcom/vitorpamplona/amethyst/model/UiSettings;->getUseTrackedBroadcasts()Lcom/vitorpamplona/amethyst/model/BooleanType; -Lcom/vitorpamplona/amethyst/model/UiSettings$$ExternalSyntheticLambda0; -SPLcom/vitorpamplona/amethyst/model/UiSettings$$ExternalSyntheticLambda0;->()V -Lcom/vitorpamplona/amethyst/model/UiSettings$$ExternalSyntheticLambda1; -SPLcom/vitorpamplona/amethyst/model/UiSettings$$ExternalSyntheticLambda1;->()V -Lcom/vitorpamplona/amethyst/model/UiSettings$$ExternalSyntheticLambda10; -SPLcom/vitorpamplona/amethyst/model/UiSettings$$ExternalSyntheticLambda10;->()V -Lcom/vitorpamplona/amethyst/model/UiSettings$$ExternalSyntheticLambda11; -SPLcom/vitorpamplona/amethyst/model/UiSettings$$ExternalSyntheticLambda11;->()V -Lcom/vitorpamplona/amethyst/model/UiSettings$$ExternalSyntheticLambda12; -SPLcom/vitorpamplona/amethyst/model/UiSettings$$ExternalSyntheticLambda12;->()V -Lcom/vitorpamplona/amethyst/model/UiSettings$$ExternalSyntheticLambda13; -SPLcom/vitorpamplona/amethyst/model/UiSettings$$ExternalSyntheticLambda13;->()V -Lcom/vitorpamplona/amethyst/model/UiSettings$$ExternalSyntheticLambda14; -SPLcom/vitorpamplona/amethyst/model/UiSettings$$ExternalSyntheticLambda14;->()V -Lcom/vitorpamplona/amethyst/model/UiSettings$$ExternalSyntheticLambda15; -SPLcom/vitorpamplona/amethyst/model/UiSettings$$ExternalSyntheticLambda15;->()V -Lcom/vitorpamplona/amethyst/model/UiSettings$$ExternalSyntheticLambda2; -SPLcom/vitorpamplona/amethyst/model/UiSettings$$ExternalSyntheticLambda2;->()V -Lcom/vitorpamplona/amethyst/model/UiSettings$$ExternalSyntheticLambda3; -SPLcom/vitorpamplona/amethyst/model/UiSettings$$ExternalSyntheticLambda3;->()V -Lcom/vitorpamplona/amethyst/model/UiSettings$$ExternalSyntheticLambda4; -SPLcom/vitorpamplona/amethyst/model/UiSettings$$ExternalSyntheticLambda4;->()V -Lcom/vitorpamplona/amethyst/model/UiSettings$$ExternalSyntheticLambda5; -SPLcom/vitorpamplona/amethyst/model/UiSettings$$ExternalSyntheticLambda5;->()V -Lcom/vitorpamplona/amethyst/model/UiSettings$$ExternalSyntheticLambda6; -SPLcom/vitorpamplona/amethyst/model/UiSettings$$ExternalSyntheticLambda6;->()V -Lcom/vitorpamplona/amethyst/model/UiSettings$$ExternalSyntheticLambda7; -SPLcom/vitorpamplona/amethyst/model/UiSettings$$ExternalSyntheticLambda7;->()V -Lcom/vitorpamplona/amethyst/model/UiSettings$$ExternalSyntheticLambda8; -SPLcom/vitorpamplona/amethyst/model/UiSettings$$ExternalSyntheticLambda8;->()V -Lcom/vitorpamplona/amethyst/model/UiSettings$$ExternalSyntheticLambda9; -SPLcom/vitorpamplona/amethyst/model/UiSettings$$ExternalSyntheticLambda9;->()V -Lcom/vitorpamplona/amethyst/model/UiSettings$Companion; -SPLcom/vitorpamplona/amethyst/model/UiSettings$Companion;->()V -SPLcom/vitorpamplona/amethyst/model/UiSettings$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -Lcom/vitorpamplona/amethyst/model/UiSettingsFlow; -SPLcom/vitorpamplona/amethyst/model/UiSettingsFlow;->()V -SPLcom/vitorpamplona/amethyst/model/UiSettingsFlow;->(Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/MutableStateFlow;)V -SPLcom/vitorpamplona/amethyst/model/UiSettingsFlow;->getAccentColor()Lkotlinx/coroutines/flow/MutableStateFlow; -SPLcom/vitorpamplona/amethyst/model/UiSettingsFlow;->getAutomaticallyPlayVideos()Lkotlinx/coroutines/flow/MutableStateFlow; -SPLcom/vitorpamplona/amethyst/model/UiSettingsFlow;->getAutomaticallyShowImages()Lkotlinx/coroutines/flow/MutableStateFlow; -SPLcom/vitorpamplona/amethyst/model/UiSettingsFlow;->getAutomaticallyShowProfilePictures()Lkotlinx/coroutines/flow/MutableStateFlow; -SPLcom/vitorpamplona/amethyst/model/UiSettingsFlow;->getAutomaticallyShowUrlPreview()Lkotlinx/coroutines/flow/MutableStateFlow; -SPLcom/vitorpamplona/amethyst/model/UiSettingsFlow;->getAutomaticallyStartPlayback()Lkotlinx/coroutines/flow/MutableStateFlow; -SPLcom/vitorpamplona/amethyst/model/UiSettingsFlow;->getFontFamily()Lkotlinx/coroutines/flow/MutableStateFlow; -SPLcom/vitorpamplona/amethyst/model/UiSettingsFlow;->getFontSize()Lkotlinx/coroutines/flow/MutableStateFlow; -SPLcom/vitorpamplona/amethyst/model/UiSettingsFlow;->getPreferredLanguage()Lkotlinx/coroutines/flow/MutableStateFlow; -SPLcom/vitorpamplona/amethyst/model/UiSettingsFlow;->getPropertyWatchFlow()Lkotlinx/coroutines/flow/Flow; -SPLcom/vitorpamplona/amethyst/model/UiSettingsFlow;->getTheme()Lkotlinx/coroutines/flow/MutableStateFlow; -SPLcom/vitorpamplona/amethyst/model/UiSettingsFlow;->toSettings()Lcom/vitorpamplona/amethyst/model/UiSettings; -Lcom/vitorpamplona/amethyst/model/UiSettingsFlow$Companion; -SPLcom/vitorpamplona/amethyst/model/UiSettingsFlow$Companion;->()V -SPLcom/vitorpamplona/amethyst/model/UiSettingsFlow$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -SPLcom/vitorpamplona/amethyst/model/UiSettingsFlow$Companion;->build(Lcom/vitorpamplona/amethyst/model/UiSettings;)Lcom/vitorpamplona/amethyst/model/UiSettingsFlow; -Lcom/vitorpamplona/amethyst/model/UiSettingsFlow$special$$inlined$combine$1; -SPLcom/vitorpamplona/amethyst/model/UiSettingsFlow$special$$inlined$combine$1;->([Lkotlinx/coroutines/flow/Flow;)V -SPLcom/vitorpamplona/amethyst/model/UiSettingsFlow$special$$inlined$combine$1;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -Lcom/vitorpamplona/amethyst/model/UiSettingsFlow$special$$inlined$combine$1$1; -SPLcom/vitorpamplona/amethyst/model/UiSettingsFlow$special$$inlined$combine$1$1;->(Lcom/vitorpamplona/amethyst/model/UiSettingsFlow$special$$inlined$combine$1;Lkotlin/coroutines/Continuation;)V -Lcom/vitorpamplona/amethyst/model/UiSettingsFlow$special$$inlined$combine$1$2; -SPLcom/vitorpamplona/amethyst/model/UiSettingsFlow$special$$inlined$combine$1$2;->([Lkotlinx/coroutines/flow/Flow;)V -SPLcom/vitorpamplona/amethyst/model/UiSettingsFlow$special$$inlined$combine$1$2;->invoke()Ljava/lang/Object; -SPLcom/vitorpamplona/amethyst/model/UiSettingsFlow$special$$inlined$combine$1$2;->invoke()[Ljava/lang/Object; -Lcom/vitorpamplona/amethyst/model/UiSettingsFlow$special$$inlined$combine$1$3; -SPLcom/vitorpamplona/amethyst/model/UiSettingsFlow$special$$inlined$combine$1$3;->(Lkotlin/coroutines/Continuation;)V -SPLcom/vitorpamplona/amethyst/model/UiSettingsFlow$special$$inlined$combine$1$3;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; -SPLcom/vitorpamplona/amethyst/model/UiSettingsFlow$special$$inlined$combine$1$3;->invoke(Lkotlinx/coroutines/flow/FlowCollector;[Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -SPLcom/vitorpamplona/amethyst/model/UiSettingsFlow$special$$inlined$combine$1$3;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; +Lcom/vitorpamplona/amethyst/commons/model/ProfileGalleryType; +SPLcom/vitorpamplona/amethyst/commons/model/ProfileGalleryType;->$values()[Lcom/vitorpamplona/amethyst/commons/model/ProfileGalleryType; +SPLcom/vitorpamplona/amethyst/commons/model/ProfileGalleryType;->()V +SPLcom/vitorpamplona/amethyst/commons/model/ProfileGalleryType;->(Ljava/lang/String;III)V +SPLcom/vitorpamplona/amethyst/commons/model/ProfileGalleryType;->valueOf(Ljava/lang/String;)Lcom/vitorpamplona/amethyst/commons/model/ProfileGalleryType; +SPLcom/vitorpamplona/amethyst/commons/model/ProfileGalleryType;->values()[Lcom/vitorpamplona/amethyst/commons/model/ProfileGalleryType; +Lcom/vitorpamplona/amethyst/commons/model/ThemeType; +SPLcom/vitorpamplona/amethyst/commons/model/ThemeType;->$values()[Lcom/vitorpamplona/amethyst/commons/model/ThemeType; +SPLcom/vitorpamplona/amethyst/commons/model/ThemeType;->()V +SPLcom/vitorpamplona/amethyst/commons/model/ThemeType;->(Ljava/lang/String;III)V +SPLcom/vitorpamplona/amethyst/commons/model/ThemeType;->valueOf(Ljava/lang/String;)Lcom/vitorpamplona/amethyst/commons/model/ThemeType; +SPLcom/vitorpamplona/amethyst/commons/model/ThemeType;->values()[Lcom/vitorpamplona/amethyst/commons/model/ThemeType; +Lcom/vitorpamplona/amethyst/commons/model/UiSettings; +SPLcom/vitorpamplona/amethyst/commons/model/UiSettings;->()V +SPLcom/vitorpamplona/amethyst/commons/model/UiSettings;->(Lcom/vitorpamplona/amethyst/commons/model/ThemeType;Ljava/lang/String;Lcom/vitorpamplona/amethyst/commons/model/ConnectivityType;Lcom/vitorpamplona/amethyst/commons/model/ConnectivityType;Lcom/vitorpamplona/amethyst/commons/model/BooleanType;Lcom/vitorpamplona/amethyst/commons/model/ConnectivityType;Lcom/vitorpamplona/amethyst/commons/model/BooleanType;Lcom/vitorpamplona/amethyst/commons/model/ConnectivityType;ZZLcom/vitorpamplona/amethyst/commons/model/FeatureSetType;Lcom/vitorpamplona/amethyst/commons/model/ProfileGalleryType;Lcom/vitorpamplona/amethyst/commons/model/BooleanType;Lcom/vitorpamplona/amethyst/commons/model/BooleanType;Lcom/vitorpamplona/amethyst/commons/model/BooleanType;ZZZZZZZZLcom/vitorpamplona/amethyst/commons/model/BooleanType;Lcom/vitorpamplona/amethyst/commons/model/AccentColorType;Lcom/vitorpamplona/amethyst/commons/model/FontFamilyType;Lcom/vitorpamplona/amethyst/commons/model/FontSizeType;Ljava/lang/String;Z)V +SPLcom/vitorpamplona/amethyst/commons/model/UiSettings;->equals(Ljava/lang/Object;)Z +SPLcom/vitorpamplona/amethyst/commons/model/UiSettings;->getAccentColor()Lcom/vitorpamplona/amethyst/commons/model/AccentColorType; +SPLcom/vitorpamplona/amethyst/commons/model/UiSettings;->getAutomaticallyCreateDrafts()Lcom/vitorpamplona/amethyst/commons/model/BooleanType; +SPLcom/vitorpamplona/amethyst/commons/model/UiSettings;->getAutomaticallyHideNavigationBars()Lcom/vitorpamplona/amethyst/commons/model/BooleanType; +SPLcom/vitorpamplona/amethyst/commons/model/UiSettings;->getAutomaticallyPlayVideos()Lcom/vitorpamplona/amethyst/commons/model/BooleanType; +SPLcom/vitorpamplona/amethyst/commons/model/UiSettings;->getAutomaticallyProposeAiImprovements()Lcom/vitorpamplona/amethyst/commons/model/BooleanType; +SPLcom/vitorpamplona/amethyst/commons/model/UiSettings;->getAutomaticallyShowImages()Lcom/vitorpamplona/amethyst/commons/model/ConnectivityType; +SPLcom/vitorpamplona/amethyst/commons/model/UiSettings;->getAutomaticallyShowProfilePictures()Lcom/vitorpamplona/amethyst/commons/model/ConnectivityType; +SPLcom/vitorpamplona/amethyst/commons/model/UiSettings;->getAutomaticallyShowUrlPreview()Lcom/vitorpamplona/amethyst/commons/model/ConnectivityType; +SPLcom/vitorpamplona/amethyst/commons/model/UiSettings;->getAutomaticallyStartPlayback()Lcom/vitorpamplona/amethyst/commons/model/ConnectivityType; +SPLcom/vitorpamplona/amethyst/commons/model/UiSettings;->getComposeSignature()Ljava/lang/String; +SPLcom/vitorpamplona/amethyst/commons/model/UiSettings;->getDontAskForNotificationPermissions()Z +SPLcom/vitorpamplona/amethyst/commons/model/UiSettings;->getDontShowOnchainPublicWarning()Z +SPLcom/vitorpamplona/amethyst/commons/model/UiSettings;->getDontShowPushNotificationSelector()Z +SPLcom/vitorpamplona/amethyst/commons/model/UiSettings;->getFeatureSet()Lcom/vitorpamplona/amethyst/commons/model/FeatureSetType; +SPLcom/vitorpamplona/amethyst/commons/model/UiSettings;->getFontFamily()Lcom/vitorpamplona/amethyst/commons/model/FontFamilyType; +SPLcom/vitorpamplona/amethyst/commons/model/UiSettings;->getFontSize()Lcom/vitorpamplona/amethyst/commons/model/FontSizeType; +SPLcom/vitorpamplona/amethyst/commons/model/UiSettings;->getGallerySet()Lcom/vitorpamplona/amethyst/commons/model/ProfileGalleryType; +SPLcom/vitorpamplona/amethyst/commons/model/UiSettings;->getPreferredLanguage()Ljava/lang/String; +SPLcom/vitorpamplona/amethyst/commons/model/UiSettings;->getShowHomeConversationsTab()Z +SPLcom/vitorpamplona/amethyst/commons/model/UiSettings;->getShowHomeEverythingTab()Z +SPLcom/vitorpamplona/amethyst/commons/model/UiSettings;->getShowHomeNewThreadsTab()Z +SPLcom/vitorpamplona/amethyst/commons/model/UiSettings;->getShowOnchainWallet()Z +SPLcom/vitorpamplona/amethyst/commons/model/UiSettings;->getShowProfileAppRecommendations()Z +SPLcom/vitorpamplona/amethyst/commons/model/UiSettings;->getShowProfileBadges()Z +SPLcom/vitorpamplona/amethyst/commons/model/UiSettings;->getShowProfileFollowersFeed()Z +SPLcom/vitorpamplona/amethyst/commons/model/UiSettings;->getShowProfileZapReceivedFeed()Z +SPLcom/vitorpamplona/amethyst/commons/model/UiSettings;->getSuggestWorkoutsFromHealthConnect()Lcom/vitorpamplona/amethyst/commons/model/BooleanType; +SPLcom/vitorpamplona/amethyst/commons/model/UiSettings;->getTheme()Lcom/vitorpamplona/amethyst/commons/model/ThemeType; +SPLcom/vitorpamplona/amethyst/commons/model/UiSettings;->getUseTrackedBroadcasts()Lcom/vitorpamplona/amethyst/commons/model/BooleanType; +Lcom/vitorpamplona/amethyst/commons/model/UiSettings$$ExternalSyntheticLambda0; +SPLcom/vitorpamplona/amethyst/commons/model/UiSettings$$ExternalSyntheticLambda0;->()V +Lcom/vitorpamplona/amethyst/commons/model/UiSettings$$ExternalSyntheticLambda1; +SPLcom/vitorpamplona/amethyst/commons/model/UiSettings$$ExternalSyntheticLambda1;->()V +Lcom/vitorpamplona/amethyst/commons/model/UiSettings$$ExternalSyntheticLambda10; +SPLcom/vitorpamplona/amethyst/commons/model/UiSettings$$ExternalSyntheticLambda10;->()V +Lcom/vitorpamplona/amethyst/commons/model/UiSettings$$ExternalSyntheticLambda11; +SPLcom/vitorpamplona/amethyst/commons/model/UiSettings$$ExternalSyntheticLambda11;->()V +Lcom/vitorpamplona/amethyst/commons/model/UiSettings$$ExternalSyntheticLambda12; +SPLcom/vitorpamplona/amethyst/commons/model/UiSettings$$ExternalSyntheticLambda12;->()V +Lcom/vitorpamplona/amethyst/commons/model/UiSettings$$ExternalSyntheticLambda13; +SPLcom/vitorpamplona/amethyst/commons/model/UiSettings$$ExternalSyntheticLambda13;->()V +Lcom/vitorpamplona/amethyst/commons/model/UiSettings$$ExternalSyntheticLambda14; +SPLcom/vitorpamplona/amethyst/commons/model/UiSettings$$ExternalSyntheticLambda14;->()V +Lcom/vitorpamplona/amethyst/commons/model/UiSettings$$ExternalSyntheticLambda15; +SPLcom/vitorpamplona/amethyst/commons/model/UiSettings$$ExternalSyntheticLambda15;->()V +Lcom/vitorpamplona/amethyst/commons/model/UiSettings$$ExternalSyntheticLambda2; +SPLcom/vitorpamplona/amethyst/commons/model/UiSettings$$ExternalSyntheticLambda2;->()V +Lcom/vitorpamplona/amethyst/commons/model/UiSettings$$ExternalSyntheticLambda3; +SPLcom/vitorpamplona/amethyst/commons/model/UiSettings$$ExternalSyntheticLambda3;->()V +Lcom/vitorpamplona/amethyst/commons/model/UiSettings$$ExternalSyntheticLambda4; +SPLcom/vitorpamplona/amethyst/commons/model/UiSettings$$ExternalSyntheticLambda4;->()V +Lcom/vitorpamplona/amethyst/commons/model/UiSettings$$ExternalSyntheticLambda5; +SPLcom/vitorpamplona/amethyst/commons/model/UiSettings$$ExternalSyntheticLambda5;->()V +Lcom/vitorpamplona/amethyst/commons/model/UiSettings$$ExternalSyntheticLambda6; +SPLcom/vitorpamplona/amethyst/commons/model/UiSettings$$ExternalSyntheticLambda6;->()V +Lcom/vitorpamplona/amethyst/commons/model/UiSettings$$ExternalSyntheticLambda7; +SPLcom/vitorpamplona/amethyst/commons/model/UiSettings$$ExternalSyntheticLambda7;->()V +Lcom/vitorpamplona/amethyst/commons/model/UiSettings$$ExternalSyntheticLambda8; +SPLcom/vitorpamplona/amethyst/commons/model/UiSettings$$ExternalSyntheticLambda8;->()V +Lcom/vitorpamplona/amethyst/commons/model/UiSettings$$ExternalSyntheticLambda9; +SPLcom/vitorpamplona/amethyst/commons/model/UiSettings$$ExternalSyntheticLambda9;->()V +Lcom/vitorpamplona/amethyst/commons/model/UiSettings$Companion; +SPLcom/vitorpamplona/amethyst/commons/model/UiSettings$Companion;->()V +SPLcom/vitorpamplona/amethyst/commons/model/UiSettings$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +Lcom/vitorpamplona/amethyst/commons/model/UiSettingsFlow; +SPLcom/vitorpamplona/amethyst/commons/model/UiSettingsFlow;->()V +SPLcom/vitorpamplona/amethyst/commons/model/UiSettingsFlow;->(Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlinx/coroutines/flow/MutableStateFlow;)V +SPLcom/vitorpamplona/amethyst/commons/model/UiSettingsFlow;->getAccentColor()Lkotlinx/coroutines/flow/MutableStateFlow; +SPLcom/vitorpamplona/amethyst/commons/model/UiSettingsFlow;->getAutomaticallyPlayVideos()Lkotlinx/coroutines/flow/MutableStateFlow; +SPLcom/vitorpamplona/amethyst/commons/model/UiSettingsFlow;->getAutomaticallyShowImages()Lkotlinx/coroutines/flow/MutableStateFlow; +SPLcom/vitorpamplona/amethyst/commons/model/UiSettingsFlow;->getAutomaticallyShowProfilePictures()Lkotlinx/coroutines/flow/MutableStateFlow; +SPLcom/vitorpamplona/amethyst/commons/model/UiSettingsFlow;->getAutomaticallyShowUrlPreview()Lkotlinx/coroutines/flow/MutableStateFlow; +SPLcom/vitorpamplona/amethyst/commons/model/UiSettingsFlow;->getAutomaticallyStartPlayback()Lkotlinx/coroutines/flow/MutableStateFlow; +SPLcom/vitorpamplona/amethyst/commons/model/UiSettingsFlow;->getFontFamily()Lkotlinx/coroutines/flow/MutableStateFlow; +SPLcom/vitorpamplona/amethyst/commons/model/UiSettingsFlow;->getFontSize()Lkotlinx/coroutines/flow/MutableStateFlow; +SPLcom/vitorpamplona/amethyst/commons/model/UiSettingsFlow;->getPreferredLanguage()Lkotlinx/coroutines/flow/MutableStateFlow; +SPLcom/vitorpamplona/amethyst/commons/model/UiSettingsFlow;->getPropertyWatchFlow()Lkotlinx/coroutines/flow/Flow; +SPLcom/vitorpamplona/amethyst/commons/model/UiSettingsFlow;->getTheme()Lkotlinx/coroutines/flow/MutableStateFlow; +SPLcom/vitorpamplona/amethyst/commons/model/UiSettingsFlow;->toSettings()Lcom/vitorpamplona/amethyst/commons/model/UiSettings; +Lcom/vitorpamplona/amethyst/commons/model/UiSettingsFlow$Companion; +SPLcom/vitorpamplona/amethyst/commons/model/UiSettingsFlow$Companion;->()V +SPLcom/vitorpamplona/amethyst/commons/model/UiSettingsFlow$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V +SPLcom/vitorpamplona/amethyst/commons/model/UiSettingsFlow$Companion;->build(Lcom/vitorpamplona/amethyst/commons/model/UiSettings;)Lcom/vitorpamplona/amethyst/commons/model/UiSettingsFlow; +Lcom/vitorpamplona/amethyst/commons/model/UiSettingsFlow$special$$inlined$combine$1; +SPLcom/vitorpamplona/amethyst/commons/model/UiSettingsFlow$special$$inlined$combine$1;->([Lkotlinx/coroutines/flow/Flow;)V +SPLcom/vitorpamplona/amethyst/commons/model/UiSettingsFlow$special$$inlined$combine$1;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +Lcom/vitorpamplona/amethyst/commons/model/UiSettingsFlow$special$$inlined$combine$1$1; +SPLcom/vitorpamplona/amethyst/commons/model/UiSettingsFlow$special$$inlined$combine$1$1;->(Lcom/vitorpamplona/amethyst/commons/model/UiSettingsFlow$special$$inlined$combine$1;Lkotlin/coroutines/Continuation;)V +Lcom/vitorpamplona/amethyst/commons/model/UiSettingsFlow$special$$inlined$combine$1$2; +SPLcom/vitorpamplona/amethyst/commons/model/UiSettingsFlow$special$$inlined$combine$1$2;->([Lkotlinx/coroutines/flow/Flow;)V +SPLcom/vitorpamplona/amethyst/commons/model/UiSettingsFlow$special$$inlined$combine$1$2;->invoke()Ljava/lang/Object; +SPLcom/vitorpamplona/amethyst/commons/model/UiSettingsFlow$special$$inlined$combine$1$2;->invoke()[Ljava/lang/Object; +Lcom/vitorpamplona/amethyst/commons/model/UiSettingsFlow$special$$inlined$combine$1$3; +SPLcom/vitorpamplona/amethyst/commons/model/UiSettingsFlow$special$$inlined$combine$1$3;->(Lkotlin/coroutines/Continuation;)V +SPLcom/vitorpamplona/amethyst/commons/model/UiSettingsFlow$special$$inlined$combine$1$3;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; +SPLcom/vitorpamplona/amethyst/commons/model/UiSettingsFlow$special$$inlined$combine$1$3;->invoke(Lkotlinx/coroutines/flow/FlowCollector;[Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +SPLcom/vitorpamplona/amethyst/commons/model/UiSettingsFlow$special$$inlined$combine$1$3;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; Lcom/vitorpamplona/amethyst/model/accountsCache/AccountCacheState; SPLcom/vitorpamplona/amethyst/model/accountsCache/AccountCacheState;->()V SPLcom/vitorpamplona/amethyst/model/accountsCache/AccountCacheState;->(Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lcom/vitorpamplona/amethyst/model/LocalCache;Lcom/vitorpamplona/quartz/nip01Core/relay/client/INostrClient;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lcom/vitorpamplona/amethyst/commons/connectedApps/signers/NostrSignerPermissionStore;Lcom/vitorpamplona/amethyst/commons/connectedApps/nip46/Nip46ClientStore;)V @@ -23460,7 +23460,7 @@ SPLcom/vitorpamplona/amethyst/model/preferences/UISharedPreferencesKt;-> SPLcom/vitorpamplona/amethyst/model/preferences/UISharedPreferencesKt;->getSharedPreferencesDataStore(Landroid/content/Context;)Landroidx/datastore/core/DataStore; Lcom/vitorpamplona/amethyst/model/preferences/UiSharedPreferences; SPLcom/vitorpamplona/amethyst/model/preferences/UiSharedPreferences;->()V -SPLcom/vitorpamplona/amethyst/model/preferences/UiSharedPreferences;->(Lcom/vitorpamplona/amethyst/model/UiSettings;Landroid/content/Context;Lkotlinx/coroutines/CoroutineScope;)V +SPLcom/vitorpamplona/amethyst/model/preferences/UiSharedPreferences;->(Lcom/vitorpamplona/amethyst/commons/model/UiSettings;Landroid/content/Context;Lkotlinx/coroutines/CoroutineScope;)V SPLcom/vitorpamplona/amethyst/model/preferences/UiSharedPreferences;->access$applyLanguage(Lcom/vitorpamplona/amethyst/model/preferences/UiSharedPreferences;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; SPLcom/vitorpamplona/amethyst/model/preferences/UiSharedPreferences;->access$getUI_ACCENT_COLOR$cp()Landroidx/datastore/preferences/core/Preferences$Key; SPLcom/vitorpamplona/amethyst/model/preferences/UiSharedPreferences;->access$getUI_AUTOMATICALLY_CREATE_DRAFTS$cp()Landroidx/datastore/preferences/core/Preferences$Key; @@ -23493,7 +23493,7 @@ SPLcom/vitorpamplona/amethyst/model/preferences/UiSharedPreferences;->access$get SPLcom/vitorpamplona/amethyst/model/preferences/UiSharedPreferences;->access$getUI_USE_TRACKED_BROADCASTS$cp()Landroidx/datastore/preferences/core/Preferences$Key; SPLcom/vitorpamplona/amethyst/model/preferences/UiSharedPreferences;->applyLanguage(Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; SPLcom/vitorpamplona/amethyst/model/preferences/UiSharedPreferences;->getContext()Landroid/content/Context; -SPLcom/vitorpamplona/amethyst/model/preferences/UiSharedPreferences;->getValue()Lcom/vitorpamplona/amethyst/model/UiSettingsFlow; +SPLcom/vitorpamplona/amethyst/model/preferences/UiSharedPreferences;->getValue()Lcom/vitorpamplona/amethyst/commons/model/UiSettingsFlow; Lcom/vitorpamplona/amethyst/model/preferences/UiSharedPreferences$Companion; SPLcom/vitorpamplona/amethyst/model/preferences/UiSharedPreferences$Companion;->()V SPLcom/vitorpamplona/amethyst/model/preferences/UiSharedPreferences$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -23526,13 +23526,13 @@ SPLcom/vitorpamplona/amethyst/model/preferences/UiSharedPreferences$Companion;-> SPLcom/vitorpamplona/amethyst/model/preferences/UiSharedPreferences$Companion;->getUI_SUGGEST_WORKOUTS_FROM_HEALTH_CONNECT()Landroidx/datastore/preferences/core/Preferences$Key; SPLcom/vitorpamplona/amethyst/model/preferences/UiSharedPreferences$Companion;->getUI_THEME()Landroidx/datastore/preferences/core/Preferences$Key; SPLcom/vitorpamplona/amethyst/model/preferences/UiSharedPreferences$Companion;->getUI_USE_TRACKED_BROADCASTS()Landroidx/datastore/preferences/core/Preferences$Key; -SPLcom/vitorpamplona/amethyst/model/preferences/UiSharedPreferences$Companion;->save(Lcom/vitorpamplona/amethyst/model/UiSettings;Landroid/content/Context;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +SPLcom/vitorpamplona/amethyst/model/preferences/UiSharedPreferences$Companion;->save(Lcom/vitorpamplona/amethyst/commons/model/UiSettings;Landroid/content/Context;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; SPLcom/vitorpamplona/amethyst/model/preferences/UiSharedPreferences$Companion;->uiPreferences(Landroid/content/Context;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; Lcom/vitorpamplona/amethyst/model/preferences/UiSharedPreferences$Companion$save$1; SPLcom/vitorpamplona/amethyst/model/preferences/UiSharedPreferences$Companion$save$1;->(Lcom/vitorpamplona/amethyst/model/preferences/UiSharedPreferences$Companion;Lkotlin/coroutines/Continuation;)V SPLcom/vitorpamplona/amethyst/model/preferences/UiSharedPreferences$Companion$save$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; Lcom/vitorpamplona/amethyst/model/preferences/UiSharedPreferences$Companion$save$2; -SPLcom/vitorpamplona/amethyst/model/preferences/UiSharedPreferences$Companion$save$2;->(Lcom/vitorpamplona/amethyst/model/UiSettings;Lkotlin/coroutines/Continuation;)V +SPLcom/vitorpamplona/amethyst/model/preferences/UiSharedPreferences$Companion$save$2;->(Lcom/vitorpamplona/amethyst/commons/model/UiSettings;Lkotlin/coroutines/Continuation;)V SPLcom/vitorpamplona/amethyst/model/preferences/UiSharedPreferences$Companion$save$2;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; SPLcom/vitorpamplona/amethyst/model/preferences/UiSharedPreferences$Companion$save$2;->invoke(Landroidx/datastore/preferences/core/MutablePreferences;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; SPLcom/vitorpamplona/amethyst/model/preferences/UiSharedPreferences$Companion$save$2;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; @@ -23549,7 +23549,7 @@ SPLcom/vitorpamplona/amethyst/model/preferences/UiSharedPreferences$languageUpda Lcom/vitorpamplona/amethyst/model/preferences/UiSharedPreferences$saving$1; SPLcom/vitorpamplona/amethyst/model/preferences/UiSharedPreferences$saving$1;->(Lcom/vitorpamplona/amethyst/model/preferences/UiSharedPreferences;Lkotlin/coroutines/Continuation;)V SPLcom/vitorpamplona/amethyst/model/preferences/UiSharedPreferences$saving$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; -SPLcom/vitorpamplona/amethyst/model/preferences/UiSharedPreferences$saving$1;->invoke(Lcom/vitorpamplona/amethyst/model/UiSettings;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +SPLcom/vitorpamplona/amethyst/model/preferences/UiSharedPreferences$saving$1;->invoke(Lcom/vitorpamplona/amethyst/commons/model/UiSettings;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; SPLcom/vitorpamplona/amethyst/model/preferences/UiSharedPreferences$saving$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; SPLcom/vitorpamplona/amethyst/model/preferences/UiSharedPreferences$saving$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; Lcom/vitorpamplona/amethyst/model/privacyOptions/IRoleBasedHttpClientBuilder; @@ -24832,49 +24832,49 @@ SPLcom/vitorpamplona/amethyst/ui/screen/ComposableSingletons$AccountScreenKt;->l Lcom/vitorpamplona/amethyst/ui/screen/ComposableSingletons$AccountScreenKt$$ExternalSyntheticLambda0; SPLcom/vitorpamplona/amethyst/ui/screen/ComposableSingletons$AccountScreenKt$$ExternalSyntheticLambda0;->()V SPLcom/vitorpamplona/amethyst/ui/screen/ComposableSingletons$AccountScreenKt$$ExternalSyntheticLambda0;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; -Lcom/vitorpamplona/amethyst/ui/screen/UiSettingsState; -SPLcom/vitorpamplona/amethyst/ui/screen/UiSettingsState;->()V -SPLcom/vitorpamplona/amethyst/ui/screen/UiSettingsState;->(Lcom/vitorpamplona/amethyst/model/UiSettingsFlow;Lkotlinx/coroutines/flow/StateFlow;Lkotlinx/coroutines/CoroutineScope;)V -Lcom/vitorpamplona/amethyst/ui/screen/UiSettingsState$WhenMappings; -SPLcom/vitorpamplona/amethyst/ui/screen/UiSettingsState$WhenMappings;->()V -Lcom/vitorpamplona/amethyst/ui/screen/UiSettingsState$showImages$1; -SPLcom/vitorpamplona/amethyst/ui/screen/UiSettingsState$showImages$1;->(Lkotlin/coroutines/Continuation;)V -SPLcom/vitorpamplona/amethyst/ui/screen/UiSettingsState$showImages$1;->invoke(Lcom/vitorpamplona/amethyst/model/ConnectivityType;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object; -SPLcom/vitorpamplona/amethyst/ui/screen/UiSettingsState$showImages$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; -SPLcom/vitorpamplona/amethyst/ui/screen/UiSettingsState$showImages$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; -Lcom/vitorpamplona/amethyst/ui/screen/UiSettingsState$showImages$1$WhenMappings; -SPLcom/vitorpamplona/amethyst/ui/screen/UiSettingsState$showImages$1$WhenMappings;->()V -Lcom/vitorpamplona/amethyst/ui/screen/UiSettingsState$showProfilePictures$1; -SPLcom/vitorpamplona/amethyst/ui/screen/UiSettingsState$showProfilePictures$1;->(Lkotlin/coroutines/Continuation;)V -SPLcom/vitorpamplona/amethyst/ui/screen/UiSettingsState$showProfilePictures$1;->invoke(Lcom/vitorpamplona/amethyst/model/ConnectivityType;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object; -SPLcom/vitorpamplona/amethyst/ui/screen/UiSettingsState$showProfilePictures$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; -SPLcom/vitorpamplona/amethyst/ui/screen/UiSettingsState$showProfilePictures$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; -Lcom/vitorpamplona/amethyst/ui/screen/UiSettingsState$showProfilePictures$1$WhenMappings; -SPLcom/vitorpamplona/amethyst/ui/screen/UiSettingsState$showProfilePictures$1$WhenMappings;->()V -Lcom/vitorpamplona/amethyst/ui/screen/UiSettingsState$showUrlPreview$1; -SPLcom/vitorpamplona/amethyst/ui/screen/UiSettingsState$showUrlPreview$1;->(Lkotlin/coroutines/Continuation;)V -SPLcom/vitorpamplona/amethyst/ui/screen/UiSettingsState$showUrlPreview$1;->invoke(Lcom/vitorpamplona/amethyst/model/ConnectivityType;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object; -SPLcom/vitorpamplona/amethyst/ui/screen/UiSettingsState$showUrlPreview$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; -SPLcom/vitorpamplona/amethyst/ui/screen/UiSettingsState$showUrlPreview$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; -Lcom/vitorpamplona/amethyst/ui/screen/UiSettingsState$showUrlPreview$1$WhenMappings; -SPLcom/vitorpamplona/amethyst/ui/screen/UiSettingsState$showUrlPreview$1$WhenMappings;->()V -Lcom/vitorpamplona/amethyst/ui/screen/UiSettingsState$special$$inlined$map$1; -SPLcom/vitorpamplona/amethyst/ui/screen/UiSettingsState$special$$inlined$map$1;->(Lkotlinx/coroutines/flow/Flow;)V -SPLcom/vitorpamplona/amethyst/ui/screen/UiSettingsState$special$$inlined$map$1;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -Lcom/vitorpamplona/amethyst/ui/screen/UiSettingsState$special$$inlined$map$1$1; -SPLcom/vitorpamplona/amethyst/ui/screen/UiSettingsState$special$$inlined$map$1$1;->(Lcom/vitorpamplona/amethyst/ui/screen/UiSettingsState$special$$inlined$map$1;Lkotlin/coroutines/Continuation;)V -Lcom/vitorpamplona/amethyst/ui/screen/UiSettingsState$special$$inlined$map$1$2; -SPLcom/vitorpamplona/amethyst/ui/screen/UiSettingsState$special$$inlined$map$1$2;->(Lkotlinx/coroutines/flow/FlowCollector;)V -SPLcom/vitorpamplona/amethyst/ui/screen/UiSettingsState$special$$inlined$map$1$2;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -Lcom/vitorpamplona/amethyst/ui/screen/UiSettingsState$special$$inlined$map$1$2$1; -SPLcom/vitorpamplona/amethyst/ui/screen/UiSettingsState$special$$inlined$map$1$2$1;->(Lcom/vitorpamplona/amethyst/ui/screen/UiSettingsState$special$$inlined$map$1$2;Lkotlin/coroutines/Continuation;)V -Lcom/vitorpamplona/amethyst/ui/screen/UiSettingsState$startVideoPlayback$1; -SPLcom/vitorpamplona/amethyst/ui/screen/UiSettingsState$startVideoPlayback$1;->(Lkotlin/coroutines/Continuation;)V -SPLcom/vitorpamplona/amethyst/ui/screen/UiSettingsState$startVideoPlayback$1;->invoke(Lcom/vitorpamplona/amethyst/model/ConnectivityType;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object; -SPLcom/vitorpamplona/amethyst/ui/screen/UiSettingsState$startVideoPlayback$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; -SPLcom/vitorpamplona/amethyst/ui/screen/UiSettingsState$startVideoPlayback$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; -Lcom/vitorpamplona/amethyst/ui/screen/UiSettingsState$startVideoPlayback$1$WhenMappings; -SPLcom/vitorpamplona/amethyst/ui/screen/UiSettingsState$startVideoPlayback$1$WhenMappings;->()V +Lcom/vitorpamplona/amethyst/commons/state/UiSettingsState; +SPLcom/vitorpamplona/amethyst/commons/state/UiSettingsState;->()V +SPLcom/vitorpamplona/amethyst/commons/state/UiSettingsState;->(Lcom/vitorpamplona/amethyst/commons/model/UiSettingsFlow;Lkotlinx/coroutines/flow/StateFlow;Lkotlinx/coroutines/CoroutineScope;)V +Lcom/vitorpamplona/amethyst/commons/state/UiSettingsState$WhenMappings; +SPLcom/vitorpamplona/amethyst/commons/state/UiSettingsState$WhenMappings;->()V +Lcom/vitorpamplona/amethyst/commons/state/UiSettingsState$showImages$1; +SPLcom/vitorpamplona/amethyst/commons/state/UiSettingsState$showImages$1;->(Lkotlin/coroutines/Continuation;)V +SPLcom/vitorpamplona/amethyst/commons/state/UiSettingsState$showImages$1;->invoke(Lcom/vitorpamplona/amethyst/commons/model/ConnectivityType;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object; +SPLcom/vitorpamplona/amethyst/commons/state/UiSettingsState$showImages$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; +SPLcom/vitorpamplona/amethyst/commons/state/UiSettingsState$showImages$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; +Lcom/vitorpamplona/amethyst/commons/state/UiSettingsState$showImages$1$WhenMappings; +SPLcom/vitorpamplona/amethyst/commons/state/UiSettingsState$showImages$1$WhenMappings;->()V +Lcom/vitorpamplona/amethyst/commons/state/UiSettingsState$showProfilePictures$1; +SPLcom/vitorpamplona/amethyst/commons/state/UiSettingsState$showProfilePictures$1;->(Lkotlin/coroutines/Continuation;)V +SPLcom/vitorpamplona/amethyst/commons/state/UiSettingsState$showProfilePictures$1;->invoke(Lcom/vitorpamplona/amethyst/commons/model/ConnectivityType;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object; +SPLcom/vitorpamplona/amethyst/commons/state/UiSettingsState$showProfilePictures$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; +SPLcom/vitorpamplona/amethyst/commons/state/UiSettingsState$showProfilePictures$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; +Lcom/vitorpamplona/amethyst/commons/state/UiSettingsState$showProfilePictures$1$WhenMappings; +SPLcom/vitorpamplona/amethyst/commons/state/UiSettingsState$showProfilePictures$1$WhenMappings;->()V +Lcom/vitorpamplona/amethyst/commons/state/UiSettingsState$showUrlPreview$1; +SPLcom/vitorpamplona/amethyst/commons/state/UiSettingsState$showUrlPreview$1;->(Lkotlin/coroutines/Continuation;)V +SPLcom/vitorpamplona/amethyst/commons/state/UiSettingsState$showUrlPreview$1;->invoke(Lcom/vitorpamplona/amethyst/commons/model/ConnectivityType;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object; +SPLcom/vitorpamplona/amethyst/commons/state/UiSettingsState$showUrlPreview$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; +SPLcom/vitorpamplona/amethyst/commons/state/UiSettingsState$showUrlPreview$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; +Lcom/vitorpamplona/amethyst/commons/state/UiSettingsState$showUrlPreview$1$WhenMappings; +SPLcom/vitorpamplona/amethyst/commons/state/UiSettingsState$showUrlPreview$1$WhenMappings;->()V +Lcom/vitorpamplona/amethyst/commons/state/UiSettingsState$special$$inlined$map$1; +SPLcom/vitorpamplona/amethyst/commons/state/UiSettingsState$special$$inlined$map$1;->(Lkotlinx/coroutines/flow/Flow;)V +SPLcom/vitorpamplona/amethyst/commons/state/UiSettingsState$special$$inlined$map$1;->collect(Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +Lcom/vitorpamplona/amethyst/commons/state/UiSettingsState$special$$inlined$map$1$1; +SPLcom/vitorpamplona/amethyst/commons/state/UiSettingsState$special$$inlined$map$1$1;->(Lcom/vitorpamplona/amethyst/commons/state/UiSettingsState$special$$inlined$map$1;Lkotlin/coroutines/Continuation;)V +Lcom/vitorpamplona/amethyst/commons/state/UiSettingsState$special$$inlined$map$1$2; +SPLcom/vitorpamplona/amethyst/commons/state/UiSettingsState$special$$inlined$map$1$2;->(Lkotlinx/coroutines/flow/FlowCollector;)V +SPLcom/vitorpamplona/amethyst/commons/state/UiSettingsState$special$$inlined$map$1$2;->emit(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +Lcom/vitorpamplona/amethyst/commons/state/UiSettingsState$special$$inlined$map$1$2$1; +SPLcom/vitorpamplona/amethyst/commons/state/UiSettingsState$special$$inlined$map$1$2$1;->(Lcom/vitorpamplona/amethyst/commons/state/UiSettingsState$special$$inlined$map$1$2;Lkotlin/coroutines/Continuation;)V +Lcom/vitorpamplona/amethyst/commons/state/UiSettingsState$startVideoPlayback$1; +SPLcom/vitorpamplona/amethyst/commons/state/UiSettingsState$startVideoPlayback$1;->(Lkotlin/coroutines/Continuation;)V +SPLcom/vitorpamplona/amethyst/commons/state/UiSettingsState$startVideoPlayback$1;->invoke(Lcom/vitorpamplona/amethyst/commons/model/ConnectivityType;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object; +SPLcom/vitorpamplona/amethyst/commons/state/UiSettingsState$startVideoPlayback$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; +SPLcom/vitorpamplona/amethyst/commons/state/UiSettingsState$startVideoPlayback$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; +Lcom/vitorpamplona/amethyst/commons/state/UiSettingsState$startVideoPlayback$1$WhenMappings; +SPLcom/vitorpamplona/amethyst/commons/state/UiSettingsState$startVideoPlayback$1$WhenMappings;->()V Lcom/vitorpamplona/amethyst/ui/screen/loggedIn/apps/recommendations/datasource/ProfileAppRecommendationsFilterAssembler; SPLcom/vitorpamplona/amethyst/ui/screen/loggedIn/apps/recommendations/datasource/ProfileAppRecommendationsFilterAssembler;->()V SPLcom/vitorpamplona/amethyst/ui/screen/loggedIn/apps/recommendations/datasource/ProfileAppRecommendationsFilterAssembler;->(Lcom/vitorpamplona/quartz/nip01Core/relay/client/INostrClient;)V @@ -25638,23 +25638,23 @@ 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;->()V -SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->AmethystTheme$lambda$0(Landroidx/compose/runtime/State;)Lcom/vitorpamplona/amethyst/model/ThemeType; -SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->AmethystTheme$lambda$1(Landroidx/compose/runtime/State;)Lcom/vitorpamplona/amethyst/model/AccentColorType; +SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->AmethystTheme$lambda$0(Landroidx/compose/runtime/State;)Lcom/vitorpamplona/amethyst/commons/model/ThemeType; +SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->AmethystTheme$lambda$1(Landroidx/compose/runtime/State;)Lcom/vitorpamplona/amethyst/commons/model/AccentColorType; SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->AmethystTheme$lambda$10$0(Landroid/view/View;ZLandroidx/compose/material3/ColorScheme;)Lkotlin/Unit; -SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->AmethystTheme$lambda$2(Landroidx/compose/runtime/State;)Lcom/vitorpamplona/amethyst/model/FontFamilyType; -SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->AmethystTheme$lambda$3(Landroidx/compose/runtime/State;)Lcom/vitorpamplona/amethyst/model/FontSizeType; +SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->AmethystTheme$lambda$2(Landroidx/compose/runtime/State;)Lcom/vitorpamplona/amethyst/commons/model/FontFamilyType; +SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->AmethystTheme$lambda$3(Landroidx/compose/runtime/State;)Lcom/vitorpamplona/amethyst/commons/model/FontSizeType; SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->AmethystTheme$lambda$9$0(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$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(Lcom/vitorpamplona/amethyst/commons/model/ThemeType;Lcom/vitorpamplona/amethyst/commons/model/AccentColorType;Lcom/vitorpamplona/amethyst/commons/model/FontFamilyType;Lcom/vitorpamplona/amethyst/commons/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/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;->accentPrimary(Lcom/vitorpamplona/amethyst/commons/model/AccentColorType;Z)J +SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->accentSecondary(Lcom/vitorpamplona/amethyst/commons/model/AccentColorType;Z)J +SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->darkColors(Lcom/vitorpamplona/amethyst/commons/model/AccentColorType;)Landroidx/compose/material3/ColorScheme; 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;->lightColors(Lcom/vitorpamplona/amethyst/commons/model/AccentColorType;)Landroidx/compose/material3/ColorScheme; 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; @@ -25669,7 +25669,7 @@ Lcom/vitorpamplona/amethyst/ui/theme/ThemeKt$$ExternalSyntheticLambda3; SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt$$ExternalSyntheticLambda3;->(Landroid/view/View;ZLandroidx/compose/material3/ColorScheme;)V SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt$$ExternalSyntheticLambda3;->invoke()Ljava/lang/Object; Lcom/vitorpamplona/amethyst/ui/theme/ThemeKt$$ExternalSyntheticLambda4; -SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt$$ExternalSyntheticLambda4;->(Lcom/vitorpamplona/amethyst/model/ThemeType;Lcom/vitorpamplona/amethyst/model/AccentColorType;Lcom/vitorpamplona/amethyst/model/FontFamilyType;Lcom/vitorpamplona/amethyst/model/FontSizeType;Lkotlin/jvm/functions/Function2;II)V +SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt$$ExternalSyntheticLambda4;->(Lcom/vitorpamplona/amethyst/commons/model/ThemeType;Lcom/vitorpamplona/amethyst/commons/model/AccentColorType;Lcom/vitorpamplona/amethyst/commons/model/FontFamilyType;Lcom/vitorpamplona/amethyst/commons/model/FontSizeType;Lkotlin/jvm/functions/Function2;II)V Lcom/vitorpamplona/amethyst/ui/theme/ThemeKt$MarkDownStyleOnDark$1; SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt$MarkDownStyleOnDark$1;->()V SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt$MarkDownStyleOnDark$1;->()V @@ -25685,7 +25685,7 @@ SPLcom/vitorpamplona/amethyst/commons/ui/theme/TypeKt;->getDefaultParagraphSpaci 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/ui/theme/ThemeKt;->toFontFamily(Lcom/vitorpamplona/amethyst/commons/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;->()V diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt index 00a7fba7f3..4ffb52fa28 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt @@ -28,6 +28,7 @@ import androidx.security.crypto.EncryptedSharedPreferences import coil3.disk.DiskCache import coil3.memory.MemoryCache import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.commons.model.UiSettings import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip03Timestamp.BitcoinExplorerEndpoint import com.vitorpamplona.amethyst.commons.model.nip03Timestamp.IncomingOtsEventVerifier @@ -55,11 +56,11 @@ import com.vitorpamplona.amethyst.commons.service.http.OnionLocationCache import com.vitorpamplona.amethyst.commons.service.lnurl.OkHttpLnurlEndpointResolver import com.vitorpamplona.amethyst.commons.service.pow.PoWPolicy import com.vitorpamplona.amethyst.commons.service.pow.PoWPublishQueue +import com.vitorpamplona.amethyst.commons.state.UiSettingsState import com.vitorpamplona.amethyst.commons.tor.TorSettings import com.vitorpamplona.amethyst.connectedApps.DataStoreNostrSignerPermissionStore 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.nip11RelayInfo.Nip11CachedRetriever import com.vitorpamplona.amethyst.model.preferences.BuzzAttestationPreferences @@ -132,7 +133,6 @@ import com.vitorpamplona.amethyst.service.uploads.nip95.Nip95CacheFactory import com.vitorpamplona.amethyst.ui.resourceCacheInit import com.vitorpamplona.amethyst.ui.screen.AccountSessionManager import com.vitorpamplona.amethyst.ui.screen.AccountState -import com.vitorpamplona.amethyst.ui.screen.UiSettingsState import com.vitorpamplona.amethyst.ui.tor.TorManager import com.vitorpamplona.amethyst.ui.tor.TorService import com.vitorpamplona.quartz.nip01Core.core.Address diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index a76ebb2365..a0acb8bad9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -26,6 +26,7 @@ import android.content.SharedPreferences import androidx.compose.runtime.Immutable import androidx.core.content.edit import com.vitorpamplona.amethyst.commons.model.HomeFeedType +import com.vitorpamplona.amethyst.commons.model.UiSettings import com.vitorpamplona.amethyst.commons.model.chats.ChatFeedType import com.vitorpamplona.amethyst.commons.model.clink.ClinkDebitWalletEntry import com.vitorpamplona.amethyst.commons.model.concord.ConcordViewMode @@ -37,7 +38,6 @@ import com.vitorpamplona.amethyst.commons.model.nip47WalletConnect.NwcWalletEntr 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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/FavoriteAppLauncher.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/FavoriteAppLauncher.kt index e4029c8627..cd6fb36b17 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/FavoriteAppLauncher.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/FavoriteAppLauncher.kt @@ -27,8 +27,8 @@ import android.os.Bundle import android.widget.Toast import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp +import com.vitorpamplona.amethyst.commons.model.ThemeType import com.vitorpamplona.amethyst.commons.model.cache.LocalCache -import com.vitorpamplona.amethyst.model.ThemeType import com.vitorpamplona.amethyst.napplet.NappletLauncher import com.vitorpamplona.amethyst.napplet.NappletWebViewProfiles import com.vitorpamplona.amethyst.napplet.WebAppNetworkRegistry diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/UISharedPreferences.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/UISharedPreferences.kt index 5909a5a3a7..78239e6911 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/UISharedPreferences.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/UISharedPreferences.kt @@ -34,16 +34,16 @@ import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore import com.vitorpamplona.amethyst.LocalPreferences -import com.vitorpamplona.amethyst.model.AccentColorType -import com.vitorpamplona.amethyst.model.BooleanType -import com.vitorpamplona.amethyst.model.ConnectivityType -import com.vitorpamplona.amethyst.model.FeatureSetType -import com.vitorpamplona.amethyst.model.FontFamilyType -import com.vitorpamplona.amethyst.model.FontSizeType -import com.vitorpamplona.amethyst.model.ProfileGalleryType -import com.vitorpamplona.amethyst.model.ThemeType -import com.vitorpamplona.amethyst.model.UiSettings -import com.vitorpamplona.amethyst.model.UiSettingsFlow +import com.vitorpamplona.amethyst.commons.model.AccentColorType +import com.vitorpamplona.amethyst.commons.model.BooleanType +import com.vitorpamplona.amethyst.commons.model.ConnectivityType +import com.vitorpamplona.amethyst.commons.model.FeatureSetType +import com.vitorpamplona.amethyst.commons.model.FontFamilyType +import com.vitorpamplona.amethyst.commons.model.FontSizeType +import com.vitorpamplona.amethyst.commons.model.ProfileGalleryType +import com.vitorpamplona.amethyst.commons.model.ThemeType +import com.vitorpamplona.amethyst.commons.model.UiSettings +import com.vitorpamplona.amethyst.commons.model.UiSettingsFlow import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletLauncher.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletLauncher.kt index 382242d133..e4619c6624 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletLauncher.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletLauncher.kt @@ -25,10 +25,10 @@ import android.content.Intent import android.content.res.Configuration import android.os.Bundle import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.commons.model.ThemeType import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.napplet.NappletArtifactPolicy import com.vitorpamplona.amethyst.commons.napplet.NappletIdentity -import com.vitorpamplona.amethyst.model.ThemeType import com.vitorpamplona.amethyst.napplethost.HostProfile import com.vitorpamplona.amethyst.napplethost.NappletHostActivity import com.vitorpamplona.amethyst.napplethost.NappletHostContract diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/authCommand/compose/RelayAuthComposeHelpers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/authCommand/compose/RelayAuthComposeHelpers.kt index eed6bfebf5..4ac7462573 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/authCommand/compose/RelayAuthComposeHelpers.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/authCommand/compose/RelayAuthComposeHelpers.kt @@ -27,6 +27,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.relayauth.AuthPurposeKind import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.relay_auth_purpose_my_inbox @@ -38,7 +39,6 @@ import com.vitorpamplona.amethyst.commons.resources.relay_auth_purpose_read_outb import com.vitorpamplona.amethyst.commons.resources.relay_auth_purpose_read_venue import com.vitorpamplona.amethyst.commons.resources.relay_auth_purpose_send_dm import com.vitorpamplona.amethyst.commons.resources.relay_auth_purpose_thread -import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.core.HexKey import org.jetbrains.compose.resources.StringResource @@ -50,12 +50,11 @@ import org.jetbrains.compose.resources.StringResource @Composable internal fun LoadRelayAuthUser( pubkey: HexKey, - accountViewModel: AccountViewModel, content: @Composable (User?) -> Unit, ) { - var user by remember(pubkey) { mutableStateOf(accountViewModel.getUserIfExists(pubkey)) } + var user by remember(pubkey) { mutableStateOf(LocalCache.getUserIfExists(pubkey)) } if (user == null) { - LaunchedEffect(pubkey) { user = accountViewModel.checkGetOrCreateUser(pubkey) } + LaunchedEffect(pubkey) { user = LocalCache.checkGetOrCreateUser(pubkey) } } content(user) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/authCommand/compose/RelayAuthPromptHost.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/authCommand/compose/RelayAuthPromptHost.kt index 4a09f6706a..82eefe3b7c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/authCommand/compose/RelayAuthPromptHost.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/authCommand/compose/RelayAuthPromptHost.kt @@ -99,6 +99,7 @@ import com.vitorpamplona.amethyst.commons.resources.relay_auth_why_thread import com.vitorpamplona.amethyst.commons.resources.relay_auth_why_thread_with import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.commons.ui.pluralStringRes +import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.RelayIconFilter import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo @@ -404,8 +405,8 @@ private fun RelayHeader( contentDescription = null, colorFilter = RelayIconFilter, modifier = Modifier.size(34.dp).clip(MaterialTheme.shapes.small), - loadProfilePicture = accountViewModel.settings.showProfilePictures(), - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + loadProfilePicture = LocalDisplaySettings.current.showProfilePictures, + loadRobohash = LocalDisplaySettings.current.loadRobohash, ) Column { Text( @@ -466,7 +467,7 @@ private fun ReasonSentence( InlineTextContent( Placeholder(22.sp, 22.sp, PlaceholderVerticalAlign.TextCenter), ) { - LoadRelayAuthUser(face, accountViewModel) { user -> + LoadRelayAuthUser(face) { user -> if (user != null) ClickableUserPicture(user, 20.dp, accountViewModel) } }, @@ -602,9 +603,9 @@ private fun rememberCounterpartyName( pubkey: HexKey, accountViewModel: AccountViewModel, ): String { - var user by remember(pubkey) { mutableStateOf(accountViewModel.getUserIfExists(pubkey)) } + var user by remember(pubkey) { mutableStateOf(LocalCache.getUserIfExists(pubkey)) } if (user == null) { - LaunchedEffect(pubkey) { user = accountViewModel.checkGetOrCreateUser(pubkey) } + LaunchedEffect(pubkey) { user = LocalCache.checkGetOrCreateUser(pubkey) } } val loaded = user ?: return stringRes(Res.string.relay_auth_someone_unloaded) val metadata by observeUserInfo(loaded, accountViewModel) @@ -649,11 +650,11 @@ private fun rememberVenueLabel( ?: when { venueId.length == 64 -> if (kind == AuthPurposeKind.POST_VENUE) { - accountViewModel.checkGetOrCreatePublicChatChannel(venueId) + LocalCache.getOrCreatePublicChatChannel(venueId) } else { LocalCache.getPublicChatChannelIfExists(venueId) } - venueId.startsWith("30311:") -> Address.parse(venueId)?.let { accountViewModel.checkGetOrCreateLiveActivityChannel(it) } + venueId.startsWith("30311:") -> Address.parse(venueId)?.let { LocalCache.getOrCreateLiveChannel(it) } else -> null } } @@ -702,9 +703,9 @@ private fun rememberDisplayName( pubkey: HexKey, accountViewModel: AccountViewModel, ): String { - var user by remember(pubkey) { mutableStateOf(accountViewModel.getUserIfExists(pubkey)) } + var user by remember(pubkey) { mutableStateOf(LocalCache.getUserIfExists(pubkey)) } if (user == null) { - LaunchedEffect(pubkey) { user = accountViewModel.checkGetOrCreateUser(pubkey) } + LaunchedEffect(pubkey) { user = LocalCache.checkGetOrCreateUser(pubkey) } } val loaded = user ?: return stringRes(Res.string.relay_auth_someone_unloaded) // Reading the observed metadata registers a snapshot read, so the name updates when it arrives. diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelObservers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelObservers.kt index 17bbf1e846..d83de98e94 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelObservers.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelObservers.kt @@ -27,6 +27,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.model.Channel import com.vitorpamplona.amethyst.commons.model.ChannelState import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -102,7 +103,7 @@ private fun channelToParticipatingUsers( val pKeys = channel.info?.participantKeys() ?: emptyList() pKeys.forEach { - val u = accountViewModel.checkGetOrCreateUser(it) + val u = LocalCache.checkGetOrCreateUser(it) if (u != null) { users.add(u) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/InformationDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/InformationDialog.kt index ed22a29806..32e9e00590 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/InformationDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/InformationDialog.kt @@ -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.components.toasts.ThrowableToastMsg +import com.vitorpamplona.amethyst.commons.ui.components.toasts.ThrowableToastMsg2 import com.vitorpamplona.amethyst.commons.ui.components.util.setText 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 kotlinx.coroutines.launch import java.io.PrintWriter import java.io.StringWriter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/broadcast/DisplayBroadcastProgress.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/broadcast/DisplayBroadcastProgress.kt index 44773b05a0..1c9c1f1772 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/broadcast/DisplayBroadcastProgress.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/broadcast/DisplayBroadcastProgress.kt @@ -38,9 +38,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.commons.model.BooleanType import com.vitorpamplona.amethyst.commons.service.broadcast.BroadcastEvent import com.vitorpamplona.amethyst.commons.service.pow.PoWJobState -import com.vitorpamplona.amethyst.model.BooleanType import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import kotlinx.collections.immutable.ImmutableList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/call/CallWidgets.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/call/CallWidgets.kt index 5fa1ae296f..94e0ca9fcf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/call/CallWidgets.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/call/CallWidgets.kt @@ -193,7 +193,7 @@ fun PeerAvatarCell( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, ) { - LoadUser(baseUserHex = peerPubKey, accountViewModel = accountViewModel) { user -> + LoadUser(baseUserHex = peerPubKey) { user -> if (user != null) { ClickableUserPicture( baseUser = user, @@ -235,7 +235,7 @@ fun GroupCallPictures( 0 -> {} 1 -> { - LoadUser(baseUserHex = userList[0], accountViewModel = accountViewModel) { user -> + LoadUser(baseUserHex = userList[0]) { user -> if (user != null) { ClickableUserPicture( baseUser = user, @@ -350,7 +350,7 @@ fun GroupCallNames( 0 -> {} 1 -> { - LoadUser(baseUserHex = userList[0], accountViewModel = accountViewModel) { user -> + LoadUser(baseUserHex = userList[0]) { user -> if (user != null) { UsernameDisplay( baseUser = user, @@ -378,7 +378,7 @@ fun GroupCallNames( color = textColor, ) } - LoadUser(baseUserHex = userList[i], accountViewModel = accountViewModel) { user -> + LoadUser(baseUserHex = userList[i]) { user -> if (user != null) { UsernameDisplay( baseUser = user, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt index 588e74c2ed..c1029548c3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt @@ -43,6 +43,7 @@ import com.vitorpamplona.amethyst.commons.model.EmptyTagList import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.model.navigation.routeFor import com.vitorpamplona.amethyst.commons.resources.Res @@ -101,7 +102,7 @@ fun LoadOrCreateNote( content: @Composable (Note?) -> Unit, ) { var note by - remember(event.id) { mutableStateOf(accountViewModel.getNoteIfExists(event.id)) } + remember(event.id) { mutableStateOf(LocalCache.getNoteIfExists(event.id)) } if (note == null) { LaunchedEffect(key1 = event.id) { @@ -146,7 +147,7 @@ fun DisplayEvent( accountViewModel: AccountViewModel, nav: INav, ) { - LoadNote(hex, accountViewModel) { + LoadNote(hex) { if (it != null) { DisplayNoteLink(it, hex, additionalChars, accountViewModel, nav) } else { @@ -224,11 +225,11 @@ private fun DisplayAddress( return } - var noteBase by remember(nip19) { mutableStateOf(accountViewModel.getNoteIfExists(nip19.aTag())) } + var noteBase by remember(nip19) { mutableStateOf(LocalCache.getNoteIfExists(nip19.aTag())) } if (noteBase == null) { LaunchedEffect(key1 = nip19) { - noteBase = accountViewModel.getOrCreateAddressableNote(nip19.address()) + noteBase = LocalCache.getOrCreateAddressableNote(nip19.address()) } } @@ -271,13 +272,13 @@ fun DisplayUser( var userBase by remember(userHex) { mutableStateOf( - accountViewModel.getUserIfExists(userHex), + LocalCache.getUserIfExists(userHex), ) } if (userBase == null) { LaunchedEffect(key1 = userHex) { - userBase = accountViewModel.checkGetOrCreateUser(userHex) + userBase = LocalCache.checkGetOrCreateUser(userHex) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ConcordInviteCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ConcordInviteCard.kt index ab6f170785..8935486076 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ConcordInviteCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ConcordInviteCard.kt @@ -51,6 +51,7 @@ import com.vitorpamplona.amethyst.commons.resources.concord_invite_card_join import com.vitorpamplona.amethyst.commons.resources.concord_invite_card_subtitle import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.concord.cord05Invites.CommunityInvite @@ -139,8 +140,8 @@ fun ConcordInvitePreviewRow( .size(52.dp) .clip(CircleShape) .border(1.5.dp, MaterialTheme.colorScheme.primary.copy(alpha = 0.35f), CircleShape), - loadProfilePicture = accountViewModel.settings.showProfilePictures(), - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + loadProfilePicture = LocalDisplaySettings.current.showProfilePictures, + loadRobohash = LocalDisplaySettings.current.loadRobohash, autoPlayGif = autoPlayGif, ) Column(Modifier.weight(1f)) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RelayGroupCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RelayGroupCard.kt index 7d7ac96c3a..cda380757e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RelayGroupCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RelayGroupCard.kt @@ -56,6 +56,7 @@ import com.vitorpamplona.amethyst.commons.resources.relay_group_open import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.pluralStringRes +import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.LoadRelayGroupChannel @@ -86,7 +87,7 @@ fun RelayGroupCard( val groupId = remember(invite) { invite.toGroupId() } - LoadRelayGroupChannel(groupId, accountViewModel) { channel -> + LoadRelayGroupChannel(groupId) { channel -> RelayGroupCardContent(channel, invite.code, accountViewModel, nav) } } @@ -148,8 +149,8 @@ private fun RelayGroupCardContent( .size(52.dp) .clip(CircleShape) .border(1.5.dp, MaterialTheme.colorScheme.primary.copy(alpha = 0.35f), CircleShape), - loadProfilePicture = accountViewModel.settings.showProfilePictures(), - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + loadProfilePicture = LocalDisplaySettings.current.showProfilePictures, + loadRobohash = LocalDisplaySettings.current.loadRobohash, autoPlayGif = autoPlayGif, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ReusableZapButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ReusableZapButton.kt index 5e801b6926..ca7a265089 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ReusableZapButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ReusableZapButton.kt @@ -57,6 +57,7 @@ import com.vitorpamplona.amethyst.commons.resources.no_wallet_found import com.vitorpamplona.amethyst.commons.resources.no_zap_amount_setup_long_press_to_change import com.vitorpamplona.amethyst.commons.resources.thank_you import com.vitorpamplona.amethyst.commons.ui.components.CrossfadeIfEnabled +import com.vitorpamplona.amethyst.commons.ui.components.toasts.multiline.UserBasedErrorMessage import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.ZapIcon import com.vitorpamplona.amethyst.commons.ui.note.ZappedIcon @@ -67,7 +68,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.commons.ui.theme.Size35dp import com.vitorpamplona.amethyst.service.ZapPaymentHandler import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderFilterAssemblerSubscription -import com.vitorpamplona.amethyst.ui.components.toasts.multiline.UserBasedErrorMessage import com.vitorpamplona.amethyst.ui.note.ObserveZapIcon import com.vitorpamplona.amethyst.ui.note.ZapAmountChoicePopup import com.vitorpamplona.amethyst.ui.note.payViaIntent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt index cec01b06fc..ede5174be2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt @@ -862,7 +862,7 @@ fun TagLink( accountViewModel: AccountViewModel, nav: INav, ) { - LoadUser(baseUserHex = word.hex, accountViewModel) { + LoadUser(baseUserHex = word.hex) { if (it == null) { Text(text = word.segmentText) } else { @@ -879,15 +879,14 @@ fun TagLink( @Composable fun LoadNote( baseNoteHex: String, - accountViewModel: AccountViewModel, content: @Composable (Note?) -> Unit, ) { var note by - remember(baseNoteHex) { mutableStateOf(accountViewModel.getNoteIfExists(baseNoteHex)) } + remember(baseNoteHex) { mutableStateOf(LocalCache.getNoteIfExists(baseNoteHex)) } if (note == null) { LaunchedEffect(key1 = baseNoteHex) { - note = accountViewModel.checkGetOrCreateNote(baseNoteHex) + note = LocalCache.checkGetOrCreateNote(baseNoteHex) } } @@ -903,7 +902,7 @@ fun TagLink( accountViewModel: AccountViewModel, nav: INav, ) { - LoadNote(baseNoteHex = word.hex, accountViewModel) { + LoadNote(baseNoteHex = word.hex) { if (it == null) { Text(text = remember { word.segmentText.toShortDisplay() }) } else { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/DisplayErrorMessages.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/DisplayErrorMessages.kt index 42a28631a6..399c6687e1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/DisplayErrorMessages.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/DisplayErrorMessages.kt @@ -22,10 +22,16 @@ package com.vitorpamplona.amethyst.ui.components.toasts import androidx.compose.runtime.Composable import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.commons.ui.components.toasts.ActionableStringToastMsg +import com.vitorpamplona.amethyst.commons.ui.components.toasts.ResourceToastMsg +import com.vitorpamplona.amethyst.commons.ui.components.toasts.StringToastMsg +import com.vitorpamplona.amethyst.commons.ui.components.toasts.ThrowableToastMsg +import com.vitorpamplona.amethyst.commons.ui.components.toasts.ThrowableToastMsg2 +import com.vitorpamplona.amethyst.commons.ui.components.toasts.ToastManager +import com.vitorpamplona.amethyst.commons.ui.components.toasts.multiline.MultiErrorToastMsg import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.ui.actions.InformationDialog -import com.vitorpamplona.amethyst.ui.components.toasts.multiline.MultiErrorToastMsg import com.vitorpamplona.amethyst.ui.components.toasts.multiline.MultiUserErrorMessageDialog import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -40,10 +46,11 @@ fun DisplayErrorMessages( openDialogMsg.value?.let { obj -> when (obj) { is ResourceToastMsg -> { - if (obj.params != null) { + val params = obj.params + if (params != null) { InformationDialog( stringRes(obj.titleResId), - stringRes(obj.resourceId, *obj.params), + stringRes(obj.resourceId, *params), ) { toastManager.clearToasts() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/ErrorList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/ErrorList.kt index b0e8d24ccd..8eebdce932 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/ErrorList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/ErrorList.kt @@ -48,6 +48,8 @@ import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.error_dialog_talk_to_user import com.vitorpamplona.amethyst.commons.resources.error_dialog_talk_to_user_name import com.vitorpamplona.amethyst.commons.resources.error_dialog_zap_error +import com.vitorpamplona.amethyst.commons.ui.components.toasts.multiline.MultiErrorToastMsg +import com.vitorpamplona.amethyst.commons.ui.components.toasts.multiline.UserBasedErrorMessage import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/MultiUserErrorMessageDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/MultiUserErrorMessageDialog.kt index a681d82254..0777b48775 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/MultiUserErrorMessageDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/MultiUserErrorMessageDialog.kt @@ -34,6 +34,7 @@ import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.error_dialog_button_ok import com.vitorpamplona.amethyst.commons.resources.error_dialog_zap_error +import com.vitorpamplona.amethyst.commons.ui.components.toasts.multiline.MultiErrorToastMsg import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/GroupBottomBarEntries.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/GroupBottomBarEntries.kt index 428b2ac946..7b12bb38e1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/GroupBottomBarEntries.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/GroupBottomBarEntries.kt @@ -42,6 +42,7 @@ import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChann import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.concord_home_title import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage +import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.ChannelFinderFilterAssemblerSubscription @@ -279,8 +280,8 @@ fun GroupEntryAvatar( model = display.model, contentDescription = display.label, modifier = Modifier.size(size).clip(CircleShape), - loadProfilePicture = accountViewModel.settings.showProfilePictures(), - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + loadProfilePicture = LocalDisplaySettings.current.showProfilePictures, + loadRobohash = LocalDisplaySettings.current.loadRobohash, autoPlayGif = false, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/AccountSwitchBottomSheet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/AccountSwitchBottomSheet.kt index 7c5ccb1d21..1df6a6cfc4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/AccountSwitchBottomSheet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/AccountSwitchBottomSheet.kt @@ -75,6 +75,7 @@ import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncIma import com.vitorpamplona.amethyst.commons.ui.loadPluralStringRes import com.vitorpamplona.amethyst.commons.ui.pluralStringRes import com.vitorpamplona.amethyst.commons.ui.richtext.CreateTextWithEmoji +import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.AccountPictureModifier import com.vitorpamplona.amethyst.commons.ui.theme.Size10dp @@ -238,8 +239,8 @@ private fun AccountPicture( model = userInfo?.info?.profilePicture(), contentDescription = stringRes(Res.string.profile_image), modifier = AccountPictureModifier, - loadProfilePicture = accountViewModel.settings.showProfilePictures(), - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + loadProfilePicture = LocalDisplaySettings.current.showProfilePictures, + loadRobohash = LocalDisplaySettings.current.loadRobohash, autoPlayGif = accountViewModel.settings.autoPlayVideosFlow .collectAsStateWithLifecycle() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt index 0ce7435def..a500753e1b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt @@ -121,6 +121,7 @@ import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncIma import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.richtext.CreateTextWithEmoji +import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.DoubleHorzSpacer @@ -315,8 +316,8 @@ fun ProfileContentTemplate( .clip(shape = CircleShape) .border(3.dp, MaterialTheme.colorScheme.onBackground, CircleShape) .clickable(onClick = onClick), - loadProfilePicture = accountViewModel.settings.showProfilePictures(), - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + loadProfilePicture = LocalDisplaySettings.current.showProfilePictures, + loadRobohash = LocalDisplaySettings.current.loadRobohash, autoPlayGif = accountViewModel.settings.autoPlayVideosFlow .collectAsStateWithLifecycle() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/UserDrawerSearchTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/UserDrawerSearchTopBar.kt index 91144ba5a5..4b3a56e022 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/UserDrawerSearchTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/UserDrawerSearchTopBar.kt @@ -42,6 +42,7 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.ShorterTopAppBar import com.vitorpamplona.amethyst.commons.ui.note.ArrowBackIcon import com.vitorpamplona.amethyst.commons.ui.note.SearchIcon +import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.HeaderPictureModifier import com.vitorpamplona.amethyst.commons.ui.theme.Size22Modifier @@ -142,8 +143,8 @@ internal fun LoggedInUserPictureDrawer( contentDescription = stringRes(id = Res.string.your_profile_image), modifier = HeaderPictureModifier, contentScale = ContentScale.Crop, - loadProfilePicture = accountViewModel.settings.showProfilePictures(), - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + loadProfilePicture = LocalDisplaySettings.current.showProfilePictures, + loadRobohash = LocalDisplaySettings.current.loadRobohash, autoPlayGif = accountViewModel.settings.autoPlayVideosFlow .collectAsStateWithLifecycle() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt index 7a6fbdc5dd..ad119a36a4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt @@ -34,6 +34,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.commons.model.nip03Timestamp.earliestOtsVerifiedTime import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel @@ -91,14 +92,13 @@ fun LoadDecryptedContentOrNull( @Composable fun LoadAddressableNote( address: Address, - accountViewModel: AccountViewModel, content: @Composable (AddressableNote?) -> Unit, ) { val note by produceState( - accountViewModel.getAddressableNoteIfExists(address), + LocalCache.getAddressableNoteIfExists(address), address, ) { - val newNote = accountViewModel.getOrCreateAddressableNote(address) + val newNote = LocalCache.getOrCreateAddressableNote(address) if (newNote != value) { value = newNote } @@ -149,11 +149,10 @@ fun LoadOts( @Composable fun LoadPublicChatChannel( id: String, - accountViewModel: AccountViewModel, content: @Composable (PublicChatChannel) -> Unit, ) { - val channel by produceStateIfNotNull(accountViewModel.getPublicChatChannelIfExists(id), id) { - value = accountViewModel.checkGetOrCreatePublicChatChannel(id) + val channel by produceStateIfNotNull(LocalCache.getPublicChatChannelIfExists(id), id) { + value = LocalCache.getOrCreatePublicChatChannel(id) } channel?.let { content(it) } @@ -162,12 +161,11 @@ fun LoadPublicChatChannel( @Composable fun LoadEphemeralChatChannel( id: RoomId, - accountViewModel: AccountViewModel, content: @Composable (EphemeralChatChannel) -> Unit, ) { val channel = - produceStateIfNotNull(accountViewModel.getEphemeralChatChannelIfExists(id), id) { - value = accountViewModel.checkGetOrCreateEphemeralChatChannel(id) + produceStateIfNotNull(LocalCache.getEphemeralChatChannelIfExists(id), id) { + value = LocalCache.getOrCreateEphemeralChannel(id) } channel.value?.let { content(it) } @@ -176,12 +174,11 @@ fun LoadEphemeralChatChannel( @Composable fun LoadLiveActivityChannel( id: Address, - accountViewModel: AccountViewModel, content: @Composable (LiveActivitiesChannel) -> Unit, ) { val channel = - produceStateIfNotNull(accountViewModel.getLiveActivityChannelIfExists(id), id) { - value = accountViewModel.checkGetOrCreateLiveActivityChannel(id) + produceStateIfNotNull(LocalCache.getLiveActivityChannelIfExists(id), id) { + value = LocalCache.getOrCreateLiveChannel(id) } channel.value?.let { content(it) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt index f2b915219f..12c29afce3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt @@ -87,6 +87,7 @@ import com.vitorpamplona.amethyst.commons.ui.note.RepostedIcon import com.vitorpamplona.amethyst.commons.ui.note.ScoreTagSmall import com.vitorpamplona.amethyst.commons.ui.note.ZappedIcon import com.vitorpamplona.amethyst.commons.ui.richtext.InLineIconRenderer +import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.BitcoinOrange import com.vitorpamplona.amethyst.commons.ui.theme.HalfTopPadding @@ -806,8 +807,8 @@ fun WatchUserMetadataAndFollowsAndRenderUserProfilePicture( contentDescription = stringRes(id = Res.string.profile_image), modifier = MaterialTheme.colorScheme.profile35dpModifier, contentScale = ContentScale.Crop, - loadProfilePicture = accountViewModel.settings.showProfilePictures(), - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + loadProfilePicture = LocalDisplaySettings.current.showProfilePictures, + loadRobohash = LocalDisplaySettings.current.loadRobohash, autoPlayGif = autoPlayGif, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt index f4d70e76ea..ee0acbe5e7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt @@ -315,7 +315,7 @@ fun DisplayStatusInner( } nostrATag != null -> { - LoadAddressableNote(nostrATag, accountViewModel) { note -> + LoadAddressableNote(nostrATag) { note -> if (note != null) { Spacer(modifier = StdHorzSpacer) IconButton( @@ -339,7 +339,7 @@ fun DisplayStatusInner( } nostrETag != null -> { - LoadNote(baseNoteHex = nostrETag.eventId, accountViewModel) { + LoadNote(baseNoteHex = nostrETag.eventId) { if (it != null) { Spacer(modifier = StdHorzSpacer) IconButton( @@ -363,7 +363,7 @@ fun DisplayStatusInner( } nostrPTag != null -> { - LoadUser(baseUserHex = nostrPTag, accountViewModel) { user -> + LoadUser(baseUserHex = nostrPTag) { user -> if (user != null) { Spacer(modifier = StdHorzSpacer) IconButton( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 771f97e549..08ae330cde 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -96,6 +96,7 @@ import com.vitorpamplona.amethyst.commons.ui.note.RenderCashuMint import com.vitorpamplona.amethyst.commons.ui.note.RenderFedimint import com.vitorpamplona.amethyst.commons.ui.note.RenderMintRecommendation import com.vitorpamplona.amethyst.commons.ui.note.elements.StaleRelayHint +import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.state.produceCachedStateAsync import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DoubleVertSpacer @@ -922,7 +923,7 @@ private fun RenderApprovalIfNeeded( baseNote.event?.communityAddress() } communityAddress?.let { - LoadAddressableNote(it, accountViewModel) { community -> + LoadAddressableNote(it) { community -> if (community != null) { val showApproveButton by observeCommunityApprovalNeedStatus(baseNote, community, accountViewModel) if (showApproveButton == true) { @@ -2427,7 +2428,7 @@ fun RenderAuthorImages( if (noteEvent is ChannelMessageEvent) { val baseChannelHex = noteEvent.channelId() if (baseChannelHex != null) { - LoadPublicChatChannel(baseChannelHex, accountViewModel) { channel -> + LoadPublicChatChannel(baseChannelHex) { channel -> ChannelNotePicture( channel, accountViewModel, @@ -2449,8 +2450,8 @@ private fun ChannelNotePicture( model = model, contentDescription = stringRes(Res.string.group_picture), modifier = MaterialTheme.colorScheme.channelNotePictureModifier, - loadProfilePicture = accountViewModel.settings.showProfilePictures(), - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + loadProfilePicture = LocalDisplaySettings.current.showProfilePictures, + loadRobohash = LocalDisplaySettings.current.loadRobohash, autoPlayGif = accountViewModel.settings.autoPlayVideosFlow .collectAsStateWithLifecycle() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index 21e93147f4..a74451ba8f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -154,6 +154,7 @@ import com.vitorpamplona.amethyst.commons.ui.components.AnimatedBorderTextCorner import com.vitorpamplona.amethyst.commons.ui.components.ClickableBox import com.vitorpamplona.amethyst.commons.ui.components.CrossfadeIfEnabled import com.vitorpamplona.amethyst.commons.ui.components.GenericLoadable +import com.vitorpamplona.amethyst.commons.ui.components.toasts.multiline.UserBasedErrorMessage import com.vitorpamplona.amethyst.commons.ui.components.util.setText import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.ChangeReactionIcon @@ -228,7 +229,6 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUse import com.vitorpamplona.amethyst.ui.actions.uploads.FloatingRecordingIndicator import com.vitorpamplona.amethyst.ui.actions.uploads.MAX_VOICE_RECORD_SECONDS import com.vitorpamplona.amethyst.ui.actions.uploads.RecordAudioBox -import com.vitorpamplona.amethyst.ui.components.toasts.multiline.UserBasedErrorMessage import com.vitorpamplona.amethyst.ui.navigation.routes.routeReplyTo import com.vitorpamplona.amethyst.ui.note.elements.ShareOptionsBottomSheet import com.vitorpamplona.amethyst.ui.note.types.EditState @@ -428,7 +428,7 @@ fun PayReaction( val authorPubkey = baseNote.author?.pubkeyHex ?: return val address = remember(authorPubkey) { PaymentTargetsEvent.createAddress(authorPubkey) } - LoadAddressableNote(address, accountViewModel) { note -> + LoadAddressableNote(address) { note -> var expanded by remember { mutableStateOf(false) } ClickableBox( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt index 3ac0cae686..bb7b302840 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt @@ -58,6 +58,7 @@ import com.vitorpamplona.amethyst.commons.ui.components.ClickableBox import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.commons.ui.components.util.setText import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.LargeRelayIconModifier import com.vitorpamplona.amethyst.commons.ui.theme.RelayIconFilter @@ -161,9 +162,9 @@ fun RenderRelay( RenderRelayIcon( displayUrl = relayInfo.id ?: relay.url, iconUrl = relayInfo.icon, - loadProfilePicture = accountViewModel.settings.showProfilePictures(), + loadProfilePicture = LocalDisplaySettings.current.showProfilePictures, pingInMs = 0, - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + loadRobohash = LocalDisplaySettings.current.loadRobohash, iconModifier = iconModifier, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateReactionTypeDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateReactionTypeDialog.kt index b85424afa0..7fc4cc5be5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateReactionTypeDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateReactionTypeDialog.kt @@ -359,7 +359,6 @@ private fun EmojiSelector( ) { LoadAddressableNote( accountViewModel.account.emoji.getEmojiPackSelectionAddress(), - accountViewModel, ) { emptyNote -> emptyNote?.let { usersEmojiList -> val collections by observeNoteEventAndMapNotNull(usersEmojiList, accountViewModel) { event: EmojiPackSelectionEvent -> @@ -387,7 +386,7 @@ fun EmojiCollectionGallery( state = listState, ) { itemsIndexed(emojiCollections, key = { _, item -> item }) { _, item -> - LoadAddressableNote(item, accountViewModel) { + LoadAddressableNote(item) { it?.let { WatchAndRenderNote(it, bgColor, accountViewModel, nav, onClick) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt index f2aeb65dba..753d91f79f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt @@ -49,6 +49,7 @@ import com.vitorpamplona.amethyst.commons.ui.components.UserAvatar import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.FollowingIcon import com.vitorpamplona.amethyst.commons.ui.note.ScoreTag +import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserContactCardsScore @@ -105,7 +106,7 @@ fun DisplayBlankAuthor( robot = "authornotfound", contentDescription = stringRes(Res.string.unknown_author), modifier = nullModifier, - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + loadRobohash = LocalDisplaySettings.current.loadRobohash, ) } @@ -117,7 +118,7 @@ fun UserPicture( accountViewModel: AccountViewModel, nav: INav, ) { - LoadUser(baseUserHex = userHex, accountViewModel) { + LoadUser(baseUserHex = userHex) { if (it != null) { UserPicture( user = it, @@ -241,7 +242,7 @@ fun NonClickableUserPictures( 0 -> {} 1 -> { - LoadUser(baseUserHex = userHexList.first(), accountViewModel) { + LoadUser(baseUserHex = userHexList.first()) { it?.let { BaseUserPicture(it, size, accountViewModel, outerModifier = Modifier) } } } @@ -249,7 +250,7 @@ fun NonClickableUserPictures( 2 -> { val userList = userHexList - LoadUser(baseUserHex = userList[0], accountViewModel) { + LoadUser(baseUserHex = userList[0]) { it?.let { BaseUserPicture( it, @@ -259,7 +260,7 @@ fun NonClickableUserPictures( ) } } - LoadUser(baseUserHex = userList[1], accountViewModel) { + LoadUser(baseUserHex = userList[1]) { it?.let { BaseUserPicture( it, @@ -274,7 +275,7 @@ fun NonClickableUserPictures( 3 -> { val userList = userHexList - LoadUser(baseUserHex = userList[0], accountViewModel) { + LoadUser(baseUserHex = userList[0]) { it?.let { BaseUserPicture( it, @@ -284,7 +285,7 @@ fun NonClickableUserPictures( ) } } - LoadUser(baseUserHex = userList[1], accountViewModel) { + LoadUser(baseUserHex = userList[1]) { it?.let { BaseUserPicture( it, @@ -294,7 +295,7 @@ fun NonClickableUserPictures( ) } } - LoadUser(baseUserHex = userList[2], accountViewModel) { + LoadUser(baseUserHex = userList[2]) { it?.let { BaseUserPicture( it, @@ -309,7 +310,7 @@ fun NonClickableUserPictures( else -> { val userList = userHexList - LoadUser(baseUserHex = userList[0], accountViewModel) { + LoadUser(baseUserHex = userList[0]) { it?.let { BaseUserPicture( it, @@ -319,7 +320,7 @@ fun NonClickableUserPictures( ) } } - LoadUser(baseUserHex = userList[1], accountViewModel) { + LoadUser(baseUserHex = userList[1]) { it?.let { BaseUserPicture( it, @@ -329,7 +330,7 @@ fun NonClickableUserPictures( ) } } - LoadUser(baseUserHex = userList[2], accountViewModel) { + LoadUser(baseUserHex = userList[2]) { it?.let { BaseUserPicture( it, @@ -339,7 +340,7 @@ fun NonClickableUserPictures( ) } } - LoadUser(baseUserHex = userList[3], accountViewModel) { + LoadUser(baseUserHex = userList[3]) { it?.let { BaseUserPicture( it, @@ -370,7 +371,6 @@ fun BaseUserPicture( userName = userName, size = size, modifier = innerModifier, - accountViewModel = accountViewModel, ) } @@ -393,7 +393,7 @@ fun BaseUserPicture( outerModifier: Modifier = Modifier.size(size), ) { Box(outerModifier, contentAlignment = Alignment.TopEnd) { - LoadUser(baseUserHex, accountViewModel) { + LoadUser(baseUserHex) { if (it != null) { ObserveAndDrawInnerUserPicture(it, size, accountViewModel, innerModifier) @@ -405,7 +405,6 @@ fun BaseUserPicture( userName = null, size = size, modifier = innerModifier, - accountViewModel = accountViewModel, ) } } @@ -433,7 +432,6 @@ fun ObserveAndDrawInnerUserPicture( userName = userProfile?.info?.bestName(), size = size, modifier = innerModifier, - accountViewModel = accountViewModel, ) } @@ -455,7 +453,6 @@ fun ScoreTag55Preview() { userName = PREVIEW_USER_NAME, size = size, modifier = Modifier, - accountViewModel = accountViewModel, ) FollowingIcon(Modifier.size(size.div(3.5f))) ScoreTag(100, size, Modifier.align(Alignment.BottomCenter)) @@ -468,7 +465,6 @@ fun ScoreTag55Preview() { userName = PREVIEW_USER_NAME, size = size, modifier = Modifier, - accountViewModel = accountViewModel, ) FollowingIcon(Modifier.size(size.div(3.5f))) ScoreTag(100, size, Modifier.align(Alignment.BottomCenter)) @@ -481,7 +477,6 @@ fun ScoreTag55Preview() { userName = PREVIEW_USER_NAME, size = size, modifier = Modifier, - accountViewModel = accountViewModel, ) FollowingIcon(Modifier.size(size.div(3.5f))) ScoreTag(100, size, Modifier.align(Alignment.BottomCenter)) @@ -494,7 +489,6 @@ fun ScoreTag55Preview() { userName = PREVIEW_USER_NAME, size = size, modifier = Modifier, - accountViewModel = accountViewModel, ) FollowingIcon(Modifier.size(size.div(3.5f))) ScoreTag(100, size, Modifier.align(Alignment.BottomCenter)) @@ -507,7 +501,6 @@ fun ScoreTag55Preview() { userName = PREVIEW_USER_NAME, size = size, modifier = Modifier, - accountViewModel = accountViewModel, ) FollowingIcon(Modifier.size(size.div(3.5f))) ScoreTag(100, size, Modifier.align(Alignment.BottomCenter)) @@ -534,7 +527,6 @@ fun InnerUserPicture( userName: String?, size: Dp, modifier: Modifier, - accountViewModel: AccountViewModel, ) { UserAvatar( userHex = userHex, @@ -547,12 +539,6 @@ fun InnerUserPicture( } else { stringRes(id = Res.string.profile_image) }, - loadProfilePicture = accountViewModel.settings.showProfilePictures(), - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), - autoPlayGif = - accountViewModel.settings.autoPlayVideosFlow - .collectAsStateWithLifecycle() - .value, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt index ed1177b147..75a78c8df2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt @@ -99,6 +99,7 @@ import com.vitorpamplona.amethyst.commons.resources.zap_type_private import com.vitorpamplona.amethyst.commons.resources.zap_type_private_explainer import com.vitorpamplona.amethyst.commons.resources.zap_type_public import com.vitorpamplona.amethyst.commons.resources.zap_type_public_explainer +import com.vitorpamplona.amethyst.commons.ui.components.toasts.multiline.UserBasedErrorMessage import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton @@ -113,7 +114,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.ZeroPadding import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.ZapPaymentHandler -import com.vitorpamplona.amethyst.ui.components.toasts.multiline.UserBasedErrorMessage import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.OnchainZapSendDialog import com.vitorpamplona.quartz.nip01Core.core.Event diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapPollNote.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapPollNote.kt index 4ca2a10e2b..445efa5a78 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapPollNote.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapPollNote.kt @@ -89,6 +89,7 @@ import com.vitorpamplona.amethyst.commons.resources.poll_is_closed_explainer import com.vitorpamplona.amethyst.commons.resources.poll_unable_to_vote import com.vitorpamplona.amethyst.commons.resources.read_only_user import com.vitorpamplona.amethyst.commons.resources.zaps +import com.vitorpamplona.amethyst.commons.ui.components.toasts.StringToastMsg import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.stringRes @@ -107,7 +108,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.subtleBorder import com.vitorpamplona.amethyst.service.ZapPaymentHandler import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteZaps import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer -import com.vitorpamplona.amethyst.ui.components.toasts.StringToastMsg import com.vitorpamplona.amethyst.ui.navigation.routes.routeToMessage import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/emojiSuggestions/WatchAndLoadMyEmojiList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/emojiSuggestions/WatchAndLoadMyEmojiList.kt index a7d5b10d9b..1687f0a6ea 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/emojiSuggestions/WatchAndLoadMyEmojiList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/emojiSuggestions/WatchAndLoadMyEmojiList.kt @@ -34,7 +34,6 @@ import kotlinx.collections.immutable.toImmutableList fun WatchAndLoadMyEmojiList(accountViewModel: AccountViewModel) { LoadAddressableNote( EmojiPackSelectionEvent.createAddress(accountViewModel.userProfile().pubkeyHex), - accountViewModel, ) { emptyNote -> emptyNote?.let { usersEmojiList -> val collections by observeNoteEventAndMapNotNull(usersEmojiList, accountViewModel) { event: EmojiPackSelectionEvent -> @@ -42,7 +41,7 @@ fun WatchAndLoadMyEmojiList(accountViewModel: AccountViewModel) { } collections?.forEach { address -> - LoadAddressableNote(address, accountViewModel) { note -> + LoadAddressableNote(address) { note -> if (note != null) { EventFinderFilterAssemblerSubscription(note, accountViewModel) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/invoice/ClinkOfferPreview.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/invoice/ClinkOfferPreview.kt index fa3759d83b..a0895e6035 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/invoice/ClinkOfferPreview.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/invoice/ClinkOfferPreview.kt @@ -244,7 +244,7 @@ private fun OfferRecipientRow( accountViewModel: AccountViewModel, nav: INav, ) { - LoadUser(baseUserHex = pubKey, accountViewModel) { user -> + LoadUser(baseUserHex = pubKey) { user -> if (user != null) { Row( verticalAlignment = Alignment.CenterVertically, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/AddInboxRelayForDMCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/AddInboxRelayForDMCard.kt index 191e3daade..15205698c9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/AddInboxRelayForDMCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/AddInboxRelayForDMCard.kt @@ -106,7 +106,6 @@ fun ObserveRelayListForDMs( ) { LoadAddressableNote( ChatMessageRelayListEvent.createAddress(pubkey), - accountViewModel, ) { relayList -> if (relayList != null) { val relayListEvent by observeNoteEvent(relayList, accountViewModel) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt index 1c9e541a98..943141e3ee 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt @@ -49,13 +49,13 @@ fun ShowForkInformation( val forkedAddress = remember(noteEvent) { noteEvent.forkFromAddress() } val forkedEvent = remember(noteEvent) { noteEvent.forkFromVersion() } if (forkedAddress != null) { - LoadAddressableNote(forkedAddress, accountViewModel) { addressableNote -> + LoadAddressableNote(forkedAddress) { addressableNote -> if (addressableNote != null) { ForkMark(addressableNote, modifier, accountViewModel, nav) } } } else if (forkedEvent != null) { - LoadNote(forkedEvent, accountViewModel) { event -> + LoadNote(forkedEvent) { event -> if (event != null) { ForkMark(event, modifier, accountViewModel, nav) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NoteHeaderMarkersPreview.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NoteHeaderMarkersPreview.kt index fb12ae4b2d..00689c692d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NoteHeaderMarkersPreview.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NoteHeaderMarkersPreview.kt @@ -29,12 +29,12 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.model.FeatureSetType import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.ui.components.GenericLoadable import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn -import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.service.location.CachedReversedGeoLocations import com.vitorpamplona.amethyst.ui.note.FirstUserInfoRow import com.vitorpamplona.amethyst.ui.note.types.EditState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt index 92e3eeecc1..8afb6da337 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt @@ -721,7 +721,7 @@ open class CommentPostViewModel : if (replyingToEvent is CommunityDefinitionEvent) { replyingToEvent.moderatorKeys().mapNotNull { if (it != replyingToEvent.pubKey) { - accountViewModel.checkGetOrCreateUser(it)?.toPTag() + LocalCache.checkGetOrCreateUser(it)?.toPTag() } else { null } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt index 3fad5f0759..208535d4b4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt @@ -65,6 +65,7 @@ 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.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.composer.AudienceSelection import com.vitorpamplona.amethyst.commons.nip30CustomEmojis.ui.ShowEmojiSuggestionList import com.vitorpamplona.amethyst.commons.relayClient.communities.CommunityFilterAssemblerSubscription @@ -149,13 +150,13 @@ fun ReplyCommentPostScreen( val context = LocalContext.current LaunchedEffect(postViewModel, accountViewModel) { - replyId?.let { accountViewModel.getNoteIfExists(it) }?.let { + replyId?.let { LocalCache.getNoteIfExists(it) }?.let { postViewModel.reply(it) } - draftId?.let { accountViewModel.getNoteIfExists(it) }?.let { + draftId?.let { LocalCache.getNoteIfExists(it) }?.let { postViewModel.editFromDraft(it) } - quoteId?.let { accountViewModel.getNoteIfExists(it) }?.let { + quoteId?.let { LocalCache.getNoteIfExists(it) }?.let { postViewModel.quote(it) } message?.ifBlank { null }?.let { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/ShareNoteAsImageScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/ShareNoteAsImageScreen.kt index 9e0ed0fed3..3c37165384 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/ShareNoteAsImageScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/ShareNoteAsImageScreen.kt @@ -123,7 +123,7 @@ fun ShareNoteAsImageScreen( accountViewModel: AccountViewModel, nav: INav, ) { - LoadNote(id, accountViewModel) { note -> + LoadNote(id) { note -> if (note != null) { ShareNoteAsImageScreen(note, accountViewModel, nav) } @@ -142,7 +142,7 @@ fun ShareNoteAsImageFileScreen( accountViewModel: AccountViewModel, nav: INav, ) { - LoadNote(id, accountViewModel) { note -> + LoadNote(id) { note -> if (note != null) { ShareNoteAsImageFileScreen(note, accountViewModel, nav) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/ShareNoteAsQrScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/ShareNoteAsQrScreen.kt index 3064770955..70320f2e5d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/ShareNoteAsQrScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/ShareNoteAsQrScreen.kt @@ -94,7 +94,7 @@ fun ShareNoteAsQrScreen( Scaffold( topBar = { TopBarWithBackButton(stringRes(Res.string.share_as_qr), nav) }, ) { pad -> - LoadNote(id, accountViewModel) { note -> + LoadNote(id) { note -> if (note != null) { ShareNoteAsQrScreenContent(note, accountViewModel, nav, pad) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AppDefinition.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AppDefinition.kt index ce0167b863..2369f4f58f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AppDefinition.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AppDefinition.kt @@ -585,7 +585,7 @@ private fun RelatedEventRow( val address = remember(aTag) { Address(aTag.kind, aTag.pubKeyHex, aTag.dTag) } val shape = RoundedCornerShape(8.dp) - LoadAddressableNote(address, accountViewModel) { note -> + LoadAddressableNote(address) { note -> if (note == null) return@LoadAddressableNote val noteState by observeNote(note, accountViewModel) @@ -709,7 +709,7 @@ fun ByAuthorChip( accountViewModel: AccountViewModel, nav: INav, ) { - LoadUser(baseUserHex = authorHex, accountViewModel = accountViewModel) { author -> + LoadUser(baseUserHex = authorHex) { author -> if (author == null) return@LoadUser Surface( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AppRecommendation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AppRecommendation.kt index f41bbd820b..9d11acf61e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AppRecommendation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AppRecommendation.kt @@ -85,7 +85,7 @@ fun RenderAppRecommendation( verticalArrangement = Arrangement.spacedBy(8.dp), ) { recommendations.forEach { recommendation -> - LoadAddressableNote(recommendation.address, accountViewModel) { appNote -> + LoadAddressableNote(recommendation.address) { appNote -> appNote?.let { if (it.event == null) { EventFinderFilterAssemblerSubscription(it, accountViewModel) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Attestation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Attestation.kt index 89bdc937bf..9b891d7f10 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Attestation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Attestation.kt @@ -221,7 +221,7 @@ fun RenderAttestation( if (quotesLeft > 0) { if (aboutAddress != null) { - LoadAddressableNote(aboutAddress, accountViewModel) { + LoadAddressableNote(aboutAddress) { if (it != null) { Spacer(modifier = DoubleVertSpacer) NoteCompose( @@ -238,7 +238,7 @@ fun RenderAttestation( } } } else if (aboutEvent != null) { - LoadNote(aboutEvent, accountViewModel) { + LoadNote(aboutEvent) { if (it != null) { Spacer(modifier = DoubleVertSpacer) NoteCompose( @@ -345,7 +345,7 @@ fun RenderAttestationRequest( if (quotesLeft > 0) { when { aboutAddress != null -> { - LoadAddressableNote(aboutAddress, accountViewModel) { + LoadAddressableNote(aboutAddress) { if (it != null) { Spacer(modifier = DoubleVertSpacer) Text( @@ -369,7 +369,7 @@ fun RenderAttestationRequest( } aboutEvent != null -> { - LoadNote(aboutEvent, accountViewModel) { + LoadNote(aboutEvent) { if (it != null) { Spacer(modifier = DoubleVertSpacer) Text( @@ -393,7 +393,7 @@ fun RenderAttestationRequest( } aboutPubkey != null -> { - LoadUser(aboutPubkey, accountViewModel) { + LoadUser(aboutPubkey) { if (it != null) { Spacer(modifier = DoubleVertSpacer) Text( @@ -458,7 +458,7 @@ fun RenderAttestorRecommendation( ) } - LoadUser(aboutPubKey, accountViewModel) { + LoadUser(aboutPubKey) { if (it != null) { Spacer(modifier = DoubleVertSpacer) UserCompose(it, accountViewModel = accountViewModel, nav = nav) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Badge.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Badge.kt index 5537e616a4..d90b4644cc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Badge.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Badge.kt @@ -315,8 +315,8 @@ fun AcceptBadgeControls( val amAwardee = remember(award, myPubkey) { award.awardeeIds().contains(myPubkey) } if (!amAwardee) return - val newNote = accountViewModel.getOrCreateAddressableNote(ProfileBadgesEvent.createAddress(myPubkey)) - val oldNote = accountViewModel.getOrCreateAddressableNote(AcceptedBadgeSetEvent.createAddress(myPubkey)) + val newNote = LocalCache.getOrCreateAddressableNote(ProfileBadgesEvent.createAddress(myPubkey)) + val oldNote = LocalCache.getOrCreateAddressableNote(AcceptedBadgeSetEvent.createAddress(myPubkey)) val newState by newNote .flow() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CalendarRsvpRender.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CalendarRsvpRender.kt index aadf78ffa2..ac30218f78 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CalendarRsvpRender.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CalendarRsvpRender.kt @@ -73,7 +73,7 @@ private fun LoadAppointmentBehind( ) { val address = remember(event) { event.calendarEventAddress() } ?: return - LoadAddressableNote(address, accountViewModel) { appointment -> + LoadAddressableNote(address) { appointment -> if (appointment != null) { EventFinderFilterAssemblerSubscription(appointment, accountViewModel) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Chess.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Chess.kt index d56e7edd8e..7f7c919335 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Chess.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Chess.kt @@ -91,7 +91,7 @@ fun RenderChessGame( ChessGameViewer( pgnContent = event.pgn(), playerContent = { playerHex -> - LoadUser(playerHex, accountViewModel) { user -> + LoadUser(playerHex) { user -> if (user != null) { UserChip(user = user, accountViewModel = accountViewModel, nav = nav) } else { @@ -196,7 +196,7 @@ fun RenderLiveChessChallenge( fontWeight = FontWeight.Bold, ) event.opponentPubkey()?.let { opponentHex -> - LoadUser(opponentHex, accountViewModel) { user -> + LoadUser(opponentHex) { user -> if (user != null) { UserChip(user = user, accountViewModel = accountViewModel, nav = nav) } @@ -313,7 +313,7 @@ fun RenderLiveChessGameEnd( ) event.opponentPubkey()?.let { opponentHex -> - LoadUser(opponentHex, accountViewModel) { user -> + LoadUser(opponentHex) { user -> if (user != null) { UserChip(user = user, accountViewModel = accountViewModel, nav = nav) } @@ -340,7 +340,7 @@ fun RenderLiveChessGameEnd( ChessGameViewer( pgnContent = pgn, playerContent = { playerHex -> - LoadUser(playerHex, accountViewModel) { user -> + LoadUser(playerHex) { user -> if (user != null) { UserChip(user = user, accountViewModel = accountViewModel, nav = nav) } else { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt index 24fc44c863..57712a02a7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt @@ -79,6 +79,7 @@ import com.vitorpamplona.amethyst.commons.resources.quick_action_share import com.vitorpamplona.amethyst.commons.resources.quick_action_share_browser_link import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.commons.ui.theme.ButtonPadding @@ -338,8 +339,8 @@ fun ShortCommunityHeader( contentDescription = stringRes(Res.string.profile_image), contentScale = ContentScale.Crop, modifier = HeaderPictureModifier, - loadProfilePicture = accountViewModel.settings.showProfilePictures(), - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + loadProfilePicture = LocalDisplaySettings.current.showProfilePictures, + loadRobohash = LocalDisplaySettings.current.loadRobohash, autoPlayGif = accountViewModel.settings.autoPlayVideosFlow .collectAsStateWithLifecycle() @@ -393,8 +394,8 @@ fun ShortCommunityHeaderNoActions( contentDescription = stringRes(Res.string.profile_image), contentScale = ContentScale.Crop, modifier = HeaderPictureModifier, - loadProfilePicture = accountViewModel.settings.showProfilePictures(), - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + loadProfilePicture = LocalDisplaySettings.current.showProfilePictures, + loadRobohash = LocalDisplaySettings.current.loadRobohash, autoPlayGif = accountViewModel.settings.autoPlayVideosFlow .collectAsStateWithLifecycle() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Emoji.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Emoji.kt index fab63e5e05..ebdf0422df 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Emoji.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Emoji.kt @@ -182,7 +182,6 @@ private fun EmojiListOptions( ) { LoadAddressableNote( accountViewModel.account.emoji.getEmojiPackSelectionAddress(), - accountViewModel, ) { it?.let { usersEmojiList -> val hasAddedThis by observeNoteAndMap(usersEmojiList, accountViewModel) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/EntityRating.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/EntityRating.kt index 3bba1f9328..af8149cfad 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/EntityRating.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/EntityRating.kt @@ -127,7 +127,7 @@ fun RenderEntityRating( Column(Modifier.fillMaxWidth()) { if (targetAddress != null) { - LoadAddressableNote(targetAddress, accountViewModel) { targetNote -> + LoadAddressableNote(targetAddress) { targetNote -> targetNote?.let { RatedPublicationCard( targetNote = it, @@ -142,7 +142,7 @@ fun RenderEntityRating( // The spec's default mark is `event`, and such a rating names its target by event id // rather than by coordinate. Without this the card printed the raw 64-hex id as its // title, which tells a reader nothing about what was rated. - LoadNote(targetEventId, accountViewModel) { targetNote -> + LoadNote(targetEventId) { targetNote -> if (targetNote != null) { RatedNoteCard(targetNote, mark, accountViewModel, nav) } else { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileStorage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileStorage.kt index 87acaabb24..5b8108f14f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileStorage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileStorage.kt @@ -50,7 +50,7 @@ fun FileStorageHeaderDisplay( val eventHeader = (baseNote.event as? FileStorageHeaderEvent) ?: return val dataEventId = eventHeader.dataEventId() ?: return - LoadNote(baseNoteHex = dataEventId, accountViewModel) { contentNote -> + LoadNote(baseNoteHex = dataEventId) { contentNote -> if (contentNote != null) { ObserverAndRenderNIP95(baseNote, contentNote, roundedCorner, contentScale, accountViewModel) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Geocache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Geocache.kt index 34d39223f1..11b3217698 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Geocache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Geocache.kt @@ -169,7 +169,7 @@ fun RenderGeocacheFoundLog( // Loaded even for an unverified log: the cache's name is what tells a reader in a feed what // was actually found, and "Found it!" on its own says nothing. - LoadAddressableNote(address, accountViewModel) { cacheNote -> + LoadAddressableNote(address) { cacheNote -> if (cacheNote == null) { GeocacheFoundLogCard(noteEvent, showImages, if (hasProof) FoundLogProof.UNKNOWN else FoundLogProof.NONE) } else { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Git.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Git.kt index 07579bc148..002c3ad80f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Git.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Git.kt @@ -451,7 +451,7 @@ private fun RenderGitPatchEvent( val repository = remember(noteEvent) { noteEvent.repositoryAddress() } if (repository != null) { Spacer(modifier = StdVertSpacer) - LoadAddressableNote(repository, accountViewModel) { + LoadAddressableNote(repository) { if (it != null) { RenderShortRepositoryHeader(it, accountViewModel, nav) } @@ -550,7 +550,7 @@ private fun RenderGitIssueEvent( val repository = remember(noteEvent) { noteEvent.repositoryAddress() } if (repository != null) { Spacer(modifier = StdVertSpacer) - LoadAddressableNote(repository, accountViewModel) { + LoadAddressableNote(repository) { if (it != null) { RenderShortRepositoryHeader(it, accountViewModel, nav) } @@ -610,7 +610,7 @@ private fun RenderGitPullRequestEvent( GitCardContainer { val repository = remember(noteEvent) { noteEvent.repositoryAddress() } if (repository != null) { - LoadAddressableNote(repository, accountViewModel) { + LoadAddressableNote(repository) { if (it != null) { RenderShortRepositoryHeader(it, accountViewModel, nav) } @@ -751,7 +751,7 @@ private fun RenderGitPullRequestUpdateEvent( val repository = remember(noteEvent) { noteEvent.repositoryAddress() } if (repository != null) { Spacer(modifier = StdVertSpacer) - LoadAddressableNote(repository, accountViewModel) { + LoadAddressableNote(repository) { if (it != null) { RenderShortRepositoryHeader(it, accountViewModel, nav) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Highlight.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Highlight.kt index 39071710d7..2fd5c0428f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Highlight.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Highlight.kt @@ -46,6 +46,7 @@ import com.vitorpamplona.amethyst.commons.model.EmptyTagList import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.highlights.HighlightQuote import com.vitorpamplona.amethyst.commons.model.navigation.routeFor import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists @@ -308,23 +309,23 @@ private fun DisplayQuoteAuthor( accountViewModel: AccountViewModel, nav: INav, ) { - var userBase by remember { mutableStateOf(authorHex?.let { accountViewModel.getUserIfExists(it) }) } + var userBase by remember { mutableStateOf(authorHex?.let { LocalCache.getUserIfExists(it) }) } if (userBase == null && authorHex != null) { LaunchedEffect(authorHex) { - userBase = accountViewModel.checkGetOrCreateUser(authorHex) + userBase = LocalCache.checkGetOrCreateUser(authorHex) } } var addressable by remember { - mutableStateOf(postAddress?.let { accountViewModel.getAddressableNoteIfExists(it) }) + mutableStateOf(postAddress?.let { LocalCache.getAddressableNoteIfExists(it) }) } if (addressable == null && postAddress != null) { LaunchedEffect(key1 = postAddress) { val newNote = withContext(Dispatchers.IO) { - accountViewModel.getOrCreateAddressableNote(postAddress) + LocalCache.getOrCreateAddressableNote(postAddress) } if (addressable != newNote) { addressable = newNote @@ -333,14 +334,14 @@ private fun DisplayQuoteAuthor( } var version by remember { - mutableStateOf(postVersion?.let { accountViewModel.getNoteIfExists(it.eventId) }) + mutableStateOf(postVersion?.let { LocalCache.getNoteIfExists(it.eventId) }) } if (version == null && postVersion != null) { LaunchedEffect(key1 = postVersion) { val newNote = withContext(Dispatchers.IO) { - accountViewModel.getOrCreateNote(postVersion.eventId) + LocalCache.getOrCreateNote(postVersion.eventId) } if (version != newNote) { version = newNote diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/InteractiveStory.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/InteractiveStory.kt index d4a7ab7c01..f3e3d65512 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/InteractiveStory.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/InteractiveStory.kt @@ -79,7 +79,7 @@ fun RenderInteractiveStory( val currentScene = readingState?.currentScene() if (currentScene != null && currentScene != rootEvent.address()) { - LoadAddressableNote(currentScene, accountViewModel) { currentSceneBaseNote -> + LoadAddressableNote(currentScene) { currentSceneBaseNote -> currentSceneBaseNote?.let { val currentSceneEvent by observeNoteEvent(it, accountViewModel) @@ -168,7 +168,7 @@ fun RenderInteractiveStory( if (options.isNotEmpty()) { Column(Modifier.padding(top = 10.dp)) { options.forEach { opt -> - LoadAddressableNote(opt.address, accountViewModel) { note -> + LoadAddressableNote(opt.address) { note -> if (note != null) { EventFinderFilterAssemblerSubscription(note, accountViewModel) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivityChatMessage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivityChatMessage.kt index bb03a0e593..a1c4297b06 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivityChatMessage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivityChatMessage.kt @@ -61,7 +61,7 @@ fun RenderLiveActivityChatMessage( } showChannelInfo?.let { - LoadLiveActivityChannel(it, accountViewModel) { + LoadLiveActivityChannel(it) { LiveActivitiesChannelHeader( baseChannel = it, showVideo = false, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MeetingSpace.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MeetingSpace.kt index 1398cd086c..bae3eb41e3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MeetingSpace.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MeetingSpace.kt @@ -340,7 +340,6 @@ private fun ParentMeetingSpaceLink( com.vitorpamplona.amethyst.ui.note.LoadAddressableNote( address = spaceAddress, - accountViewModel = accountViewModel, ) { spaceNote -> spaceNote ?: return@LoadAddressableNote val spaceEvent = spaceNote.event as? MeetingSpaceEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MusicPlaylist.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MusicPlaylist.kt index fca84ed76d..d3518bc2f1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MusicPlaylist.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MusicPlaylist.kt @@ -260,7 +260,7 @@ fun MusicPlaylistHeader( color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.06f), ) } - LoadAddressableNote(address, accountViewModel) { trackNote -> + LoadAddressableNote(address) { trackNote -> if (trackNote != null) { PlaylistTrackRow( position = index + 1, @@ -678,7 +678,7 @@ private fun RenderMusicPlaylistPreview() { } ThemeComparisonColumn { - LoadNote(baseNoteHex = event.address().toValue(), accountViewModel = mockAccountViewModel()) { note -> + LoadNote(baseNoteHex = event.address().toValue()) { note -> note?.let { RenderMusicPlaylist( note = it, @@ -710,7 +710,7 @@ private fun RenderMusicPlaylistCollaborativePrivatePreview() { remember(event) { runBlocking { withContext(Dispatchers.IO) { LocalCache.justConsume(event, null, true) } } } ThemeComparisonColumn { - LoadNote(baseNoteHex = event.address().toValue(), accountViewModel = mockAccountViewModel()) { note -> + LoadNote(baseNoteHex = event.address().toValue()) { note -> note?.let { RenderMusicPlaylist( note = it, @@ -740,7 +740,7 @@ private fun MusicPlaylistCoverPreview() { remember(event) { runBlocking { withContext(Dispatchers.IO) { LocalCache.justConsume(event, null, true) } } } ThemeComparisonColumn { - LoadNote(baseNoteHex = event.address().toValue(), accountViewModel = mockAccountViewModel()) { note -> + LoadNote(baseNoteHex = event.address().toValue()) { note -> note?.let { MusicPlaylistCover( image = null, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MusicTrack.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MusicTrack.kt index b56425eabd..ec896a8963 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MusicTrack.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MusicTrack.kt @@ -517,7 +517,7 @@ private fun RenderMusicTrackPreview() { remember(event) { runBlocking { withContext(Dispatchers.IO) { LocalCache.justConsume(event, null, true) } } } ThemeComparisonColumn { - LoadNote(baseNoteHex = event.address().toValue(), accountViewModel = mockAccountViewModel()) { note -> + LoadNote(baseNoteHex = event.address().toValue()) { note -> note?.let { RenderMusicTrack( note = it, @@ -549,7 +549,7 @@ private fun RenderMusicTrackExplicitPreview() { remember(event) { runBlocking { withContext(Dispatchers.IO) { LocalCache.justConsume(event, null, true) } } } ThemeComparisonColumn { - LoadNote(baseNoteHex = event.address().toValue(), accountViewModel = mockAccountViewModel()) { note -> + LoadNote(baseNoteHex = event.address().toValue()) { note -> note?.let { RenderMusicTrack( note = it, @@ -571,7 +571,7 @@ private fun MusicTrackCoverPreview() { remember(event) { runBlocking { withContext(Dispatchers.IO) { LocalCache.justConsume(event, null, true) } } } ThemeComparisonColumn { - LoadNote(baseNoteHex = event.address().toValue(), accountViewModel = mockAccountViewModel()) { note -> + LoadNote(baseNoteHex = event.address().toValue()) { note -> note?.let { MusicTrackCover(image = null, note = it, accountViewModel = mockAccountViewModel()) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PodcastPeople.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PodcastPeople.kt index 8f8a2a185a..5e00ab1f3a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PodcastPeople.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PodcastPeople.kt @@ -47,6 +47,7 @@ import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.podcast_hosts_and_guests import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp import com.vitorpamplona.amethyst.commons.ui.theme.grayText @@ -104,7 +105,7 @@ private fun PersonItem( val pubKey = remember(person) { person.nostrPubKey() } if (pubKey != null) { - LoadUser(pubKey, accountViewModel) { user -> + LoadUser(pubKey) { user -> if (user != null) { NostrPersonCard(user, person.role, accountViewModel, nav) } else { @@ -157,8 +158,8 @@ private fun FreeTextPersonCard( model = person.img, contentDescription = person.name, modifier = Modifier.size(56.dp).clip(CircleShape), - loadProfilePicture = accountViewModel.settings.showProfilePictures(), - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + loadProfilePicture = LocalDisplaySettings.current.showProfilePictures, + loadRobohash = LocalDisplaySettings.current.loadRobohash, ) }, name = { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Publication.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Publication.kt index db29a5d3b3..ae654a1f05 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Publication.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Publication.kt @@ -319,7 +319,7 @@ internal fun PublicationSectionRow( val address = ref.address if (address != null) { - LoadAddressableNote(address, accountViewModel) { sectionNote -> + LoadAddressableNote(address) { sectionNote -> if (sectionNote != null) { ObservedSectionRow(position, ref, sectionNote, accountViewModel, nav) } else { @@ -327,7 +327,7 @@ internal fun PublicationSectionRow( } } } else if (ref.eventId != null) { - LoadNote(ref.eventId!!, accountViewModel) { sectionNote -> + LoadNote(ref.eventId!!) { sectionNote -> if (sectionNote != null) { ObservedSectionRow(position, ref, sectionNote, accountViewModel, nav) } else { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PublicationSection.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PublicationSection.kt index 62a07a7526..73fea0a6e4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PublicationSection.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PublicationSection.kt @@ -113,7 +113,7 @@ fun RenderPublicationSection( // A chapter arrived at from a search, a mention or a wikilink has nothing around it to say // what it is a chapter *of*. The index names itself, so show it above the title. noteEvent.publicationAddress()?.let { address -> - LoadAddressableNote(address, accountViewModel) { indexNote -> + LoadAddressableNote(address) { indexNote -> indexNote?.let { PublicationCrumb(it, accountViewModel, nav) } } } @@ -142,7 +142,7 @@ fun RenderPublicationSection( } noteEvent.publicationAddress()?.let { address -> - LoadAddressableNote(address, accountViewModel) { indexNote -> + LoadAddressableNote(address) { indexNote -> indexNote?.let { PublicationPager(noteEvent, it, accountViewModel, nav) } } } @@ -253,11 +253,11 @@ private fun PagerEnd( // The index's own title shows immediately; the neighbour is still observed, because observing // is what fetches it and it carries the better title. if (address != null) { - LoadAddressableNote(address, accountViewModel) { target -> + LoadAddressableNote(address) { target -> target?.let { PagerButton(ref, it, isBack, accountViewModel, nav, modifier) } } } else if (eventId != null) { - LoadNote(eventId, accountViewModel) { target -> + LoadNote(eventId) { target -> target?.let { PagerButton(ref, it, isBack, accountViewModel, nav, modifier) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayGroupMessage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayGroupMessage.kt index ab17537244..a090661335 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayGroupMessage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayGroupMessage.kt @@ -51,6 +51,7 @@ import com.vitorpamplona.amethyst.commons.nip29RelayGroups.ui.RelayNameChip import com.vitorpamplona.amethyst.commons.ui.components.GenericLoadable import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.ui.theme.replyModifier import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserName @@ -135,8 +136,8 @@ fun RelayGroupChannelHeader( model = channel.profilePicture(), contentDescription = channel.toBestDisplayName(), modifier = Modifier.size(28.dp).clip(CircleShape), - loadProfilePicture = accountViewModel.settings.showProfilePictures(), - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + loadProfilePicture = LocalDisplaySettings.current.showProfilePictures, + loadRobohash = LocalDisplaySettings.current.loadRobohash, ) if (dmOther != null) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RememberCaptionTracks.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RememberCaptionTracks.kt index 15402138f7..b122f7fa43 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RememberCaptionTracks.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RememberCaptionTracks.kt @@ -24,6 +24,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.key import androidx.compose.runtime.remember +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip71Video.CaptionTrack import com.vitorpamplona.amethyst.commons.model.nip71Video.captionTracks import com.vitorpamplona.amethyst.commons.model.nip71Video.mergeCaptionTracks @@ -60,7 +61,7 @@ fun rememberCaptionTracks( val resolved = refs.pending.map { pending -> key(pending.address.toValue()) { - val note = remember(pending.address) { accountViewModel.getOrCreateAddressableNote(pending.address) } + val note = remember(pending.address) { LocalCache.getOrCreateAddressableNote(pending.address) } val trackEvent by observeNoteEvent(note, accountViewModel) trackEvent?.toCaptionTrack(pending.tag) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RenderPostApproval.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RenderPostApproval.kt index a2dbb2dc01..1936c22fb9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RenderPostApproval.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RenderPostApproval.kt @@ -58,7 +58,7 @@ fun RenderPostApproval( Column(Modifier.fillMaxWidth()) { noteEvent.communityAddresses().forEach { tag -> - LoadAddressableNote(tag, accountViewModel) { baseNote -> + LoadAddressableNote(tag) { baseNote -> baseNote?.let { RenderCommunity( baseNote = it, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RenderVideoCollaboration.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RenderVideoCollaboration.kt index 66f946325d..6eccfe2176 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RenderVideoCollaboration.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RenderVideoCollaboration.kt @@ -82,7 +82,7 @@ fun RenderVideoCollaboration( // quotesLeft guards the recursion: the embedded video can itself credit other work, and // NoteCompose is what decrements it on the way down. if (video != null && quotesLeft > 0) { - LoadAddressableNote(video, accountViewModel) { videoNote -> + LoadAddressableNote(video) { videoNote -> videoNote?.let { NoteCompose( baseNote = it, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RenderVideoCredits.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RenderVideoCredits.kt index dbc69f86d3..7dd1ebb208 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RenderVideoCredits.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RenderVideoCredits.kt @@ -103,7 +103,7 @@ private fun RenderVideoCredit( ) { when (val target = credit.target) { is CreditTarget.Person -> - LoadUser(target.pubKey, accountViewModel) { user -> + LoadUser(target.pubKey) { user -> // Until the profile arrives there is no name to print, and a hex key in the // credits reads as noise rather than as a person. user?.let { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RenderVideoCurationSet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RenderVideoCurationSet.kt index cd11449ce6..d47478fa3f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RenderVideoCurationSet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RenderVideoCurationSet.kt @@ -169,12 +169,12 @@ private fun VideoListMemberPoster( ) { when (item) { is AddressBookmark -> - LoadAddressableNote(item.address, accountViewModel) { note -> + LoadAddressableNote(item.address) { note -> note?.let { VideoPoster(it, accountViewModel) { nav.nav(Route.Note(addressTag(item.address))) } } } is EventBookmark -> - LoadNote(item.eventId, accountViewModel) { note -> + LoadNote(item.eventId) { note -> note?.let { VideoPoster(it, accountViewModel) { nav.nav(Route.Note(item.eventId)) } } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/SnoPalettes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/SnoPalettes.kt index 7072fa516f..06ce9e82d9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/SnoPalettes.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/SnoPalettes.kt @@ -76,7 +76,7 @@ fun WithSnoPalette( is NEvent -> LoadPaletteNote(entity.hex, accountViewModel, content) is NNote -> LoadPaletteNote(entity.hex, accountViewModel, content) is NAddress -> - LoadAddressableNote(entity.address(), accountViewModel) { note -> + LoadAddressableNote(entity.address()) { note -> content(remember(note?.event) { note?.event?.let { SnoPaletteEventReader.read(it) } }) } // A reference the parser accepted as well-formed but that names nothing @@ -91,7 +91,7 @@ private fun LoadPaletteNote( accountViewModel: AccountViewModel, content: @Composable (SnoPalette?) -> Unit, ) { - LoadNote(hex, accountViewModel) { note -> + LoadNote(hex) { note -> // The kind is deliberately not checked: §1.3b says this format "does // not define a palette kind and does not want one", and a reader that // accepts the shape reads whatever convention wins. An event that is diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/SoftwareApp.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/SoftwareApp.kt index 83714f95a5..2bb1f341e6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/SoftwareApp.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/SoftwareApp.kt @@ -587,7 +587,7 @@ private fun BundledAssetsList( Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { assetIds.forEach { id -> key(id) { - LoadAssetNote(id, accountViewModel) { assetNote -> + LoadAssetNote(id) { assetNote -> if (assetNote != null) { SoftwareAssetRow(assetNote, accountViewModel) } @@ -600,12 +600,11 @@ private fun BundledAssetsList( @Composable private fun LoadAssetNote( eventId: String, - accountViewModel: AccountViewModel, content: @Composable (Note?) -> Unit, ) { - val note by produceState(initialValue = accountViewModel.getNoteIfExists(eventId), eventId) { + val note by produceState(initialValue = LocalCache.getNoteIfExists(eventId), eventId) { if (value == null) { - value = accountViewModel.checkGetOrCreateNote(eventId) + value = LocalCache.checkGetOrCreateNote(eventId) } } content(note) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TextModification.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TextModification.kt index 593c22c8b4..627d61916c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TextModification.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TextModification.kt @@ -45,6 +45,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.commons.model.EmptyTagList import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.navigation.routeFor import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.accept_the_suggestion @@ -88,13 +89,13 @@ fun RenderTextModificationEvent( val isAuthorTheLoggedUser = remember { val authorOfTheOriginalNote = - noteEvent.editedNote()?.let { accountViewModel.getNoteIfExists(it.eventId)?.author?.pubkeyHex ?: it.author } + noteEvent.editedNote()?.let { LocalCache.getNoteIfExists(it.eventId)?.author?.pubkeyHex ?: it.author } mutableStateOf(accountViewModel.isLoggedUser(authorOfTheOriginalNote)) } noteEvent.editedNote()?.let { - LoadNote(baseNoteHex = it.eventId, accountViewModel = accountViewModel) { baseOriginalNote -> + LoadNote(baseNoteHex = it.eventId) { baseOriginalNote -> baseOriginalNote?.let { } } @@ -132,7 +133,7 @@ fun RenderTextModificationEvent( } noteEvent.editedNote()?.let { - LoadNote(baseNoteHex = it.eventId, accountViewModel = accountViewModel) { baseNote -> + LoadNote(baseNoteHex = it.eventId) { baseNote -> baseNote?.let { val noteState by observeNote(baseNote, accountViewModel) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TorrentComment.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TorrentComment.kt index b899bf43a3..c6814f0d7e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TorrentComment.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TorrentComment.kt @@ -212,7 +212,7 @@ fun TorrentHeader( accountViewModel: AccountViewModel, nav: INav, ) { - LoadNote(baseNoteHex = torrentHex, accountViewModel = accountViewModel) { + LoadNote(baseNoteHex = torrentHex) { if (it != null) { ShortTorrentHeader(it, modifier, accountViewModel, nav) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/WikiCollaboration.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/WikiCollaboration.kt index 530d1bddf3..4338782fd5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/WikiCollaboration.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/WikiCollaboration.kt @@ -213,7 +213,7 @@ private fun WikiArticleRow( accountViewModel: AccountViewModel, nav: INav, ) { - LoadAddressableNote(address, accountViewModel) { articleNote -> + LoadAddressableNote(address) { articleNote -> if (articleNote != null) { val article by observeNoteEvent(articleNote, accountViewModel) @@ -238,7 +238,7 @@ private fun WikiVersionRow( accountViewModel: AccountViewModel, nav: INav, ) { - LoadNote(eventId, accountViewModel) { versionNote -> + LoadNote(eventId) { versionNote -> if (versionNote != null) { val article by observeNoteEvent(versionNote, accountViewModel) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/lists/ListItemRows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/lists/ListItemRows.kt index df811da87b..bf722835da 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/lists/ListItemRows.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/lists/ListItemRows.kt @@ -58,7 +58,7 @@ fun UserMemberRow( accountViewModel: AccountViewModel, nav: INav, ) { - LoadUser(pubKey, accountViewModel) { user -> + LoadUser(pubKey) { user -> user?.let { Column(modifier = Modifier.fillMaxWidth()) { UserCompose(it, accountViewModel = accountViewModel, nav = nav) @@ -88,7 +88,7 @@ fun BookmarkMemberRow( is AddressBookmark -> AddressMemberRow(item.address, quotesLeft, backgroundColor, accountViewModel, nav) is EventBookmark -> - LoadNote(item.eventId, accountViewModel) { note -> + LoadNote(item.eventId) { note -> note?.let { NoteCompose( baseNote = it, @@ -114,7 +114,7 @@ fun AddressMemberRow( ) { if (quotesLeft <= 0) return - LoadAddressableNote(address, accountViewModel) { note -> + LoadAddressableNote(address) { note -> note?.let { NoteCompose( baseNote = it, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/lists/MuteList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/lists/MuteList.kt index 87008d7ba0..9d1b21782d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/lists/MuteList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/lists/MuteList.kt @@ -95,7 +95,7 @@ private fun MutedThreadRow( ) { if (quotesLeft <= 0) return - LoadNote(eventId, accountViewModel) { note -> + LoadNote(eventId) { note -> note?.let { NoteCompose( baseNote = it, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 57381f76b9..04244170d0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -46,18 +46,17 @@ import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Dao import com.vitorpamplona.amethyst.commons.model.LiveHiddenUsers import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.UiSettingsFlow import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.backups.ReplaceableBackupConflict import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.concord.ConcordChannel import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel -import com.vitorpamplona.amethyst.commons.model.geohashChat.GeohashChatChannel import com.vitorpamplona.amethyst.commons.model.navigation.BottomBarEntry import com.vitorpamplona.amethyst.commons.model.navigation.NavBarItem import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel -import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.commons.model.nip56Reports.UserReportWarningState import com.vitorpamplona.amethyst.commons.model.nip56Reports.dmReportWarningFor import com.vitorpamplona.amethyst.commons.model.observables.CreatedAtComparator @@ -101,8 +100,10 @@ import com.vitorpamplona.amethyst.commons.service.broadcast.BroadcastTracker import com.vitorpamplona.amethyst.commons.service.http.EmptyRoleBasedHttpClientBuilder import com.vitorpamplona.amethyst.commons.service.http.IRoleBasedHttpClientBuilder import com.vitorpamplona.amethyst.commons.service.pow.PoWCategory +import com.vitorpamplona.amethyst.commons.state.UiSettingsState import com.vitorpamplona.amethyst.commons.tor.TorType import com.vitorpamplona.amethyst.commons.ui.components.UrlPreviewState +import com.vitorpamplona.amethyst.commons.ui.components.toasts.ToastManager import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.notifications.CardFeedState import com.vitorpamplona.amethyst.commons.ui.state.GenericBaseCache @@ -111,7 +112,6 @@ import com.vitorpamplona.amethyst.logTime import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LatestKeyPackageOwner -import com.vitorpamplona.amethyst.model.UiSettingsFlow import com.vitorpamplona.amethyst.model.UrlCachedPreviewer import com.vitorpamplona.amethyst.model.privacyOptions.RoleBasedHttpClientBuilder import com.vitorpamplona.amethyst.service.ClinkDebitPayer @@ -127,13 +127,11 @@ import com.vitorpamplona.amethyst.service.pow.powKindLabelRes import com.vitorpamplona.amethyst.service.relayClient.reqCommand.RelaySubscriptionsCoordinator import com.vitorpamplona.amethyst.ui.actions.MediaSaverToDisk import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger -import com.vitorpamplona.amethyst.ui.components.toasts.ToastManager import com.vitorpamplona.amethyst.ui.note.ZapAmountCommentNotification import com.vitorpamplona.amethyst.ui.note.ZapraiserStatus import com.vitorpamplona.amethyst.ui.note.payViaIntent import com.vitorpamplona.amethyst.ui.note.showAmount import com.vitorpamplona.amethyst.ui.note.showAmountInteger -import com.vitorpamplona.amethyst.ui.screen.UiSettingsState import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.send.MarmotGroupIconChange import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.send.MarmotGroupIconUpload import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.send.MarmotGroupIconUploader @@ -146,7 +144,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.ReloadMintRequest import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow import com.vitorpamplona.quartz.experimental.clink.debits.DebitResponse import com.vitorpamplona.quartz.experimental.clink.pointers.NDebit -import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryBaseEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryReadingStateEvent import com.vitorpamplona.quartz.marmot.appComponents.EncryptedMediaReferenceV2 @@ -195,7 +192,6 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.NPub import com.vitorpamplona.quartz.nip19Bech32.entities.NRelay import com.vitorpamplona.quartz.nip19Bech32.entities.NSec import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent -import com.vitorpamplona.quartz.nip29RelayGroups.GroupId import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect import com.vitorpamplona.quartz.nip47WalletConnect.rpc.IErrorResponseLike @@ -2266,32 +2262,6 @@ class AccountViewModel( override fun getOrCreateAddressableNote(address: Address): AddressableNote = LocalCache.getOrCreateAddressableNote(address) - fun getAddressableNoteIfExists(key: String): AddressableNote? = LocalCache.getAddressableNoteIfExists(key) - - fun getAddressableNoteIfExists(key: Address): AddressableNote? = LocalCache.getAddressableNoteIfExists(key) - - fun checkGetOrCreatePublicChatChannel(key: HexKey): PublicChatChannel = LocalCache.getOrCreatePublicChatChannel(key) - - fun checkGetOrCreateLiveActivityChannel(key: Address): LiveActivitiesChannel = LocalCache.getOrCreateLiveChannel(key) - - fun checkGetOrCreateEphemeralChatChannel(key: RoomId): EphemeralChatChannel = LocalCache.getOrCreateEphemeralChannel(key) - - fun getPublicChatChannelIfExists(hex: HexKey) = LocalCache.getPublicChatChannelIfExists(hex) - - fun getEphemeralChatChannelIfExists(key: RoomId) = LocalCache.getEphemeralChatChannelIfExists(key) - - fun checkGetOrCreateGeohashChannel(geohash: String): GeohashChatChannel = LocalCache.getOrCreateGeohashChannel(geohash) - - fun getGeohashChannelIfExists(geohash: String) = LocalCache.getGeohashChannelIfExists(geohash) - - fun checkGetOrCreateRelayGroupChannel(key: GroupId): RelayGroupChannel = LocalCache.getOrCreateRelayGroupChannel(key) - - fun getRelayGroupChannelIfExists(key: GroupId) = LocalCache.getRelayGroupChannelIfExists(key) - - fun getRelayGroupChannelsOnRelay(relay: NormalizedRelayUrl) = LocalCache.getRelayGroupChannelsOnRelay(relay) - - fun getLiveActivityChannelIfExists(key: Address) = LocalCache.getLiveActivityChannelIfExists(key) - fun loadParticipants( participants: List, onReady: (ImmutableList>) -> Unit, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictEventViews.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictEventViews.kt index 9c552c4f0f..419bd3b2a2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictEventViews.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictEventViews.kt @@ -72,6 +72,7 @@ import com.vitorpamplona.amethyst.commons.model.navigation.routeFor import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.util.toShortDisplay import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel @@ -269,7 +270,7 @@ private fun PersonCell( Badge.ADDED -> "+" to tones.added Badge.CHANGED -> "~" to tones.changed } - LoadUser(pubKey, accountViewModel) { user -> + LoadUser(pubKey) { user -> Column( modifier.clip(RoundedCornerShape(12.dp)).clickable { nav.nav(Route.Profile(pubKey)) }.padding(vertical = 4.dp), horizontalAlignment = Alignment.CenterHorizontally, @@ -437,7 +438,7 @@ private fun LazyListScope.muteListItems( if (threads.isNotEmpty()) { item(key = "mute-threads-caption", contentType = "caption") { SectionCaption(stringRes(R.string.backup_entry_thread), Pad.padding(top = 18.dp)) } items(threads, key = { "mute-thread-$it" }, contentType = { "note" }) { id -> - LoadNote(id, accountViewModel) { note -> + LoadNote(id) { note -> if (note != null) { NoteCompose(note, modifier = Modifier.padding(horizontal = 16.dp, vertical = 6.dp), makeItShort = true, quotesLeft = 1, accountViewModel = accountViewModel, nav = nav) } else { @@ -489,8 +490,8 @@ private fun FacePile( model = observedPicture(pubKey, accountViewModel), contentDescription = null, modifier = Modifier.size(42.dp).clip(CircleShape), - loadProfilePicture = accountViewModel.settings.showProfilePictures(), - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + loadProfilePicture = LocalDisplaySettings.current.showProfilePictures, + loadRobohash = LocalDisplaySettings.current.loadRobohash, ) } } @@ -515,9 +516,9 @@ internal fun observedUserInfo( pubKey: HexKey, accountViewModel: AccountViewModel, ): UserMetadata? { - var user by remember(pubKey) { mutableStateOf(accountViewModel.getUserIfExists(pubKey)) } + var user by remember(pubKey) { mutableStateOf(LocalCache.getUserIfExists(pubKey)) } if (user == null) { - LaunchedEffect(pubKey) { user = accountViewModel.checkGetOrCreateUser(pubKey) } + LaunchedEffect(pubKey) { user = LocalCache.checkGetOrCreateUser(pubKey) } } val loaded = user ?: return null val info by observeUserInfo(loaded, accountViewModel) @@ -688,8 +689,8 @@ private fun MiniProfileCard( model = meta.picture, contentDescription = null, modifier = Modifier.size(46.dp).clip(CircleShape), - loadProfilePicture = accountViewModel.settings.showProfilePictures(), - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + loadProfilePicture = LocalDisplaySettings.current.showProfilePictures, + loadRobohash = LocalDisplaySettings.current.loadRobohash, ) } Column(Modifier.offset(y = (-20).dp), verticalArrangement = Arrangement.spacedBy(3.dp)) { @@ -1122,7 +1123,7 @@ private fun GroupTileCard( GroupTileContent(tile, tile.group.name ?: tile.group.groupId, null, accountViewModel, modifier, null) return } - LoadRelayGroupChannel(GroupId(tile.group.groupId, relay), accountViewModel) { channel -> + LoadRelayGroupChannel(GroupId(tile.group.groupId, relay)) { channel -> // Subscribes to the group's relay-signed metadata (even when not joined) and recomposes // when its name or picture arrives. val state by observeChannel(channel, accountViewModel) @@ -1158,8 +1159,8 @@ private fun GroupTileContent( model = picture, contentDescription = null, modifier = Modifier.size(44.dp).clip(RoundedCornerShape(14.dp)), - loadProfilePicture = accountViewModel.settings.showProfilePictures(), - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + loadProfilePicture = LocalDisplaySettings.current.showProfilePictures, + loadRobohash = LocalDisplaySettings.current.loadRobohash, ) Column { Text(name, style = MaterialTheme.typography.titleSmall, maxLines = 1, overflow = TextOverflow.Ellipsis) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictListViews.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictListViews.kt index b8f7dca79e..55473cce27 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictListViews.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictListViews.kt @@ -60,10 +60,12 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.backups.BackupEventType import com.vitorpamplona.amethyst.commons.model.backups.ReplaceableBackupConflict +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.model.navigation.routeFor import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.util.toShortDisplay import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel @@ -424,8 +426,8 @@ private fun SpaceTile( model = picture, contentDescription = null, modifier = Modifier.size(48.dp).clip(CircleShape), - loadProfilePicture = accountViewModel.settings.showProfilePictures(), - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + loadProfilePicture = LocalDisplaySettings.current.showProfilePictures, + loadRobohash = LocalDisplaySettings.current.loadRobohash, ) Column { Text(name, style = MaterialTheme.typography.titleSmall, maxLines = 1, overflow = TextOverflow.Ellipsis) @@ -451,7 +453,7 @@ private fun LazyListScope.publicChatItems( ) { val saved = (conflict.saved as? ChannelListEvent)?.tags?.channels().orEmpty() spaceGrid("chat-", fates(saved, diff.channels) { it.eventId }, { it.eventId }) { tag, fate, modifier -> - LoadPublicChatChannel(tag.eventId, accountViewModel) { channel -> + LoadPublicChatChannel(tag.eventId) { channel -> // Subscribes to the channel's metadata on relays and recomposes when it arrives. val state by observeChannel(channel, accountViewModel) val current = state?.channel ?: channel @@ -492,7 +494,7 @@ private fun LazyListScope.favoriteFeedItems( val saved = (conflict.saved as? FavoriteAlgoFeedsListEvent)?.publicFavoriteAlgoFeeds().orEmpty() spaceGrid("feed-", fates(saved, diff.feeds) { it.address }, { it.address.toValue() }) { bookmark, fate, modifier -> AddressableSpaceTile(bookmark.address, fate, accountViewModel, modifier) { - nav.nav { accountViewModel.getAddressableNoteIfExists(bookmark.address)?.let { routeFor(it, accountViewModel.account) } } + nav.nav { LocalCache.getAddressableNoteIfExists(bookmark.address)?.let { routeFor(it, accountViewModel.account) } } } } privateItemsCard(diff.privateItems) @@ -507,7 +509,7 @@ private fun AddressableSpaceTile( modifier: Modifier, onClick: () -> Unit, ) { - LoadAddressableNote(address, accountViewModel) { note -> + LoadAddressableNote(address) { note -> val event = if (note != null) { val state by observeNote(note, accountViewModel) @@ -788,7 +790,7 @@ private fun ProviderCell( Text("—", modifier, color = MaterialTheme.colorScheme.placeholderText) return } - LoadUser(pubKey, accountViewModel) { user -> + LoadUser(pubKey) { user -> Row( modifier.clip(RoundedCornerShape(8.dp)).clickable { nav.nav(Route.Profile(pubKey)) }, verticalAlignment = Alignment.CenterVertically, @@ -800,8 +802,8 @@ private fun ProviderCell( model = observedPicture(pubKey, accountViewModel), contentDescription = null, modifier = Modifier.size(22.dp).clip(CircleShape), - loadProfilePicture = accountViewModel.settings.showProfilePictures(), - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + loadProfilePicture = LocalDisplaySettings.current.showProfilePictures, + loadRobohash = LocalDisplaySettings.current.loadRobohash, ) ProvideTextStyle(MaterialTheme.typography.labelMedium.copy(color = if (struck) color else MaterialTheme.colorScheme.onSurface, textDecoration = if (struck) TextDecoration.LineThrough else null)) { if (user != null) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictReviewScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictReviewScreen.kt index 1a89df595d..8b5d7b0150 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictReviewScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictReviewScreen.kt @@ -422,7 +422,7 @@ internal fun ReviewEntry( when (item) { is ReviewItem.Person -> PersonEntry(item, accountViewModel, nav) is ReviewItem.Thread -> - LoadNote(item.eventId, accountViewModel) { note -> + LoadNote(item.eventId) { note -> if (note != null) { Column(EntryPadding) { NoteCompose(note, makeItShort = true, quotesLeft = 1, accountViewModel = accountViewModel, nav = nav) @@ -433,7 +433,7 @@ internal fun ReviewEntry( } } is ReviewItem.Addressable -> - LoadAddressableNote(item.address, accountViewModel) { note -> + LoadAddressableNote(item.address) { note -> if (note != null) { Column(EntryPadding) { NoteCompose(note, makeItShort = true, quotesLeft = 1, accountViewModel = accountViewModel, nav = nav) @@ -457,7 +457,7 @@ private fun PersonEntry( accountViewModel: AccountViewModel, nav: INav, ) { - LoadUser(item.pubKey, accountViewModel) { user -> + LoadUser(item.pubKey) { user -> if (user == null) { TextEntry(item.pubKey.toShortDisplay(), item.detail) } else { @@ -482,7 +482,7 @@ private fun PublicChatEntry( accountViewModel: AccountViewModel, nav: INav, ) { - LoadPublicChatChannel(item.channelId, accountViewModel) { channel -> + LoadPublicChatChannel(item.channelId) { channel -> // Subscribes to the channel's metadata on relays and recomposes when it arrives. val state by observeChannel(channel, accountViewModel) val name = state?.channel?.toBestDisplayName() ?: channel.toBestDisplayName() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/profile/ProfileBadgesScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/profile/ProfileBadgesScreen.kt index df7ba7bea0..ae2a6a0ff8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/profile/ProfileBadgesScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/profile/ProfileBadgesScreen.kt @@ -86,8 +86,8 @@ fun ProfileBadgesScreen( // historic awards land in cache while this screen is open. ProfileBadgesFilterAssemblerSubscription(accountViewModel) - val newNote = accountViewModel.getOrCreateAddressableNote(ProfileBadgesEvent.createAddress(myPubkey)) - val oldNote = accountViewModel.getOrCreateAddressableNote(AcceptedBadgeSetEvent.createAddress(myPubkey)) + val newNote = LocalCache.getOrCreateAddressableNote(ProfileBadgesEvent.createAddress(myPubkey)) + val oldNote = LocalCache.getOrCreateAddressableNote(AcceptedBadgeSetEvent.createAddress(myPubkey)) val newState by newNote .flow() @@ -208,7 +208,7 @@ private fun AwardRow( return } - LoadAddressableNote(defAddr, accountViewModel) { defNote -> + LoadAddressableNote(defAddr) { defNote -> if (defNote == null) { StaticAwardRow( definition = null, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/ArticleBookmarkListManagementScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/ArticleBookmarkListManagementScreen.kt index ad39788817..d1e3f9749c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/ArticleBookmarkListManagementScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/ArticleBookmarkListManagementScreen.kt @@ -59,7 +59,7 @@ fun ArticleBookmarkListManagementScreen( accountViewModel: AccountViewModel, nav: INav, ) { - LoadAddressableNote(address = articleAddress, accountViewModel = accountViewModel) { + LoadAddressableNote(address = articleAddress) { it?.let { ListManagementView( modifier = Modifier.fillMaxSize().recalculateWindowInsets(), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/PostBookmarkListManagementScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/PostBookmarkListManagementScreen.kt index abe4d003d5..12e2a0860a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/PostBookmarkListManagementScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/PostBookmarkListManagementScreen.kt @@ -58,7 +58,7 @@ fun PostBookmarkListManagementScreen( accountViewModel: AccountViewModel, nav: INav, ) { - LoadNote(baseNoteHex = postId, accountViewModel = accountViewModel) { + LoadNote(baseNoteHex = postId) { it?.let { ListManagementView( modifier = Modifier.fillMaxSize().recalculateWindowInsets(), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/AgentWorkBoardScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/AgentWorkBoardScreen.kt index bcd8b3ac64..54ae79e5a1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/AgentWorkBoardScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/AgentWorkBoardScreen.kt @@ -450,7 +450,7 @@ private fun Person( Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(5.dp)) { Text(prefix, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant) UserPicture(userHex = hex, size = Size20dp, accountViewModel = accountViewModel, nav = nav) - LoadUser(baseUserHex = hex, accountViewModel = accountViewModel) { user -> + LoadUser(baseUserHex = hex) { user -> if (user != null) UsernameDisplay(baseUser = user, fontWeight = FontWeight.Medium, accountViewModel = accountViewModel) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/JobBoardScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/JobBoardScreen.kt index f1da2ca54c..2f3505c4ab 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/JobBoardScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/JobBoardScreen.kt @@ -387,7 +387,7 @@ private fun Person( Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(5.dp)) { Text(prefix, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant) UserPicture(userHex = hex, size = Size20dp, accountViewModel = accountViewModel, nav = nav) - LoadUser(baseUserHex = hex, accountViewModel = accountViewModel) { user -> + LoadUser(baseUserHex = hex) { user -> if (user != null) { UsernameDisplay(baseUser = user, fontWeight = FontWeight.Medium, accountViewModel = accountViewModel) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/WorkflowRunBoardScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/WorkflowRunBoardScreen.kt index c4343738e9..42717bf0f3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/WorkflowRunBoardScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/WorkflowRunBoardScreen.kt @@ -741,7 +741,7 @@ private fun Person( Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(5.dp)) { Text(prefix, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant) UserPicture(userHex = hex, size = Size20dp, accountViewModel = accountViewModel, nav = nav) - LoadUser(baseUserHex = hex, accountViewModel = accountViewModel) { user -> + LoadUser(baseUserHex = hex) { user -> if (user != null) { UsernameDisplay(baseUser = user, fontWeight = FontWeight.Medium, accountViewModel = accountViewModel) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/create/NewCalendarEventScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/create/NewCalendarEventScreen.kt index 5a1070edaf..e9b7f684a2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/create/NewCalendarEventScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/create/NewCalendarEventScreen.kt @@ -373,7 +373,7 @@ private fun ParticipantsRow( size = Size30dp, accountViewModel = accountViewModel, ) - LoadUser(baseUserHex = pubKey, accountViewModel = accountViewModel) { user -> + LoadUser(baseUserHex = pubKey) { user -> if (user != null) { UsernameDisplay( baseUser = user, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/detail/CalendarEventDetailScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/detail/CalendarEventDetailScreen.kt index 297bd289a8..beece330fa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/detail/CalendarEventDetailScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/detail/CalendarEventDetailScreen.kt @@ -774,7 +774,7 @@ private fun UserRow( nav: INav, trailing: (@Composable () -> Unit)?, ) { - LoadUser(baseUserHex = pubKey, accountViewModel = accountViewModel) { user -> + LoadUser(baseUserHex = pubKey) { user -> Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatDeliveryTicks.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatDeliveryTicks.kt index 26ee722473..d5d5e83096 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatDeliveryTicks.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatDeliveryTicks.kt @@ -292,7 +292,7 @@ private fun RecipientDeliveryRow( horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth(), ) { - LoadUser(baseUserHex = recipient.recipient, accountViewModel = accountViewModel) { user -> + LoadUser(baseUserHex = recipient.recipient) { user -> if (user != null) { UserPicture(user, Size20dp, Modifier, accountViewModel, nav) Row(modifier = Modifier.weight(1f)) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/DrawAuthorInfo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/DrawAuthorInfo.kt index 84ebcb66d7..38541c96f9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/DrawAuthorInfo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/DrawAuthorInfo.kt @@ -103,7 +103,6 @@ private fun WatchAndDisplayUser( userName = displayName, size = Size20dp, modifier = Modifier, - accountViewModel = accountViewModel, ) WatchUserFollows(author.pubkeyHex, accountViewModel) { newFollowingState -> diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderBuzzSystemMessage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderBuzzSystemMessage.kt index 67e4a081ac..47f30f5858 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderBuzzSystemMessage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderBuzzSystemMessage.kt @@ -28,6 +28,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import com.vitorpamplona.amethyst.commons.chats.ui.ChatSystemMessage import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.buzz_system_channel_archived @@ -219,10 +220,10 @@ fun observeUserNameByHex( ): String { if (pubkey == null) return "" - var user by remember(pubkey) { mutableStateOf(accountViewModel.getUserIfExists(pubkey)) } + var user by remember(pubkey) { mutableStateOf(LocalCache.getUserIfExists(pubkey)) } if (user == null) { - LaunchedEffect(pubkey) { user = accountViewModel.checkGetOrCreateUser(pubkey) } + LaunchedEffect(pubkey) { user = LocalCache.checkGetOrCreateUser(pubkey) } } val loaded = user ?: return remember(pubkey) { pubkey.take(8) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderChatClip.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderChatClip.kt index 407abdf3fe..493524718c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderChatClip.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderChatClip.kt @@ -72,7 +72,7 @@ fun RenderChatClip( Row(verticalAlignment = Alignment.CenterVertically) { UserPicture(authorHex, Size20dp, Modifier, accountViewModel, nav) Spacer(StdHorzSpacer) - LoadUser(baseUserHex = authorHex, accountViewModel = accountViewModel) { user -> + LoadUser(baseUserHex = authorHex) { user -> if (user != null) { UsernameDisplay( baseUser = user, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderChatRaid.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderChatRaid.kt index c4b61d0849..2d42798e9c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderChatRaid.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderChatRaid.kt @@ -84,7 +84,7 @@ fun RenderChatRaid( Row(verticalAlignment = Alignment.CenterVertically) { UserPicture(from.pubKeyHex, Size20dp, Modifier, accountViewModel, nav) Spacer(StdHorzSpacer) - LoadUser(baseUserHex = from.pubKeyHex, accountViewModel = accountViewModel) { user -> + LoadUser(baseUserHex = from.pubKeyHex) { user -> if (user != null) { UsernameDisplay( baseUser = user, @@ -104,7 +104,7 @@ fun RenderChatRaid( UserPicture(to.pubKeyHex, Size20dp, Modifier, accountViewModel, nav) Spacer(StdHorzSpacer) - LoadUser(baseUserHex = to.pubKeyHex, accountViewModel = accountViewModel) { user -> + LoadUser(baseUserHex = to.pubKeyHex) { user -> if (user != null) { UsernameDisplay( baseUser = user, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/GeohashChatScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/GeohashChatScreen.kt index 3d29f92eab..1cb5d049aa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/GeohashChatScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/GeohashChatScreen.kt @@ -69,6 +69,7 @@ import com.vitorpamplona.amethyst.commons.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarExtensibleWithBackButton +import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserPicture import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold @@ -115,7 +116,7 @@ fun GeohashChatScreen( accountViewModel: AccountViewModel, nav: INav, ) { - LoadGeohashChannel(geohash, accountViewModel) { channel -> + LoadGeohashChannel(geohash) { channel -> GeohashChatRoom(channel, teleported, accountViewModel, nav) } } @@ -317,8 +318,8 @@ private fun GeohashIdentityAvatar( model = picture, contentDescription = "Posting as yourself — tap to go anonymous, long-press for a nickname", modifier = avatarModifier, - loadProfilePicture = accountViewModel.settings.showProfilePictures(), - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + loadProfilePicture = LocalDisplaySettings.current.showProfilePictures, + loadRobohash = LocalDisplaySettings.current.loadRobohash, autoPlayGif = false, ) } else { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/LoadGeohashChannel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/LoadGeohashChannel.kt index ffc2f6a685..e6aac28ec9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/LoadGeohashChannel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/LoadGeohashChannel.kt @@ -21,9 +21,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.geohashChat import androidx.compose.runtime.Composable +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.geohashChat.GeohashChatChannel import com.vitorpamplona.amethyst.ui.note.produceStateIfNotNull -import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel /** * Resolves the [GeohashChatChannel] for [geohash] from LocalCache, creating an @@ -34,12 +34,11 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @Composable fun LoadGeohashChannel( geohash: String, - accountViewModel: AccountViewModel, content: @Composable (GeohashChatChannel) -> Unit, ) { val channel = - produceStateIfNotNull(accountViewModel.getGeohashChannelIfExists(geohash), geohash) { - value = accountViewModel.checkGetOrCreateGeohashChannel(geohash) + produceStateIfNotNull(LocalCache.getGeohashChannelIfExists(geohash), geohash) { + value = LocalCache.getOrCreateGeohashChannel(geohash) } channel.value?.let { content(it) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatView.kt index 96d1f7c04f..1a480a9adc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatView.kt @@ -49,6 +49,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.chats.ui.ThinSendButton +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.marmot_group_composer_disbanding import com.vitorpamplona.amethyst.commons.resources.marmot_group_composer_leaving @@ -121,7 +122,7 @@ fun MarmotGroupChatView( // landed in LocalCache. checkGetOrCreateNote is a no-op for unknown ids. if (replyToInnerNote != null) { LaunchedEffect(replyToInnerNote) { - val parent = accountViewModel.checkGetOrCreateNote(replyToInnerNote) + val parent = LocalCache.checkGetOrCreateNote(replyToInnerNote) if (parent != null) { newMessageModel.reply(parent) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupIconEditor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupIconEditor.kt index faa029d834..06b8f8649c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupIconEditor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupIconEditor.kt @@ -45,6 +45,7 @@ import com.vitorpamplona.amethyst.commons.resources.marmot_change_photo import com.vitorpamplona.amethyst.commons.resources.marmot_group_icon import com.vitorpamplona.amethyst.commons.resources.marmot_remove_photo import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage +import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -99,8 +100,8 @@ fun MarmotGroupIconEditor( .size(96.dp) .clip(CircleShape) .let { if (enabled) it.clickable { launcher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) } else it }, - loadProfilePicture = accountViewModel.settings.showProfilePictures(), - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + loadProfilePicture = LocalDisplaySettings.current.showProfilePictures, + loadRobohash = LocalDisplaySettings.current.loadRobohash, ) Row( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupInfoScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupInfoScreen.kt index ba58c3bfa8..66c97b3f0d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupInfoScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupInfoScreen.kt @@ -132,6 +132,7 @@ import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.pluralStringRes +import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.MediumRelayIconModifier import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightChat @@ -760,7 +761,7 @@ fun MemberRow( nav = nav, ) Column(modifier = Modifier.weight(1f)) { - LoadUser(baseUserHex = member.pubkey, accountViewModel = accountViewModel) { user -> + LoadUser(baseUserHex = member.pubkey) { user -> val displayName = user?.toBestDisplayName() ?: stringRes(Res.string.marmot_user_fallback_name, member.pubkey.take(16)) val youSuffix = stringRes(Res.string.marmot_member_suffix_you) val adminSuffix = stringRes(Res.string.marmot_member_suffix_admin) @@ -875,7 +876,7 @@ private fun ConfirmRemoveMemberDialog( onDismissRequest = onDismiss, title = { Text(stringRes(Res.string.marmot_remove_member)) }, text = { - LoadUser(baseUserHex = memberPubkey, accountViewModel = accountViewModel) { user -> + LoadUser(baseUserHex = memberPubkey) { user -> val name = user?.toBestDisplayName() ?: stringRes(Res.string.marmot_user_fallback_name, memberPubkey.take(16)) Text(stringRes(Res.string.marmot_remove_member_confirm, name)) } @@ -904,7 +905,7 @@ private fun ConfirmGrantAdminDialog( onDismissRequest = onDismiss, title = { Text(stringRes(Res.string.marmot_grant_admin_title)) }, text = { - LoadUser(baseUserHex = memberPubkey, accountViewModel = accountViewModel) { user -> + LoadUser(baseUserHex = memberPubkey) { user -> val name = user?.toBestDisplayName() ?: stringRes(Res.string.marmot_user_fallback_name, memberPubkey.take(16)) Text(stringRes(Res.string.marmot_grant_admin_confirm, name)) } @@ -933,7 +934,7 @@ private fun ConfirmRevokeAdminDialog( onDismissRequest = onDismiss, title = { Text(stringRes(Res.string.marmot_revoke_admin_title)) }, text = { - LoadUser(baseUserHex = memberPubkey, accountViewModel = accountViewModel) { user -> + LoadUser(baseUserHex = memberPubkey) { user -> val name = user?.toBestDisplayName() ?: stringRes(Res.string.marmot_user_fallback_name, memberPubkey.take(16)) Text(stringRes(Res.string.marmot_revoke_admin_confirm, name)) } @@ -1025,9 +1026,9 @@ fun GroupRelayTile( RenderRelayIcon( displayUrl = relayInfo.id ?: fallbackUrl, iconUrl = relayInfo.icon, - loadProfilePicture = accountViewModel.settings.showProfilePictures(), + loadProfilePicture = LocalDisplaySettings.current.showProfilePictures, pingInMs = 0, - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + loadRobohash = LocalDisplaySettings.current.loadRobohash, iconModifier = MediumRelayIconModifier, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupListScreen.kt index df8c893e5d..4c8d52184f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupListScreen.kt @@ -59,6 +59,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.chats.ui.ChatUnreadBadge import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.marmotGroups.MarmotGroupChatroom import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.resources.Res @@ -329,7 +330,7 @@ fun MarmotGroupListItem( previewEvent ?.pubKey ?.takeIf { it != myPubKey } - ?.let { accountViewModel.getUserIfExists(it) } + ?.let { LocalCache.getUserIfExists(it) } val senderName = if (senderUser != null) { observeUserInfo(senderUser, accountViewModel).value?.info?.bestName() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotSystemRowRenderer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotSystemRowRenderer.kt index 1442015c2a..6f7a2c9adf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotSystemRowRenderer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotSystemRowRenderer.kt @@ -33,6 +33,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.marmot_system_admin_added import com.vitorpamplona.amethyst.commons.resources.marmot_system_admin_added_passive @@ -167,7 +168,7 @@ class MarmotSystemRowRenderer( /** A known display name, or a short key when the account is a stranger. */ @Composable private fun displayName(pubkeyHex: String): String { - val user = accountViewModel.getUserIfExists(pubkeyHex) + val user = LocalCache.getUserIfExists(pubkeyHex) return user?.toBestDisplayName() ?: pubkeyHex.take(8) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomView.kt index 78c91bea19..4a9c15f240 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomView.kt @@ -40,6 +40,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.feeds.FeedContentState import com.vitorpamplona.amethyst.commons.feeds.FeedState +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.privateChatLastReadRoute import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.chats_history_proto_nip04 @@ -106,7 +107,7 @@ fun ChatroomView( if (replyToNote != null) { LaunchedEffect(key1 = replyToNote, newPostModel, accountViewModel) { - val replyNote = accountViewModel.checkGetOrCreateNote(replyToNote) + val replyNote = LocalCache.checkGetOrCreateNote(replyToNote) if (replyNote != null) { newPostModel.reply(replyNote) } @@ -114,7 +115,7 @@ fun ChatroomView( } if (editFromDraft != null) { LaunchedEffect(editFromDraft, newPostModel, accountViewModel) { - val draftNote = accountViewModel.checkGetOrCreateNote(editFromDraft) + val draftNote = LocalCache.checkGetOrCreateNote(editFromDraft) if (draftNote != null) { newPostModel.editFromDraft(draftNote) } @@ -130,7 +131,6 @@ fun ChatroomView( for (userHex in room.users) { LoadAddressableNote( ChatMessageRelayListEvent.createAddress(userHex), - accountViewModel, ) { note -> if (note != null) { EventFinderFilterAssemblerSubscription(note, accountViewModel) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/ChatroomHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/ChatroomHeader.kt index 4af0d7cde0..12aa554715 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/ChatroomHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/ChatroomHeader.kt @@ -49,7 +49,7 @@ fun ChatroomHeader( onClick: () -> Unit, ) { if (room.users.size == 1) { - LoadUser(baseUserHex = room.users.first(), accountViewModel) { baseUser -> + LoadUser(baseUserHex = room.users.first()) { baseUser -> if (baseUser != null) { UserChatroomHeader( baseUser = baseUser, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/DmReportWarningCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/DmReportWarningCard.kt index 446bdf449f..a84f4d0f17 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/DmReportWarningCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/DmReportWarningCard.kt @@ -96,7 +96,7 @@ fun DmReportWarningCard( ) { val counterpartHex = room.users.singleOrNull() ?: return - LoadUser(baseUserHex = counterpartHex, accountViewModel = accountViewModel) { user -> + LoadUser(baseUserHex = counterpartHex) { user -> if (user != null) { val flow = remember(user) { accountViewModel.createUserReportWarningFlow(user) } val state by flow.collectAsStateWithLifecycle() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/RenderRoomTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/RenderRoomTopBar.kt index 34a7d023e3..e9c636c095 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/RenderRoomTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/RenderRoomTopBar.kt @@ -81,7 +81,7 @@ fun RenderRoomTopBar( if (room.users.size == 1) { TopBarExtensibleWithBackButton( title = { - LoadUser(baseUserHex = room.users.first(), accountViewModel) { baseUser -> + LoadUser(baseUserHex = room.users.first()) { baseUser -> if (baseUser != null) { ClickableUserPicture( baseUser = baseUser, @@ -124,7 +124,7 @@ fun RenderRoomTopBar( } }, extendableRow = { - LoadUser(baseUserHex = room.users.first(), accountViewModel) { + LoadUser(baseUserHex = room.users.first()) { if (it != null) { UserCompose( baseUser = it, @@ -216,7 +216,7 @@ fun GroupMembersHeader( state = rememberLazyListState(), ) { itemsIndexed(list, key = { _, item -> item }) { _, item -> - LoadUser(baseUserHex = item, accountViewModel) { + LoadUser(baseUserHex = item) { if (it != null) { UserCompose( baseUser = it, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/RoomNameOnlyDisplay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/RoomNameOnlyDisplay.kt index 5aeb07a68e..363ba6a17e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/RoomNameOnlyDisplay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/RoomNameOnlyDisplay.kt @@ -81,14 +81,14 @@ fun DisplayUserSetAsSubject( if (userList.size == 1) { // Regular Design Row { - LoadUser(baseUserHex = userList[0], accountViewModel) { + LoadUser(baseUserHex = userList[0]) { it?.let { UsernameDisplay(it, Modifier.weight(1f), fontWeight = fontWeight, accountViewModel = accountViewModel) } } } } else { Row { userList.take(4).forEachIndexed { index, value -> - LoadUser(baseUserHex = value, accountViewModel) { + LoadUser(baseUserHex = value) { it?.let { ShortUsernameDisplay(baseUser = it, fontWeight = fontWeight, accountViewModel = accountViewModel) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt index ad9e3471ea..3bb0e5dce0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt @@ -407,7 +407,7 @@ class ChatNewMessageViewModel : } if (replyId != null) { - replyTo.value = accountViewModel.checkGetOrCreateNote(replyId) + replyTo.value = LocalCache.checkGetOrCreateNote(replyId) } } else if (draftEvent is PrivateDmEvent) { val recipientNPub = draftEvent.verifiedRecipientPubKey()?.let { Hex.decode(it).toNpub() } @@ -415,7 +415,7 @@ class ChatNewMessageViewModel : val replyId = draftEvent.replyTo() if (replyId != null) { - replyTo.value = accountViewModel.checkGetOrCreateNote(replyId) + replyTo.value = LocalCache.checkGetOrCreateNote(replyId) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelListScreen.kt index 8a3250abb0..3075b9a87a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelListScreen.kt @@ -589,7 +589,7 @@ private fun rememberConcordDisplayName( hex: HexKey, accountViewModel: AccountViewModel, ): String { - val user = remember(hex) { accountViewModel.checkGetOrCreateUser(hex) } ?: return remember(hex) { hex.take(8) } + val user = remember(hex) { LocalCache.checkGetOrCreateUser(hex) } ?: return remember(hex) { hex.take(8) } val name by observeUserName(user, accountViewModel) return name } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelScreen.kt index e921ea0531..fc1c1f025b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelScreen.kt @@ -392,7 +392,7 @@ private fun rememberTypistName( hex: String, accountViewModel: AccountViewModel, ): String { - val user = remember(hex) { accountViewModel.checkGetOrCreateUser(hex) } ?: return remember(hex) { hex.take(8) } + val user = remember(hex) { LocalCache.checkGetOrCreateUser(hex) } ?: return remember(hex) { hex.take(8) } val info by observeUserInfo(user, accountViewModel) return info?.info?.bestName() ?: remember(user) { user.pubkeyDisplayHex() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordHomeScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordHomeScreen.kt index 82407fe7ab..d95ba0659d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordHomeScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordHomeScreen.kt @@ -72,6 +72,7 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.FabBottomBarP import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.ShorterTopAppBar import com.vitorpamplona.amethyst.commons.ui.pluralStringRes +import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserName @@ -313,8 +314,8 @@ private fun CommunityHeader( model = iconModel, contentDescription = name, modifier = Modifier.size(40.dp).clip(CircleShape).clickable(onClick = onOpen), - loadProfilePicture = accountViewModel.settings.showProfilePictures(), - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + loadProfilePicture = LocalDisplaySettings.current.showProfilePictures, + loadRobohash = LocalDisplaySettings.current.loadRobohash, autoPlayGif = autoPlayGif, ) // The identity block — icon *and* name/subtitle — opens the community; the rest of the row and diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordMembersScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordMembersScreen.kt index a2bf94ddc7..d09ff1be8d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordMembersScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordMembersScreen.kt @@ -57,6 +57,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.concord.ConcordMembership import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.back @@ -250,7 +251,7 @@ private fun ConcordMemberRow( accountViewModel: AccountViewModel, nav: INav, ) { - val user = remember(entry.pubkey) { accountViewModel.checkGetOrCreateUser(entry.pubkey) } + val user = remember(entry.pubkey) { LocalCache.checkGetOrCreateUser(entry.pubkey) } val isOwnerTarget = entry.membership == ConcordMembership.OWNER val isBanned = entry.membership == ConcordMembership.BANNED val isAdmin = entry.membership == ConcordMembership.ADMIN diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordMetadataForm.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordMetadataForm.kt index ffdc668609..476a3b0bf7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordMetadataForm.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordMetadataForm.kt @@ -78,6 +78,7 @@ import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncIma import com.vitorpamplona.amethyst.commons.ui.components.util.setText import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.MediumRelayIconModifier import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo @@ -193,8 +194,8 @@ private fun ConcordIconHero( model = iconModel, contentDescription = displayName.ifBlank { stringRes(Res.string.concord_create_title) }, modifier = Modifier.size(104.dp).clip(CircleShape), - loadProfilePicture = accountViewModel.settings.showProfilePictures(), - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + loadProfilePicture = LocalDisplaySettings.current.showProfilePictures, + loadRobohash = LocalDisplaySettings.current.loadRobohash, autoPlayGif = autoPlayGif, ) if (uploading) CircularProgressIndicator(modifier = Modifier.size(36.dp)) @@ -345,8 +346,8 @@ private fun ConcordRelayRow( RenderRelayIcon( displayUrl = relayInfo.id ?: relay.displayUrl(), iconUrl = relayInfo.icon, - loadProfilePicture = accountViewModel.settings.showProfilePictures(), - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + loadProfilePicture = LocalDisplaySettings.current.showProfilePictures, + loadRobohash = LocalDisplaySettings.current.loadRobohash, pingInMs = 0, iconModifier = MediumRelayIconModifier, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/ChannelView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/ChannelView.kt index e2535af660..6692e456ba 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/ChannelView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/ChannelView.kt @@ -53,7 +53,7 @@ fun EphemeralChatChannelView( ) { if (channelId == null) return - LoadEphemeralChatChannel(channelId, accountViewModel) { ephem -> + LoadEphemeralChatChannel(channelId) { ephem -> PrepareChannelViewModels( baseChannel = ephem, draft = draft, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/EphemeralChatScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/EphemeralChatScreen.kt index 2e88e40e26..37080d9668 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/EphemeralChatScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/EphemeralChatScreen.kt @@ -25,6 +25,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -44,13 +45,13 @@ fun EphemeralChatScreen( ) { val relay = remember(relayUrl) { RelayUrlNormalizer.normalizeOrNull(relayUrl) } ?: return val channelId = remember(id, relay) { RoomId(id, relay) } - val draft = remember(draftId) { draftId?.let { accountViewModel.getNoteIfExists(it) } } - val replyTo = remember(replyToId) { replyToId?.let { accountViewModel.checkGetOrCreateNote(it) } } + val draft = remember(draftId) { draftId?.let { LocalCache.getNoteIfExists(it) } } + val replyTo = remember(replyToId) { replyToId?.let { LocalCache.checkGetOrCreateNote(it) } } DisappearingScaffold( isInvertedLayout = true, topBar = { - LoadEphemeralChatChannel(channelId, accountViewModel) { + LoadEphemeralChatChannel(channelId) { EphemeralChatTopBar(it, accountViewModel, nav) } }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/LoadEphemeralChatChannel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/LoadEphemeralChatChannel.kt index 51af13e271..df81acc0fa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/LoadEphemeralChatChannel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/LoadEphemeralChatChannel.kt @@ -21,20 +21,19 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat import androidx.compose.runtime.Composable +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.ui.note.produceStateIfNotNull -import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId @Composable fun LoadEphemeralChatChannel( id: RoomId, - accountViewModel: AccountViewModel, content: @Composable (EphemeralChatChannel) -> Unit, ) { val channel = - produceStateIfNotNull(accountViewModel.getEphemeralChatChannelIfExists(id), id) { - value = accountViewModel.checkGetOrCreateEphemeralChatChannel(id) + produceStateIfNotNull(LocalCache.getEphemeralChatChannelIfExists(id), id) { + value = LocalCache.getOrCreateEphemeralChannel(id) } channel.value?.let { content(it) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/ShortEphemeralChatChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/ShortEphemeralChatChannelHeader.kt index da2bce20a3..357f3576cf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/ShortEphemeralChatChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/ShortEphemeralChatChannelHeader.kt @@ -42,6 +42,7 @@ import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.profile_image import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.HeaderPictureModifier import com.vitorpamplona.amethyst.commons.ui.theme.Size35dp @@ -103,8 +104,8 @@ private fun DrawRelayIcon( contentDescription = stringRes(Res.string.profile_image), contentScale = ContentScale.Crop, modifier = HeaderPictureModifier, - loadProfilePicture = accountViewModel.settings.showProfilePictures(), - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + loadProfilePicture = LocalDisplaySettings.current.showProfilePictures, + loadRobohash = LocalDisplaySettings.current.loadRobohash, autoPlayGif = accountViewModel.settings.autoPlayVideosFlow .collectAsStateWithLifecycle() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/ChannelView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/ChannelView.kt index e90c287666..b1721b7f7a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/ChannelView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/ChannelView.kt @@ -53,7 +53,7 @@ fun PublicChatChannelView( ) { if (channelId == null) return - LoadPublicChatChannel(channelId, accountViewModel) { + LoadPublicChatChannel(channelId) { PrepareChannelViewModels( baseChannel = it, draft = draft, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/PublicChatChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/PublicChatChannelHeader.kt index fd4340b361..d501a77dff 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/PublicChatChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/PublicChatChannelHeader.kt @@ -57,7 +57,7 @@ fun PublicChatChannelHeader( accountViewModel: AccountViewModel, nav: INav, ) { - LoadPublicChatChannel(channelHex, accountViewModel) { + LoadPublicChatChannel(channelHex) { PublicChatChannelHeader( it, sendToChannel, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/PublicChatChannelScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/PublicChatChannelScreen.kt index 1461c1fe7c..da19432246 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/PublicChatChannelScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/PublicChatChannelScreen.kt @@ -25,6 +25,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold @@ -44,14 +45,14 @@ fun PublicChatChannelScreen( ) { if (channelId == null) return - val draft = remember(draftId) { draftId?.let { accountViewModel.getNoteIfExists(it) } } - val replyTo = remember(replyToId) { replyToId?.let { accountViewModel.checkGetOrCreateNote(it) } } + val draft = remember(draftId) { draftId?.let { LocalCache.getNoteIfExists(it) } } + val replyTo = remember(replyToId) { replyToId?.let { LocalCache.checkGetOrCreateNote(it) } } val selfRoute = remember(channelId) { Route.PublicChatChannel(channelId) } DisappearingScaffold( isInvertedLayout = true, topBar = { - LoadPublicChatChannel(channelId, accountViewModel) { + LoadPublicChatChannel(channelId) { PublicChatTopBar(it, accountViewModel, nav) } }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/LongPublicChatChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/LongPublicChatChannelHeader.kt index 4f29ec5f8c..3f2a7a7496 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/LongPublicChatChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/LongPublicChatChannelHeader.kt @@ -53,6 +53,7 @@ import com.vitorpamplona.amethyst.commons.resources.owner import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.richtext.CreateTextWithEmoji +import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DoubleHorzSpacer import com.vitorpamplona.amethyst.commons.ui.theme.Size25dp @@ -94,8 +95,8 @@ fun LongPublicChatChannelHeader( model = it, contentDescription = stringRes(Res.string.channel_image), modifier = MaterialTheme.colorScheme.largeProfilePictureModifier, - loadProfilePicture = accountViewModel.settings.showProfilePictures(), - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + loadProfilePicture = LocalDisplaySettings.current.showProfilePictures, + loadRobohash = LocalDisplaySettings.current.loadRobohash, autoPlayGif = accountViewModel.settings.autoPlayVideosFlow .collectAsStateWithLifecycle() @@ -144,7 +145,7 @@ fun LongPublicChatChannelHeader( } } - LoadNote(baseNoteHex = channel.idHex, accountViewModel) { loadingNote -> + LoadNote(baseNoteHex = channel.idHex) { loadingNote -> loadingNote?.let { note -> Row( lineModifier, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/ShortPublicChatChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/ShortPublicChatChannelHeader.kt index 531c6c4399..3d25da077a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/ShortPublicChatChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/ShortPublicChatChannelHeader.kt @@ -43,6 +43,7 @@ import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.profile_image import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.HeaderPictureModifier import com.vitorpamplona.amethyst.commons.ui.theme.RowColSpacing @@ -72,8 +73,8 @@ fun ShortPublicChatChannelHeader( contentDescription = stringRes(Res.string.profile_image), contentScale = ContentScale.Crop, modifier = HeaderPictureModifier, - loadProfilePicture = accountViewModel.settings.showProfilePictures(), - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + loadProfilePicture = LocalDisplaySettings.current.showProfilePictures, + loadRobohash = LocalDisplaySettings.current.loadRobohash, autoPlayGif = accountViewModel.settings.autoPlayVideosFlow .collectAsStateWithLifecycle() @@ -115,7 +116,7 @@ fun ShortChannelActionOptions( accountViewModel: AccountViewModel, nav: INav, ) { - LoadNote(baseNoteHex = channel.idHex, accountViewModel) { + LoadNote(baseNoteHex = channel.idHex) { it?.let { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = RowColSpacing) { LikeReaction( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataScreen.kt index ef403698eb..e809b75853 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataScreen.kt @@ -94,7 +94,7 @@ fun ChannelMetadataScreen( if (channelId == null) { ChannelMetadataScreen(null as PublicChatChannel?, accountViewModel, nav) } else { - LoadPublicChatChannel(channelId, accountViewModel) { + LoadPublicChatChannel(channelId) { ChannelMetadataScreen(it, accountViewModel, nav) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ChannelView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ChannelView.kt index 855a71428e..640a432263 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ChannelView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ChannelView.kt @@ -56,7 +56,7 @@ fun LiveActivityChannelView( ) { if (channelId == null) return - LoadLiveActivityChannel(channelId, accountViewModel) { + LoadLiveActivityChannel(channelId) { PrepareChannelViewModels( baseChannel = it, draft = draft, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LiveActivityChannelScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LiveActivityChannelScreen.kt index 2ce86fc6ca..3ff4e70d03 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LiveActivityChannelScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LiveActivityChannelScreen.kt @@ -26,6 +26,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.service.OnlineChecker import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold @@ -45,13 +46,13 @@ fun LiveActivityChannelScreen( ) { if (channelId == null) return - val draft = remember(draftId) { draftId?.let { accountViewModel.getNoteIfExists(it) } } - val replyTo = remember(replyToId) { replyToId?.let { accountViewModel.checkGetOrCreateNote(it) } } + val draft = remember(draftId) { draftId?.let { LocalCache.getNoteIfExists(it) } } + val replyTo = remember(replyToId) { replyToId?.let { LocalCache.checkGetOrCreateNote(it) } } DisappearingScaffold( isInvertedLayout = true, topBar = { - LoadLiveActivityChannel(channelId, accountViewModel) { + LoadLiveActivityChannel(channelId) { LaunchedEffect(it.info) { it.info?.streaming()?.let { OnlineChecker.resetIfOfflineToRetry(it) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveStreamGoalHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveStreamGoalHeader.kt index fc6781e7dc..8ff9d9a3fd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveStreamGoalHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveStreamGoalHeader.kt @@ -63,7 +63,7 @@ fun LiveStreamGoalHeader( ) { val goalId = channel.info?.goalEventId() ?: return - LoadNote(baseNoteHex = goalId, accountViewModel = accountViewModel) { goalNote -> + LoadNote(baseNoteHex = goalId) { goalNote -> if (goalNote != null) { GoalHeaderContent(goalNote, accountViewModel, nav) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveStreamTopZappers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveStreamTopZappers.kt index e933a5ea41..9744b58ed0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveStreamTopZappers.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveStreamTopZappers.kt @@ -48,6 +48,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.commons.nip53LiveActivities.TopZapperEntry @@ -92,11 +93,11 @@ fun LiveStreamTopZappers( val goalId = channel.info?.goalEventId() if (goalId != null) { var goalNoteHolder by remember(goalId) { - mutableStateOf(accountViewModel.getNoteIfExists(goalId)) + mutableStateOf(LocalCache.getNoteIfExists(goalId)) } LaunchedEffect(goalId) { if (goalNoteHolder == null) { - goalNoteHolder = accountViewModel.checkGetOrCreateNote(goalId) + goalNoteHolder = LocalCache.checkGetOrCreateNote(goalId) } topZappersVm.setGoalNote(goalNoteHolder) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LongLiveActivityChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LongLiveActivityChannelHeader.kt index 8092e4704f..acfaebc195 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LongLiveActivityChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LongLiveActivityChannelHeader.kt @@ -91,7 +91,7 @@ fun LongLiveActivityChannelHeader( RenderSummary(activity, callbackUri, accountViewModel, nav) } - LoadAddressableNote(channel.address, accountViewModel) { loadingNote -> + LoadAddressableNote(channel.address) { loadingNote -> loadingNote?.let { note -> Row( lineModifier, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/BuzzTypingIndicator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/BuzzTypingIndicator.kt index 5779fd3a09..9e5dc3bd1e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/BuzzTypingIndicator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/BuzzTypingIndicator.kt @@ -56,6 +56,7 @@ import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.model.buzz.BuzzTypingState +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.buzz_typing_many import com.vitorpamplona.amethyst.commons.resources.buzz_typing_one @@ -184,7 +185,7 @@ private fun rememberTypistName( hex: HexKey, accountViewModel: AccountViewModel, ): String { - val user = remember(hex) { accountViewModel.checkGetOrCreateUser(hex) } ?: return remember(hex) { hex.take(8) } + val user = remember(hex) { LocalCache.checkGetOrCreateUser(hex) } ?: return remember(hex) { hex.take(8) } val info by observeUserInfo(user, accountViewModel) return info?.info?.bestName() ?: remember(user) { user.pubkeyDisplayHex() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/LoadRelayGroupChannel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/LoadRelayGroupChannel.kt index 0704bd9195..65baa02dab 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/LoadRelayGroupChannel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/LoadRelayGroupChannel.kt @@ -21,20 +21,19 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup import androidx.compose.runtime.Composable +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel import com.vitorpamplona.amethyst.ui.note.produceStateIfNotNull -import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip29RelayGroups.GroupId @Composable fun LoadRelayGroupChannel( id: GroupId, - accountViewModel: AccountViewModel, content: @Composable (RelayGroupChannel) -> Unit, ) { val channel = - produceStateIfNotNull(accountViewModel.getRelayGroupChannelIfExists(id), id) { - value = accountViewModel.checkGetOrCreateRelayGroupChannel(id) + produceStateIfNotNull(LocalCache.getRelayGroupChannelIfExists(id), id) { + value = LocalCache.getOrCreateRelayGroupChannel(id) } channel.value?.let { content(it) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelListScreen.kt index 72c810d975..7ce0a5aed3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelListScreen.kt @@ -99,6 +99,7 @@ import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncIma 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.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.warningColor import com.vitorpamplona.amethyst.commons.util.sortedBySnapshot @@ -210,12 +211,12 @@ fun RelayGroupChannelListScreen( // so the first frame doesn't reshuffle when the first emission arrives. // remember the seed so the per-relay cache scan + sort runs once (on first composition / relay // change), not on every recomposition — produceState evaluates its initialValue argument eagerly. - val initialChannels = remember(relay) { accountViewModel.getRelayGroupChannelsOnRelay(relay).sortedBySnapshot { it.toBestDisplayName().lowercase() } } + val initialChannels = remember(relay) { LocalCache.getRelayGroupChannelsOnRelay(relay).sortedBySnapshot { it.toBestDisplayName().lowercase() } } val allChannels by produceState(initialValue = initialChannels, relay) { LocalCache .observeEvents(Filter(kinds = listOf(GroupMetadataEvent.KIND))) .collect { - value = accountViewModel.getRelayGroupChannelsOnRelay(relay).sortedBySnapshot { it.toBestDisplayName().lowercase() } + value = LocalCache.getRelayGroupChannelsOnRelay(relay).sortedBySnapshot { it.toBestDisplayName().lowercase() } } } @@ -973,8 +974,8 @@ private fun RelayGroupChannelRow( model = channel.profilePicture(), contentDescription = channel.toBestDisplayName(), modifier = Modifier.size(40.dp).clip(CircleShape), - loadProfilePicture = accountViewModel.settings.showProfilePictures(), - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + loadProfilePicture = LocalDisplaySettings.current.showProfilePictures, + loadRobohash = LocalDisplaySettings.current.loadRobohash, autoPlayGif = autoPlayGif, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelView.kt index df589ab915..9a6aec581c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelView.kt @@ -88,7 +88,7 @@ fun RelayGroupChannelView( ) { if (channelId == null) return - LoadRelayGroupChannel(channelId, accountViewModel) { group -> + LoadRelayGroupChannel(channelId) { group -> PrepareChannelViewModels( baseChannel = group, draft = draft, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChatScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChatScreen.kt index a10b1932ea..d6b6f376a1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChatScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChatScreen.kt @@ -25,6 +25,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold @@ -46,14 +47,14 @@ fun RelayGroupChatScreen( ) { val relay = remember(relayUrl) { RelayUrlNormalizer.normalizeOrNull(relayUrl) } ?: return val channelId = remember(id, relay) { GroupId(id, relay) } - val draft = remember(draftId) { draftId?.let { accountViewModel.getNoteIfExists(it) } } - val replyTo = remember(replyToId) { replyToId?.let { accountViewModel.checkGetOrCreateNote(it) } } + val draft = remember(draftId) { draftId?.let { LocalCache.getNoteIfExists(it) } } + val replyTo = remember(replyToId) { replyToId?.let { LocalCache.checkGetOrCreateNote(it) } } val selfRoute = remember(id, relayUrl) { Route.RelayGroup(id, relayUrl) } DisappearingScaffold( isInvertedLayout = true, topBar = { - LoadRelayGroupChannel(channelId, accountViewModel) { + LoadRelayGroupChannel(channelId) { RelayGroupTopBar(it, inviteCode, accountViewModel, nav) } }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupDiscoveryScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupDiscoveryScreen.kt index 359966439c..afdf3562b4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupDiscoveryScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupDiscoveryScreen.kt @@ -78,6 +78,7 @@ import com.vitorpamplona.amethyst.commons.ui.layouts.rememberFeedContentPadding import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.FabBottomBarPadded import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.pluralStringRes +import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding import com.vitorpamplona.amethyst.commons.ui.theme.Size20dp @@ -369,8 +370,8 @@ private fun RelayRailHeader( RenderRelayIcon( displayUrl = host, iconUrl = info.value.icon, - loadProfilePicture = accountViewModel.settings.showProfilePictures(), - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + loadProfilePicture = LocalDisplaySettings.current.showProfilePictures, + loadRobohash = LocalDisplaySettings.current.loadRobohash, pingInMs = 0, iconModifier = Modifier.size(Size25dp).clip(CircleShape), ) @@ -520,8 +521,8 @@ private fun RelayGroupRailRowContent( .size(40.dp) .clip(CircleShape) .border(1.5.dp, MaterialTheme.colorScheme.primary.copy(alpha = 0.35f), CircleShape), - loadProfilePicture = accountViewModel.settings.showProfilePictures(), - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + loadProfilePicture = LocalDisplaySettings.current.showProfilePictures, + loadRobohash = LocalDisplaySettings.current.loadRobohash, autoPlayGif = autoPlayGif, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMembersScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMembersScreen.kt index c8bd0df5ca..9d9b4b5e5c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMembersScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMembersScreen.kt @@ -68,6 +68,7 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.buzz.BuzzAgentActivityState import com.vitorpamplona.amethyst.commons.model.buzz.BuzzRelayDialect +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupMembership @@ -126,7 +127,7 @@ fun RelayGroupMembersScreen( val relay = remember(relayUrl) { RelayUrlNormalizer.normalizeOrNull(relayUrl) } ?: return val channelId = remember(id, relay) { GroupId(id, relay) } - LoadRelayGroupChannel(channelId, accountViewModel) { channel -> + LoadRelayGroupChannel(channelId) { channel -> RelayGroupMembers(channel, accountViewModel, nav) } } @@ -352,7 +353,7 @@ private fun RelayGroupMemberRow( // Create-or-get (never a one-shot null): UsernameDisplay observes the user's // metadata flow, so the name fills in when the kind:0 arrives instead of being // stuck on truncated hex forever. - val user = remember(entry.pubkey) { accountViewModel.checkGetOrCreateUser(entry.pubkey) } + val user = remember(entry.pubkey) { LocalCache.checkGetOrCreateUser(entry.pubkey) } var menuOpen by remember { mutableStateOf(false) } var confirmRemove by remember { mutableStateOf(false) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMetadataScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMetadataScreen.kt index ea0795cfb2..ad1cef5f5d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMetadataScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMetadataScreen.kt @@ -175,7 +175,7 @@ fun RelayGroupEditScreen( val groupId = remember(id, relay) { GroupId(id, relay) } val viewModel: RelayGroupMetadataViewModel = viewModel(key = "RelayGroupEdit:${groupId.toKey()}") - LoadRelayGroupChannel(groupId, accountViewModel) { channel -> + LoadRelayGroupChannel(groupId) { channel -> // Keep the relay-signed metadata fresh while editing so a late load prefills. RelayGroupCardWarmupSubscription(channel, accountViewModel.dataSources().relayGroupCardWarmup, accountViewModel) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupParentPicker.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupParentPicker.kt index 627c5face7..0fc9ff32be 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupParentPicker.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupParentPicker.kt @@ -77,6 +77,7 @@ import com.vitorpamplona.amethyst.commons.resources.relay_group_parent_top_level import com.vitorpamplona.amethyst.commons.resources.relay_group_section_structure import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.commons.ui.pluralStringRes +import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.util.sortedBySnapshot import com.vitorpamplona.amethyst.model.nip11RelayInfo.isRelaySignedRelayGroup @@ -152,7 +153,7 @@ private fun ParentSelectorCard( // yet), warm its single 39000, and observe it so the name/picture fill in as they arrive. val liveParent: RelayGroupChannel? = parentId?.let { id -> - val channel = remember(id, relay) { accountViewModel.checkGetOrCreateRelayGroupChannel(GroupId(id, relay)) } + val channel = remember(id, relay) { LocalCache.getOrCreateRelayGroupChannel(GroupId(id, relay)) } RelayGroupCardWarmupSubscription(channel, accountViewModel.dataSources().relayGroupCardWarmup, accountViewModel) val state by channel .flow() @@ -182,8 +183,8 @@ private fun ParentSelectorCard( model = liveParent.profilePicture(), contentDescription = liveParent.toBestDisplayName(), modifier = Modifier.size(46.dp).clip(CircleShape), - loadProfilePicture = accountViewModel.settings.showProfilePictures(), - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + loadProfilePicture = LocalDisplaySettings.current.showProfilePictures, + loadRobohash = LocalDisplaySettings.current.loadRobohash, autoPlayGif = false, ) } else { @@ -263,7 +264,7 @@ private fun ParentGroupPickerSheet( // rejects), so exclude them from the candidate set. val forbidden = remember(selfGroupId, relay) { - descendantIdsOf(accountViewModel, selfGroupId, relay) + selfGroupId + descendantIdsOf(selfGroupId, relay) + selfGroupId } // Re-read the relay's genuine, relay-signed groups whenever a kind-39000 lands. The initial @@ -275,10 +276,10 @@ private fun ParentGroupPickerSheet( relayInfo, forbidden, ) { - value = pickCandidates(accountViewModel, relay, relayInfo, forbidden) + value = pickCandidates(relay, relayInfo, forbidden) LocalCache .observeEvents(Filter(kinds = listOf(GroupMetadataEvent.KIND))) - .collect { value = pickCandidates(accountViewModel, relay, relayInfo, forbidden) } + .collect { value = pickCandidates(relay, relayInfo, forbidden) } } val filtered = @@ -393,8 +394,8 @@ private fun GroupPickRow( model = channel.profilePicture(), contentDescription = channel.toBestDisplayName(), modifier = Modifier.size(46.dp).clip(CircleShape), - loadProfilePicture = accountViewModel.settings.showProfilePictures(), - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + loadProfilePicture = LocalDisplaySettings.current.showProfilePictures, + loadRobohash = LocalDisplaySettings.current.loadRobohash, autoPlayGif = false, ) Column(Modifier.weight(1f)) { @@ -468,12 +469,11 @@ private fun SelectionMark(selected: Boolean) { /** Genuine, relay-signed groups on [relay], minus the forbidden (self + descendant) ids, name-sorted. */ private fun pickCandidates( - accountViewModel: AccountViewModel, relay: NormalizedRelayUrl, relayInfo: Nip11RelayInformation, forbidden: Set, ): List = - accountViewModel + LocalCache .getRelayGroupChannelsOnRelay(relay) .asSequence() .filter { it.groupId.id !in forbidden } @@ -486,17 +486,16 @@ private fun pickCandidates( * advertised `child` links. Visited-guarded so a malformed cycle can't loop forever. */ private fun descendantIdsOf( - accountViewModel: AccountViewModel, rootId: String, relay: NormalizedRelayUrl, ): Set { val result = mutableSetOf() val queue = ArrayDeque() - accountViewModel.getRelayGroupChannelIfExists(GroupId(rootId, relay))?.childGroupIds()?.let { queue.addAll(it) } + LocalCache.getRelayGroupChannelIfExists(GroupId(rootId, relay))?.childGroupIds()?.let { queue.addAll(it) } while (queue.isNotEmpty()) { val id = queue.removeFirst() if (!result.add(id)) continue - accountViewModel.getRelayGroupChannelIfExists(GroupId(id, relay))?.childGroupIds()?.let { queue.addAll(it) } + LocalCache.getRelayGroupChannelIfExists(GroupId(id, relay))?.childGroupIds()?.let { queue.addAll(it) } } return result } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupPinnedBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupPinnedBar.kt index 12bee2e615..b4fb5f5abb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupPinnedBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupPinnedBar.kt @@ -45,6 +45,7 @@ import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.relay_group_pinned_content_description @@ -76,7 +77,7 @@ fun RelayGroupPinnedBar( val safeIndex = index.coerceIn(0, pinnedIds.lastIndex) val currentId = pinnedIds[safeIndex] - val note = remember(currentId) { accountViewModel.checkGetOrCreateNote(currentId) } ?: return + val note = remember(currentId) { LocalCache.checkGetOrCreateNote(currentId) } ?: return // Fetch + observe the pinned message so its author and content fill in once it loads. val noteState by observeNote(note, accountViewModel) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupServerList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupServerList.kt index 9562e675ab..b471fe199c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupServerList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupServerList.kt @@ -39,6 +39,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.relay_group_relay_not_nip29 +import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.LargeRelayIconModifier import com.vitorpamplona.amethyst.commons.ui.theme.warningColor @@ -74,8 +75,8 @@ fun RelayGroupServerRow( RenderRelayIcon( displayUrl = host, iconUrl = info?.value?.icon, - loadProfilePicture = accountViewModel.settings.showProfilePictures(), - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + loadProfilePicture = LocalDisplaySettings.current.showProfilePictures, + loadRobohash = LocalDisplaySettings.current.loadRobohash, pingInMs = 0, iconModifier = LargeRelayIconModifier, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupSubgroupsBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupSubgroupsBar.kt index c6d4ed9311..0010259629 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupSubgroupsBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupSubgroupsBar.kt @@ -135,7 +135,7 @@ private fun SubgroupChip( accountViewModel: AccountViewModel, nav: INav, ) { - LoadRelayGroupChannel(groupId, accountViewModel) { child -> + LoadRelayGroupChannel(groupId) { child -> val childState by child .flow() .metadata.stateFlow diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupThreadsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupThreadsScreen.kt index 3b956f6d38..d190ffe3eb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupThreadsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupThreadsScreen.kt @@ -111,7 +111,7 @@ fun RelayGroupThreadsScreen( val relay = remember(relayUrl) { RelayUrlNormalizer.normalizeOrNull(relayUrl) } ?: return val channelId = remember(id, relay) { GroupId(id, relay) } - LoadRelayGroupChannel(channelId, accountViewModel) { channel -> + LoadRelayGroupChannel(channelId) { channel -> RelayGroupThreads(channel, accountViewModel, nav) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelFileUploadDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelFileUploadDialog.kt index ec0e05f483..467917ee63 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelFileUploadDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelFileUploadDialog.kt @@ -40,6 +40,7 @@ import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.profile_image import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.HeaderPictureModifier import com.vitorpamplona.amethyst.commons.ui.theme.Size34dp @@ -73,8 +74,8 @@ fun ChannelFileUploadDialog( contentDescription = stringRes(Res.string.profile_image), contentScale = ContentScale.Crop, modifier = HeaderPictureModifier, - loadProfilePicture = accountViewModel.settings.showProfilePictures(), - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + loadProfilePicture = LocalDisplaySettings.current.showProfilePictures, + loadRobohash = LocalDisplaySettings.current.loadRobohash, autoPlayGif = accountViewModel.settings.autoPlayVideosFlow .collectAsStateWithLifecycle() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt index 043b16cef0..993ee2c827 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt @@ -375,7 +375,7 @@ open class ChannelNewMessageViewModel : (draftEvent as? ChannelMessageEvent) ?: (draftEvent as? LiveActivitiesChatMessageEvent) threadedDraft?.reply()?.eventId?.let { replyId -> - replyTo.value = accountViewModel.checkGetOrCreateNote(replyId) + replyTo.value = LocalCache.checkGetOrCreateNote(replyId) } message.setTextAndPlaceCursorAtEnd(draftEvent.content) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt index 32e8fac851..5cde7e0866 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt @@ -109,6 +109,7 @@ import com.vitorpamplona.amethyst.commons.resources.unpin_conversation import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.HeaderPill +import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.AccountPictureModifier import com.vitorpamplona.amethyst.commons.ui.theme.ChatLabelMaxWidth @@ -310,14 +311,14 @@ private fun ChatroomEntry( // ChannelHideMessageEvent/ChannelMuteUserEvent keep falling through to `else`. is ChannelMessageEvent, is ChannelMetadataEvent -> { baseNoteEvent.channelId()?.let { - LoadPublicChatChannel(it, accountViewModel) { channel -> + LoadPublicChatChannel(it) { channel -> ChannelRoomCompose(lastMessage, channel, accountViewModel, nav) } } } is ChannelCreateEvent -> { - LoadPublicChatChannel(baseNoteEvent.id, accountViewModel) { channel -> + LoadPublicChatChannel(baseNoteEvent.id) { channel -> ChannelRoomCompose(lastMessage, channel, accountViewModel, nav) } } @@ -329,7 +330,7 @@ private fun ChatroomEntry( is EphemeralChatEvent -> { baseNoteEvent.roomId()?.let { - LoadEphemeralChatChannel(it, accountViewModel) { channel -> + LoadEphemeralChatChannel(it) { channel -> ChannelRoomCompose(lastMessage, channel, accountViewModel, nav) } } @@ -396,8 +397,8 @@ private fun ChannelRoomCompose( channelLastTime = lastMessage.createdAt(), channelLastContent = "$authorName: $description", hasNewMessages = hasNewMessages, - loadProfilePicture = accountViewModel.settings.showProfilePictures(), - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + loadProfilePicture = LocalDisplaySettings.current.showProfilePictures, + loadRobohash = LocalDisplaySettings.current.loadRobohash, autoPlayGif = accountViewModel.settings.autoPlayVideosFlow .collectAsStateWithLifecycle() @@ -456,8 +457,8 @@ private fun ChannelRoomCompose( channelLastTime = lastMessage.createdAt(), channelLastContent = "$authorName: $description", hasNewMessages = (noteEvent?.createdAt ?: Long.MIN_VALUE) > lastReadTime, - loadProfilePicture = accountViewModel.settings.showProfilePictures(), - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + loadProfilePicture = LocalDisplaySettings.current.showProfilePictures, + loadRobohash = LocalDisplaySettings.current.loadRobohash, autoPlayGif = accountViewModel.settings.autoPlayVideosFlow .collectAsStateWithLifecycle() @@ -491,8 +492,8 @@ private fun GeohashRoomCompose( channelLastTime = lastMessage.createdAt(), channelLastContent = lastContent, hasNewMessages = (noteEvent?.createdAt ?: Long.MIN_VALUE) > lastReadTime, - loadProfilePicture = accountViewModel.settings.showProfilePictures(), - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + loadProfilePicture = LocalDisplaySettings.current.showProfilePictures, + loadRobohash = LocalDisplaySettings.current.loadRobohash, autoPlayGif = accountViewModel.settings.autoPlayVideosFlow .collectAsStateWithLifecycle() @@ -545,8 +546,8 @@ private fun MarmotGroupRoomCompose( channelLastTime = lastMessage.createdAt(), channelLastContent = lastContent, hasNewMessages = (lastMessage.createdAt() ?: Long.MIN_VALUE) > lastReadTime, - loadProfilePicture = accountViewModel.settings.showProfilePictures(), - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + loadProfilePicture = LocalDisplaySettings.current.showProfilePictures, + loadRobohash = LocalDisplaySettings.current.loadRobohash, autoPlayGif = accountViewModel.settings.autoPlayVideosFlow .collectAsStateWithLifecycle() @@ -780,8 +781,8 @@ fun RelayGroupRow( channelLastTime = lastTime, channelLastContent = lastContent, hasNewMessages = (lastTime ?: Long.MIN_VALUE) > lastReadTime, - loadProfilePicture = accountViewModel.settings.showProfilePictures(), - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + loadProfilePicture = LocalDisplaySettings.current.showProfilePictures, + loadRobohash = LocalDisplaySettings.current.loadRobohash, autoPlayGif = accountViewModel.settings.autoPlayVideosFlow .collectAsStateWithLifecycle() @@ -945,8 +946,8 @@ private fun ConcordRoomCompose( channelLastTime = lastMessage.createdAt(), channelLastContent = lastContent, hasNewMessages = (lastMessage.createdAt() ?: Long.MIN_VALUE) > lastReadTime, - loadProfilePicture = accountViewModel.settings.showProfilePictures(), - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + loadProfilePicture = LocalDisplaySettings.current.showProfilePictures, + loadRobohash = LocalDisplaySettings.current.loadRobohash, autoPlayGif = accountViewModel.settings.autoPlayVideosFlow .collectAsStateWithLifecycle() @@ -1002,8 +1003,8 @@ private fun RelayGroupServerRoomCompose( channelLastTime = row.newestMessage?.createdAt(), channelLastContent = lastContent, hasNewMessages = hasNewMessages, - loadProfilePicture = accountViewModel.settings.showProfilePictures(), - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + loadProfilePicture = LocalDisplaySettings.current.showProfilePictures, + loadRobohash = LocalDisplaySettings.current.loadRobohash, autoPlayGif = accountViewModel.settings.autoPlayVideosFlow .collectAsStateWithLifecycle() @@ -1053,8 +1054,8 @@ private fun ConcordServerRoomCompose( channelLastTime = row.newestMessage?.createdAt(), channelLastContent = lastContent, hasNewMessages = hasNewMessages, - loadProfilePicture = accountViewModel.settings.showProfilePictures(), - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + loadProfilePicture = LocalDisplaySettings.current.showProfilePictures, + loadRobohash = LocalDisplaySettings.current.loadRobohash, autoPlayGif = accountViewModel.settings.autoPlayVideosFlow .collectAsStateWithLifecycle() @@ -1164,7 +1165,7 @@ private fun UserRoomCompose( if (counterpartHex != null) { // 1:1 room: resolve the counterpart once and share it between the name and the // report-warning icon below, instead of each doing its own LoadUser. - LoadUser(baseUserHex = counterpartHex, accountViewModel = accountViewModel) { counterpart -> + LoadUser(baseUserHex = counterpartHex) { counterpart -> RoomNameDisplay(room, Modifier.weight(1f), accountViewModel, preloadedUser = counterpart) RoomReportWarningIcon(counterpart, accountViewModel) } @@ -1271,15 +1272,14 @@ private fun RowScope.LastMessagePreview( @Composable fun LoadUser( baseUserHex: String, - accountViewModel: AccountViewModel, content: @Composable (User?) -> Unit, ) { var user by - remember(baseUserHex) { mutableStateOf(accountViewModel.getUserIfExists(baseUserHex)) } + remember(baseUserHex) { mutableStateOf(LocalCache.getUserIfExists(baseUserHex)) } if (user == null) { LaunchedEffect(key1 = baseUserHex) { - user = accountViewModel.checkGetOrCreateUser(baseUserHex) + user = LocalCache.checkGetOrCreateUser(baseUserHex) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessGameScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessGameScreen.kt index 10eadd3e19..d013cb4f1c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessGameScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessGameScreen.kt @@ -60,6 +60,7 @@ import androidx.fragment.app.FragmentActivity 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.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.nip64Chess.ChessBroadcastStatus import com.vitorpamplona.amethyst.commons.nip64Chess.ui.ChessBroadcastBanner @@ -306,7 +307,7 @@ fun ChessGameScreen( // Resolve opponent display name and avatar val opponentUser = remember(gameState.opponentPubkey) { - accountViewModel.checkGetOrCreateUser(gameState.opponentPubkey) + LocalCache.checkGetOrCreateUser(gameState.opponentPubkey) } val opponentDisplayName = remember(gameState.opponentPubkey) { @@ -320,7 +321,7 @@ fun ChessGameScreen( // Resolve player display name and avatar val playerUser = remember(gameState.playerPubkey) { - accountViewModel.checkGetOrCreateUser(gameState.playerPubkey) + LocalCache.checkGetOrCreateUser(gameState.playerPubkey) } val playerDisplayName = remember(gameState.playerPubkey) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessLobbyScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessLobbyScreen.kt index 639ad50264..811cbb1c66 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessLobbyScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessLobbyScreen.kt @@ -60,6 +60,7 @@ import androidx.fragment.app.FragmentActivity 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.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.nip64Chess.ChessChallenge import com.vitorpamplona.amethyst.commons.nip64Chess.ui.ActiveGameCard @@ -306,7 +307,7 @@ fun ChessLobbyContent( val userPubkey = accountViewModel.account.userProfile().pubkeyHex val currentUser = remember(userPubkey) { - accountViewModel.checkGetOrCreateUser(userPubkey) + LocalCache.checkGetOrCreateUser(userPubkey) } val hasContent = @@ -375,7 +376,7 @@ fun ChessLobbyContent( items(activeGames.entries.toList(), key = { "active-${it.key}" }) { (gameId, state) -> val opponent = remember(state.opponentPubkey) { - accountViewModel.checkGetOrCreateUser(state.opponentPubkey) + LocalCache.checkGetOrCreateUser(state.opponentPubkey) } val displayName = opponent?.toBestDisplayName() ?: state.opponentPubkey.take(8) ActiveGameCard( @@ -411,7 +412,7 @@ fun ChessLobbyContent( items(outgoingChallenges, key = { "outgoing-${it.eventId}" }) { challenge -> val opponentUser = remember(challenge.opponentPubkey) { - challenge.opponentPubkey?.let { accountViewModel.checkGetOrCreateUser(it) } + challenge.opponentPubkey?.let { LocalCache.checkGetOrCreateUser(it) } } val opponentName = opponentUser?.toBestDisplayName() @@ -476,7 +477,7 @@ fun ChessLobbyContent( val displayName = remember(challenge.challengerPubkey, challenge.challengerDisplayName) { challenge.challengerDisplayName - ?: accountViewModel.checkGetOrCreateUser(challenge.challengerPubkey)?.toBestDisplayName() + ?: LocalCache.checkGetOrCreateUser(challenge.challengerPubkey)?.toBestDisplayName() ?: challenge.challengerPubkey.take(8) } ChallengeCard( @@ -513,7 +514,7 @@ fun ChessLobbyContent( val displayName = remember(challenge.challengerPubkey, challenge.challengerDisplayName) { challenge.challengerDisplayName - ?: accountViewModel.checkGetOrCreateUser(challenge.challengerPubkey)?.toBestDisplayName() + ?: LocalCache.checkGetOrCreateUser(challenge.challengerPubkey)?.toBestDisplayName() ?: challenge.challengerPubkey.take(8) } ChallengeCard( @@ -587,7 +588,7 @@ fun ChessLobbyContent( if (game.whitePubkey == userPubkey) game.blackPubkey else game.whitePubkey val opponentUser = remember(opponentPubkey) { - accountViewModel.checkGetOrCreateUser(opponentPubkey) + LocalCache.checkGetOrCreateUser(opponentPubkey) } val opponentName = opponentUser?.toBestDisplayName() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/CommunityScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/CommunityScreen.kt index 57fe8ac2b1..f06d203aa1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/CommunityScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/CommunityScreen.kt @@ -78,7 +78,7 @@ fun CommunityScreen( accountViewModel: AccountViewModel, nav: INav, ) { - LoadAddressableNote(aTagHex, accountViewModel) { + LoadAddressableNote(aTagHex) { it?.let { PrepareViewModelsCommunityScreen( note = it, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/NewCommunityNoteButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/NewCommunityNoteButton.kt index 39e2d12e8f..5caf54d578 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/NewCommunityNoteButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/NewCommunityNoteButton.kt @@ -44,7 +44,7 @@ fun NewCommunityNoteButton( accountViewModel: AccountViewModel, nav: INav, ) { - LoadNote(baseNoteHex = communityIdHex, accountViewModel) { + LoadNote(baseNoteHex = communityIdHex) { it?.let { NewCommunityNoteButton(it, accountViewModel, nav) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/newCommunity/NewCommunityScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/newCommunity/NewCommunityScreen.kt index f3eadd1b43..0f9b946033 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/newCommunity/NewCommunityScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/newCommunity/NewCommunityScreen.kt @@ -98,6 +98,7 @@ import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.ActionTopBar import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.CreatingTopBar +import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightPage import com.vitorpamplona.amethyst.ui.actions.StrippingFailureDialog @@ -573,8 +574,8 @@ private fun RelaysSection( Column { BasicRelaySetupInfoClickableRow( item = info, - loadProfilePicture = accountViewModel.settings.showProfilePictures(), - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + loadProfilePicture = LocalDisplaySettings.current.showProfilePictures, + loadRobohash = LocalDisplaySettings.current.loadRobohash, onClick = {}, onDelete = { model.removeRelay(entry) }, nip11CachedRetriever = Amethyst.instance.nip11Cache, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/contactList/ContactListUsersScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/contactList/ContactListUsersScreen.kt index ea7869932e..56ae45f75f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/contactList/ContactListUsersScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/contactList/ContactListUsersScreen.kt @@ -33,6 +33,7 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.contact_list_screen_title import com.vitorpamplona.amethyst.commons.resources.contact_list_screen_title_with_count @@ -56,7 +57,7 @@ fun ContactListUsersScreen( accountViewModel: AccountViewModel, nav: INav, ) { - LoadNote(baseNoteHex = noteId, accountViewModel) { note -> + LoadNote(baseNoteHex = noteId) { note -> if (note == null) { Scaffold( topBar = { @@ -78,7 +79,7 @@ fun ContactListUsersScreen( event ?.verifiedFollowKeySet() ?.filter { !accountViewModel.account.isHidden(it) } - ?.mapNotNull { accountViewModel.checkGetOrCreateUser(it) } + ?.mapNotNull { LocalCache.checkGetOrCreateUser(it) } ?.sortedByDescending { accountViewModel.account.isKnown(it.pubkeyHex) } ?.toPersistentList() ?: persistentListOf() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostScreen.kt index 2545f6e935..2ece98c9c1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostScreen.kt @@ -81,6 +81,7 @@ 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.model.EmptyTagList +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.nip30CustomEmojis.ui.ShowEmojiSuggestionList import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.add_a_tag @@ -156,8 +157,8 @@ fun LongFormPostScreen( postViewModel.init(accountViewModel) LaunchedEffect(postViewModel, accountViewModel) { - val draft = draftId?.let { accountViewModel.getNoteIfExists(it) } - val version = versionId?.let { accountViewModel.getNoteIfExists(it) } + val draft = draftId?.let { LocalCache.getNoteIfExists(it) } + val version = versionId?.let { LocalCache.getNoteIfExists(it) } postViewModel.load(draft, version) if (draftId == null && versionId == null) { postViewModel.applySignature() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt index 0065dae58c..4623df34fe 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt @@ -81,7 +81,7 @@ fun RenderPublicChatChannelThumb( accountViewModel: AccountViewModel, nav: INav, ) { - LoadPublicChatChannel(baseNote.idHex, accountViewModel) { + LoadPublicChatChannel(baseNote.idHex) { RenderPublicChatChannelThumb(baseNote = baseNote, channel = it, accountViewModel, nav) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/LiveActivityCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/LiveActivityCard.kt index 75404c3494..246fadcb6d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/LiveActivityCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/LiveActivityCard.kt @@ -240,7 +240,7 @@ fun RenderLiveActivityThumb( Spacer(modifier = DoubleVertSpacer) baseNote.address()?.let { - LoadLiveActivityChannel(it, accountViewModel) { + LoadLiveActivityChannel(it) { LiveActivitiesChannelHeader( baseChannel = it, showVideo = false, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt index a3ffe17186..d92f87629f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt @@ -44,6 +44,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.core.net.toUri import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.nip30CustomEmojis.ui.ShowEmojiSuggestionList import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.description @@ -111,10 +112,10 @@ fun NewProductScreen( LaunchedEffect(postViewModel, accountViewModel) { postViewModel.reloadRelaySet() - draftId?.let { accountViewModel.getNoteIfExists(it) }?.let { + draftId?.let { LocalCache.getNoteIfExists(it) }?.let { postViewModel.editFromDraft(it) } - quoteId?.let { accountViewModel.getNoteIfExists(it) }?.let { + quoteId?.let { LocalCache.getNoteIfExists(it) }?.let { postViewModel.quote(it) } message?.ifBlank { null }?.let { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt index 3ae4bacb3a..5bcbfa664a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt @@ -111,7 +111,7 @@ fun DvmContentDiscoveryScreen( }, accountViewModel = accountViewModel, ) { - LoadNote(baseNoteHex = appDefinitionEventId, accountViewModel = accountViewModel) { note -> + LoadNote(baseNoteHex = appDefinitionEventId) { note -> note?.let { baseNote -> WatchNoteEvent( baseNote, @@ -462,7 +462,7 @@ fun DvmPaymentActions( } } } else if (amount != null) { - LoadNote(baseNoteHex = latestStatus.id, accountViewModel = accountViewModel) { stateNote -> + LoadNote(baseNoteHex = latestStatus.id) { stateNote -> stateNote?.let { ZapDVMButton( baseNote = it, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmHeartbeatObservation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmHeartbeatObservation.kt index 2d95606f37..5f9ae8e46e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmHeartbeatObservation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmHeartbeatObservation.kt @@ -40,6 +40,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip90DVMs.DvmHeartbeatRegistry import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.dvm_offline_banner @@ -72,11 +73,11 @@ fun rememberDvmHeartbeatFresh( var heartbeatNote by remember(heartbeatAddressTag) { - mutableStateOf(accountViewModel.getNoteIfExists(heartbeatAddressTag)) + mutableStateOf(LocalCache.getNoteIfExists(heartbeatAddressTag)) } LaunchedEffect(heartbeatAddressTag) { if (heartbeatNote == null) { - heartbeatNote = accountViewModel.checkGetOrCreateNote(heartbeatAddressTag) + heartbeatNote = LocalCache.checkGetOrCreateNote(heartbeatAddressTag) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmTopBar.kt index 7ab3f9ba55..fb4fff3272 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmTopBar.kt @@ -49,7 +49,7 @@ fun DvmTopBar( ) { MyExtensibleTopAppBar( title = { - LoadNote(baseNoteHex = appDefinitionId, accountViewModel = accountViewModel) { appDefinitionNote -> + LoadNote(baseNoteHex = appDefinitionId) { appDefinitionNote -> if (appDefinitionNote != null) { val card = observeAppDefinition(appDefinitionNote, accountViewModel) @@ -93,7 +93,7 @@ fun DvmTopBar( // The route passes the event's hex id, so LoadNote returns a plain Note, // not the AddressableNote the toggle needs. Derive the AddressableNote // from the loaded AppDefinitionEvent's address() once the event exists. - LoadNote(baseNoteHex = appDefinitionId, accountViewModel = accountViewModel) { appDefinitionNote -> + LoadNote(baseNoteHex = appDefinitionId) { appDefinitionNote -> if (appDefinitionNote != null) { val addressableNote by observeNoteAndMap(appDefinitionNote, accountViewModel) { note -> diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedTabFactory.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedTabFactory.kt index fab2842f38..8422c85477 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedTabFactory.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedTabFactory.kt @@ -27,9 +27,9 @@ import android.os.Bundle import androidx.annotation.RequiresApi import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp +import com.vitorpamplona.amethyst.commons.model.ThemeType import com.vitorpamplona.amethyst.commons.tor.TorType import com.vitorpamplona.amethyst.favorites.FavoriteAppLauncher -import com.vitorpamplona.amethyst.model.ThemeType import com.vitorpamplona.amethyst.napplet.WebAppNetworkRegistry import com.vitorpamplona.amethyst.napplethost.NappletHostContract import com.vitorpamplona.amethyst.ui.screen.loggedIn.browser.EmbeddedWebAppController diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedTabThemeWatcher.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedTabThemeWatcher.kt index 9c0ddcf11a..d3e8c770f6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedTabThemeWatcher.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedTabThemeWatcher.kt @@ -30,7 +30,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.Amethyst -import com.vitorpamplona.amethyst.model.ThemeType +import com.vitorpamplona.amethyst.commons.model.ThemeType /** * Keeps the warm embedded tabs in sync with the app's DARK/LIGHT theme. An embed WebView resolves its diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/list/ListOfEmojiPacksScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/list/ListOfEmojiPacksScreen.kt index 0f4d8dc8e8..3d1af6ad19 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/list/ListOfEmojiPacksScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/list/ListOfEmojiPacksScreen.kt @@ -312,11 +312,10 @@ private fun EmojiListToggleRow( remember(pack, accountViewModel) { Address(EmojiPackEvent.KIND, accountViewModel.account.signer.pubKey, pack.identifier) } - LoadAddressableNote(packAddress, accountViewModel) { + LoadAddressableNote(packAddress) { it?.let { packNote -> LoadAddressableNote( accountViewModel.account.emoji.getEmojiPackSelectionAddress(), - accountViewModel, ) { selectionNote -> selectionNote?.let { usersEmojiList -> val hasAddedThis by observeNoteAndMap(usersEmojiList, accountViewModel) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/membershipManagement/EmojiPackSelectionScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/membershipManagement/EmojiPackSelectionScreen.kt index fa5a65218e..798e89e207 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/membershipManagement/EmojiPackSelectionScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/membershipManagement/EmojiPackSelectionScreen.kt @@ -72,7 +72,7 @@ fun EmojiPackSelectionScreen( accountViewModel: AccountViewModel, nav: INav, ) { - LoadAddressableNote(address = packAddress, accountViewModel = accountViewModel) { note -> + LoadAddressableNote(address = packAddress) { note -> note?.let { EmojiPackSelectionView( modifier = Modifier.fillMaxSize().recalculateWindowInsets(), @@ -122,7 +122,6 @@ private fun EmojiPackSelectionBody( item { LoadAddressableNote( address = accountViewModel.account.emoji.getEmojiPackSelectionAddress(), - accountViewModel = accountViewModel, ) { selectionNote -> selectionNote?.let { val hasAddedThis by observeNoteAndMap(it, accountViewModel) { currentNote -> diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/membershipManagement/MyEmojiListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/membershipManagement/MyEmojiListScreen.kt index 132d1f34d7..6f5feccd25 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/membershipManagement/MyEmojiListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/membershipManagement/MyEmojiListScreen.kt @@ -80,7 +80,6 @@ fun MyEmojiListScreen( ) { LoadAddressableNote( address = accountViewModel.account.emoji.getEmojiPackSelectionAddress(), - accountViewModel = accountViewModel, ) { selectionNote -> selectionNote?.let { MyEmojiListView( @@ -162,7 +161,6 @@ private fun MyEmojiListFeed( ) { address -> LoadAddressableNote( address = address, - accountViewModel = accountViewModel, ) { packNote -> packNote?.let { SelectedEmojiPackCard( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/FollowPackFeedScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/FollowPackFeedScreen.kt index 92aaaaf86a..5d9412c8fb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/FollowPackFeedScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/FollowPackFeedScreen.kt @@ -95,7 +95,7 @@ fun FollowPackFeedScreen( accountViewModel: AccountViewModel, nav: INav, ) { - LoadAddressableNote(address, accountViewModel) { + LoadAddressableNote(address) { it?.let { PrepareViewModelsFollowPackScreen( note = it, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/detail/GeocacheDetailParts.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/detail/GeocacheDetailParts.kt index 97b7d67d03..4bcf6e1d24 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/detail/GeocacheDetailParts.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/detail/GeocacheDetailParts.kt @@ -126,7 +126,7 @@ fun GeocacheOwnerRow( accountViewModel: AccountViewModel, nav: INav, ) { - LoadUser(listing.pubKey, accountViewModel) { user -> + LoadUser(listing.pubKey) { user -> if (user != null) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { ClickableUserPicture(user, Size25dp, accountViewModel) @@ -142,7 +142,7 @@ fun GeocacheWinnerStrip( winner: String, accountViewModel: AccountViewModel, ) { - LoadUser(winner, accountViewModel) { user -> + LoadUser(winner) { user -> // LoadUser resolves the User object, not its metadata, so reading the display name off it // left the winner as a hex stub whenever their kind 0 arrived after this composed. // observeUserName asks the relays for the profile and recomposes when it lands. The name @@ -227,7 +227,7 @@ fun GeocacheLogRow( ).padding(vertical = 8.dp), ) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { - LoadUser(author, accountViewModel) { user -> + LoadUser(author) { user -> if (user != null) { ClickableUserPicture(user, Size25dp, accountViewModel) UsernameDisplay(user, accountViewModel = accountViewModel) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/hunt/GeocacheHuntScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/hunt/GeocacheHuntScreen.kt index d3e9da2b8f..f1aa5a1344 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/hunt/GeocacheHuntScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/hunt/GeocacheHuntScreen.kt @@ -217,7 +217,7 @@ private fun GeocacheHuntStop( accountViewModel: AccountViewModel, nav: INav, ) { - LoadAddressableNote(address, accountViewModel) { cacheNote -> + LoadAddressableNote(address) { cacheNote -> // A hunt is a list of caches the reader has most likely never fetched, so the note comes // back empty and `cacheNote.event` would stay null forever. observeNoteEvent asks the // relays for it and recomposes this stop when it lands. diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/hunt/NewGeocacheHuntScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/hunt/NewGeocacheHuntScreen.kt index 6512bed293..3b2f7f7ba5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/hunt/NewGeocacheHuntScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/hunt/NewGeocacheHuntScreen.kt @@ -182,7 +182,7 @@ fun NewGeocacheHuntScreen( } model.caches.forEachIndexed { index, address -> - LoadAddressableNote(address, accountViewModel) { cacheNote -> + LoadAddressableNote(address) { cacheNote -> // Same reason as the hunt screen's stops: a cache added by naddr has no event // in the cache yet, so this has to ask the relays and watch rather than read. val listing = cacheNote?.let { observeNoteEvent(it, accountViewModel).value } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashPostScreen.kt index 8860ebac00..2b0eaf0089 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashPostScreen.kt @@ -26,6 +26,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.platform.LocalContext import androidx.core.net.toUri import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.navigation.navs.Nav import com.vitorpamplona.amethyst.ui.note.nip22Comments.CommentPostViewModel @@ -57,13 +58,13 @@ fun GeoHashPostScreen( geohash?.let { postViewModel.newPostFor(GeohashId(it)) } - replyId?.let { accountViewModel.getNoteIfExists(it) }?.let { + replyId?.let { LocalCache.getNoteIfExists(it) }?.let { postViewModel.reply(it) } - draftId?.let { accountViewModel.getNoteIfExists(it) }?.let { + draftId?.let { LocalCache.getNoteIfExists(it) }?.let { postViewModel.editFromDraft(it) } - quoteId?.let { accountViewModel.getNoteIfExists(it) }?.let { + quoteId?.let { LocalCache.getNoteIfExists(it) }?.let { postViewModel.quote(it) } message?.ifBlank { null }?.let { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepo/GitNewIssue.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepo/GitNewIssue.kt index 4df69197ca..4142c962af 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepo/GitNewIssue.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepo/GitNewIssue.kt @@ -68,7 +68,7 @@ fun GitNewIssueScreen( accountViewModel: AccountViewModel, nav: INav, ) { - LoadAddressableNote(address, accountViewModel) { note -> + LoadAddressableNote(address) { note -> note?.let { GitNewIssueForm(it, accountViewModel, nav) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepo/GitRepositoryScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepo/GitRepositoryScreen.kt index b469200252..f6859edce3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepo/GitRepositoryScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepo/GitRepositoryScreen.kt @@ -122,7 +122,7 @@ fun GitRepositoryScreen( accountViewModel: AccountViewModel, nav: INav, ) { - LoadAddressableNote(address, accountViewModel) { note -> + LoadAddressableNote(address) { note -> note?.let { GitRepositoryHome( note = it, @@ -139,7 +139,7 @@ fun GitRepositoryCodeScreen( accountViewModel: AccountViewModel, nav: INav, ) { - LoadAddressableNote(address, accountViewModel) { note -> + LoadAddressableNote(address) { note -> note?.let { GitRepositoryCode(it, accountViewModel, nav) } } } @@ -150,7 +150,7 @@ fun GitRepositoryIssuesScreen( accountViewModel: AccountViewModel, nav: INav, ) { - LoadAddressableNote(address, accountViewModel) { note -> + LoadAddressableNote(address) { note -> note?.let { GitRepositoryIssues(it, accountViewModel, nav) } } } @@ -161,7 +161,7 @@ fun GitRepositoryPullsScreen( accountViewModel: AccountViewModel, nav: INav, ) { - LoadAddressableNote(address, accountViewModel) { note -> + LoadAddressableNote(address) { note -> note?.let { GitRepositoryPulls(it, accountViewModel, nav) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagPostScreen.kt index c752fb9b01..b1eb0e449b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagPostScreen.kt @@ -26,6 +26,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.platform.LocalContext import androidx.core.net.toUri import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.navigation.navs.Nav import com.vitorpamplona.amethyst.ui.note.nip22Comments.CommentPostViewModel @@ -57,13 +58,13 @@ fun HashtagPostScreen( hashtag?.let { postViewModel.newPostFor(HashtagId(it)) } - replyId?.let { accountViewModel.getNoteIfExists(it) }?.let { + replyId?.let { LocalCache.getNoteIfExists(it) }?.let { postViewModel.reply(it) } - draftId?.let { accountViewModel.getNoteIfExists(it) }?.let { + draftId?.let { LocalCache.getNoteIfExists(it) }?.let { postViewModel.editFromDraft(it) } - quoteId?.let { accountViewModel.getNoteIfExists(it) }?.let { + quoteId?.let { LocalCache.getNoteIfExists(it) }?.let { postViewModel.quote(it) } message?.ifBlank { null }?.let { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/highlights/NewHighlightPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/highlights/NewHighlightPostViewModel.kt index 19882e082c..573cbbe5da 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/highlights/NewHighlightPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/highlights/NewHighlightPostViewModel.kt @@ -29,6 +29,7 @@ import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState.EmojiMedia import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiSuggestionState import com.vitorpamplona.amethyst.commons.ui.note.creators.messagefield.IMessageField @@ -134,8 +135,8 @@ class NewHighlightPostViewModel : if (accountViewModel != null) { originalNote = when { - !sourceAddress.isNullOrBlank() -> Address.parse(sourceAddress)?.let { accountViewModel.getOrCreateAddressableNote(it) } - !sourceEventId.isNullOrBlank() -> accountViewModel.getOrCreateNote(sourceEventId) + !sourceAddress.isNullOrBlank() -> Address.parse(sourceAddress)?.let { LocalCache.getOrCreateAddressableNote(it) } + !sourceEventId.isNullOrBlank() -> LocalCache.getOrCreateNote(sourceEventId) else -> null } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/AlgoFeedStatusBanner.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/AlgoFeedStatusBanner.kt index 4f9b3d670e..ac92ec3436 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/AlgoFeedStatusBanner.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/AlgoFeedStatusBanner.kt @@ -108,7 +108,7 @@ private fun SingleAlgoFeedBanner( val feedAddressValue = favFeed.address.toValue() - LoadNote(baseNoteHex = feedAddressValue, accountViewModel = accountViewModel) { feedNote -> + LoadNote(baseNoteHex = feedAddressValue) { feedNote -> val resolvedName by observeNoteAndMap(feedNote ?: return@LoadNote, accountViewModel) { note -> (note.event as? AppDefinitionEvent) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt index e97012e2ce..142a83683e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt @@ -81,6 +81,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.model.BooleanType +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.composer.AudienceSelection import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.nip30CustomEmojis.ui.ShowEmojiSuggestionList @@ -135,7 +137,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightPa import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.ui.theme.replyModifier -import com.vitorpamplona.amethyst.model.BooleanType import com.vitorpamplona.amethyst.ui.actions.StrippingFailureDialog import com.vitorpamplona.amethyst.ui.actions.mediaServers.FileServerSelectionRow import com.vitorpamplona.amethyst.ui.actions.uploads.MAX_VOICE_RECORD_SECONDS @@ -217,11 +218,11 @@ fun ShortNotePostScreen( } LaunchedEffect(postViewModel, accountViewModel) { - val baseReplyTo = baseReplyToId?.let { accountViewModel.getNoteIfExists(it) } - val quote = quoteId?.let { accountViewModel.getNoteIfExists(it) } - val fork = forkId?.let { accountViewModel.getNoteIfExists(it) } - val version = versionId?.let { accountViewModel.getNoteIfExists(it) } - val draft = draftId?.let { accountViewModel.getNoteIfExists(it) } + val baseReplyTo = baseReplyToId?.let { LocalCache.getNoteIfExists(it) } + val quote = quoteId?.let { LocalCache.getNoteIfExists(it) } + val fork = forkId?.let { LocalCache.getNoteIfExists(it) } + val version = versionId?.let { LocalCache.getNoteIfExists(it) } + val draft = draftId?.let { LocalCache.getNoteIfExists(it) } postViewModel.load(baseReplyTo, quote, fork, version, draft) message?.ifBlank { null }?.let { postViewModel.message.setTextAndPlaceCursorAtEnd(it) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index 72bc69b916..625d16fd37 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt @@ -35,6 +35,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.BooleanType import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.cache.LocalCache @@ -69,7 +70,6 @@ import com.vitorpamplona.amethyst.commons.ui.text.onUiThread import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord import com.vitorpamplona.amethyst.commons.ui.text.setTextAndPlaceCursorAtBeginning import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.BooleanType import com.vitorpamplona.amethyst.model.accountsCache.AccountCacheState import com.vitorpamplona.amethyst.service.ai.WritingAssistantFactory import com.vitorpamplona.amethyst.service.location.LocationState @@ -1506,7 +1506,7 @@ open class ShortNotePostViewModel : val tags = prepareETagsAsReplyTo(replyingTo, forkingFrom) // fixes wrong tags from previous clients tags.forEach { - val note = accountViewModel.getNoteIfExists(it.eventId) + val note = LocalCache.getNoteIfExists(it.eventId) val ourAuthor = note?.author?.pubkeyHex val ourHint = note?.relayHintUrl() if (it.author == null || it.author?.isBlank() == true) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt index 2e912fd133..c1b223f5fb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt @@ -28,6 +28,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.error @@ -115,7 +116,7 @@ class VoiceReplyViewModel : ViewModel() { duration: Int, amplitudesJson: String, ) { - replyToNote = accountViewModel.getNoteIfExists(replyToNoteId) + replyToNote = LocalCache.getNoteIfExists(replyToNoteId) val amplitudes = try { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicPlaylistScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicPlaylistScreen.kt index bb427ee154..8ef8fa212d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicPlaylistScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicPlaylistScreen.kt @@ -323,7 +323,7 @@ private fun TrackManagementSection( } tracks.forEachIndexed { index, address -> - LoadAddressableNote(address, accountViewModel) { trackNote -> + LoadAddressableNote(address) { trackNote -> EditableTrackRow( position = index + 1, trackNote = trackNote, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/activity/NestActivityContent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/activity/NestActivityContent.kt index f1a3bb2726..b6d187044f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/activity/NestActivityContent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/activity/NestActivityContent.kt @@ -101,7 +101,7 @@ internal fun NestActivityContent( return } - LoadAddressableNote(parsedAddress, accountViewModel) { addressableNote -> + LoadAddressableNote(parsedAddress) { addressableNote -> if (addressableNote == null) { // Note hasn't resolved yet — relay subscription is in // flight. Render a centered spinner so the user sees that diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt index e55351ddbc..b72f99f4b1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt @@ -293,7 +293,7 @@ open class NestNewMessageViewModel : if (draftEvent as? LiveActivitiesChatMessageEvent != null) { val replyId = draftEvent.reply()?.eventId if (replyId != null) { - replyTo.value = accountViewModel.checkGetOrCreateNote(replyId) + replyTo.value = LocalCache.checkGetOrCreateNote(replyId) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/lobby/NestJoinCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/lobby/NestJoinCard.kt index 27b6242818..b3e36b6ded 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/lobby/NestJoinCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/lobby/NestJoinCard.kt @@ -86,7 +86,7 @@ fun NestJoinCard( accountViewModel: AccountViewModel, nav: INav, ) { - LoadAddressableNote(baseChannel.address, accountViewModel) { addressableNote -> + LoadAddressableNote(baseChannel.address) { addressableNote -> addressableNote ?: return@LoadAddressableNote val event = addressableNote.event as? MeetingSpaceEvent ?: return@LoadAddressableNote NestJoinCardContent(event, accountViewModel, nav) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/lobby/NestLobbyScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/lobby/NestLobbyScreen.kt index 47316d842d..cbbcac7877 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/lobby/NestLobbyScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/lobby/NestLobbyScreen.kt @@ -127,7 +127,7 @@ fun NestLobbyScreen( nav: INav, ) { val address = remember(addressValue) { Address.parse(addressValue) } ?: return - LoadAddressableNote(address, accountViewModel) { addressableNote -> + LoadAddressableNote(address) { addressableNote -> addressableNote ?: return@LoadAddressableNote val event = addressableNote.event as? MeetingSpaceEvent ?: return@LoadAddressableNote NestLobbyContent(event, addressableNote, accountViewModel, nav) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/participants/ParticipantHostActionsSheet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/participants/ParticipantHostActionsSheet.kt index 0ddbf3547c..8714898475 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/participants/ParticipantHostActionsSheet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/participants/ParticipantHostActionsSheet.kt @@ -82,12 +82,12 @@ import com.vitorpamplona.amethyst.commons.resources.nest_promote_speaker import com.vitorpamplona.amethyst.commons.resources.nest_toast_host_action_failed_template_null import com.vitorpamplona.amethyst.commons.resources.nest_toast_host_action_failed_title import com.vitorpamplona.amethyst.commons.resources.no_wallet_found +import com.vitorpamplona.amethyst.commons.ui.components.toasts.multiline.UserBasedErrorMessage import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.NestViewModel import com.vitorpamplona.amethyst.commons.viewmodels.RoomSpeakerCatalog import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo import com.vitorpamplona.amethyst.ui.MainActivity -import com.vitorpamplona.amethyst.ui.components.toasts.multiline.UserBasedErrorMessage import com.vitorpamplona.amethyst.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.ui.note.ZapCustomDialog import com.vitorpamplona.amethyst.ui.note.payViaIntent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/screen/NestActionBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/screen/NestActionBar.kt index 066ad00d60..f01f0b7500 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/screen/NestActionBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/screen/NestActionBar.kt @@ -90,13 +90,13 @@ import com.vitorpamplona.amethyst.commons.resources.nest_reconnecting import com.vitorpamplona.amethyst.commons.resources.nest_talk import com.vitorpamplona.amethyst.commons.resources.no_wallet_found import com.vitorpamplona.amethyst.commons.resources.zap_description +import com.vitorpamplona.amethyst.commons.ui.components.toasts.multiline.UserBasedErrorMessage import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.BroadcastUiState import com.vitorpamplona.amethyst.commons.viewmodels.ConnectionUiState import com.vitorpamplona.amethyst.commons.viewmodels.NestUiState import com.vitorpamplona.amethyst.commons.viewmodels.NestViewModel -import com.vitorpamplona.amethyst.ui.components.toasts.multiline.UserBasedErrorMessage import com.vitorpamplona.amethyst.ui.note.ZapAmountChoicePopup import com.vitorpamplona.amethyst.ui.note.ZapCustomDialog import com.vitorpamplona.amethyst.ui.note.payViaIntent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/newUser/ImportFollowListPickFollowsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/newUser/ImportFollowListPickFollowsScreen.kt index db63f47a19..4ab3f4ce13 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/newUser/ImportFollowListPickFollowsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/newUser/ImportFollowListPickFollowsScreen.kt @@ -55,6 +55,7 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.accounts_found @@ -85,7 +86,7 @@ fun ImportFollowListPickFollowsScreen( ) { val contactListNote = remember(userHex) { - accountViewModel.getOrCreateAddressableNote(ContactListEvent.createAddress(userHex)) + LocalCache.getOrCreateAddressableNote(ContactListEvent.createAddress(userHex)) } Scaffold( @@ -137,7 +138,7 @@ fun DisplayFollowList( ?.unverifiedFollowKeySet() ?.toSet() ?.mapNotNull { - accountViewModel.checkGetOrCreateUser(it) + LocalCache.checkGetOrCreateUser(it) }?.toPersistentList() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationScreen.kt index f7227a9362..cd772a90de 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationScreen.kt @@ -32,6 +32,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.commons.model.UiSettingsFlow import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.notification_tab_everyone @@ -44,7 +45,6 @@ import com.vitorpamplona.amethyst.commons.ui.feeds.rememberForeverPagerState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.TabRowHeight -import com.vitorpamplona.amethyst.model.UiSettingsFlow import com.vitorpamplona.amethyst.ui.components.SelectNotificationProvider import com.vitorpamplona.amethyst.ui.feeds.WatchScrollToTop import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/donations/ShowDonationCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/donations/ShowDonationCard.kt index da65f74b1c..75a287d35e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/donations/ShowDonationCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/donations/ShowDonationCard.kt @@ -38,7 +38,6 @@ fun ShowDonationCard( if (!donated) { LoadNote( BuildConfig.RELEASE_NOTES_ID, - accountViewModel, ) { loadedNoteId -> if (loadedNoteId != null) { ZapTheDevsCard( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/donations/ZapTheDevsCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/donations/ZapTheDevsCard.kt index 26c85c7ee3..94ec7a0b5c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/donations/ZapTheDevsCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/donations/ZapTheDevsCard.kt @@ -118,7 +118,6 @@ fun ZapTheDevsCardPreview() { LoadNote( baseNoteHex = "0465b20da0adf45dd612024d124e1ed384f7ecd2cd7358e77998828e7bf35fa2", - accountViewModel, ) { releaseNote -> if (releaseNote != null) { ThemeComparisonColumn { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt index 22770a5c52..febe2b7ffe 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt @@ -53,6 +53,7 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.nip30CustomEmojis.ui.ShowEmojiSuggestionList import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.messages_new_message_to @@ -128,10 +129,10 @@ fun NewPublicMessageScreen( to?.let { postViewModel.load(it) } - replyId?.let { accountViewModel.getNoteIfExists(it) }?.let { + replyId?.let { LocalCache.getNoteIfExists(it) }?.let { postViewModel.reply(it) } - draftId?.let { accountViewModel.getNoteIfExists(it) }?.let { + draftId?.let { LocalCache.getNoteIfExists(it) }?.let { postViewModel.editFromDraft(it) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PictureCardComposePreview.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PictureCardComposePreview.kt index c71f3eff20..f07f651977 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PictureCardComposePreview.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PictureCardComposePreview.kt @@ -99,7 +99,6 @@ private fun PictureCardComposePreview() { LoadNote( baseNoteHex = "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2", - accountViewModel = accountViewModel, ) { baseNote -> ThemeComparisonColumn { if (baseNote != null) { @@ -128,7 +127,6 @@ private fun PictureCardComposeNoTitlePreview() { LoadNote( baseNoteHex = "b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3", - accountViewModel = accountViewModel, ) { baseNote -> ThemeComparisonColumn { if (baseNote != null) { @@ -157,7 +155,6 @@ private fun PictureCardComposeMultiImagePreview() { LoadNote( baseNoteHex = "c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4", - accountViewModel = accountViewModel, ) { baseNote -> ThemeComparisonColumn { if (baseNote != null) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastAuthors.kt index e5b4c014ea..65b4ceb89f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastAuthors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastAuthors.kt @@ -91,10 +91,10 @@ private fun PodcastAuthorRow( accountViewModel: AccountViewModel, nav: INav, ) { - var user by remember(author.pubKey) { mutableStateOf(accountViewModel.getUserIfExists(author.pubKey)) } + var user by remember(author.pubKey) { mutableStateOf(LocalCache.getUserIfExists(author.pubKey)) } if (user == null) { LaunchedEffect(author.pubKey) { - user = accountViewModel.checkGetOrCreateUser(author.pubKey) + user = LocalCache.checkGetOrCreateUser(author.pubKey) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastTopSupporters.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastTopSupporters.kt index aecc3b5f67..9ce804d10e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastTopSupporters.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastTopSupporters.kt @@ -136,7 +136,7 @@ private fun SupporterRow( modifier = Modifier.weight(1f), ) } else { - LoadUser(entry.bucketKey, accountViewModel) { user -> + LoadUser(entry.bucketKey) { user -> if (user != null) { ClickableUserPicture(user, Size35dp, accountViewModel, onClick = { nav.nav(routeFor(it)) }) UsernameDisplay(user, Modifier.weight(1f), accountViewModel = accountViewModel) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/PollPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/PollPostScreen.kt index 102f0dc38d..38eacaafdc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/PollPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/PollPostScreen.kt @@ -25,6 +25,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.ui.navigation.navs.Nav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.NewPostScreenInner @@ -44,7 +45,7 @@ fun PollPostScreen( postViewModel.init(accountViewModel) LaunchedEffect(postViewModel, accountViewModel) { - val draft = draftId?.let { accountViewModel.getNoteIfExists(it) } + val draft = draftId?.let { LocalCache.getNoteIfExists(it) } postViewModel.load(null, null, null, null, draft) message?.ifBlank { null }?.let { postViewModel.message.setTextAndPlaceCursorAtEnd(it) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/results/PollResultsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/results/PollResultsScreen.kt index 3a84c47426..05441ae449 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/results/PollResultsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/results/PollResultsScreen.kt @@ -141,7 +141,7 @@ fun PollResultsScreen( accountViewModel: AccountViewModel, nav: INav, ) { - LoadNote(baseNoteHex = noteId, accountViewModel) { note -> + LoadNote(baseNoteHex = noteId) { note -> if (note == null) { PollResultsScaffold(nav) { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryCardCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryCardCompose.kt index c41a2b6ab7..ab7322b1a8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryCardCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryCardCompose.kt @@ -68,7 +68,7 @@ fun GalleryCardCompose( } if (redirectToEventId != null) { - LoadNote(baseNoteHex = redirectToEventId, accountViewModel = accountViewModel) { baseSourceNote -> + LoadNote(baseNoteHex = redirectToEventId) { baseSourceNote -> if (baseSourceNote != null) { val sourceNote by observeNote(baseSourceNote, accountViewModel) RedirectableGalleryCard( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfilePaymentRailChips.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfilePaymentRailChips.kt index 85eca033f8..775012e84e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfilePaymentRailChips.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfilePaymentRailChips.kt @@ -128,7 +128,7 @@ fun DisplayPaymentRailChips( Bolt12OfferListEvent.createAddress(baseUser.pubkeyHex) } - LoadAddressableNote(targetsAddress, accountViewModel) { targetsNote -> + LoadAddressableNote(targetsAddress) { targetsNote -> val targets = if (targetsNote != null) { EventFinderFilterAssemblerSubscription(targetsNote, accountViewModel) @@ -138,7 +138,7 @@ fun DisplayPaymentRailChips( emptyList() } - LoadAddressableNote(bolt12Address, accountViewModel) { bolt12Note -> + LoadAddressableNote(bolt12Address) { bolt12Note -> val bolt12Offers = if (bolt12Note != null) { EventFinderFilterAssemblerSubscription(bolt12Note, accountViewModel) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/badges/DisplayBadges.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/badges/DisplayBadges.kt index 37e2865fe1..ddb814058a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/badges/DisplayBadges.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/badges/DisplayBadges.kt @@ -74,6 +74,7 @@ import com.vitorpamplona.amethyst.commons.resources.profile_badges_title import com.vitorpamplona.amethyst.commons.ui.components.RobohashAsyncImage import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssemblerSubscription import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent @@ -107,8 +108,8 @@ fun DisplayBadges( val oldDesign = AcceptedBadgeSetEvent.createAddress(baseUser.pubkeyHex) val newDesign = ProfileBadgesEvent.createAddress(baseUser.pubkeyHex) - val oldNote = accountViewModel.getOrCreateAddressableNote(oldDesign) - val newNote = accountViewModel.getOrCreateAddressableNote(newDesign) + val oldNote = LocalCache.getOrCreateAddressableNote(oldDesign) + val newNote = LocalCache.getOrCreateAddressableNote(newDesign) WatchAndRenderBadgeList(baseUser, oldNote, newNote, accountViewModel, nav) } @@ -344,7 +345,7 @@ private fun LoadDefinitionForAward( awardNote.value?.let { note -> val awardEvent by observeNoteEvent(note, accountViewModel) awardEvent?.awardDefinition()?.firstOrNull()?.let { defAddr -> - LoadAddressableNote(defAddr, accountViewModel) { defNote -> + LoadAddressableNote(defAddr) { defNote -> defNote?.let { content(it) } } } @@ -411,7 +412,7 @@ private fun RenderBadgeImage( robot = "badgenotfound", contentDescription = description, modifier = modifier, - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + loadRobohash = LocalDisplaySettings.current.loadRobohash, ) } else { RobohashFallbackAsyncImage( @@ -419,8 +420,8 @@ private fun RenderBadgeImage( model = image, contentDescription = description, modifier = modifier, - loadProfilePicture = accountViewModel.settings.showProfilePictures(), - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + loadProfilePicture = LocalDisplaySettings.current.showProfilePictures, + loadRobohash = LocalDisplaySettings.current.loadRobohash, autoPlayGif = accountViewModel.settings.autoPlayVideosFlow .collectAsStateWithLifecycle() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/payment/SendPaymentScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/payment/SendPaymentScreen.kt index 447e00e743..adaec88edf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/payment/SendPaymentScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/payment/SendPaymentScreen.kt @@ -152,7 +152,7 @@ fun SendPaymentScreen( Scaffold( topBar = { TopBarWithBackButton(stringRes(Res.string.send_payment_title), nav) }, ) { pad -> - LoadUser(baseUserHex = userHex, accountViewModel) { user -> + LoadUser(baseUserHex = userHex) { user -> if (user != null) { UserFinderFilterAssemblerSubscription(user, accountViewModel) SendPaymentLoaded( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/ShowQRScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/ShowQRScreen.kt index d649af657e..fc0f30163e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/ShowQRScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/ShowQRScreen.kt @@ -60,6 +60,7 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.commons.ui.note.ArrowBackIcon import com.vitorpamplona.amethyst.commons.ui.richtext.CreateTextWithEmoji +import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Font14SP import com.vitorpamplona.amethyst.commons.ui.theme.Size10dp @@ -124,7 +125,7 @@ fun ShowQRScreen( nav: INav, startScanning: Boolean = false, ) { - LoadUser(pubkey, accountViewModel) { user -> + LoadUser(pubkey) { user -> if (user != null) { ShowQRScreen( user = user, @@ -242,8 +243,8 @@ fun RenderName( model = userInfo?.info?.profilePicture(), contentDescription = stringRes(Res.string.profile_image), modifier = MaterialTheme.colorScheme.largeProfilePictureModifier, - loadProfilePicture = accountViewModel.settings.showProfilePictures(), - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + loadProfilePicture = LocalDisplaySettings.current.showProfilePictures, + loadRobohash = LocalDisplaySettings.current.loadRobohash, autoPlayGif = accountViewModel.settings.autoPlayVideosFlow .collectAsStateWithLifecycle() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/redirect/LoadRedirectScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/redirect/LoadRedirectScreen.kt index c768821527..02a3d3cf14 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/redirect/LoadRedirectScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/redirect/LoadRedirectScreen.kt @@ -89,7 +89,7 @@ fun LoadRedirectScreen( ) { CircularProgressIndicator() - LoadNote(eventId, accountViewModel) { note -> + LoadNote(eventId) { note -> Text( // A private id is not something to show a user, and it must not be // copyable off the screen either. diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relayauth/RelayAuthSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relayauth/RelayAuthSettingsScreen.kt index 83deb6a5f8..b0bd3b3ed1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relayauth/RelayAuthSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relayauth/RelayAuthSettingsScreen.kt @@ -112,6 +112,7 @@ import com.vitorpamplona.amethyst.commons.resources.relay_info import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton +import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.MediumRelayIconModifier import com.vitorpamplona.amethyst.commons.ui.theme.RelayIconFilter @@ -794,7 +795,7 @@ private fun RelayIcon( contentDescription = stringRes(Res.string.relay_info, url), colorFilter = RelayIconFilter, modifier = MediumRelayIconModifier, - loadProfilePicture = accountViewModel.settings.showProfilePictures(), - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + loadProfilePicture = LocalDisplaySettings.current.showProfilePictures, + loadRobohash = LocalDisplaySettings.current.loadRobohash, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt index e3382c8646..a49752d925 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt @@ -158,6 +158,7 @@ import com.vitorpamplona.amethyst.commons.ui.components.CrossfadeIfEnabled import com.vitorpamplona.amethyst.commons.ui.components.appendLink import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.commons.ui.theme.Height25Modifier @@ -856,7 +857,7 @@ private fun DisplayOwnerInformation( accountViewModel: AccountViewModel, nav: INav, ) { - LoadUser(baseUserHex = userHex, accountViewModel) { loadedUser -> + LoadUser(baseUserHex = userHex) { loadedUser -> CrossfadeIfEnabled(loadedUser) { if (it != null) { Card(colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)) { @@ -890,8 +891,8 @@ private fun RelayHeader( RenderRelayIcon( displayUrl = relay.displayUrl(), iconUrl = relayInfo.icon, - loadProfilePicture = accountViewModel.settings.showProfilePictures(), - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + loadProfilePicture = LocalDisplaySettings.current.showProfilePictures, + loadRobohash = LocalDisplaySettings.current.loadRobohash, pingInMs = relayStats.pingInMs, iconModifier = Modifier @@ -1422,7 +1423,7 @@ private fun RelayMonitorReportCard( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth(), ) { - LoadUser(baseUserHex = event.pubKey, accountViewModel) { user -> + LoadUser(baseUserHex = event.pubKey) { user -> if (user != null) { Row( verticalAlignment = Alignment.CenterVertically, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoDialog.kt index b2281985e1..d5f1e6e2af 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoDialog.kt @@ -26,6 +26,7 @@ import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.relays.ui.RelayCountResult import com.vitorpamplona.amethyst.commons.relays.ui.RelayDragState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.model.nip11RelayInfo.Nip11CachedRetriever import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -43,8 +44,8 @@ fun BasicRelaySetupInfoDialog( ) { BasicRelaySetupInfoClickableRow( item = item, - loadProfilePicture = accountViewModel.settings.showProfilePictures(), - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + loadProfilePicture = LocalDisplaySettings.current.showProfilePictures, + loadRobohash = LocalDisplaySettings.current.loadRobohash, onDelete = onDelete, onClick = { nav.nav(Route.RelayInfo(item.relay.url)) }, nip11CachedRetriever = nip11CachedRetriever, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/ShowRelaySuggestionList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/ShowRelaySuggestionList.kt index dca4068719..3b91761c41 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/ShowRelaySuggestionList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/ShowRelaySuggestionList.kt @@ -27,6 +27,7 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.HalfVertPadding import com.vitorpamplona.amethyst.model.nip11RelayInfo.Nip11CachedRetriever @@ -49,8 +50,8 @@ fun ShowRelaySuggestionList( suggestions.forEachIndexed { index, relayInfo -> BasicRelaySetupInfoClickableRow( item = relayInfo, - loadProfilePicture = accountViewModel.settings.showProfilePictures(), - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + loadProfilePicture = LocalDisplaySettings.current.showProfilePictures, + loadRobohash = LocalDisplaySettings.current.loadRobohash, onClick = { onSelect(relayInfo.relay) }, onDelete = null, nip11CachedRetriever = nip11CachedRetriever, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/subscriptions/ActiveSubscriptionsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/subscriptions/ActiveSubscriptionsScreen.kt index 42fcf0d8e5..d2e0a0496f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/subscriptions/ActiveSubscriptionsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/subscriptions/ActiveSubscriptionsScreen.kt @@ -90,6 +90,7 @@ import com.vitorpamplona.amethyst.commons.resources.marmot_group_fallback_name import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton +import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.theme.RelayIconFilter import com.vitorpamplona.amethyst.commons.ui.theme.allGoodColor import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText @@ -475,7 +476,7 @@ private fun EntityBlock( model = resolved.picture, contentDescription = label, modifier = Modifier.size(24.dp).clip(CircleShape), - loadProfilePicture = accountViewModel.settings.showProfilePictures(), + loadProfilePicture = LocalDisplaySettings.current.showProfilePictures, loadRobohash = false, ) Spacer(Modifier.width(8.dp)) @@ -522,7 +523,7 @@ private fun RelayLine( contentDescription = relay.displayUrl(), colorFilter = RelayIconFilter, modifier = Modifier.size(20.dp).clip(CircleShape), - loadProfilePicture = accountViewModel.settings.showProfilePictures(), + loadProfilePicture = LocalDisplaySettings.current.showProfilePictures, loadRobohash = false, ) Spacer(Modifier.width(8.dp)) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt index b2c953d106..f165f0cb52 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt @@ -121,6 +121,7 @@ import com.vitorpamplona.amethyst.commons.ui.layouts.rememberFeedContentPadding import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.ClearTextIcon import com.vitorpamplona.amethyst.commons.ui.note.SearchIcon +import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.search.GroupCandidate import com.vitorpamplona.amethyst.commons.ui.search.SEARCH_PICKER_LIMIT import com.vitorpamplona.amethyst.commons.ui.search.SearchFieldState @@ -822,8 +823,8 @@ private fun DisplaySearchResults( ) { _, relayInfo -> BasicRelaySetupInfoClickableRow( item = relayInfo, - loadProfilePicture = accountViewModel.settings.showProfilePictures(), - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + loadProfilePicture = LocalDisplaySettings.current.showProfilePictures, + loadRobohash = LocalDisplaySettings.current.loadRobohash, onClick = { nav.nav(Route.RelayInfo(relayInfo.relay.url)) }, onDelete = null, nip11CachedRetriever = Amethyst.instance.nip11Cache, @@ -849,8 +850,8 @@ private fun DisplaySearchResults( channelLastTime = null, channelLastContent = item.summary(), hasNewMessages = false, - loadProfilePicture = accountViewModel.settings.showProfilePictures(), - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + loadProfilePicture = LocalDisplaySettings.current.showProfilePictures, + loadRobohash = LocalDisplaySettings.current.loadRobohash, autoPlayGif = accountViewModel.settings.autoPlayVideosFlow .collectAsStateWithLifecycle() @@ -882,8 +883,8 @@ private fun DisplaySearchResults( channelLastTime = null, channelLastContent = stringRes(Res.string.ephemeral_relay_chat), hasNewMessages = false, - loadProfilePicture = accountViewModel.settings.showProfilePictures(), - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + loadProfilePicture = LocalDisplaySettings.current.showProfilePictures, + loadRobohash = LocalDisplaySettings.current.loadRobohash, autoPlayGif = accountViewModel.settings.autoPlayVideosFlow .collectAsStateWithLifecycle() @@ -913,8 +914,8 @@ private fun DisplaySearchResults( channelLastTime = null, channelLastContent = item.summary(), hasNewMessages = false, - loadProfilePicture = accountViewModel.settings.showProfilePictures(), - loadRobohash = accountViewModel.settings.isNotPerformanceMode(), + loadProfilePicture = LocalDisplaySettings.current.showProfilePictures, + loadRobohash = LocalDisplaySettings.current.loadRobohash, autoPlayGif = accountViewModel.settings.autoPlayVideosFlow .collectAsStateWithLifecycle() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt index 4f580033c9..1651960528 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt @@ -73,6 +73,15 @@ import com.vitorpamplona.amethyst.R 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.AccentColorType +import com.vitorpamplona.amethyst.commons.model.BooleanType +import com.vitorpamplona.amethyst.commons.model.ConnectivityType +import com.vitorpamplona.amethyst.commons.model.FeatureSetType +import com.vitorpamplona.amethyst.commons.model.FontFamilyType +import com.vitorpamplona.amethyst.commons.model.FontSizeType +import com.vitorpamplona.amethyst.commons.model.ThemeType +import com.vitorpamplona.amethyst.commons.model.UiSettingsFlow +import com.vitorpamplona.amethyst.commons.model.resourceId import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.accent_color import com.vitorpamplona.amethyst.commons.resources.accent_color_description @@ -122,14 +131,6 @@ import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonRow import com.vitorpamplona.amethyst.commons.ui.theme.contentColorOnAccent import com.vitorpamplona.amethyst.commons.ui.theme.isLight -import com.vitorpamplona.amethyst.model.AccentColorType -import com.vitorpamplona.amethyst.model.BooleanType -import com.vitorpamplona.amethyst.model.ConnectivityType -import com.vitorpamplona.amethyst.model.FeatureSetType -import com.vitorpamplona.amethyst.model.FontFamilyType -import com.vitorpamplona.amethyst.model.FontSizeType -import com.vitorpamplona.amethyst.model.ThemeType -import com.vitorpamplona.amethyst.model.UiSettingsFlow import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.previewColor import com.vitorpamplona.amethyst.ui.theme.toFontFamily diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ComposeSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ComposeSettingsScreen.kt index 933b81c1ee..3f0691f2b7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ComposeSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ComposeSettingsScreen.kt @@ -45,6 +45,8 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.model.BooleanType +import com.vitorpamplona.amethyst.commons.model.UiSettingsFlow import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.add_client_tag_explainer import com.vitorpamplona.amethyst.commons.resources.add_client_tag_title @@ -96,8 +98,6 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackBu import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.model.AccountPoWPreferences -import com.vitorpamplona.amethyst.model.BooleanType -import com.vitorpamplona.amethyst.model.UiSettingsFlow import com.vitorpamplona.amethyst.service.ai.WritingAssistantFactory import com.vitorpamplona.amethyst.service.pow.deviceHashesPerSecond import com.vitorpamplona.amethyst.service.pow.loadApproxDuration diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/HomeTabsSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/HomeTabsSettingsScreen.kt index 838ca8b799..ba83376d7a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/HomeTabsSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/HomeTabsSettingsScreen.kt @@ -36,6 +36,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.HomeFeedType +import com.vitorpamplona.amethyst.commons.model.UiSettingsFlow import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.conversations import com.vitorpamplona.amethyst.commons.resources.home_content_type_articles @@ -72,7 +73,6 @@ 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.ThemeComparisonRow -import com.vitorpamplona.amethyst.model.UiSettingsFlow import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import org.jetbrains.compose.resources.StringResource diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/NotificationSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/NotificationSettingsScreen.kt index eab7fbfaa5..02f18f92e9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/NotificationSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/NotificationSettingsScreen.kt @@ -215,7 +215,7 @@ private fun AccountParticipationRow( // resolves (and lazily creates) the User behind each npub off the main thread, // then the shared UserPicture / UsernameDisplay observe its live metadata. if (pubkeyHex != null) { - LoadUser(pubkeyHex, accountViewModel) { user -> + LoadUser(pubkeyHex) { user -> if (user != null) { ClickableUserPicture( baseUser = user, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ProfileUiSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ProfileUiSettingsScreen.kt index e533d60817..1ed130938a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ProfileUiSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ProfileUiSettingsScreen.kt @@ -34,6 +34,9 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.model.ProfileGalleryType +import com.vitorpamplona.amethyst.commons.model.UiSettingsFlow +import com.vitorpamplona.amethyst.commons.model.resourceId import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.gallery_style import com.vitorpamplona.amethyst.commons.resources.gallery_style_description @@ -50,8 +53,6 @@ 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.ThemeComparisonRow -import com.vitorpamplona.amethyst.model.ProfileGalleryType -import com.vitorpamplona.amethyst.model.UiSettingsFlow import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt index 0e38c36ff5..0714583d75 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt @@ -38,7 +38,10 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.model.WarningType import com.vitorpamplona.amethyst.commons.model.navigation.Route +import com.vitorpamplona.amethyst.commons.model.parseWarningType +import com.vitorpamplona.amethyst.commons.model.resourceId import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.blocked_users import com.vitorpamplona.amethyst.commons.resources.filter_spam_from_strangers_explainer @@ -65,8 +68,6 @@ 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.ThemeComparisonColumn -import com.vitorpamplona.amethyst.model.WarningType -import com.vitorpamplona.amethyst.model.parseWarningType import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/softwareapps/SoftwareAppDetailScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/softwareapps/SoftwareAppDetailScreen.kt index fdc0e0cb4b..52869265fc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/softwareapps/SoftwareAppDetailScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/softwareapps/SoftwareAppDetailScreen.kt @@ -110,7 +110,7 @@ fun SoftwareAppDetailScreen( accountViewModel: AccountViewModel, nav: INav, ) { - LoadAddressableNote(address, accountViewModel) { note -> + LoadAddressableNote(address) { note -> note?.let { SoftwareAppDetailScreenContent( note = it, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt index bfcbcefe10..fbd05a0120 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt @@ -1272,7 +1272,7 @@ private fun RenderApprovalIfNeeded( baseNote.event?.communityAddress() } communityAddress?.let { - LoadAddressableNote(it, accountViewModel) { community -> + LoadAddressableNote(it) { community -> if (community != null) { val showApproveButton by observeCommunityApprovalNeedStatus(baseNote, community, accountViewModel) if (showApproveButton == true) { @@ -1574,7 +1574,7 @@ private fun RenderWikiHeaderForThreadPreview() { val nav = EmptyNav() - LoadNote(baseNoteHex = "277f982a4cd3f67cc47ad9282176acabee1713848f547d6021e0c155572078e1", accountViewModel = accountViewModel) { baseNote -> + LoadNote(baseNoteHex = "277f982a4cd3f67cc47ad9282176acabee1713848f547d6021e0c155572078e1") { baseNote -> ThemeComparisonColumn { val bg = MaterialTheme.colorScheme.background val backgroundColor = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadScreen.kt index 3fb68d5ffc..994eed7348 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadScreen.kt @@ -53,7 +53,7 @@ fun ThreadScreen( WatchLifecycleAndUpdateModel(feedViewModel) ThreadFilterAssemblerSubscription(noteId, accountViewModel) - LoadNote(noteId, accountViewModel) { + LoadNote(noteId) { if (it != null) { // this will force loading every post from this thread. EventFinderFilterAssemblerSubscription(it, accountViewModel) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/UrlPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/UrlPostScreen.kt index 5a2cdbba51..cb8b3032c1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/UrlPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/UrlPostScreen.kt @@ -26,6 +26,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.platform.LocalContext import androidx.core.net.toUri import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.navigation.navs.Nav import com.vitorpamplona.amethyst.ui.note.nip22Comments.CommentPostViewModel @@ -59,13 +60,13 @@ fun UrlPostScreen( postViewModel.newPostFor(UrlId(normalizedUrl)) } } - replyId?.let { accountViewModel.getNoteIfExists(it) }?.let { + replyId?.let { LocalCache.getNoteIfExists(it) }?.let { postViewModel.reply(it) } - draftId?.let { accountViewModel.getNoteIfExists(it) }?.let { + draftId?.let { LocalCache.getNoteIfExists(it) }?.let { postViewModel.editFromDraft(it) } - quoteId?.let { accountViewModel.getNoteIfExists(it) }?.let { + quoteId?.let { LocalCache.getNoteIfExists(it) }?.let { postViewModel.quote(it) } message?.ifBlank { null }?.let { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/CashuWalletScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/CashuWalletScreen.kt index 8d768e7bff..d1413d332e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/CashuWalletScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/CashuWalletScreen.kt @@ -1193,7 +1193,7 @@ private fun HistoryUserName( pubkeyHex: String, accountViewModel: AccountViewModel, ) { - LoadUser(baseUserHex = pubkeyHex, accountViewModel = accountViewModel) { user -> + LoadUser(baseUserHex = pubkeyHex) { user -> if (user != null) { UsernameDisplay( baseUser = user, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/OnchainTransactionsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/OnchainTransactionsScreen.kt index bc9ba5694f..69554eea7c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/OnchainTransactionsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/OnchainTransactionsScreen.kt @@ -449,7 +449,7 @@ private fun OnchainCounterpartyName( pubkeyHex: String, accountViewModel: AccountViewModel, ) { - LoadUser(baseUserHex = pubkeyHex, accountViewModel = accountViewModel) { user -> + LoadUser(baseUserHex = pubkeyHex) { user -> if (user != null) { UsernameDisplay( baseUser = user, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletTransactionsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletTransactionsScreen.kt index d98103cb23..b01f4380c0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletTransactionsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletTransactionsScreen.kt @@ -403,7 +403,7 @@ private fun TransactionUserName( fallbackName: String?, accountViewModel: AccountViewModel, ) { - LoadUser(baseUserHex = pubkeyHex, accountViewModel = accountViewModel) { user -> + LoadUser(baseUserHex = pubkeyHex) { user -> if (user != null) { UsernameDisplay( baseUser = user, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/workouts/WorkoutDisplay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/workouts/WorkoutDisplay.kt index 1b0bc4b082..ad005c6a8c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/workouts/WorkoutDisplay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/workouts/WorkoutDisplay.kt @@ -591,7 +591,7 @@ private fun ExerciseRow( // Resolve the kind-33401 template to show its real title; falls back to the slug // until the template is fetched (the workout event's relay hints drive the fetch). - LoadAddressableNote(address, accountViewModel) { templateNote -> + LoadAddressableNote(address) { templateNote -> val name = if (templateNote != null) { val templateEvent by observeNoteEvent(templateNote, accountViewModel) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/workouts/suggestion/DetectedWorkoutCarousel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/workouts/suggestion/DetectedWorkoutCarousel.kt index fb28dfd9fd..ffc23b3d25 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/workouts/suggestion/DetectedWorkoutCarousel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/workouts/suggestion/DetectedWorkoutCarousel.kt @@ -60,6 +60,7 @@ import com.vitorpamplona.amethyst.commons.fitness.TrainingLog import com.vitorpamplona.amethyst.commons.fitness.WorkoutOrigin import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.model.BooleanType import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.workout_from_health_connect @@ -71,7 +72,6 @@ import com.vitorpamplona.amethyst.commons.resources.workout_suggestion_connect_t import com.vitorpamplona.amethyst.commons.resources.workout_suggestion_distance_km import com.vitorpamplona.amethyst.commons.ui.pluralStringRes import com.vitorpamplona.amethyst.commons.ui.stringRes -import com.vitorpamplona.amethyst.model.BooleanType import com.vitorpamplona.amethyst.service.workouts.health.HealthConnectManager import com.vitorpamplona.amethyst.service.workouts.health.publishedWorkoutsOf import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt index 78ea2ecf9d..5521b8f719 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt @@ -44,8 +44,15 @@ import com.patrykandpatrick.vico.compose.common.VicoTheme import com.patrykandpatrick.vico.compose.common.VicoTheme.CandlestickCartesianLayerColors import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.icons.symbols.ProvideAppIcons +import com.vitorpamplona.amethyst.commons.model.AccentColorType +import com.vitorpamplona.amethyst.commons.model.FontFamilyType +import com.vitorpamplona.amethyst.commons.model.FontSizeType +import com.vitorpamplona.amethyst.commons.model.ThemeType import com.vitorpamplona.amethyst.commons.ui.components.LocalAnimationsEnabled import com.vitorpamplona.amethyst.commons.ui.components.LocalProfilePictureCache +import com.vitorpamplona.amethyst.commons.ui.screen.DisplaySettings +import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings +import com.vitorpamplona.amethyst.commons.ui.screen.collectDisplaySettings import com.vitorpamplona.amethyst.commons.ui.theme.AccentBlueDark import com.vitorpamplona.amethyst.commons.ui.theme.AccentBlueLight import com.vitorpamplona.amethyst.commons.ui.theme.AccentGreenDark @@ -66,11 +73,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.amethystLightColorScheme import com.vitorpamplona.amethyst.commons.ui.theme.isLight import com.vitorpamplona.amethyst.commons.ui.theme.transparentBackground import com.vitorpamplona.amethyst.commons.ui.theme.withFontFamily -import com.vitorpamplona.amethyst.model.AccentColorType -import com.vitorpamplona.amethyst.model.FeatureSetType -import com.vitorpamplona.amethyst.model.FontFamilyType -import com.vitorpamplona.amethyst.model.FontSizeType -import com.vitorpamplona.amethyst.model.ThemeType // The accent color (primary/secondary/tertiary) is user-selectable in Settings -> Accent Color. // Purple keeps the original Amethyst look (purple primary + teal secondary). Every other accent @@ -148,9 +150,9 @@ fun AmethystTheme(content: @Composable () -> Unit) { val accentColor by uiPrefs.accentColor.collectAsStateWithLifecycle() val fontFamily by uiPrefs.fontFamily.collectAsStateWithLifecycle() val fontSize by uiPrefs.fontSize.collectAsStateWithLifecycle() - val featureSet by uiPrefs.featureSet.collectAsStateWithLifecycle() + val displaySettings = collectDisplaySettings(Amethyst.instance.uiState) - AmethystTheme(theme, accentColor, fontFamily, fontSize, animationsEnabled = featureSet != FeatureSetType.PERFORMANCE, content = content) + AmethystTheme(theme, accentColor, fontFamily, fontSize, displaySettings, content) } @Composable @@ -159,7 +161,7 @@ fun AmethystTheme( accentColor: AccentColorType = AccentColorType.PURPLE, fontFamily: FontFamilyType = FontFamilyType.SYSTEM, fontSize: FontSizeType = FontSizeType.NORMAL, - animationsEnabled: Boolean = true, + displaySettings: DisplaySettings = DisplaySettings(), content: @Composable () -> Unit, ) { // Deliberately no UiModeManager.nightMode write: changing the device night mode needs @@ -196,8 +198,9 @@ fun AmethystTheme( LocalDensity provides scaledDensity, // ImageLoaderSetup registers the avatar thumbnail cache and the local Blossom bridge. LocalProfilePictureCache provides true, + LocalDisplaySettings provides displaySettings, // Performance mode turns decorative animations (crossfades) off app-wide. - LocalAnimationsEnabled provides animationsEnabled, + LocalAnimationsEnabled provides !displaySettings.performanceMode, LocalTextStyle provides LocalTextStyle.current.merge(TextStyle(fontFamily = resolvedFontFamily)), content = content, ) diff --git a/amethyst/src/main/res/values-hi-rIN/strings.xml b/amethyst/src/main/res/values-hi-rIN/strings.xml index 61e90faa74..b1b4776a01 100644 --- a/amethyst/src/main/res/values-hi-rIN/strings.xml +++ b/amethyst/src/main/res/values-hi-rIN/strings.xml @@ -295,6 +295,7 @@ श्रमप्रमाण खनन… (%1$d पत्र पंक्ति में) श्रमप्रमाण खनन + पुनःप्रसारकों को भेजा जा रहा है… निरस्त लगभग %1$s शेष अब किसी भी क्षण @@ -349,4 +350,322 @@ ध्वनि सक्रिय। मौन करने के लिए टाँकें चलाएँ विराम + आपने अपना %1$s अन्य क्रमक में परिवर्तित किया + हटाए गए (%1$s) + जोडे गए (%1$s) + परिवर्तित (%1$s) + आपने सभी निजी (रहस्यीकृत) वस्तुओं को हटा दिया। + आपने निजी (रहस्यीकृत) वस्तुओं का परिवर्तन भी किया। उनकी तुलना यहाँ नहीं की जा सकती। + आपने निजी (रहस्यीकृत) वस्तुओं को जोडा। + सुरक्षित संस्करण पुनःस्थापन + इस यन्त्र पर + नया संस्करण + स्थापित नहीं + %1$s हटाए गए + %1$s जोडे गए + %1$s परिवर्तित + + आपने %1$d व्यक्ति का अनुगमन हटाया + आपने %1$d व्यक्तियों का अनुगमन हटाए + + स्वीकार + %1$s अद्यतनकृत + %1$s पारित + अन्य क्रमक में परिवर्तित। %1$s + रखें %1$s + पुनःस्थापन %1$s + पुनःमौन %1$s + पुनःजुडें %1$s + परिचय पुनःस्थापध + पुनःप्रसारक पुनःस्थापन + %1$s रखे गए + %1$s परित्यक्त + %1$s नए + परित्यक्त। %1$s + नए। %1$s + सम्पादित। %1$s + %1$s लोग खोजें + यहाँ कुछ नहीं + %1$s मौनावस्था हटाए गए + ये पुनः आपके सूचनावलियों तक अभिगम्य + %1$s नए मौनकृत + शब्द तथा विषयसूचक + %1$s तथा %2$s अधिक + आपने सभी निजी वस्तुओं को हटा दिया + वे रहस्यीकृत हैं। इसलिए केवल आप देख सकते हैं कि वे क्या थे। पुनःस्थापन से सब लौटके आ जाएँगे। + आपने वस्तुओं को निजी किया + वे अब रहस्यीकृत हैं। जो कि सम्भाव्यतः ऊपर के सार्वजनिक वस्तुओं का गन्तव्य था। + आपने निजी वस्तुओं का परिवर्तन किया + वे रहस्यीकृत हैं। इसलिए उनकी तुलना यहाँ नहीं की जा सकती। + चला गया + नव्य + परित्यक्त + स्थानान्तरित + इकाई + शेष + पुनःनामकृत + निर्गतपेटिका + आप जहाँ प्रकाशित करते हैं + आगतपेटिका + जहाँ अन्य आप तक अभिगमन करते हैं + ग्राहक कुंचिका प्रतिस्थापित + ग्राहक कुंचिका हटाया गया + नटज्साप आगे से इस कुंचिका से बद्ध होंगे। यदि इस यन्त्र में वह नहीं है तो आप सम्भाव्यतः उनको यहाँ नहीं चुका सकते। + शेष + जुड गए + पुनःनामकृत + पूर्वतः %1$s था + पुनःबाधित करें %1$s को + सूचनावलियाँ पुनःस्थापन + विषय पुनःस्थापन + स्थल पुनःस्थापन + प्रदाता पुनःस्थापन + धनकोष पुनःस्थापन + रखे गए + ग्रहण मध्य + सूचीबद्ध + खोज मध्य + अभिलेखन मध्य + विश्वसनीय + अविश्वसनीय + %1$s अबाधित + अमेथिस्ट पुनः उनसे जुडेगा + अब से बाधित नहीं + नव्यतः बाधित + अभी भी बाधित + अनुमत + स्थानान्तरित + सूचनावली + विषय हटाए गए + स्थल हटाए गए + नव्य + विषयसूचक जिनका आप अनुगमन करते हैं + स्थल जिनका आप अनुगमन करते हैं + %2$s में से %1$s सेवाएँ परिवर्तित + वे अब अन्य व्यक्ति से आते है। अथवा किसी से नहीं + सेवा + श्रेणी + अनुचर + विषयसूची + श्रेणीयाँ तथा अनुचर गिनती तथा कचरालेख छालन जो आप देखते हैं इन प्रदाताओं से आते हैं। + + आपको भुगतान करने की %1$d विधि हटाया गया + आपको भुगतान करने की %1$d विधियाँ हटाए गए + + + %1$d प्रस्ताव हटाया गया + %1$d प्रस्ताव हटाए गए + + लोग जो आपको भुगतान करते हैं उसी को देखते हैं जो इस सूची में है। + बोल्ट॰बारह प्रस्ताव + आपने उसे रिक्त किया + आपने उसे बनाया + आपने उसे परिवर्तित किया + आपके धनकोष के टकसाल तथा कुंचिकाएँ आपके लिए रहस्यीकृत हैं। इसलिए उनकी तुलना वस्तु प्रति वस्तु यहाँ नहीं की जा सकती। पुनःस्थापन उन्हीं को लौट डालेगा जिनका अभिलेखन इस यन्त्र ने किया। + आपके कोंकोर्ड समुदाय आपके लिए रहस्यीकृत हैं। इसलिए उनकी तुलना यहाँ नहीं की जा सकती। पुनःस्थापन उन्हीं को लौट डालेगा जिनका अभिलेखन इस यन्त्र ने किया। + + आपने %1$d की मौनावस्था हटाया + आपने %1$d की मौनावस्था हटाए + + + आपने %1$d परिचय प्रपत्रस्थान हटाया + आपने %1$d परिचय प्रपत्रस्थान हटाए + + + आपने %1$d सार्वजनिक पुनःप्रसारक हटाया + आपने %1$d सार्वजनिक पुनःप्रसारकों को हटाए + + + आपने %1$d सीधासन्देश पुनःप्रसारक हटाया + आपने %1$d सीधासन्देश पुनःप्रसारकों को हटाए + + + आपने %1$d समूहचर्चा कुंचिका पुनःप्रसारक हटाया + आपने %1$d समूहचर्चा कुंचिका पुनःप्रसारकों को हटाए + + + आपने %1$d खोज पुनःप्रसारक हटाया + आपने %1$d खोज पुनःप्रसारकों को हटाए + + + आपने %1$d अनुक्रमणीकार पुनःप्रसारक हटाया + आपने %1$d अनुक्रमणीकार पुनःप्रसारकों को हटाए + + + आपने %1$d पुनःप्रसारक सूचनावली हटाया + आपने %1$d पुनःप्रसारक सूचनावलियाँ हटाए + + + आपने %1$d निजी पुनःप्रसारक हटाया + आपने %1$d निजी पुनःप्रसारकों को हटाए + + + आपने %1$d पुनःप्रसारक पर विश्वास करना रोक दिया + आपने %1$d पुनःप्रसारकों पर विश्वास करना रोक दिए + + + आपने %1$d पुनःप्रसारक अबाधित किया + आपने %1$d पुनःप्रसारकों को अबाधित किए + + + आपने %1$d सार्वजनिक चर्चा छोड दिया + आपने %1$d सार्वजनिक चर्चाओं को छोड दिए + + + आपने %1$d समुदाय छोड दिया + आपने %1$d समुदायों को छोड दिए + + + आपने %1$d चर्चाशाला छोड दिया + आपने %1$d चर्चाशालाओं को छोड दिए + + + आपने %1$d समूह छोड दिया + आपने %1$d समूहों को छोड दिए + + + आपने %1$d इष्ट सूचनावली हटाया + आपने %1$d इष्ट सूचनावलियों को हटाए + + + आपने %1$d विषयसूचक का अनुगमन हटाया + आपने %1$d विषयसूचकों का अनुगमन हटाए + + + आपने %1$d स्थल का अनुगमन हटाया + आपने %1$d स्थलों का अनुगमन हटाए + + + आपने %1$d विश्वाससेवा प्रदाता परिवर्तित किया + आपने %1$d विश्वाससेवाओं के प्रदाता को परिवर्तित किया + + + आपने %1$d नटज्साप टकसाल हटाया + आपने %1$d नटज्साप टकसालों को हटाए + + + आपने %1$d भुगतान प्राप्त करने की विधि हटाया + आपने %1$d भुगतान प्राप्त करने के विधियों को हटाए + + + आपने %1$d बोल्ट॰बारह प्रस्ताव हटाया + आपने %1$d बोल्ट॰बारह प्रस्तावों को हटाए + + + %1$s तथा %2$d जोडा + %1$s तथा %2$d जोडे + + + %1$s तथा %2$d का अनुगमन किया + %1$s तथा %2$d का अनुगमन किए + + + %1$s तथा %2$d को मौनकृत किया + %1$s तथा %2$d को मौनकृत किए + + + %1$s तथा %2$d से जुडे + %1$s तथा %2$d से जुडे + + + %1$s तथा %2$d बाधित किया + %1$s तथा %2$d को बाधित किए + + आपके अमेथिस्ट समचरणीकृत क्रमक स्थापना विकल्पों को परिवर्तित किया गया एक अन्य क्रमक में और वह अमेथिस्ट नहीं था। इससे समस्याएँ उत्पन्न हो सकते हैं। + आपने अपनी नटज्साप कुंचिका का प्रतिस्थापन किया + आपने अपनी नटज्साप कुंचिका को हटाया + आपने अपना काशयू धनकोष रिक्त किया + आपने अपने काशयू धनकोष को परिवर्तित किया + आपने अपनी कोंकोर्ड सूची रिक्त की + आपने अपनी कोंकोर्ड सूची का परिवर्तन किया + नया संस्करण का उपयोग करें + पठनेवशक्य + लेखनेवशक्य + पठन तथा लेखन + परिचय + आपका नाम चित्र चरित तथा भुगतान पते। जैसे सब को दिखते हैं वैसे। + अनुचरण सूची + निर्णय करता है किसके पत्र आपकी मुख्य सूचनावली में प्रकट होंगे। + मौन सूची + छिपाता है लोग शब्द विषयसूचक तथा सूत्र उन सब में से जो आप देखते हैं। + सार्वजनिक पुनःप्रसारक सूची + सबको बताता है आपके पत्र कहाँ प्राप्त करें तथा आपको उत्तर तथा उल्लेखों को कहाँ भेजें। + सीधासन्देश पुनःप्रसारक सूची + जहाँ अन्य आपके निजी सन्देश भेजते हैं। + समूहचर्चा कुंचिका पुनःप्रसारक + जहाँ अन्य उन कुंचिकाओं को प्राप्त करते हैं आपको रहस्यीकृत समूहचर्चाओं में जोडने के लिए। + खोज पुनःप्रसारक सूची + उपयुक्त पुनःप्रसारक जब आप खोजते हैं। + अनुक्रमणीकार पुनःप्रसारक सूची + पुनःप्रसारक उपयुक्त अन्य लोगों के परिचय तथा पुनःप्रसारक सूचियों को प्राप्त करने के लिए। + इष्ट पुनःप्रसारक सूचनावलियाँ + पुनःप्रसारक जिनका आप वीक्षण करते हैं सूचनावलियों के रूप में। + बाधित पुनःप्रसारक सूची + पुनःप्रसारक जिनसे यह क्रमक कभी भी संयोजन नहीं करता। + विश्वसनीय पुनःप्रसारक सूची + पुनःप्रसारक जिनपर आप पर्याप्त विश्वास करते हैं बिना पूछे उनसे संयोजन करने के लिए। + निजी पुनःप्रसारक सूची + जहाँ आपके पाण्डुलिपियाँ तथा निजी जानकारी रखे जाते हैं। + समचरणीकृत क्रमक स्थापना विकल्प + अमेथिस्ट स्थापना विकल्प समचरणीकृत आपके यन्त्रों में। वे रहस्यीकृत हैं। इसलिए केवल उनकी उपस्थिति की तुलना की जा सकती है। + सार्वजनिक चर्चा सूची + सार्वजनिक चर्चाएँ जिनसे आप जुडे। + समुदाय सूची + समुदाय जिनसे आप जुडे। + अनुचरित विषयसूचक + विषयसूचक जिससे युक्त पत्र आपकी मुख्य सूचनावली में प्रकट होंगे। + अनुचरित स्थान + स्थान जहाँ से पत्र आपकी मुख्य सूचनावली में प्रकट होंगे। + इष्ट सूचनावली सूची + कलनविध्यात्मक सूचनावलियाँ जिनको आपने अभिलेखित किया। + अस्थायी चर्चा सूची + अस्थायी चर्चा शालाएँ जिनसे आप जुडे। + समूह सूची + पुनःप्रसारक आधारित समूह जिनसे आप जुडे। + कोंकोर्ड समुदाय सूची + कोंकोर्ड समुदाय जिनसे आप जुडे। वे रहस्यीकृत हैं। इसलिए केवल उनकी उपस्थिति की तुलना की जा सकती है। + विश्वाससेवा प्रदाता सूची + सेवाएँ जिनपर आप विश्वास करते है आपके लिए लोगों का सत्यापन तथा श्रेणीकरण करने के लिए। + काशयू धनकोष + आपके ईकाश धनकोष स्थापना विकल्प तथा टकसाल। वे रहस्यीकृत हैं। इसलिए केवल उनकी उपस्थिति की तुलना की जा सकती है। + नटज्साप प्राप्ती स्थापना विकल्प + टकसाल तथा कुंचिका जिनका उपयोग अन्य करते हैं आपको नटज्साप भेजने के लिए। + भुगतान पते + विधियाँ जिनसे अन्य लोग आपको भुगतान कर सके। + बोल्ट॰बारह प्रस्ताव + लैटनिंग प्रस्ताव जिनका उपयोग करके अन्य लोग आपको भुगतान करते हैं। + सूची + इस यन्त्र पर अभिलेखित सूची। + परिचय प्रपत्रस्थान + लोग + विषयसूचक + शब्द + सूत्र + सार्वजनिक चर्चाएँ + समुदाय + सूचनावलियाँ + स्थान + पुनःप्रसारक + समूह + चर्चा शालाएँ + विश्वाससेवा प्रदाता + टकसाल + नटज्साप कुंचिकाएँ + भुगतान पते + प्रस्ताव + सम्बन्धित व्यक्तिगण + अन्य प्रविष्टियाँ + प्रयोक्तानाम + प्रदर्शन नाम + जानकारी + चित्र + ध्वज + जालस्थान + नोस्टर पता + लैटनिंग पता + लैटनिंग जालपता + सर्वनाम + जन्मदिन + क्लिंक॰ प्रस्ताव + क्रमचर चिह्न diff --git a/amethyst/src/main/res/values-hu-rHU/strings.xml b/amethyst/src/main/res/values-hu-rHU/strings.xml index 09389627ba..f44b17c264 100644 --- a/amethyst/src/main/res/values-hu-rHU/strings.xml +++ b/amethyst/src/main/res/values-hu-rHU/strings.xml @@ -295,6 +295,7 @@ Proof of work bányászata… (%1$d bejegyzés a sorban) „Munkabizonyítás” bányászata + Küldés az átjátszókra… Mégse ≈ %1$s van hátra mostmár bármelyik pillanatban @@ -349,4 +350,322 @@ Hang bekapcsolva. Koppintson a némításhoz Lejátszás Szüneteltetés + Ön módosította a következőt: %1$s, egy másik alkalmazásban + Eltávolítva (%1$s) + Hozzáadva (%1$s) + Módosítva (%1$s) + Ön eltávolította az összes privát (titkosított) elemet. + Ön privát (titkosított) elemeket is módosított. Ezek itt nem hasonlíthatók össze. + Ön privát (titkosított) elemeket adott hozzá. + Mentett verzió visszaállítása + Ezen az eszközön + Új verzió + Nincs beállítva + %1$s eltávolítva + %1$s hozzáadva + %1$s módosítva + + %1$d személyt nem követ többé + %1$d személyt nem követ többé + + Elfogadás + %1$s frissítve + %1$s törölve + Módosítva egy másik alkalmazásban · %1$s + %1$s megtartása + %1$s visszaállítása + %1$s újbóli némítása + Újbóli csatlakozás ehhez: %1$s + Profil visszaállítása + Átjátszók visszaállítása + %1$s megtartva + %1$s elvetve + %1$s új + Elvetve · %1$s + Új · %1$s + Szerkesztve · %1$s + %1$s személy keresése + Itt nincs semmi + %1$s némítása feloldva + Ezek újra megjelenhetnek a hírfolyamaiban + %1$s újonnan némítva + Szavak és kulcsszavak + %1$s és %2$s további + Ön eltávolította az összes privát elemet + Titkosítva vannak, így csak Ön láthatja, mik voltak. A visszaállítás mindet visszahozza. + Ön priváttá tette az elemeket + Most már titkosítva vannak, valószínűleg ide kerültek a fenti nyilvános elemek. + Ön módosította a privát elemeket + Titkosítva vannak, így itt nem hasonlíthatók össze. + Eltűnt + Új + Elvetve + Áthelyezve + Egységek + Elhagyva + Átnevezve + Kimenő + ahol közzétesz + Bejövő + ahol mások elérik Önt + Fogadási kulcs lecserélve + Fogadási kulcs eltávolítva + A nutzapok ehhez az új kulcshoz lesznek kötve. Ha ez az eszköz nem rendelkezik vele, lehet, hogy itt nem tudja beváltani őket. + elhagyta a csoportot + csatlakozott + átnevezte + „%1$s” volt + %1$s újbóli letiltása + Hírfolyamok visszaállítása + Témák visszaállítása + Helyek visszaállítása + Szolgáltatók visszaállítása + Pénztárca visszaállítása + megtartva + Fogadás + Listázva + Keresés + Tárolás + Megbízható + Nem megbízható + %1$s letiltása feloldva + Az Amethyst újra kapcsolódni fog hozzájuk + Már nincs letiltva + Újonnan letiltva + Továbbra is le van tiltva + Engedélyezve + Áthelyezve + Hírfolyam + eltűnt témák + eltűnt helyek + új + Ön által követett kulcsszavak + Ön által követett helyek + %2$s szolgáltatásból %1$s módosult + Most már valaki mástól vagy senkitől sem származnak + Szolgáltatás + Rang + Követők + Témák + A látott rangok, követőszámok és a kéretlen tartalomszűrés ezektől a szolgáltatóktól származnak. + + %1$d fizetési mód eltávolítva + %1$d fizetési mód eltávolítva + + + %1$d ajánlat eltávolítva + %1$d ajánlat eltávolítva + + A fizető emberek csak azt látják, ami ebben a listában van. + BOLT12 ajánlat + Ön kiürítette + Ön hozta létre + Ön módosította + A saját pénztárca pénzverdéi és kulcsai Önhöz vannak titkosítva, így itt nem hasonlíthatók össze tételesen. A visszaállítás pontosan azt hozza vissza, amit ez az eszköz mentett. + A Concord közösségei Önhöz vannak titkosítva, így itt nem hasonlíthatók össze. A visszaállítás pontosan azt hozza vissza, amit ez az eszköz mentett. + + Ön %1$d dolog némítását oldotta fel + %1$d dolog némítását oldotta fel + + + Ön %1$d profilmezőt távolított el + %1$d profilmezőt távolított el + + + Ön %1$d nyilvános átjátszót távolított el + %1$d nyilvános átjátszót távolított el + + + Ön %1$d privát üzenetátjátszót távolított el + %1$d privát üzenet átjátszót távolított el + + + Ön %1$d csoportos csevegési kulcsátjátszót távolított el + %1$d csoportcsevegés kulcs átjátszót távolított el + + + Ön %1$d kereső átjátszót távolított el + %1$d kereső átjátszót távolított el + + + Ön %1$d indexelő átjátszót távolított el + %1$d indexelő átjátszót távolított el + + + Ön %1$d átjátszó-hírfolyamot távolított el + %1$d átjátszó hírfolyamot távolított el + + + Ön %1$d privát átjátszót távolított el + %1$d privát átjátszót távolított el + + + Ön %1$d átjátszóban nem bízik többé + %1$d átjátszóban nem bízik többé + + + Ön %1$d átjátszó letiltását oldotta fel + %1$d átjátszó blokkolását oldotta fel + + + Ön %1$d nyilvános csevegést hagyott el + %1$d nyilvános csevegést hagyott el + + + Ön %1$d közösséget hagyott el + %1$d közösséget hagyott el + + + Ön %1$d csevegőszobát hagyott el + %1$d csevegőszobát hagyott el + + + Ön %1$d csoportot hagyott el + %1$d csoportot hagyott el + + + Ön %1$d kedvenc hírfolyamot távolított el + %1$d kedvenc hírfolyamot távolított el + + + Ön %1$d kulcsszót nem követ többé + %1$d hashtaget nem követ többé + + + Ön %1$d helyet nem követ többé + %1$d helyet nem követ többé + + + Ön %1$d bizalmi szolgáltatás szolgáltatóját módosította + %1$d bizalmi szolgáltatás szolgáltatóját változtatta meg + + + Ön %1$d nutzap pénzverdét távolított el + %1$d nutzap pénzverdét távolított el + + + Ön %1$d fizetési módot távolított el + %1$d fizetési módot távolított el + + + Ön %1$d BOLT12 ajánlatot távolított el + %1$d BOLT12 ajánlatot távolított el + + + %1$s és %2$d hozzáadva + %1$s és %2$d hozzáadva + + + %1$s és %2$d követve + %1$s és %2$d követve + + + %1$s és %2$d némítva + %1$s és %2$d némítva + + + %1$s és %2$d csatlakozva + %1$s és %2$d csatlakozva + + + %1$s és %2$d letiltva + %1$s és %2$d letiltva + + A szinkronizált Amethyst alkalmazásbeállításai egy másik, nem Amethyst alkalmazásban módosultak. Ez problémákat okozhat. + Ön lecserélte a nutzap kulcsát + Ön eltávolította a nutzap kulcsát + Ön kiürítette a Cashu pénztárcáját + Ön módosította a Cashu pénztárcáját + Ön kiürítette a Concord listáját + Ön módosította a Concord listáját + Új verzió használata + csak olvasható + csak írható + olvasás és írás + profil + Az Ön neve, képe, bemutatkozása és fizetési címei, ahogy mindenki látja őket. + követési lista + Meghatározza, kinek a bejegyzései jelenjenek meg a kezdőlapon. + némítási lista + Elrejti a személyeket, szavakat, kulcsszavakat és beszélgetéseket mindenből, amit lát. + nyilvános átjátszólista + Megmondja mindenkinek, hol találhatók a bejegyzései, és hová küldjék a válaszokat és említéseket. + privát üzenet átjátszólista + Ahová mások kézbesítik a privát üzeneteit. + csoportos csevegési kulcsátjátszók + Ahol mások megtalálják a kulcsokat, hogy titkosított csoportos csevegésekhez adhassák Önt. + kereső átjátszólista + Kereséskor használt átjátszók. + indexelő átjátszólista + Azok az átjátszók, amelyeket mások profiljainak és átjátszólistáinak megtalálásához használnak. + kedvenc átjátszóhírfolyamok + Olyan átjátszók, amelyeket hírfolyamként böngész. + letiltott átjátszólista + Átjátszók, amelyekhez ez az alkalmazás soha nem kapcsolódik. + megbízható átjátszólista + Átjátszók, amelyekhez kérdezés nélkül kapcsolódhat. + privát átjátszólista + Ahol a piszkozatai és privát adatai tárolódnak. + szinkronizált alkalmazásbeállítások + Az eszközök között szinkronizált Amethyst-beállítások. Titkosítva vannak, így csak a jelenlétük hasonlítható össze. + nyilvános csevegési lista + Nyilvános csevegések, amelyekhez csatlakozott. + közösséglista + Közösségek, amelyekhez csatlakozott. + követett kulcsszavak + Kulcsszavak, amelyek bejegyzései megjelennek a hírfolyamban. + követett helyek + Helyek, amelyek bejegyzései megjelennek a hírfolyamaiban. + kedvenc hírfolyamlista + Mentett algoritmus-hírfolyamok. + rövid életű csevegési lista + Rövid életű csevegőszobák, amelyekhez csatlakozott. + csoportlista + Átjátszó-alapú csoportok, amelyekhez csatlakozott. + Concord közösséglista + A Concord közösségek, amelyekhez csatlakozott. Titkosítva vannak, így csak a jelenlétük hasonlítható össze. + bizalmi szolgáltatólista + A szolgáltatások, amelyekben megbízik, hogy rangsorolják és ellenőrizzék Önnek az embereket. + Cashu pénztárca + Az ecash pénztárca beállításai és pénzverdéi. Titkosítva vannak, így csak a jelenlétük hasonlítható össze. + nutzap fogadási beállítások + A pénzverdék és a kulcs, amelyeket mások a nutzapok küldéséhez használnak az Ön számára. + fizetési címek + A módok, ahogy mások fizethetnek Önnek. + BOLT12 ajánlatok + A lightning ajánlatok, amelyeket mások a fizetéshez használnak az Ön számára. + lista + Egy listaelem ezen az eszközön mentve. + Profilmezők + Személyek + Kulcsszavak + Szavak + Beszélgetések + Nyilvános csevegések + Közösségek + Hírfolyamok + Helyek + Átjátszók + Csoportok + Csevegőszobák + Bizalmi szolgáltatók + Pénzverdék + Nutzap kulcsok + Fizetési címek + Ajánlatok + Kapcsolt identitások + Egyéb bejegyzések + Felhasználónév + Megjelenített név + Névjegy + Kép + Borító + Weboldal + Nostr-cím + Lightning-cím + LNURL + Névmások + Születésnap + CLINK-ajánlat + Bot jelölő diff --git a/amethyst/src/play/java/com/vitorpamplona/amethyst/ui/components/SelectNotificationProvider.kt b/amethyst/src/play/java/com/vitorpamplona/amethyst/ui/components/SelectNotificationProvider.kt index dc32830cc4..0915fcb2d2 100644 --- a/amethyst/src/play/java/com/vitorpamplona/amethyst/ui/components/SelectNotificationProvider.kt +++ b/amethyst/src/play/java/com/vitorpamplona/amethyst/ui/components/SelectNotificationProvider.kt @@ -29,7 +29,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberPermissionState -import com.vitorpamplona.amethyst.model.UiSettingsFlow +import com.vitorpamplona.amethyst.commons.model.UiSettingsFlow @OptIn(ExperimentalPermissionsApi::class) @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettings.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/UiSettings.kt similarity index 66% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettings.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/UiSettings.kt index 159afa0514..eb8ae128e1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettings.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/UiSettings.kt @@ -18,40 +18,10 @@ * 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 +package com.vitorpamplona.amethyst.commons.model import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.commons.resources.Res -import com.vitorpamplona.amethyst.commons.resources.accent_color_blue -import com.vitorpamplona.amethyst.commons.resources.accent_color_green -import com.vitorpamplona.amethyst.commons.resources.accent_color_orange -import com.vitorpamplona.amethyst.commons.resources.accent_color_pink -import com.vitorpamplona.amethyst.commons.resources.accent_color_purple -import com.vitorpamplona.amethyst.commons.resources.accent_color_red -import com.vitorpamplona.amethyst.commons.resources.connectivity_type_always -import com.vitorpamplona.amethyst.commons.resources.connectivity_type_never -import com.vitorpamplona.amethyst.commons.resources.connectivity_type_unmetered_wifi_only -import com.vitorpamplona.amethyst.commons.resources.content_warning_hide_all_sensitive_content_option -import com.vitorpamplona.amethyst.commons.resources.content_warning_see_warnings_option -import com.vitorpamplona.amethyst.commons.resources.content_warning_show_all_sensitive_content_option -import com.vitorpamplona.amethyst.commons.resources.dark -import com.vitorpamplona.amethyst.commons.resources.font_family_monospace -import com.vitorpamplona.amethyst.commons.resources.font_family_sans_serif -import com.vitorpamplona.amethyst.commons.resources.font_family_serif -import com.vitorpamplona.amethyst.commons.resources.font_family_system -import com.vitorpamplona.amethyst.commons.resources.font_size_huge -import com.vitorpamplona.amethyst.commons.resources.font_size_large -import com.vitorpamplona.amethyst.commons.resources.font_size_normal -import com.vitorpamplona.amethyst.commons.resources.font_size_small -import com.vitorpamplona.amethyst.commons.resources.gallery_type_classic -import com.vitorpamplona.amethyst.commons.resources.gallery_type_modern -import com.vitorpamplona.amethyst.commons.resources.light -import com.vitorpamplona.amethyst.commons.resources.system -import com.vitorpamplona.amethyst.commons.resources.ui_feature_set_type_complete -import com.vitorpamplona.amethyst.commons.resources.ui_feature_set_type_performance -import com.vitorpamplona.amethyst.commons.resources.ui_feature_set_type_simplified import kotlinx.serialization.Serializable -import org.jetbrains.compose.resources.StringResource @Stable @Serializable @@ -98,11 +68,10 @@ data class UiSettings( enum class ThemeType( val screenCode: Int, - val resourceId: StringResource, ) { - SYSTEM(0, Res.string.system), - LIGHT(1, Res.string.light), - DARK(2, Res.string.dark), + SYSTEM(0), + LIGHT(1), + DARK(2), } fun parseThemeType(code: Int?): ThemeType = @@ -115,14 +84,13 @@ fun parseThemeType(code: Int?): ThemeType = enum class AccentColorType( val screenCode: Int, - val resourceId: StringResource, ) { - PURPLE(0, Res.string.accent_color_purple), - BLUE(1, Res.string.accent_color_blue), - GREEN(2, Res.string.accent_color_green), - ORANGE(3, Res.string.accent_color_orange), - RED(4, Res.string.accent_color_red), - PINK(5, Res.string.accent_color_pink), + PURPLE(0), + BLUE(1), + GREEN(2), + ORANGE(3), + RED(4), + PINK(5), } fun parseAccentColorType(screenCode: Int): AccentColorType = @@ -138,12 +106,11 @@ fun parseAccentColorType(screenCode: Int): AccentColorType = enum class FontFamilyType( val screenCode: Int, - val resourceId: StringResource, ) { - SYSTEM(0, Res.string.font_family_system), - SANS_SERIF(1, Res.string.font_family_sans_serif), - SERIF(2, Res.string.font_family_serif), - MONOSPACE(3, Res.string.font_family_monospace), + SYSTEM(0), + SANS_SERIF(1), + SERIF(2), + MONOSPACE(3), } fun parseFontFamilyType(screenCode: Int): FontFamilyType = @@ -158,12 +125,11 @@ fun parseFontFamilyType(screenCode: Int): FontFamilyType = enum class FontSizeType( val scale: Float, val screenCode: Int, - val resourceId: StringResource, ) { - SMALL(0.85f, 0, Res.string.font_size_small), - NORMAL(1.0f, 1, Res.string.font_size_normal), - LARGE(1.15f, 2, Res.string.font_size_large), - HUGE(1.3f, 3, Res.string.font_size_huge), + SMALL(0.85f, 0), + NORMAL(1.0f, 1), + LARGE(1.15f, 2), + HUGE(1.3f, 3), } fun parseFontSizeType(screenCode: Int): FontSizeType = @@ -178,28 +144,25 @@ fun parseFontSizeType(screenCode: Int): FontSizeType = enum class ConnectivityType( val prefCode: Boolean?, val screenCode: Int, - val resourceId: StringResource, ) { - ALWAYS(null, 0, Res.string.connectivity_type_always), - WIFI_ONLY(true, 1, Res.string.connectivity_type_unmetered_wifi_only), - NEVER(false, 2, Res.string.connectivity_type_never), + ALWAYS(null, 0), + WIFI_ONLY(true, 1), + NEVER(false, 2), } enum class FeatureSetType( val screenCode: Int, - val resourceId: StringResource, ) { - COMPLETE(0, Res.string.ui_feature_set_type_complete), - SIMPLIFIED(1, Res.string.ui_feature_set_type_simplified), - PERFORMANCE(2, Res.string.ui_feature_set_type_performance), + COMPLETE(0), + SIMPLIFIED(1), + PERFORMANCE(2), } enum class ProfileGalleryType( val screenCode: Int, - val resourceId: StringResource, ) { - CLASSIC(0, Res.string.gallery_type_classic), - MODERN(1, Res.string.gallery_type_modern), + CLASSIC(0), + MODERN(1), } fun parseConnectivityType(code: Boolean?): ConnectivityType = @@ -236,10 +199,9 @@ fun parseGalleryType(screenCode: Int): ProfileGalleryType = enum class BooleanType( val prefCode: Boolean?, val screenCode: Int, - val reourceId: StringResource, ) { - ALWAYS(null, 0, Res.string.connectivity_type_always), - NEVER(false, 1, Res.string.connectivity_type_never), + ALWAYS(null, 0), + NEVER(false, 1), } fun parseBooleanType(code: Boolean?): BooleanType = @@ -259,11 +221,10 @@ fun parseBooleanType(screenCode: Int): BooleanType = enum class WarningType( val prefCode: Boolean?, val screenCode: Int, - val resourceId: StringResource, ) { - WARN(null, 0, Res.string.content_warning_see_warnings_option), - SHOW(true, 1, Res.string.content_warning_show_all_sensitive_content_option), - HIDE(false, 2, Res.string.content_warning_hide_all_sensitive_content_option), + WARN(null, 0), + SHOW(true, 1), + HIDE(false, 2), } fun parseWarningType(screenCode: Int): WarningType = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettingsFlow.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/UiSettingsFlow.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettingsFlow.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/UiSettingsFlow.kt index d2528689c2..6a0870cfa8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettingsFlow.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/UiSettingsFlow.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.model +package com.vitorpamplona.amethyst.commons.model import androidx.compose.runtime.Stable import kotlinx.coroutines.flow.Flow diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UiSettingsState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/state/UiSettingsState.kt similarity index 94% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UiSettingsState.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/state/UiSettingsState.kt index ea6ed3fce7..b4edc59b77 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UiSettingsState.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/state/UiSettingsState.kt @@ -18,14 +18,14 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen +package com.vitorpamplona.amethyst.commons.state import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.model.BooleanType -import com.vitorpamplona.amethyst.model.ConnectivityType -import com.vitorpamplona.amethyst.model.FeatureSetType -import com.vitorpamplona.amethyst.model.ProfileGalleryType -import com.vitorpamplona.amethyst.model.UiSettingsFlow +import com.vitorpamplona.amethyst.commons.model.BooleanType +import com.vitorpamplona.amethyst.commons.model.ConnectivityType +import com.vitorpamplona.amethyst.commons.model.FeatureSetType +import com.vitorpamplona.amethyst.commons.model.ProfileGalleryType +import com.vitorpamplona.amethyst.commons.model.UiSettingsFlow import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/model/UiSettingsSerializationTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/model/UiSettingsSerializationTest.kt new file mode 100644 index 0000000000..07f15b4f75 --- /dev/null +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/model/UiSettingsSerializationTest.kt @@ -0,0 +1,53 @@ +/* + * 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.commons.model + +import com.vitorpamplona.quartz.nip01Core.core.JsonMapper +import kotlin.test.Test +import kotlin.test.assertEquals + +/** + * Settings saved by older versions are read back with `JsonMapper.fromJson` (the + * encrypted-preferences fallback in `UiSharedPreferences`). That needs the kotlinx serializer the + * serialization plugin generates, which a module without the plugin silently does not produce. + */ +class UiSettingsSerializationTest { + @Test + fun uiSettingsRoundTripsThroughJson() { + val settings = + UiSettings( + theme = ThemeType.DARK, + featureSet = FeatureSetType.PERFORMANCE, + automaticallyShowProfilePictures = ConnectivityType.WIFI_ONLY, + fontSize = FontSizeType.LARGE, + composeSignature = "sig", + ) + + val json = JsonMapper.toJson(settings) + + assertEquals(settings, JsonMapper.fromJson(json)) + } + + @Test + fun anEmptyObjectDecodesToTheDefaults() { + assertEquals(UiSettings(), JsonMapper.fromJson("{}")) + } +} diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/search/RenderableKindsTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/search/RenderableKindsTest.kt index 3527b67818..4ffeac2061 100644 --- a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/search/RenderableKindsTest.kt +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/search/RenderableKindsTest.kt @@ -173,6 +173,11 @@ class RenderableKindsTest { // geocache curation lists: the caches themselves are in the window, but a list of // them has no card yet, so a result would be a blank row 37517, + // decentralized list headers and items: no card yet, so a result would be a blank row + 9998, + 9999, + 39998, + 39999, ).sorted(), omitted.sorted(), "the set of searchable kinds not shown in search changed", diff --git a/commonsUI/src/commonMain/composeResources/values-hi-rIN/strings.xml b/commonsUI/src/commonMain/composeResources/values-hi-rIN/strings.xml index df7a1c5d77..0addde1cf5 100644 --- a/commonsUI/src/commonMain/composeResources/values-hi-rIN/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-hi-rIN/strings.xml @@ -81,7 +81,7 @@ सूचनाएँ उल्लेख प्रत्युत्तर तथा प्रतिक्रियाएँ यहाँ दिखेंगे। - प्रयोक्ता चित्र + प्रयोक्ता परिचायकचित्र मार्गदर्शन आवहन चालू : @@ -506,7 +506,7 @@ स्वागतम उष्ट्रपक्षी! उपयोगकर्ता नाम मेरा परिचय - अवतारचित्र जालपता + परिचायकचित्र जालपता ध्वजचित्र जालपता जालस्थान पता सर्वनाम @@ -1804,7 +1804,7 @@ नियामक आम्बेर के साथ प्रवेशांकन करें आपकी स्थिति का नवीकरण करें - निर्वाचन का भार ज्साप मात्रा के अनुसार माना जाता है। आप एक न्यूनतम मात्रा स्थापित कर सकते हैं कचरालेख भेजनेवालों से बचने के लिए तथा एक अधिकतम मात्रा बडे ज्सापकर्ताओं के द्वारा मतदान का अधिग्रहण होने से बचने के लिए। दोनों स्थानों में एक ही मात्रा का प्रयोग करें प्रत्येक निर्वाचन का मूल्य समान मानने को सुनिश्चित करने के लिए। रिक्त रख दें कुछ भी मात्रा स्वीकार करने के लिए। + निर्वाचन का भार ज्साप मात्रा के अनुसार माना जाता है। आप एक न्यूनतम मात्रा स्थापित कर सकते हैं कचरालेख भेजनेवालों से बचने के लिए तथा एक अधिकतम मात्रा बडे ज्सापकर्ताओं के द्वारा मतदान का अधिग्रहण होने से बचने के लिए। दोनों प्रपत्रस्थानों में एक ही मात्रा का प्रयोग करें प्रत्येक निर्वाचन का मूल्य समान मानने को सुनिश्चित करने के लिए। रिक्त रख दें कुछ भी मात्रा स्वीकार करने के लिए। उपयोगकर्ता को संदेश भेजें सूचना %1$s ठीक है @@ -2903,7 +2903,7 @@ समूह विघटन करें क्या %1$s को सभी के लिए विघटित किया जाए। संवाद का अन्त होगा सब के लिए तथा पुनः खोला नहीं जा सकता। एक नया समूह का निर्माण करना पडेगा। विघटन - परिचायक चित्र योजक + परिचायकचित्र योजक https://example.com/avatar.png एक एचटीटीपीएस॰ योजक जिसका आवहन सभी सदस्य कर सकते है। इसके स्थान पर आरोहित चित्र का उपयोग करने के लिए इसे रिक्त छोड दें। इस समूह का निर्माण किया गया था परिचायकचित्र योजकों का अवलम्बन पूर्व। इसीलिए यह केवल आरोहित चित्र का उपयोग कर सकता है। विद्यमान समूहों का उन्नतीकरण नहीं हो सकता। एक नया समूह बनाएँ योजकों का उपयोग करने के लिए। @@ -3066,7 +3066,7 @@ प्रतिरूप (विकल्पात्मक) प्रदाता (विकल्पात्मक) चालनपर्यावरण (विकल्पात्मक) - चित्र जालपता (वैकल्पिक) + परिचायकचित्र जालपता (वैकल्पिक) प्रकाशन… व्यक्तिशैली प्रकाशन आपका अनुचर @@ -5244,4 +5244,35 @@ %1$d नया सन्देश %1$d नए सन्देश + त्रिमा वस्तु + %1$d अस्र। %2$d मुख + इस त्रिमा वस्तु का पठन असफल। नियम %1$s + यन्त्रलोक परिचायकचित्र + इस परिचायकचित्र ने अपने आकार के लिए भुगतान नहीं किया। इसलिए इसे नहीं दिखाया + दीप्त। दबाएँ वस्तु को समतल रूप में दिखाने के लिए + समतल। दबाएँ वस्तु को प्रकाशान्वित करने के लिए + एक इकाई तु %1$s + तथ्यलोक में छिपाया गया। %1$s + विचारलोक में छिपाया गया। %1$s + मूलविकल्प परिचायकचित्र + कोई आकृति प्रकाशित नहीं। इसलिए सबको यह दिखेगा + एक स्थल में छिपाया गया + छिपाया गया %1$s क्षेत्रों की एक पेटिका में + एक क्षेत्र में छिपाया गया। जिसका नाम सहायक संकेत में उपलब्द यथार्थतः + कोई सहायक संकेत नहीं। इसलिए यह यन्त्रलोक में कहीं भी हो सकता है + अप्राप्य। सहायक संकेत में %1$d घातद्वौ क्षेत्र नामित हैं खोजने के लिए + उसके लिए ढूँढें + रोकें + खोज का मूल्यांकन इस यन्त्र पर… + खोज सम्पन्न %2$s में से %1$s क्षेत्रों में + अप्राप्य। लगभग %1$d मिनुट का खोज इस यन्त्र पर + अप्राप्य। लगभग %1$d घण्टों का खोज इस यन्त्र पर + सहायक संकेत में नामित पेटिका में नहीं + क्षेत्र प्राप्त। पर विषयवस्तु पढने में असफल + %1$d वस्तु जो यहाँ छिपाया गया था उसके हस्ताक्षर से मेल नहीं तथा परित्यक्त + %1$d वस्तु जो यहाँ छिपाए गए थे उनके हस्ताक्षरों से मेल नहीं तथा परित्यक्त + खोला। पर भीतर कुछ नहीं था + हस्ताक्षररहित। इसलिए यह कर्ता केवल एक कथन है + प्रकार %1$d का वस्तु। जिसे यह ग्राहक चित्रित नहीं करता + %1$d अष्टकों का अमुक जिसे यह ग्राहक पढता नहीं diff --git a/commonsUI/src/commonMain/composeResources/values-hu-rHU/strings.xml b/commonsUI/src/commonMain/composeResources/values-hu-rHU/strings.xml index 2b5065de18..a80b41a6f1 100644 --- a/commonsUI/src/commonMain/composeResources/values-hu-rHU/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-hu-rHU/strings.xml @@ -5245,4 +5245,35 @@ %1$d új üzenet %1$d új üzenet + 3D-objektum + %1$d csúcs · %2$d lap + Ezt a 3D-objektumot nem sikerült beolvasni (%1$s szabály) + Cyberspace-avatar + Ez az avatar nem fizetett a méretéért, ezért nem jelenik meg + Megvilágítva; koppintson az objektum síkban való megjelenítéséhez + Sík; koppintson az objektum megvilágításához + 1 egység = %1$s + Elrejtve az adattérben · %1$s + Elrejtve az ötlettérben · %1$s + Alapértelmezett avatar + Nincs közzétett forma, ezért mindenki ezt látja + Egy helyen elrejtve + Elrejtve egy %1$s régióból álló dobozban + Egy régióban elrejtve, amelyet a nyom pontosan megnevez + Nincs nyom, így ez a kibertér bármely pontján lehet + Elérhetetlen: a nyom 2^%1$d régiót nevez meg keresésre + Keresés erre + Leállítás + A keresés árazása ezen az eszközön… + %2$s régióból %1$s átkutatva + Elérhetetlen: körülbelül %1$d perc keresés ezen az eszközön + Elérhetetlen: körülbelül %1$d óra keresés ezen az eszközön + Nincs abban a dobozban, amelyet a nyom megnevezett + A régió megvan, de a tartalmát nem sikerült beolvasni + Az itt elrejtett %1$d elem nem egyezett az aláírásával, ezért el lett vetve + Az itt elrejtett %1$d elem nem egyezett az aláírásával, ezért el lettek vetve + Kinyitva, és nem volt benne semmi + Aláírás nélküli, így ez a szerző csak állítás + Egy %1$d típusú elem, amelyet ez a kliens nem jelenít meg + %1$d bájt valamiből, amit ez a kliens nem olvas diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/UiSettingsLabels.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/UiSettingsLabels.kt new file mode 100644 index 0000000000..5a1e45ccb1 --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/UiSettingsLabels.kt @@ -0,0 +1,123 @@ +/* + * 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.commons.model + +import com.vitorpamplona.amethyst.commons.resources.Res +import com.vitorpamplona.amethyst.commons.resources.accent_color_blue +import com.vitorpamplona.amethyst.commons.resources.accent_color_green +import com.vitorpamplona.amethyst.commons.resources.accent_color_orange +import com.vitorpamplona.amethyst.commons.resources.accent_color_pink +import com.vitorpamplona.amethyst.commons.resources.accent_color_purple +import com.vitorpamplona.amethyst.commons.resources.accent_color_red +import com.vitorpamplona.amethyst.commons.resources.connectivity_type_always +import com.vitorpamplona.amethyst.commons.resources.connectivity_type_never +import com.vitorpamplona.amethyst.commons.resources.connectivity_type_unmetered_wifi_only +import com.vitorpamplona.amethyst.commons.resources.content_warning_hide_all_sensitive_content_option +import com.vitorpamplona.amethyst.commons.resources.content_warning_see_warnings_option +import com.vitorpamplona.amethyst.commons.resources.content_warning_show_all_sensitive_content_option +import com.vitorpamplona.amethyst.commons.resources.dark +import com.vitorpamplona.amethyst.commons.resources.font_family_monospace +import com.vitorpamplona.amethyst.commons.resources.font_family_sans_serif +import com.vitorpamplona.amethyst.commons.resources.font_family_serif +import com.vitorpamplona.amethyst.commons.resources.font_family_system +import com.vitorpamplona.amethyst.commons.resources.font_size_huge +import com.vitorpamplona.amethyst.commons.resources.font_size_large +import com.vitorpamplona.amethyst.commons.resources.font_size_normal +import com.vitorpamplona.amethyst.commons.resources.font_size_small +import com.vitorpamplona.amethyst.commons.resources.gallery_type_classic +import com.vitorpamplona.amethyst.commons.resources.gallery_type_modern +import com.vitorpamplona.amethyst.commons.resources.light +import com.vitorpamplona.amethyst.commons.resources.system +import com.vitorpamplona.amethyst.commons.resources.ui_feature_set_type_complete +import com.vitorpamplona.amethyst.commons.resources.ui_feature_set_type_performance +import com.vitorpamplona.amethyst.commons.resources.ui_feature_set_type_simplified +import org.jetbrains.compose.resources.StringResource + +// Display labels for the settings enums. The enums live in headless commons; their labels are +// Compose resources, so they are attached here, in the UI module. + +val ThemeType.resourceId: StringResource + get() = + when (this) { + ThemeType.SYSTEM -> Res.string.system + ThemeType.LIGHT -> Res.string.light + ThemeType.DARK -> Res.string.dark + } + +val AccentColorType.resourceId: StringResource + get() = + when (this) { + AccentColorType.PURPLE -> Res.string.accent_color_purple + AccentColorType.BLUE -> Res.string.accent_color_blue + AccentColorType.GREEN -> Res.string.accent_color_green + AccentColorType.ORANGE -> Res.string.accent_color_orange + AccentColorType.RED -> Res.string.accent_color_red + AccentColorType.PINK -> Res.string.accent_color_pink + } + +val FontFamilyType.resourceId: StringResource + get() = + when (this) { + FontFamilyType.SYSTEM -> Res.string.font_family_system + FontFamilyType.SANS_SERIF -> Res.string.font_family_sans_serif + FontFamilyType.SERIF -> Res.string.font_family_serif + FontFamilyType.MONOSPACE -> Res.string.font_family_monospace + } + +val FontSizeType.resourceId: StringResource + get() = + when (this) { + FontSizeType.SMALL -> Res.string.font_size_small + FontSizeType.NORMAL -> Res.string.font_size_normal + FontSizeType.LARGE -> Res.string.font_size_large + FontSizeType.HUGE -> Res.string.font_size_huge + } + +val ConnectivityType.resourceId: StringResource + get() = + when (this) { + ConnectivityType.ALWAYS -> Res.string.connectivity_type_always + ConnectivityType.WIFI_ONLY -> Res.string.connectivity_type_unmetered_wifi_only + ConnectivityType.NEVER -> Res.string.connectivity_type_never + } + +val FeatureSetType.resourceId: StringResource + get() = + when (this) { + FeatureSetType.COMPLETE -> Res.string.ui_feature_set_type_complete + FeatureSetType.SIMPLIFIED -> Res.string.ui_feature_set_type_simplified + FeatureSetType.PERFORMANCE -> Res.string.ui_feature_set_type_performance + } + +val ProfileGalleryType.resourceId: StringResource + get() = + when (this) { + ProfileGalleryType.CLASSIC -> Res.string.gallery_type_classic + ProfileGalleryType.MODERN -> Res.string.gallery_type_modern + } + +val WarningType.resourceId: StringResource + get() = + when (this) { + WarningType.WARN -> Res.string.content_warning_see_warnings_option + WarningType.SHOW -> Res.string.content_warning_show_all_sensitive_content_option + WarningType.HIDE -> Res.string.content_warning_hide_all_sensitive_content_option + } diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/UserAvatar.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/UserAvatar.kt index 1e294c544f..a8d470f9f7 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/UserAvatar.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/UserAvatar.kt @@ -30,6 +30,7 @@ import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.Dp +import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings /** * Wrapper class for profile picture URLs that signals Coil to use the thumbnail @@ -58,7 +59,8 @@ val LocalProfilePictureCache = staticCompositionLocalOf { false } * @param size Size of the avatar (both width and height) * @param modifier Additional modifiers to apply * @param contentDescription Accessibility description - * @param loadProfilePicture Whether to load the profile picture (false = show robohash only) + * @param loadProfilePicture Whether to load the profile picture (false = show robohash only). + * Defaults to the user's display settings ([LocalDisplaySettings]), as do the next two. * @param loadRobohash Whether to generate robohash (false = show generic icon) * @param autoPlayGif Whether animated (GIF/AVIF) pictures play. Only Android can animate them; * desktop shows their first frame either way. @@ -74,9 +76,9 @@ fun UserAvatar( size: Dp, modifier: Modifier = Modifier, contentDescription: String? = null, - loadProfilePicture: Boolean = true, - loadRobohash: Boolean = true, - autoPlayGif: Boolean = true, + loadProfilePicture: Boolean = LocalDisplaySettings.current.showProfilePictures, + loadRobohash: Boolean = LocalDisplaySettings.current.loadRobohash, + autoPlayGif: Boolean = LocalDisplaySettings.current.autoPlayVideos, badge: @Composable (BoxScope.() -> Unit)? = null, ) { if (badge != null) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/ResourceToastMsg.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/toasts/ResourceToastMsg.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/ResourceToastMsg.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/toasts/ResourceToastMsg.kt index e8a3c263c8..11b62ccb8d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/ResourceToastMsg.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/toasts/ResourceToastMsg.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.components.toasts +package com.vitorpamplona.amethyst.commons.ui.components.toasts import androidx.compose.runtime.Immutable import org.jetbrains.compose.resources.StringResource diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/StringToastMsg.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/toasts/StringToastMsg.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/StringToastMsg.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/toasts/StringToastMsg.kt index 9a50a24425..ecfdd7a0c2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/StringToastMsg.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/toasts/StringToastMsg.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.components.toasts +package com.vitorpamplona.amethyst.commons.ui.components.toasts import androidx.compose.runtime.Immutable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/ThrowableToastMsg.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/toasts/ThrowableToastMsg.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/ThrowableToastMsg.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/toasts/ThrowableToastMsg.kt index 2d7f77f9f3..8236773234 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/ThrowableToastMsg.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/toasts/ThrowableToastMsg.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.components.toasts +package com.vitorpamplona.amethyst.commons.ui.components.toasts import androidx.compose.runtime.Immutable import org.jetbrains.compose.resources.StringResource diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/ToastManager.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/toasts/ToastManager.kt similarity index 92% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/ToastManager.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/toasts/ToastManager.kt index 2e30e70d17..9d95a7b633 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/ToastManager.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/toasts/ToastManager.kt @@ -18,12 +18,12 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.components.toasts +package com.vitorpamplona.amethyst.commons.ui.components.toasts import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.commons.model.User -import com.vitorpamplona.amethyst.ui.components.toasts.multiline.MultiErrorToastMsg -import com.vitorpamplona.amethyst.ui.components.toasts.multiline.UserBasedErrorMessage +import com.vitorpamplona.amethyst.commons.ui.components.toasts.multiline.MultiErrorToastMsg +import com.vitorpamplona.amethyst.commons.ui.components.toasts.multiline.UserBasedErrorMessage import kotlinx.coroutines.flow.MutableStateFlow import org.jetbrains.compose.resources.StringResource diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/ToastMsg.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/toasts/ToastMsg.kt similarity index 94% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/ToastMsg.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/toasts/ToastMsg.kt index b57e7a4ac6..91e73080bd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/ToastMsg.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/toasts/ToastMsg.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.components.toasts +package com.vitorpamplona.amethyst.commons.ui.components.toasts import androidx.compose.runtime.Immutable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/MultiErrorToastMsg.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/toasts/multiline/MultiErrorToastMsg.kt similarity index 92% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/MultiErrorToastMsg.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/toasts/multiline/MultiErrorToastMsg.kt index 2d7c34bf76..644d8a7e3e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/MultiErrorToastMsg.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/toasts/multiline/MultiErrorToastMsg.kt @@ -18,11 +18,11 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.components.toasts.multiline +package com.vitorpamplona.amethyst.commons.ui.components.toasts.multiline import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.commons.model.User -import com.vitorpamplona.amethyst.ui.components.toasts.ToastMsg +import com.vitorpamplona.amethyst.commons.ui.components.toasts.ToastMsg import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update import org.jetbrains.compose.resources.StringResource diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/DisplaySettings.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/DisplaySettings.kt new file mode 100644 index 0000000000..d70342d600 --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/DisplaySettings.kt @@ -0,0 +1,71 @@ +/* + * 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.commons.ui.screen + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.compositionLocalOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.commons.model.FeatureSetType +import com.vitorpamplona.amethyst.commons.state.UiSettingsState + +/** + * The display choices avatars read on almost every card, resolved once near the composition + * root and handed down through [LocalDisplaySettings], so shared composables do not need the + * account's ViewModel to follow them. + * + * The defaults are what a front end without these settings (previews, desktop today) gets: + * everything on. + */ +@Immutable +data class DisplaySettings( + /** Load profile pictures (off on metered connections when the user asked for WIFI_ONLY). */ + val showProfilePictures: Boolean = true, + /** Performance mode: skip robohashes, crossfades and other decoration. */ + val performanceMode: Boolean = false, + /** Play videos and animated images (GIF/AVIF avatars) without a tap. */ + val autoPlayVideos: Boolean = true, +) { + /** Robohash fallbacks are skipped in performance mode (a plain face icon instead). */ + val loadRobohash: Boolean get() = !performanceMode +} + +// Not static: showProfilePictures follows the connection type (Wi-Fi only), so this changes when the +// network does, and only its readers should recompose, not everything under the theme. +val LocalDisplaySettings = compositionLocalOf { DisplaySettings() } + +/** Collects [settings] into a [DisplaySettings] that follows every change to it. */ +@Composable +fun collectDisplaySettings(settings: UiSettingsState): DisplaySettings { + val showProfilePictures by settings.showProfilePictures.collectAsStateWithLifecycle() + val featureSet by settings.uiSettingsFlow.featureSet.collectAsStateWithLifecycle() + val autoPlayVideos by settings.autoPlayVideosFlow.collectAsStateWithLifecycle() + + return remember(showProfilePictures, featureSet, autoPlayVideos) { + DisplaySettings( + showProfilePictures = showProfilePictures, + performanceMode = featureSet == FeatureSetType.PERFORMANCE, + autoPlayVideos = autoPlayVideos, + ) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/CoordinateShape.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/CoordinateShape.kt new file mode 100644 index 0000000000..e311973efb --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/CoordinateShape.kt @@ -0,0 +1,45 @@ +/* + * 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.quartz.experimental.decentralizedLists + +/** + * A cheap, allocation-free check that a value has the `:<64-hex pubkey>:` coordinate + * shape, done before anything reaches `AddressSerializer.parse`. That parser splits the string + * and logs a warning for every value it rejects — and in this family a rejected value is + * routine: `z` tags legitimately carry plain list names, which may be long and contain colons. + */ +internal object CoordinateShape { + fun matches(value: String): Boolean { + val firstColon = value.indexOf(':') + // kinds are at most 5 digits (0..65535) + if (firstColon !in 1..5) return false + for (i in 0 until firstColon) { + if (value[i] !in '0'..'9') return false + } + val pubKeyEnd = firstColon + 1 + 64 + if (value.length <= pubKeyEnd || value[pubKeyEnd] != ':') return false + for (i in firstColon + 1 until pubKeyEnd) { + val c = value[i] + if (c !in '0'..'9' && c !in 'a'..'f' && c !in 'A'..'F') return false + } + return true + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/DecentralizedListEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/DecentralizedListEvent.kt new file mode 100644 index 0000000000..090afc28e8 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/DecentralizedListEvent.kt @@ -0,0 +1,51 @@ +/* + * 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.quartz.experimental.decentralizedLists + +import com.vitorpamplona.quartz.nip01Core.core.IEvent + +/** + * Common face of the four Decentralized Lists kinds (9998, 39998, 9999, 39999). + * + * Any of them can be the parent of a list item: headers are the standard parents, and the + * spec's "nonstandard" method declares a list with a 9999/39999 item whose own parent is a + * list of lists. So every kind knows how children must point at it. + */ +interface DecentralizedListEvent : IEvent { + /** + * The value a child item writes in its `z` tag to point at this event: the event id for + * the regular kinds, the `kind:pubkey:d` coordinate for the addressable ones. + */ + fun listPointer(): String +} + +/** + * The item kinds (9999, 39999). Items point at their parent list with one or more `z` tags and + * carry the item itself in `p` / `e` / `t` / `a` tags. + */ +interface DecentralizedListItem : DecentralizedListEvent + +/** + * The addressable kinds (39998, 39999): the only ones the Tapestry extensions — the `b` + * inherit-from tag, the `json` payload — are defined on, since those point at and resolve + * through `kind:pubkey:d` coordinates. + */ +interface AddressableDecentralizedListEvent : DecentralizedListEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/EventExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/EventExt.kt new file mode 100644 index 0000000000..bedb1f0233 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/EventExt.kt @@ -0,0 +1,34 @@ +/* + * 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.quartz.experimental.decentralizedLists + +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.parentListPointers +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.parentLists +import com.vitorpamplona.quartz.nip01Core.core.Event + +/** + * The lists an event of any kind claims to be on. Cross-NIP Compatibility lets a foreign event + * (a NIP-72 community, say) list itself by carrying `z` tags — the creator's voice, "I claim to + * be on this list" — next to its own NIP's tags. Such lists declare the kind with `item-kind`. + */ +fun Event.dListParents() = tags.parentLists() + +fun Event.dListParentPointers() = tags.parentListPointers() diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/README.md b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/README.md new file mode 100644 index 0000000000..060f22cfb4 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/README.md @@ -0,0 +1,151 @@ +# Decentralized Lists (kinds 9998, 39998, 9999, 39999) + +Lists that anyone can add to. Where a NIP-51 list is fully controlled by its +author, a Decentralized List only has its *header* controlled by its author; +the *items* are contributed by the community, one event each, and curation is +left to the reader's trust metric (typically fed by NIP-25 `+`/`-` reactions +to the item events). + +This is a pre-NIP wire format drafted by Tapestry +([spec](https://github.com/nous-clawds4/tapestry/blob/main/protocols/nips/decentralized-lists.md)). +The package mirrors `nip88Polls`: one sub-package per role, each with its +events, `TagArrayExt` (readers), `TagArrayBuilderExt` (writers) and `tags/`. + +| Kind | Role | Editable | Quartz class | +|---|---|---|---| +| 9998 | list header | no | `header.ListHeaderEvent` | +| 39998 | list header | yes (addressable) | `header.AddressableListHeaderEvent` | +| 9999 | list item | no | `item.ListItemEvent` | +| 39999 | list item | yes (addressable) | `item.AddressableListItemEvent` | + +All four implement `DecentralizedListEvent`, whose `listPointer()` is what a +child writes in its `z` tag: the event id for 9998/9999, the +`kind:pubkey:d` coordinate for 39998/39999. + +## Header tags + +- `["names", , ]` — required. `titles` and `slugs` have the + same two-form shape (`SingularPlural`); a tag missing either form is dropped. +- `["required" | "allowed" | "recommended" | "disallowed", , ?]` + — one tag per constrained name, with an optional human-readable description + at index 2 (`TagRule`). +- `["description", ]`. + +## Item tags + +- `["z", ]` — required, one per parent list. `ParentListTag.classify` + tells the three allowed shapes apart by form: a 64-hex event id + (`ParentList.EventId`), a `kind:pubkey:d` coordinate + (`ParentList.Coordinate`), or anything else, the singular name of an + undeclared list such as `"dog"` (`ParentList.Name`). +- The items themselves: `p` (pubkeys), `e` (events), `t` (strings, kept in + their original case — they are values like "Fido", not hashtags) and `a` + (addressables; the spec's own example uses an `naddr1…`, which parses too). +- Optional `name`, `title`, `slug`, `description`, `comments`. + +## The nonstandard method + +The spec also allows declaring a list with an *item*: a 9999 on the list of +lists (`["z", "list"]` or `["z", ]`) that carries the +header tags. Item events therefore expose the header accessors too +(`names()`, `requiredTags()`, …), and `declaresList()` says whether one is +doing so. The header builder extensions are typed on the whole family for the +same reason. + +## Building + +```kotlin +val header = + ListHeaderEvent.build("dog name", "dog names", "Commonly used dog names.") { + required("t") + allowed("comments", "Why this name") + } + +// once `header` is signed: +val item = + ListItemEvent.build(signedHeader) { + itemString("Fido") + comments("A classic") + } +``` + +## Retrieval + +```kotlin +Filter(kinds = listOf(ListHeaderEvent.KIND, AddressableListHeaderEvent.KIND)) + +// every item on a list, across redundant declarations of it +Filter( + kinds = listOf(ListItemEvent.KIND, AddressableListItemEvent.KIND), + tags = mapOf("z" to listOf("dog", header.listPointer())), +) +``` + +## Search + +All four kinds are `SearchableEvent`s sharing one walk +(`forEachSearchableListField`): `names` and `titles` (singular then plural), +`name`, `title`, `description`, `comments`, then every `t` item value. +`content` is not part of the spec, and ids, pubkeys and coordinates are +served by `#p`/`#e`/`#a`/`#z` filters, so none of them are indexed. + +## Tapestry extensions + +The Tapestry drafts +([`protocols/drafts`](https://github.com/nous-clawds4/tapestry/tree/main/protocols/drafts)) +add tags and conventions on these same four kinds. None of them adds a kind. + +Concept addresses such as `39998::tag` or `nostr-user-tag` are +published by each deployment's own assistant key, and the drafts forbid +hardcoding them, so every builder and parser below takes them as parameters. + +### Tags + +| Tag | Draft | Kinds | Quartz | +|---|---|---|---| +| `["b", , "pointer"\|"inherit"\|"inherit-items"]` | Inherit-From | 39998, 39999 | `tags.InheritFromTag`, `inheritFrom()` | +| `["b", "b-tag-deferred"]` | Shared Concepts | 39998, 39999 | `isDeliberatelyUnaffiliated()` | +| `["n", ]` / `["s", ]` | Class Thread Relationships | 39999 | `item.tags.ElementOfTag` / `SubsetOfTag` | +| `["json", ""]` | Tapestry Concepts | 39998, 39999 | `tags.JsonTag`, `wordWrapper()` → `concepts.WordWrapper` | +| `["concept-graph", "39999::-concept-graph"]` | Tapestry Concepts | 39998 | `conceptGraph()`, falls back to the computed address | +| `["item-kind", , ?]` | Cross-NIP Compatibility | headers | `itemKinds()`, `acceptedItemKinds()` | + +An unknown or missing `b` type reads as `pointer`, and code that acts on +deference must check for `inherit` / `inherit-items` explicitly. + +Events of other NIPs can list themselves by carrying `z` tags (Cross-NIP +Compatibility "Method 3"); `Event.dListParents()` reads them on any kind. + +### Taggings (`taggings/`) + +All kind 39999 items, told apart by their `z` tags: + +- `TagElement`: a tag ("Podcaster"), `d` = slug, JSON in `content`, plus + optional `tag-for-nostr-pubkey` / `tag-for-nostr-event` hint `z`s. +- `PubKeyTagging`: "Avi is a Podcaster", with the deterministic + `profile-tag---` `d`. +- `TagPin`: a viewer's pin with a `curation-method` JSON tag. Unpin with a + NIP-09 deletion. +- `TaggingHeader` + `EventTagging`: tagging events. The target sits in + `a`/`e`, so the tag is reached through a `z` to a per-tag header. +- Federation: every builder takes a collection of concept namespaces and + writes one concept `z` per namespace; every parser takes the set of + namespaces the reader honors. Pass them all: a concept `z` the reader didn't + list would be taken for something else, e.g. a tagging header. +- `polarity()`: no tag means apply, `≥ 0.5` applied, `≤ -0.5` disputed, and + anything in between is not counted in v1. + +### Assistant designation (`assistant/`) + +- Kind 10040 entries next to NIP-85's: the blanket + `["39998:dlist-header", , ]` and one + `[":", , ]` per curated list. Read with + `dListAssistant()` / `dListCurations()`; write with the `replace…` / + `remove…` helpers, which keep every other tag verbatim. The first + occurrence wins on duplicates. +- `CurationCopy`: the assistant's copy of an accepted item, with + `d` = `copy-`, two `q` tags back to + the original, and only the tags the spec lists. `buildRemoval` is the + matching NIP-09 deletion. +- `HeaderResolution`: which header governs a user's concept. Their own + header wins, then their assistant's, and recency never decides. diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/SearchExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/SearchExt.kt new file mode 100644 index 0000000000..e866f645b9 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/SearchExt.kt @@ -0,0 +1,97 @@ +/* + * 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.quartz.experimental.decentralizedLists + +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.tags.NamesTag +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.tags.SingularPlural +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.tags.TitlesTag +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.tags.CommentsTag +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.tags.NameTag +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.tags.TitleTag +import com.vitorpamplona.quartz.experimental.decentralizedLists.tags.DescriptionTag +import com.vitorpamplona.quartz.nip01Core.core.TagArray +import com.vitorpamplona.quartz.nip01Core.core.fastForEach +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.HashtagTag +import com.vitorpamplona.quartz.nip50Search.IndexableFieldVisitor + +/** + * The human-authored text of any kind in the family, in a fixed order: the header's `names` + * and `titles` (singular, then plural), the item's `name` and `title`, `description`, + * `comments`, then each `t` item value. Headers and items share one walk because the spec's + * nonstandard method lets an item carry header tags; each kind simply has fewer of them set. + * + * `content` is not part of the spec and ids/pubkeys/coordinates are served by tag filters, + * so neither is indexed. + * + * @return false when the visitor stopped the walk. + */ +fun TagArray.forEachSearchableListField(visitor: IndexableFieldVisitor): Boolean { + // The read path runs once per event per keystroke, so this is two allocation-free passes + // instead of one full scan (and one parsed object) per field. The first pass only remembers + // the first well-formed tag of each field; the fixed visiting order is applied afterwards. + var names: Array? = null + var titles: Array? = null + var name: String? = null + var title: String? = null + var description: String? = null + var comments: String? = null + + fastForEach { tag -> + if (tag.size < 2 || tag[1].isEmpty()) return@fastForEach + when (tag[0]) { + NamesTag.TAG_NAME -> if (names == null && SingularPlural.isTag(tag, NamesTag.TAG_NAME)) names = tag + TitlesTag.TAG_NAME -> if (titles == null && SingularPlural.isTag(tag, TitlesTag.TAG_NAME)) titles = tag + NameTag.TAG_NAME -> if (name == null) name = tag[1] + TitleTag.TAG_NAME -> if (title == null) title = tag[1] + DescriptionTag.TAG_NAME -> if (description == null) description = tag[1] + CommentsTag.TAG_NAME -> if (comments == null) comments = tag[1] + } + } + + names?.let { + if (!visitor.visit(it[1])) return false + if (!visitor.visit(it[2])) return false + } + titles?.let { + if (!visitor.visit(it[1])) return false + if (!visitor.visit(it[2])) return false + } + name?.let { if (!visitor.visit(it)) return false } + title?.let { if (!visitor.visit(it)) return false } + description?.let { if (!visitor.visit(it)) return false } + comments?.let { if (!visitor.visit(it)) return false } + fastForEach { tag -> + HashtagTag.parse(tag)?.let { if (!visitor.visit(it)) return false } + } + return true +} + +/** The write-path join of [forEachSearchableListField]: one field per line. */ +fun TagArray.searchableListContent() = + buildString { + forEachSearchableListField { field -> + if (field != null) { + if (isNotEmpty()) append('\n') + append(field) + } + true + } + } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/TagArrayBuilderExt.kt new file mode 100644 index 0000000000..aec7d12de8 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/TagArrayBuilderExt.kt @@ -0,0 +1,45 @@ +/* + * 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.quartz.experimental.decentralizedLists + +import com.vitorpamplona.quartz.experimental.decentralizedLists.tags.DescriptionTag +import com.vitorpamplona.quartz.experimental.decentralizedLists.tags.InheritFromTag +import com.vitorpamplona.quartz.experimental.decentralizedLists.tags.InheritType +import com.vitorpamplona.quartz.experimental.decentralizedLists.tags.JsonTag +import com.vitorpamplona.quartz.nip01Core.core.Address +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder + +fun TagArrayBuilder.description(description: String) = addUnique(DescriptionTag.assemble(description)) + +fun TagArrayBuilder.inheritFrom( + target: Address, + type: InheritType, +) = add(InheritFromTag.assemble(target, type)) + +fun TagArrayBuilder.inheritFrom( + target: String, + type: InheritType, +) = add(InheritFromTag.assemble(target, type)) + +/** Writes `["b", "b-tag-deferred"]`: considered a shared twin and chose none. */ +fun TagArrayBuilder.deliberatelyUnaffiliated() = addUniqueValueIfNew(InheritFromTag.assembleUnaffiliated()) + +fun TagArrayBuilder.json(json: String) = addUnique(JsonTag.assemble(json)) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/TagArrayExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/TagArrayExt.kt new file mode 100644 index 0000000000..6eaa5f03bf --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/TagArrayExt.kt @@ -0,0 +1,50 @@ +/* + * 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.quartz.experimental.decentralizedLists + +import com.vitorpamplona.quartz.experimental.decentralizedLists.concepts.WordWrapper +import com.vitorpamplona.quartz.experimental.decentralizedLists.tags.DescriptionTag +import com.vitorpamplona.quartz.experimental.decentralizedLists.tags.InheritFromTag +import com.vitorpamplona.quartz.experimental.decentralizedLists.tags.InheritType +import com.vitorpamplona.quartz.experimental.decentralizedLists.tags.JsonTag +import com.vitorpamplona.quartz.nip01Core.core.TagArray +import com.vitorpamplona.quartz.nip01Core.core.fastAny +import com.vitorpamplona.quartz.nip01Core.core.fastFirstNotNullOfOrNull + +fun TagArray.description() = fastFirstNotNullOfOrNull(DescriptionTag::parse) + +/** Every `b` link, in tag order. Excludes the `b-tag-deferred` marker. */ +fun TagArray.inheritFrom() = mapNotNull(InheritFromTag::parse) + +/** + * The targets of `b` tags of exactly [type], in tag order. For [InheritType.INHERIT] the order + * is load-bearing: the first-listed parent wins a field both ancestors state. + */ +fun TagArray.inheritFromTargets(type: InheritType) = mapNotNull { InheritFromTag.parseTarget(it, type) } + +/** True when the event carries `["b", "b-tag-deferred"]`: deliberately affiliated with nothing. */ +fun TagArray.isDeliberatelyUnaffiliated() = fastAny(InheritFromTag::isUnaffiliatedMarker) + +/** The raw `json` tag. */ +fun TagArray.json() = fastFirstNotNullOfOrNull(JsonTag::parse) + +/** The `json` tag parsed as a JSON object, or null when absent or malformed. */ +fun TagArray.wordWrapper() = json()?.let(WordWrapper::parse) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/assistant/CurationCopy.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/assistant/CurationCopy.kt new file mode 100644 index 0000000000..bb6d37a238 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/assistant/CurationCopy.kt @@ -0,0 +1,117 @@ +/* + * 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.quartz.experimental.decentralizedLists.assistant + +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.AddressableListItemEvent +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.tags.CommentsTag +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.tags.NameTag +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.tags.ParentListTag +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.tags.SlugTag +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.tags.TitleTag +import com.vitorpamplona.quartz.experimental.decentralizedLists.tags.DescriptionTag +import com.vitorpamplona.quartz.nip01Core.core.Address +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.toHexKey +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.HashtagTag +import com.vitorpamplona.quartz.nip01Core.tags.kinds.KindTag +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent +import com.vitorpamplona.quartz.utils.TimeUtils +import com.vitorpamplona.quartz.utils.sha256.sha256 + +/** + * Tapestry Assistant Designation, *curation copies*: how an empowered assistant fills the list + * it curates. It files its own kind 39999 copy of each item it accepts under the curated + * header, and deletes the copies it stops accepting. + * + * A copy's tags are exactly its `d`, one `z` (the curated header), its `q` pointers back to the + * original, and the original's `name`/`title`/`slug`/`description`/`comments` and item tags + * (`p`/`e`/`t`/`a`) verbatim, plus the original's `content`. Nothing else is carried — not + * `json`, `n` or `s`, which describe the original's place in its author's own graph — and the + * copy is never stamped with the original list's `z`s. + */ +object CurationCopy { + const val Q_TAG = "q" + + /** + * `copy-` + hex SHA-256 of `
\n`, where the ref is the + * original's `39999::` address for a 39999, else its event id. Copying the same + * original into the same list again therefore replaces the copy. + */ + fun dTag( + curatedHeader: Address, + original: Event, + ) = "copy-" + sha256((curatedHeader.toValue() + "\n" + originalRef(original)).encodeToByteArray()).toHexKey() + + fun originalRef(original: Event) = if (original is AddressableListItemEvent) original.addressTag() else original.id + + private val CARRIED = + setOf( + NameTag.TAG_NAME, + TitleTag.TAG_NAME, + SlugTag.TAG_NAME, + DescriptionTag.TAG_NAME, + CommentsTag.TAG_NAME, + PTag.TAG_NAME, + ETag.TAG_NAME, + HashtagTag.TAG_NAME, + ATag.TAG_NAME, + ) + + /** + * @param curatedHeader the assistant's curated header, `::`. + * @param relay where the original can be fetched; written into the `q` tags. + */ + fun build( + curatedHeader: Address, + original: Event, + relay: NormalizedRelayUrl?, + createdAt: Long = TimeUtils.now(), + ) = AddressableListItemEvent.build( + parent = ParentListTag.classify(curatedHeader.toValue()), + dTag = dTag(curatedHeader, original), + createdAt = createdAt, + content = original.content, + ) { + val relayUrl = relay?.url ?: "" + // the stable address, surviving the author's edits + if (original is AddressableListItemEvent) add(arrayOf(Q_TAG, original.addressTag(), relayUrl)) + // the exact version copied, NIP-18 style, naming the author here rather than in a `p` + add(arrayOf(Q_TAG, original.id, relayUrl, original.pubKey)) + original.tags.forEach { tag -> + if (tag.isNotEmpty() && tag[0] in CARRIED) add(tag) + } + } + + /** The NIP-09 removal of a copy: by address, by event id and by kind. */ + fun buildRemoval( + copy: AddressableListItemEvent, + createdAt: Long = TimeUtils.now(), + ) = eventTemplate(DeletionEvent.KIND, "", createdAt) { + add(ATag.assemble(copy.address(), null)) + add(ETag.assemble(copy.id, null, null)) + add(KindTag.assemble(AddressableListItemEvent.KIND)) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/assistant/HeaderResolution.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/assistant/HeaderResolution.kt new file mode 100644 index 0000000000..e659137211 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/assistant/HeaderResolution.kt @@ -0,0 +1,67 @@ +/* + * 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.quartz.experimental.decentralizedLists.assistant + +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.AddressableListHeaderEvent +import com.vitorpamplona.quartz.nip01Core.core.Address +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArray + +/** + * Tapestry Assistant Designation, dual-author precedence: which header governs a user's + * concept `slug` when both the user and their assistant may have written one. + * + * 1. the user's own `39998::`, if it exists; + * 2. else their assistant's `39998::`, the assistant found through the user's + * kind 10040 `39998:dlist-header` entry; + * 3. else none. + * + * Never by recency: a newer assistant header does not shadow a personal one. A user who wants + * the assistant's to govern says so with `["b", , "inherit"]` on their own. + */ +object HeaderResolution { + /** The candidate addresses, in precedence order. [treasureMap] is the user's 10040 tags. */ + fun candidates( + user: HexKey, + slug: String, + treasureMap: TagArray?, + ): List
{ + val personal = Address(AddressableListHeaderEvent.KIND, user, slug) + val assistant = treasureMap?.dListAssistant()?.assistant ?: return listOf(personal) + if (assistant == user) return listOf(personal) + return listOf(personal, Address(AddressableListHeaderEvent.KIND, assistant, slug)) + } + + /** + * Picks the governing header among the fetched [headers]: the first candidate that has one. + * Headers signed by anyone else, or for another slug, are ignored. + */ + fun governing( + user: HexKey, + slug: String, + treasureMap: TagArray?, + headers: Collection, + ): AddressableListHeaderEvent? = + candidates(user, slug, treasureMap).firstNotNullOfOrNull { candidate -> + // several versions of one addressable header may be at hand: the newest is the header + headers.filter { it.pubKey == candidate.pubKeyHex && it.dTag() == candidate.dTag }.maxByOrNull { it.createdAt } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/assistant/TagArrayExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/assistant/TagArrayExt.kt new file mode 100644 index 0000000000..fe0172e3da --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/assistant/TagArrayExt.kt @@ -0,0 +1,86 @@ +/* + * 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.quartz.experimental.decentralizedLists.assistant + +import com.vitorpamplona.quartz.experimental.decentralizedLists.assistant.tags.AssistantDesignation +import com.vitorpamplona.quartz.experimental.decentralizedLists.assistant.tags.AssistantDesignationTag +import com.vitorpamplona.quartz.experimental.decentralizedLists.assistant.tags.DListCuration +import com.vitorpamplona.quartz.experimental.decentralizedLists.assistant.tags.DListCurationTag +import com.vitorpamplona.quartz.nip01Core.core.TagArray +import com.vitorpamplona.quartz.nip01Core.core.fastFirstNotNullOfOrNull + +// Both entry families ride NIP-85's kind 10040 but are bounded away from it: NIP-85 only reads +// 30382-30385 keys, and these parsers only read 39998/39999 ones. + +/** The blanket assistant designation. On duplicates the first occurrence wins. */ +fun TagArray.dListAssistant() = fastFirstNotNullOfOrNull(AssistantDesignationTag::parse) + +/** Every per-list curation entry, first occurrence per (kind, d-tag). */ +fun TagArray.dListCurations() = mapNotNull(DListCurationTag::parse).distinctBy { it.kind to it.dTag } + +fun TagArray.dListCuration( + kind: Int, + dTag: String, +) = fastFirstNotNullOfOrNull { tag -> DListCurationTag.parse(tag)?.takeIf { it.kind == kind && it.dTag == dTag } } + +/** + * Sets the blanket designation in place, keeping every other tag verbatim — 10040 is + * replaceable, so whatever is dropped here is gone from the Map. Duplicates collapse onto the + * first position. + */ +fun TagArray.replaceDListAssistant(designation: AssistantDesignation): TagArray = replaceFirstMatch(designation.toTagArray()) { AssistantDesignationTag.isTag(it) } + +fun TagArray.removeDListAssistant(): TagArray = filterNot { AssistantDesignationTag.isTag(it) }.toTypedArray() + +/** Adds or replaces the entry for the curation's (kind, d-tag), as [replaceDListAssistant]. */ +fun TagArray.replaceDListCuration(entry: DListCuration): TagArray = + replaceFirstMatch(entry.toTagArray()) { tag -> + DListCurationTag.parse(tag)?.let { it.kind == entry.kind && it.dTag == entry.dTag } ?: false + } + +/** Revokes the empowerment. The header and its `b` stay on relays. */ +fun TagArray.removeDListCuration( + kind: Int, + dTag: String, +): TagArray = + filterNot { tag -> + DListCurationTag.parse(tag)?.let { it.kind == kind && it.dTag == dTag } ?: false + }.toTypedArray() + +private inline fun TagArray.replaceFirstMatch( + replacement: Array, + matches: (Array) -> Boolean, +): TagArray { + var replaced = false + val out = ArrayList>(size + 1) + forEach { tag -> + if (matches(tag)) { + if (!replaced) { + out.add(replacement) + replaced = true + } + } else { + out.add(tag) + } + } + if (!replaced) out.add(replacement) + return out.toTypedArray() +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/assistant/TrustProviderListEventExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/assistant/TrustProviderListEventExt.kt new file mode 100644 index 0000000000..def53d26df --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/assistant/TrustProviderListEventExt.kt @@ -0,0 +1,41 @@ +/* + * 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.quartz.experimental.decentralizedLists.assistant + +import com.vitorpamplona.quartz.nip01Core.core.TagArray +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip85TrustedAssertions.list.TrustProviderListEvent + +/** The public half alone: no signer, so no private entries. */ +fun TrustProviderListEvent.publicDListAssistant() = tags.dListAssistant() + +/** The public half alone: no signer, so no private entries. */ +fun TrustProviderListEvent.publicDListCurations() = tags.dListCurations() + +/** + * Both halves of the Map. With anyone else's signer, or a private half that will not decrypt, + * this sees the public half alone. Public tags come first, so a public entry wins a duplicate. + */ +suspend fun TrustProviderListEvent.dListAssistant(signer: NostrSigner) = mergedTags(signer).dListAssistant() + +suspend fun TrustProviderListEvent.dListCurations(signer: NostrSigner) = mergedTags(signer).dListCurations() + +private suspend fun TrustProviderListEvent.mergedTags(signer: NostrSigner): TagArray = tags + (privateTags(signer) ?: emptyArray()) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/assistant/tags/AssistantDesignationTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/assistant/tags/AssistantDesignationTag.kt new file mode 100644 index 0000000000..180d408017 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/assistant/tags/AssistantDesignationTag.kt @@ -0,0 +1,70 @@ +/* + * 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.quartz.experimental.decentralizedLists.assistant.tags + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer +import com.vitorpamplona.quartz.utils.Hex +import com.vitorpamplona.quartz.utils.ensure + +/** + * Tapestry Assistant Designation, the blanket entry on a user's kind 10040: + * `["39998:dlist-header", , ]` — "this pubkey authors my concept / + * list headers on my behalf". + * + * Signed by the user, so it is a delegation the user granted; revoked by republishing the Map + * without it. The relay may be the empty string when the writer had none configured, and the + * designation stands without it. + */ +@Immutable +data class AssistantDesignation( + val assistant: HexKey, + val relay: NormalizedRelayUrl? = null, +) { + fun toTagArray() = AssistantDesignationTag.assemble(assistant, relay) +} + +class AssistantDesignationTag { + companion object { + const val RESERVED_D_TAG = "dlist-header" + const val KEY = "39998:$RESERVED_D_TAG" + + fun isTag(tag: Array) = tag.has(1) && tag[0] == KEY && isPubKey(tag[1]) + + fun parse(tag: Array): AssistantDesignation? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == KEY) { return null } + ensure(isPubKey(tag[1])) { return null } + return AssistantDesignation(tag[1], tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) }) + } + + // Always three elements: an empty relay keeps the shape the spec fixes. + fun assemble( + assistant: HexKey, + relay: NormalizedRelayUrl?, + ) = arrayOf(KEY, assistant, relay?.url ?: "") + + internal fun isPubKey(value: String) = value.length == 64 && Hex.isHex64(value) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/assistant/tags/DListCurationTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/assistant/tags/DListCurationTag.kt new file mode 100644 index 0000000000..0cf6e693b9 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/assistant/tags/DListCurationTag.kt @@ -0,0 +1,82 @@ +/* + * 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.quartz.experimental.decentralizedLists.assistant.tags + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.Address +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer +import com.vitorpamplona.quartz.utils.ensure + +/** + * Tapestry Assistant Designation, a per-list entry on a user's kind 10040: + * `[":", , ]` — "my assistant curates this list for me, + * with the header at `::`". + * + * The d-tag is everything after the **first** colon and may itself contain colons. The word + * `dlist-header` is reserved for [AssistantDesignationTag] and is never a per-list d-tag. + */ +@Immutable +data class DListCuration( + val kind: Int, + val dTag: String, + val assistant: HexKey, + val relay: NormalizedRelayUrl? = null, +) { + /** The curated header: authorization is read from the Map, composition from this header. */ + fun headerAddress() = Address(kind, assistant, dTag) + + fun toTagArray() = DListCurationTag.assemble(this) +} + +class DListCurationTag { + companion object { + val KINDS = setOf(39998, 39999) + + fun key( + kind: Int, + dTag: String, + ) = "$kind:$dTag" + + fun isTag(tag: Array) = parse(tag) != null + + fun parse(tag: Array): DListCuration? { + ensure(tag.has(1)) { return null } + ensure(AssistantDesignationTag.isPubKey(tag[1])) { return null } + + val divider = tag[0].indexOf(':') + ensure(divider > 0) { return null } + + val kind = tag[0].substring(0, divider).toIntOrNull() ?: return null + ensure(kind in KINDS) { return null } + + val dTag = tag[0].substring(divider + 1) + ensure(dTag.isNotEmpty()) { return null } + ensure(dTag != AssistantDesignationTag.RESERVED_D_TAG) { return null } + + return DListCuration(kind, dTag, tag[1], tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) }) + } + + fun assemble(entry: DListCuration) = arrayOf(key(entry.kind, entry.dTag), entry.assistant, entry.relay?.url ?: "") + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/concepts/WordWrapper.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/concepts/WordWrapper.kt new file mode 100644 index 0000000000..4bdae9b55d --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/concepts/WordWrapper.kt @@ -0,0 +1,104 @@ +/* + * 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.quartz.experimental.decentralizedLists.concepts + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.JsonMapper +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.decodeFromJsonElement +import kotlinx.serialization.json.jsonObject + +/** + * The parsed `json` tag of a Tapestry node: a JSON object keyed by section. Every node in the + * word-wrapper layout has a [word] section plus one or more role sections (`conceptHeader`, + * `superset`, `set`, `property`, `graph`, `conceptGraph`, …), readable raw via [section]. + * Plain elements use the same object, keyed by concept slug (`{"dog": {...}}`). + */ +@Immutable +class WordWrapper( + val root: JsonObject, +) { + /** One section as raw JSON, or null when absent or not an object. */ + fun section(key: String) = root[key] as? JsonObject + + fun word(): WordSection? = decode(WORD) + + fun conceptHeader(): ConceptHeaderSection? = decode(CONCEPT_HEADER) + + private inline fun decode(key: String): T? { + val element = section(key) ?: return null + return runCatching { JsonMapper.jsonInstance.decodeFromJsonElement(element) }.getOrNull() + } + + companion object { + const val WORD = "word" + const val CONCEPT_HEADER = "conceptHeader" + + /** Null for anything that is not a JSON object: the tag is authored data, not trusted. */ + fun parse(json: String): WordWrapper? = runCatching { WordWrapper(JsonMapper.jsonInstance.parseToJsonElement(json).jsonObject) }.getOrNull() + } +} + +/** A reference to another node inside a word-wrapper payload. `uuid` carries its a-tag address. */ +@Immutable +@Serializable +data class NodeRef( + val slug: String? = null, + val uuid: String? = null, +) + +/** The universal `word` section. */ +@Immutable +@Serializable +data class WordSection( + val slug: String? = null, + val name: String? = null, + val title: String? = null, + // Nullable: this is authored JSON, and an explicit `null` must read as "absent" rather + // than fail the whole section. Use [types] / [memberOf] for the non-null view. + val wordTypes: List? = null, + /** Set on a concept's core nodes, pointing back at the concept; omitted by the header itself. */ + val coreMemberOf: List? = null, +) { + fun types() = wordTypes.orEmpty() + + fun memberOf() = coreMemberOf.orEmpty() +} + +@Immutable +@Serializable +data class SingularPluralJson( + val singular: String? = null, + val plural: String? = null, +) + +/** The `conceptHeader` section of a Concept Header node. */ +@Immutable +@Serializable +data class ConceptHeaderSection( + val description: String? = null, + val oNames: SingularPluralJson? = null, + val oSlugs: SingularPluralJson? = null, + val oKeys: SingularPluralJson? = null, + val oTitles: SingularPluralJson? = null, + val oLabels: SingularPluralJson? = null, +) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/header/AddressableListHeaderEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/header/AddressableListHeaderEvent.kt new file mode 100644 index 0000000000..97b91311b4 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/header/AddressableListHeaderEvent.kt @@ -0,0 +1,131 @@ +/* + * 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.quartz.experimental.decentralizedLists.header + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.experimental.decentralizedLists.AddressableDecentralizedListEvent +import com.vitorpamplona.quartz.experimental.decentralizedLists.description +import com.vitorpamplona.quartz.experimental.decentralizedLists.forEachSearchableListField +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.acceptedItemKinds +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.conceptGraph +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.itemKinds +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.tags.ConceptGraphTag +import com.vitorpamplona.quartz.experimental.decentralizedLists.inheritFrom +import com.vitorpamplona.quartz.experimental.decentralizedLists.inheritFromTargets +import com.vitorpamplona.quartz.experimental.decentralizedLists.isDeliberatelyUnaffiliated +import com.vitorpamplona.quartz.experimental.decentralizedLists.json +import com.vitorpamplona.quartz.experimental.decentralizedLists.searchableListContent +import com.vitorpamplona.quartz.experimental.decentralizedLists.tags.InheritType +import com.vitorpamplona.quartz.experimental.decentralizedLists.wordWrapper +import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag +import com.vitorpamplona.quartz.nip50Search.IndexableFieldVisitor +import com.vitorpamplona.quartz.nip50Search.SearchableEvent +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid + +/** + * Decentralized Lists: an editable list declaration, a.k.a. list header (kind 39998). + * + * Same tags as [ListHeaderEvent], but addressable: the author can revise it in place, and + * items point at it with a `z` tag holding its `39998::` coordinate, since its + * event id changes with every edit. + */ +@Immutable +class AddressableListHeaderEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig), + AddressableDecentralizedListEvent, + SearchableEvent { + override fun listPointer() = addressTag() + + override fun indexableContent() = tags.searchableListContent() + + override fun forEachIndexableField(visitor: IndexableFieldVisitor) { + tags.forEachSearchableListField(visitor) + } + + fun names() = tags.names() + + fun titles() = tags.titles() + + fun slugs() = tags.slugs() + + fun description() = tags.description() + + fun tagRules() = tags.tagRules() + + fun requiredTags() = tags.requiredTags() + + fun allowedTags() = tags.allowedTags() + + fun recommendedTags() = tags.recommendedTags() + + fun disallowedTags() = tags.disallowedTags() + + fun itemKinds() = tags.itemKinds() + + fun acceptedItemKinds() = tags.acceptedItemKinds() + + fun inheritFrom() = tags.inheritFrom() + + fun inheritFromTargets(type: InheritType) = tags.inheritFromTargets(type) + + fun isDeliberatelyUnaffiliated() = tags.isDeliberatelyUnaffiliated() + + fun json() = tags.json() + + fun wordWrapper() = tags.wordWrapper() + + /** + * The Concept Graph core node of this concept: the `concept-graph` tag if present, else + * computed from this header's pubkey and `d`, as the resolution contract requires. + */ + fun conceptGraph() = tags.conceptGraph() ?: ConceptGraphTag.compute(pubKey, dTag()) + + companion object { + const val KIND = 39998 + + @OptIn(ExperimentalUuidApi::class) + fun build( + singularName: String, + pluralName: String, + description: String? = null, + dTag: String = Uuid.random().toString(), + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, "", createdAt) { + dTag(dTag) + names(singularName, pluralName) + description?.let { this.description(it) } + initializer() + } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/header/ListHeaderEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/header/ListHeaderEvent.kt new file mode 100644 index 0000000000..cf3b8e6e97 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/header/ListHeaderEvent.kt @@ -0,0 +1,102 @@ +/* + * 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.quartz.experimental.decentralizedLists.header + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.experimental.decentralizedLists.DecentralizedListEvent +import com.vitorpamplona.quartz.experimental.decentralizedLists.description +import com.vitorpamplona.quartz.experimental.decentralizedLists.forEachSearchableListField +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.acceptedItemKinds +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.itemKinds +import com.vitorpamplona.quartz.experimental.decentralizedLists.searchableListContent +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip50Search.IndexableFieldVisitor +import com.vitorpamplona.quartz.nip50Search.SearchableEvent +import com.vitorpamplona.quartz.utils.TimeUtils + +/** + * Decentralized Lists: an immutable list declaration, a.k.a. list header (kind 9998). + * + * The author declares *what* the list is — its `names`, and which tags its items must, may, or + * must not carry — but not its members: anyone contributes items with kind 9999/39999 events + * whose `z` tag holds this event's id. Use [AddressableListHeaderEvent] (39998) for an editable + * header. + */ +@Immutable +class ListHeaderEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : Event(id, pubKey, createdAt, KIND, tags, content, sig), + DecentralizedListEvent, + SearchableEvent { + override fun listPointer() = id + + override fun indexableContent() = tags.searchableListContent() + + override fun forEachIndexableField(visitor: IndexableFieldVisitor) { + tags.forEachSearchableListField(visitor) + } + + fun names() = tags.names() + + fun titles() = tags.titles() + + fun slugs() = tags.slugs() + + fun description() = tags.description() + + fun tagRules() = tags.tagRules() + + fun requiredTags() = tags.requiredTags() + + fun allowedTags() = tags.allowedTags() + + fun recommendedTags() = tags.recommendedTags() + + fun disallowedTags() = tags.disallowedTags() + + fun itemKinds() = tags.itemKinds() + + fun acceptedItemKinds() = tags.acceptedItemKinds() + + companion object { + const val KIND = 9998 + + fun build( + singularName: String, + pluralName: String, + description: String? = null, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, "", createdAt) { + names(singularName, pluralName) + description?.let { this.description(it) } + initializer() + } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/header/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/header/TagArrayBuilderExt.kt new file mode 100644 index 0000000000..b1338126c5 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/header/TagArrayBuilderExt.kt @@ -0,0 +1,90 @@ +/* + * 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.quartz.experimental.decentralizedLists.header + +import com.vitorpamplona.quartz.experimental.decentralizedLists.AddressableDecentralizedListEvent +import com.vitorpamplona.quartz.experimental.decentralizedLists.DecentralizedListEvent +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.tags.ConceptGraphTag +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.tags.ItemKindTag +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.tags.NamesTag +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.tags.SlugsTag +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.tags.TagRule +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.tags.TagRuleTag +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.tags.TagRuleType +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.tags.TitlesTag +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder + +// Typed on the whole family, not just the header kinds: the spec's nonstandard method +// declares a list with a 9999/39999 item, which then carries these same tags. + +fun TagArrayBuilder.names( + singular: String, + plural: String, +) = addUnique(NamesTag.assemble(singular, plural)) + +fun TagArrayBuilder.titles( + singular: String, + plural: String, +) = addUnique(TitlesTag.assemble(singular, plural)) + +fun TagArrayBuilder.slugs( + singular: String, + plural: String, +) = addUnique(SlugsTag.assemble(singular, plural)) + +fun TagArrayBuilder.tagRule(rule: TagRule) = add(TagRuleTag.assemble(rule)) + +fun TagArrayBuilder.tagRules(rules: List) = addAll(TagRuleTag.assemble(rules)) + +fun TagArrayBuilder.required( + tagName: String, + description: String? = null, +) = add(TagRuleTag.assemble(TagRuleType.REQUIRED, tagName, description)) + +fun TagArrayBuilder.allowed( + tagName: String, + description: String? = null, +) = add(TagRuleTag.assemble(TagRuleType.ALLOWED, tagName, description)) + +fun TagArrayBuilder.recommended( + tagName: String, + description: String? = null, +) = add(TagRuleTag.assemble(TagRuleType.RECOMMENDED, tagName, description)) + +fun TagArrayBuilder.disallowed( + tagName: String, + description: String? = null, +) = add(TagRuleTag.assemble(TagRuleType.DISALLOWED, tagName, description)) + +fun TagArrayBuilder.itemKind( + kind: Int, + description: String? = null, +) = add(ItemKindTag.assemble(kind, description)) + +/** + * The header's own Concept Graph pointer. Takes the signing pubkey and `d` because the value + * is derived from them; a template has no pubkey until it is signed. + */ +fun TagArrayBuilder.conceptGraph( + pubKey: HexKey, + dTag: String, +) = addUnique(ConceptGraphTag.assemble(pubKey, dTag)) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/header/TagArrayExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/header/TagArrayExt.kt new file mode 100644 index 0000000000..27d2e7318b --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/header/TagArrayExt.kt @@ -0,0 +1,68 @@ +/* + * 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.quartz.experimental.decentralizedLists.header + +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.tags.ConceptGraphTag +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.tags.ItemKindTag +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.tags.NamesTag +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.tags.SlugsTag +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.tags.TagRuleTag +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.tags.TagRuleType +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.tags.TitlesTag +import com.vitorpamplona.quartz.nip01Core.core.TagArray +import com.vitorpamplona.quartz.nip01Core.core.fastAny +import com.vitorpamplona.quartz.nip01Core.core.fastFirstNotNullOfOrNull + +fun TagArray.names() = fastFirstNotNullOfOrNull(NamesTag::parse) + +fun TagArray.titles() = fastFirstNotNullOfOrNull(TitlesTag::parse) + +fun TagArray.slugs() = fastFirstNotNullOfOrNull(SlugsTag::parse) + +/** Every `required` / `allowed` / `recommended` / `disallowed` rule, in tag order. */ +fun TagArray.tagRules() = mapNotNull(TagRuleTag::parse) + +fun TagArray.tagRuleNames(type: TagRuleType) = mapNotNull { TagRuleTag.parseTagName(it, type) } + +fun TagArray.requiredTags() = tagRuleNames(TagRuleType.REQUIRED) + +fun TagArray.allowedTags() = tagRuleNames(TagRuleType.ALLOWED) + +fun TagArray.recommendedTags() = tagRuleNames(TagRuleType.RECOMMENDED) + +fun TagArray.disallowedTags() = tagRuleNames(TagRuleType.DISALLOWED) + +/** + * True when the tags carry a list declaration. Standard headers (9998/39998) must; an item + * (9999/39999) that does is using the spec's "nonstandard" method to declare a list. + */ +fun TagArray.declaresList() = fastAny(NamesTag::isTag) + +/** The `concept-graph` pointer when present. See [ConceptGraphTag.compute] for the fallback. */ +fun TagArray.conceptGraph() = fastFirstNotNullOfOrNull(ConceptGraphTag::parse) + +/** The `item-kind` declarations, in tag order. */ +fun TagArray.itemKinds() = mapNotNull(ItemKindTag::parse) + +/** The kinds to query for this list's items: the declared `item-kind`s, else 9999 and 39999. */ +fun TagArray.acceptedItemKinds(): List = itemKinds().map { it.kind }.distinct().ifEmpty { STANDARD_ITEM_KINDS } + +val STANDARD_ITEM_KINDS = listOf(9999, 39999) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/header/tags/ConceptGraphTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/header/tags/ConceptGraphTag.kt new file mode 100644 index 0000000000..45a1a639be --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/header/tags/ConceptGraphTag.kt @@ -0,0 +1,66 @@ +/* + * 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.quartz.experimental.decentralizedLists.header.tags + +import com.vitorpamplona.quartz.nip01Core.core.Address +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.utils.ensure + +/** + * Tapestry Concepts: `["concept-graph", "39999::-concept-graph"]` on a kind 39998 + * concept header, pointing at the concept's Concept Graph core node. + * + * The value is computed from the header's own pubkey and `d`, never looked up, so it is right + * even before that node exists — and a reader facing a header without the tag computes the + * same address ([compute]). + */ +class ConceptGraphTag { + companion object { + const val TAG_NAME = "concept-graph" + const val SUFFIX = "-concept-graph" + + fun isTag(tag: Array) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty() + + fun parse(tag: Array): String? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + return tag[1] + } + + /** The Concept Graph address of the concept whose header is `39998::`. */ + fun compute( + pubKey: HexKey, + dTag: String, + ) = Address.assemble(CONCEPT_GRAPH_NODE_KIND, pubKey, dTag + SUFFIX) + + fun assemble(address: String) = arrayOf(TAG_NAME, address) + + fun assemble( + pubKey: HexKey, + dTag: String, + ) = assemble(compute(pubKey, dTag)) + + /** Core nodes are kind 39999 items. */ + const val CONCEPT_GRAPH_NODE_KIND = 39999 + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/header/tags/ItemKindTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/header/tags/ItemKindTag.kt new file mode 100644 index 0000000000..39cbee0a47 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/header/tags/ItemKindTag.kt @@ -0,0 +1,64 @@ +/* + * 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.quartz.experimental.decentralizedLists.header.tags + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.utils.arrayOfNotNull +import com.vitorpamplona.quartz.utils.ensure + +/** + * Decentralized Lists Cross-NIP Compatibility: `["item-kind", "", ?]` on a + * list header, one per accepted kind. It widens the items a list accepts beyond 9999/39999 to + * events of other NIPs (e.g. 34550 NIP-72 communities) that carry `z` tags themselves, or that + * a curator's 9999/39999 references with an `a` tag. + * + * A header without any `item-kind` accepts only 9999/39999. Its `required`/`allowed` rules + * apply on top of whatever the foreign kind's own NIP requires, never instead of it. + */ +@Immutable +data class ItemKind( + val kind: Int, + val description: String? = null, +) + +class ItemKindTag { + companion object { + const val TAG_NAME = "item-kind" + + fun isTag(tag: Array) = parse(tag) != null + + fun parse(tag: Array): ItemKind? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + val kind = tag[1].toIntOrNull() ?: return null + ensure(kind >= 0) { return null } + return ItemKind(kind, tag.getOrNull(2)?.ifEmpty { null }) + } + + fun assemble( + kind: Int, + description: String? = null, + ) = arrayOfNotNull(TAG_NAME, kind.toString(), description) + + fun assemble(itemKind: ItemKind) = assemble(itemKind.kind, itemKind.description) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/header/tags/NamesTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/header/tags/NamesTag.kt new file mode 100644 index 0000000000..6e5164314d --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/header/tags/NamesTag.kt @@ -0,0 +1,39 @@ +/* + * 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.quartz.experimental.decentralizedLists.header.tags + +/** `["names", , ]`. Required on every list header, e.g. `["names", "widget", "widgets"]`. */ +class NamesTag { + companion object { + const val TAG_NAME = "names" + + fun isTag(tag: Array) = SingularPlural.isTag(tag, TAG_NAME) + + fun parse(tag: Array) = SingularPlural.parse(tag, TAG_NAME) + + fun assemble( + singular: String, + plural: String, + ) = SingularPlural.assemble(TAG_NAME, singular, plural) + + fun assemble(value: SingularPlural) = assemble(value.singular, value.plural) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/header/tags/SingularPlural.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/header/tags/SingularPlural.kt new file mode 100644 index 0000000000..cf25129482 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/header/tags/SingularPlural.kt @@ -0,0 +1,61 @@ +/* + * 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.quartz.experimental.decentralizedLists.header.tags + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.utils.ensure + +/** + * The value of the `names`, `titles` and `slugs` header tags: a singular form ("widget") + * followed by a plural form ("widgets"). + */ +@Immutable +data class SingularPlural( + val singular: String, + val plural: String, +) { + companion object { + // Both forms are required by the spec. A tag missing either one is malformed and + // dropped rather than guessed at: pluralizing someone else's noun is not our call. + fun isTag( + tag: Array, + tagName: String, + ) = tag.has(2) && tag[0] == tagName && tag[1].isNotEmpty() && tag[2].isNotEmpty() + + fun parse( + tag: Array, + tagName: String, + ): SingularPlural? { + ensure(tag.has(2)) { return null } + ensure(tag[0] == tagName) { return null } + ensure(tag[1].isNotEmpty()) { return null } + ensure(tag[2].isNotEmpty()) { return null } + return SingularPlural(tag[1], tag[2]) + } + + fun assemble( + tagName: String, + singular: String, + plural: String, + ) = arrayOf(tagName, singular, plural) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/header/tags/SlugsTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/header/tags/SlugsTag.kt new file mode 100644 index 0000000000..0050b83235 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/header/tags/SlugsTag.kt @@ -0,0 +1,39 @@ +/* + * 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.quartz.experimental.decentralizedLists.header.tags + +/** `["slugs", , ]`. Optional URL-friendly identifier in singular and plural form. */ +class SlugsTag { + companion object { + const val TAG_NAME = "slugs" + + fun isTag(tag: Array) = SingularPlural.isTag(tag, TAG_NAME) + + fun parse(tag: Array) = SingularPlural.parse(tag, TAG_NAME) + + fun assemble( + singular: String, + plural: String, + ) = SingularPlural.assemble(TAG_NAME, singular, plural) + + fun assemble(value: SingularPlural) = assemble(value.singular, value.plural) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/header/tags/TagRuleTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/header/tags/TagRuleTag.kt new file mode 100644 index 0000000000..84060fd5c6 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/header/tags/TagRuleTag.kt @@ -0,0 +1,105 @@ +/* + * 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.quartz.experimental.decentralizedLists.header.tags + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.utils.arrayOfNotNull +import com.vitorpamplona.quartz.utils.ensure + +/** How a list header constrains a tag on its children. The code is also the tag name. */ +enum class TagRuleType( + val code: String, +) { + REQUIRED("required"), + ALLOWED("allowed"), + RECOMMENDED("recommended"), + DISALLOWED("disallowed"), + ; + + companion object { + fun fromCode(code: String) = + when (code) { + REQUIRED.code -> REQUIRED + ALLOWED.code -> ALLOWED + RECOMMENDED.code -> RECOMMENDED + DISALLOWED.code -> DISALLOWED + else -> null + } + } +} + +/** + * One constraint a list header places on its items: `["required", "p"]`, optionally with a + * human-readable description of what the named tag represents: + * `["required", "p", "Pubkey of the person being endorsed"]`. + * + * The spec requires one tag per constrained name — `["required", "foo"], ["required", "bar"]`, + * never `["required", "foo", "bar"]`. That makes index 2 unambiguous: it is always the + * description, never a second tag name. The description is informational only. + */ +@Immutable +data class TagRule( + val type: TagRuleType, + val tagName: String, + val description: String? = null, +) { + fun toTagArray() = TagRuleTag.assemble(type, tagName, description) +} + +class TagRuleTag { + companion object { + fun isTag(tag: Array) = tag.has(1) && TagRuleType.fromCode(tag[0]) != null && tag[1].isNotEmpty() + + fun isTag( + tag: Array, + type: TagRuleType, + ) = tag.has(1) && tag[0] == type.code && tag[1].isNotEmpty() + + fun parse(tag: Array): TagRule? { + ensure(tag.has(1)) { return null } + ensure(tag[1].isNotEmpty()) { return null } + val type = TagRuleType.fromCode(tag[0]) ?: return null + return TagRule(type, tag[1], tag.getOrNull(2)?.ifEmpty { null }) + } + + /** The constrained tag name when [tag] is a rule of [type]. Skips building the rule. */ + fun parseTagName( + tag: Array, + type: TagRuleType, + ): String? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == type.code) { return null } + ensure(tag[1].isNotEmpty()) { return null } + return tag[1] + } + + fun assemble( + type: TagRuleType, + tagName: String, + description: String? = null, + ) = arrayOfNotNull(type.code, tagName, description) + + fun assemble(rule: TagRule) = assemble(rule.type, rule.tagName, rule.description) + + fun assemble(rules: List) = rules.map { assemble(it) } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/header/tags/TitlesTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/header/tags/TitlesTag.kt new file mode 100644 index 0000000000..98ceff1af7 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/header/tags/TitlesTag.kt @@ -0,0 +1,39 @@ +/* + * 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.quartz.experimental.decentralizedLists.header.tags + +/** `["titles", , ]`. Optional display title in singular and plural form. */ +class TitlesTag { + companion object { + const val TAG_NAME = "titles" + + fun isTag(tag: Array) = SingularPlural.isTag(tag, TAG_NAME) + + fun parse(tag: Array) = SingularPlural.parse(tag, TAG_NAME) + + fun assemble( + singular: String, + plural: String, + ) = SingularPlural.assemble(TAG_NAME, singular, plural) + + fun assemble(value: SingularPlural) = assemble(value.singular, value.plural) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/item/AddressableListItemEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/item/AddressableListItemEvent.kt new file mode 100644 index 0000000000..cdc356d13b --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/item/AddressableListItemEvent.kt @@ -0,0 +1,207 @@ +/* + * 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.quartz.experimental.decentralizedLists.item + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.experimental.decentralizedLists.AddressableDecentralizedListEvent +import com.vitorpamplona.quartz.experimental.decentralizedLists.DecentralizedListEvent +import com.vitorpamplona.quartz.experimental.decentralizedLists.DecentralizedListItem +import com.vitorpamplona.quartz.experimental.decentralizedLists.description +import com.vitorpamplona.quartz.experimental.decentralizedLists.forEachSearchableListField +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.acceptedItemKinds +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.allowedTags +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.declaresList +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.disallowedTags +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.itemKinds +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.names +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.recommendedTags +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.requiredTags +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.slugs +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.tagRules +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.titles +import com.vitorpamplona.quartz.experimental.decentralizedLists.inheritFrom +import com.vitorpamplona.quartz.experimental.decentralizedLists.inheritFromTargets +import com.vitorpamplona.quartz.experimental.decentralizedLists.isDeliberatelyUnaffiliated +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.tags.ParentList +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.tags.ParentListTag +import com.vitorpamplona.quartz.experimental.decentralizedLists.json +import com.vitorpamplona.quartz.experimental.decentralizedLists.searchableListContent +import com.vitorpamplona.quartz.experimental.decentralizedLists.tags.InheritType +import com.vitorpamplona.quartz.experimental.decentralizedLists.wordWrapper +import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.core.fastForEach +import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag +import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip50Search.IndexableFieldVisitor +import com.vitorpamplona.quartz.nip50Search.SearchableEvent +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid + +/** + * Decentralized Lists: an editable list item declaration (kind 39999). + * + * Same tags as [ListItemEvent], plus the `d` tag that makes it addressable, so the contributor + * can revise the item in place. When it declares a list through the nonstandard method, its + * children point at its `39999:pubkey:d` coordinate. + */ +@Immutable +class AddressableListItemEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig), + DecentralizedListItem, + AddressableDecentralizedListEvent, + EventHintProvider, + AddressHintProvider, + PubKeyHintProvider, + SearchableEvent { + override fun listPointer() = addressTag() + + override fun eventHints() = tags.mapNotNull(ETag::parseAsHint) + + /** Event items plus any parent lists referenced by id, in one pass. */ + override fun linkedEventIds(): List { + val out = ArrayList() + tags.fastForEach { tag -> (ETag.parseId(tag) ?: ParentListTag.parseEventId(tag))?.let { out.add(it) } } + return out + } + + override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) + + /** Addressable items plus any parent lists referenced by coordinate, in one pass. */ + override fun linkedAddressIds(): List { + val out = ArrayList() + tags.fastForEach { tag -> (ATag.parseValidAddress(tag) ?: ParentListTag.parseCoordinate(tag))?.let { out.add(it) } } + return out + } + + override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + + override fun linkedPubKeys() = tags.mapNotNull(PTag::parseKey) + + override fun indexableContent() = tags.searchableListContent() + + override fun forEachIndexableField(visitor: IndexableFieldVisitor) { + tags.forEachSearchableListField(visitor) + } + + fun parentLists() = tags.parentLists() + + fun parentListPointers() = tags.parentListPointers() + + fun name() = tags.name() + + fun title() = tags.title() + + fun slug() = tags.slug() + + fun description() = tags.description() + + fun comments() = tags.comments() + + fun itemPubKeys() = tags.itemPubKeys() + + fun itemEvents() = tags.itemEvents() + + fun itemStrings() = tags.itemStrings() + + fun itemAddresses() = tags.itemAddresses() + + /** + * True when this item uses the spec's nonstandard method to declare a list: it carries a + * `names` tag and sits on a list of lists. Its header fields then read the same way as a + * 9998's, and children point at it through [listPointer]. + */ + fun declaresList() = tags.declaresList() + + fun names() = tags.names() + + fun titles() = tags.titles() + + fun slugs() = tags.slugs() + + fun tagRules() = tags.tagRules() + + fun requiredTags() = tags.requiredTags() + + fun allowedTags() = tags.allowedTags() + + fun recommendedTags() = tags.recommendedTags() + + fun disallowedTags() = tags.disallowedTags() + + fun itemKinds() = tags.itemKinds() + + fun acceptedItemKinds() = tags.acceptedItemKinds() + + fun inheritFrom() = tags.inheritFrom() + + fun inheritFromTargets(type: InheritType) = tags.inheritFromTargets(type) + + fun isDeliberatelyUnaffiliated() = tags.isDeliberatelyUnaffiliated() + + fun json() = tags.json() + + fun wordWrapper() = tags.wordWrapper() + + fun elementOf() = tags.elementOf() + + fun subsetOf() = tags.subsetOf() + + companion object { + const val KIND = 39999 + + @OptIn(ExperimentalUuidApi::class) + fun build( + parent: DecentralizedListEvent, + dTag: String = Uuid.random().toString(), + createdAt: Long = TimeUtils.now(), + content: String = "", + initializer: TagArrayBuilder.() -> Unit = {}, + ) = build(ParentListTag.classify(parent.listPointer()), dTag, createdAt, content, initializer) + + @OptIn(ExperimentalUuidApi::class) + fun build( + parent: ParentList, + dTag: String = Uuid.random().toString(), + createdAt: Long = TimeUtils.now(), + content: String = "", + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, content, createdAt) { + dTag(dTag) + parentList(parent) + initializer() + } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/item/ListItemEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/item/ListItemEvent.kt new file mode 100644 index 0000000000..ba838382d3 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/item/ListItemEvent.kt @@ -0,0 +1,182 @@ +/* + * 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.quartz.experimental.decentralizedLists.item + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.experimental.decentralizedLists.DecentralizedListEvent +import com.vitorpamplona.quartz.experimental.decentralizedLists.DecentralizedListItem +import com.vitorpamplona.quartz.experimental.decentralizedLists.description +import com.vitorpamplona.quartz.experimental.decentralizedLists.forEachSearchableListField +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.acceptedItemKinds +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.allowedTags +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.declaresList +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.disallowedTags +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.itemKinds +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.names +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.recommendedTags +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.requiredTags +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.slugs +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.tagRules +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.titles +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.tags.ParentList +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.tags.ParentListTag +import com.vitorpamplona.quartz.experimental.decentralizedLists.searchableListContent +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.core.fastForEach +import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip50Search.IndexableFieldVisitor +import com.vitorpamplona.quartz.nip50Search.SearchableEvent +import com.vitorpamplona.quartz.utils.TimeUtils + +/** + * Decentralized Lists: an immutable list item declaration (kind 9999). + * + * Anyone can publish one. Its `z` tags name the list(s) it belongs to — a header's event id, a + * header's `39998:pubkey:d` coordinate, or just a list name for undeclared lists — and its + * `p` / `e` / `t` / `a` tags carry the item(s) themselves, as the parent header's `required` + * and `allowed` rules ask. One `z` and one item tag per event is recommended, but several of + * each are valid. Use [AddressableListItemEvent] (39999) for an editable item. + * + * Curation is out of scope for the spec: whether an item is accepted is up to the reader's + * trust metric, typically fed by NIP-25 `+`/`-` reactions to this event. + */ +@Immutable +class ListItemEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : Event(id, pubKey, createdAt, KIND, tags, content, sig), + DecentralizedListItem, + EventHintProvider, + AddressHintProvider, + PubKeyHintProvider, + SearchableEvent { + override fun listPointer() = id + + override fun eventHints() = tags.mapNotNull(ETag::parseAsHint) + + /** Event items plus any parent lists referenced by id, in one pass. */ + override fun linkedEventIds(): List { + val out = ArrayList() + tags.fastForEach { tag -> (ETag.parseId(tag) ?: ParentListTag.parseEventId(tag))?.let { out.add(it) } } + return out + } + + override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) + + /** Addressable items plus any parent lists referenced by coordinate, in one pass. */ + override fun linkedAddressIds(): List { + val out = ArrayList() + tags.fastForEach { tag -> (ATag.parseValidAddress(tag) ?: ParentListTag.parseCoordinate(tag))?.let { out.add(it) } } + return out + } + + override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + + override fun linkedPubKeys() = tags.mapNotNull(PTag::parseKey) + + override fun indexableContent() = tags.searchableListContent() + + override fun forEachIndexableField(visitor: IndexableFieldVisitor) { + tags.forEachSearchableListField(visitor) + } + + fun parentLists() = tags.parentLists() + + fun parentListPointers() = tags.parentListPointers() + + fun name() = tags.name() + + fun title() = tags.title() + + fun slug() = tags.slug() + + fun description() = tags.description() + + fun comments() = tags.comments() + + fun itemPubKeys() = tags.itemPubKeys() + + fun itemEvents() = tags.itemEvents() + + fun itemStrings() = tags.itemStrings() + + fun itemAddresses() = tags.itemAddresses() + + /** + * True when this item uses the spec's nonstandard method to declare a list: it carries a + * `names` tag and sits on a list of lists. Its header fields then read the same way as a + * 9998's, and children point at it through [listPointer]. + */ + fun declaresList() = tags.declaresList() + + fun names() = tags.names() + + fun titles() = tags.titles() + + fun slugs() = tags.slugs() + + fun tagRules() = tags.tagRules() + + fun requiredTags() = tags.requiredTags() + + fun allowedTags() = tags.allowedTags() + + fun recommendedTags() = tags.recommendedTags() + + fun disallowedTags() = tags.disallowedTags() + + fun itemKinds() = tags.itemKinds() + + fun acceptedItemKinds() = tags.acceptedItemKinds() + + companion object { + const val KIND = 9999 + + fun build( + parent: DecentralizedListEvent, + createdAt: Long = TimeUtils.now(), + content: String = "", + initializer: TagArrayBuilder.() -> Unit = {}, + ) = build(ParentListTag.classify(parent.listPointer()), createdAt, content, initializer) + + fun build( + parent: ParentList, + createdAt: Long = TimeUtils.now(), + content: String = "", + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, content, createdAt) { + parentList(parent) + initializer() + } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/item/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/item/TagArrayBuilderExt.kt new file mode 100644 index 0000000000..430a10c3a3 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/item/TagArrayBuilderExt.kt @@ -0,0 +1,86 @@ +/* + * 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.quartz.experimental.decentralizedLists.item + +import com.vitorpamplona.quartz.experimental.decentralizedLists.DecentralizedListEvent +import com.vitorpamplona.quartz.experimental.decentralizedLists.DecentralizedListItem +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.tags.CommentsTag +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.tags.ElementOfTag +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.tags.NameTag +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.tags.ParentList +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.tags.ParentListTag +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.tags.SlugTag +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.tags.SubsetOfTag +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.tags.TitleTag +import com.vitorpamplona.quartz.nip01Core.core.Address +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.HashtagTag +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.utils.arrayOfNotNull + +fun TagArrayBuilder.parentList(parent: DecentralizedListEvent) = addUniqueValueIfNew(ParentListTag.assemble(parent)) + +fun TagArrayBuilder.parentList(parent: ParentList) = addUniqueValueIfNew(ParentListTag.assemble(parent)) + +/** Points at a list that was never formally declared, by its singular name (e.g. "dog"). */ +fun TagArrayBuilder.parentListName(name: String) = addUniqueValueIfNew(ParentListTag.assemble(name)) + +fun TagArrayBuilder.parentLists(parents: List) = addAllUniqueValueIfNew(ParentListTag.assemble(parents)) + +fun TagArrayBuilder.name(name: String) = addUnique(NameTag.assemble(name)) + +fun TagArrayBuilder.title(title: String) = addUnique(TitleTag.assemble(title)) + +fun TagArrayBuilder.slug(slug: String) = addUnique(SlugTag.assemble(slug)) + +fun TagArrayBuilder.comments(comments: String) = addUnique(CommentsTag.assemble(comments)) + +fun TagArrayBuilder.itemPubKey( + pubKey: HexKey, + relayHint: NormalizedRelayUrl? = null, +) = add(PTag.assemble(pubKey, relayHint)) + +fun TagArrayBuilder.itemEvent( + eventId: HexKey, + relayHint: NormalizedRelayUrl? = null, + author: HexKey? = null, +) = add( + // Pads the relay slot when only the author is known, so the author stays at index 3. + arrayOfNotNull(ETag.TAG_NAME, eventId, relayHint?.url ?: author?.let { "" }, author), +) + +/** A string item, written verbatim: unlike a hashtag it is not duplicated in lowercase. */ +fun TagArrayBuilder.itemString(value: String) = add(HashtagTag.assemble(value)) + +fun TagArrayBuilder.itemAddress( + address: Address, + relayHint: NormalizedRelayUrl? = null, +) = add(ATag.assemble(address, relayHint)) + +// `n` and `s` are defined for kind 39999 only. + +fun TagArrayBuilder.elementOf(parent: Address) = addUniqueValueIfNew(ElementOfTag.assemble(parent)) + +fun TagArrayBuilder.subsetOf(parent: Address) = addUniqueValueIfNew(SubsetOfTag.assemble(parent)) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/item/TagArrayExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/item/TagArrayExt.kt new file mode 100644 index 0000000000..1ffb8c4897 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/item/TagArrayExt.kt @@ -0,0 +1,70 @@ +/* + * 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.quartz.experimental.decentralizedLists.item + +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.tags.CommentsTag +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.tags.ElementOfTag +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.tags.NameTag +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.tags.ParentListTag +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.tags.SlugTag +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.tags.SubsetOfTag +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.tags.TitleTag +import com.vitorpamplona.quartz.nip01Core.core.TagArray +import com.vitorpamplona.quartz.nip01Core.core.fastFirstNotNullOfOrNull +import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.HashtagTag +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag + +/** Every list this item belongs to. One `z` tag per list; an item may sit on several. */ +fun TagArray.parentLists() = mapNotNull(ParentListTag::parse) + +/** The raw `z` values, as they would go into a `#z` filter. */ +fun TagArray.parentListPointers() = mapNotNull(ParentListTag::parseValue) + +fun TagArray.name() = fastFirstNotNullOfOrNull(NameTag::parse) + +fun TagArray.title() = fastFirstNotNullOfOrNull(TitleTag::parse) + +fun TagArray.slug() = fastFirstNotNullOfOrNull(SlugTag::parse) + +fun TagArray.comments() = fastFirstNotNullOfOrNull(CommentsTag::parse) + +/** Pubkeys declared as items (`p`). */ +fun TagArray.itemPubKeys() = mapNotNull(PTag::parse) + +/** Events declared as items (`e`). */ +fun TagArray.itemEvents() = mapNotNull(ETag::parse) + +/** + * Strings declared as items (`t`), with their case preserved. These are list values like + * "Switzerland" or "Fido", not hashtags, so they are not lowercased the way hashtag readers do. + */ +fun TagArray.itemStrings() = mapNotNull(HashtagTag::parse) + +/** Addressable events declared as items (`a`). Accepts both `kind:pubkey:d` and `naddr1…`. */ +fun TagArray.itemAddresses() = mapNotNull(ATag::parse) + +/** The sets/supersets this item claims to be an element of (`n`). */ +fun TagArray.elementOf() = mapNotNull(ElementOfTag::parse) + +/** The supersets this set claims to be a subset of (`s`). */ +fun TagArray.subsetOf() = mapNotNull(SubsetOfTag::parse) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/item/tags/CommentsTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/item/tags/CommentsTag.kt new file mode 100644 index 0000000000..1fca4dd2fb --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/item/tags/CommentsTag.kt @@ -0,0 +1,42 @@ +/* + * 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.quartz.experimental.decentralizedLists.item.tags + +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.utils.ensure + +/** `["comments", ]`. Free-text commentary from the contributor about the item. */ +class CommentsTag { + companion object { + const val TAG_NAME = "comments" + + fun isTag(tag: Array) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty() + + fun parse(tag: Array): String? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + return tag[1] + } + + fun assemble(value: String) = arrayOf(TAG_NAME, value) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/item/tags/ElementOfTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/item/tags/ElementOfTag.kt new file mode 100644 index 0000000000..9e209e0f9c --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/item/tags/ElementOfTag.kt @@ -0,0 +1,53 @@ +/* + * 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.quartz.experimental.decentralizedLists.item.tags + +import com.vitorpamplona.quartz.nip01Core.core.Address +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.utils.ensure + +/** + * Class Thread Relationships: `["n", ]` — this item is an element of that set or superset. A consumer derives `(parent)-[HAS_ELEMENT]->(this)`. + * + * Defined for kind 39999 only, child-claims-parent, several allowed. Consumers must attribute + * the derived relationship to a curator's graph only when the event is signed by that curator: + * anyone can publish a tag naming someone else's node. + */ +class ElementOfTag { + companion object { + const val TAG_NAME = "n" + + fun isTag(tag: Array) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty() + + fun parse(tag: Array): String? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + return tag[1] + } + + fun parseAddress(tag: Array): Address? = parse(tag)?.let { Address.parse(it) } + + fun assemble(parent: String) = arrayOf(TAG_NAME, parent) + + fun assemble(parent: Address) = assemble(parent.toValue()) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/item/tags/NameTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/item/tags/NameTag.kt new file mode 100644 index 0000000000..b94f914b6e --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/item/tags/NameTag.kt @@ -0,0 +1,42 @@ +/* + * 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.quartz.experimental.decentralizedLists.item.tags + +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.utils.ensure + +/** `["name", ]`. The item's name, e.g. `["name", "Derek Ross"]`. */ +class NameTag { + companion object { + const val TAG_NAME = "name" + + fun isTag(tag: Array) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty() + + fun parse(tag: Array): String? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + return tag[1] + } + + fun assemble(value: String) = arrayOf(TAG_NAME, value) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/item/tags/ParentListTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/item/tags/ParentListTag.kt new file mode 100644 index 0000000000..19753a4e7b --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/item/tags/ParentListTag.kt @@ -0,0 +1,133 @@ +/* + * 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.quartz.experimental.decentralizedLists.item.tags + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.experimental.decentralizedLists.CoordinateShape +import com.vitorpamplona.quartz.experimental.decentralizedLists.DecentralizedListEvent +import com.vitorpamplona.quartz.nip01Core.core.Address +import com.vitorpamplona.quartz.nip01Core.core.AddressSerializer +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.utils.Hex +import com.vitorpamplona.quartz.utils.ensure + +/** + * What a `z` tag points at. The spec allows three shapes in the same slot, told apart by form: + * + * - [EventId]: a 64-char hex id, for a kind 9998 header (or a 9999 used as a nonstandard header); + * - [Coordinate]: `kind:pubkey:d`, for a kind 39998 header, whose id changes on every edit; + * - [Name]: anything else — the singular name of a list that was never formally declared, + * e.g. `["z", "dog"]`. The spec allows it but prefers a declared header. + */ +@Immutable +sealed interface ParentList { + /** The raw value to write back in the `z` tag. */ + val value: String + + @Immutable + data class EventId( + val eventId: HexKey, + ) : ParentList { + override val value get() = eventId + } + + @Immutable + data class Coordinate( + val address: Address, + ) : ParentList { + override val value get() = address.toValue() + } + + @Immutable + data class Name( + val name: String, + ) : ParentList { + override val value get() = name + } +} + +/** `["z",
]`: the list an item belongs to. */ +class ParentListTag { + companion object { + const val TAG_NAME = "z" + + fun isTag(tag: Array) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty() + + fun isTagged( + tag: Array, + pointer: String, + ) = tag.has(1) && tag[0] == TAG_NAME && tag[1] == pointer + + fun isTagged( + tag: Array, + pointers: Set, + ) = tag.has(1) && tag[0] == TAG_NAME && tag[1] in pointers + + /** The raw pointer, without classifying it. */ + fun parseValue(tag: Array): String? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + return tag[1] + } + + fun parse(tag: Array): ParentList? = parseValue(tag)?.let(::classify) + + fun parseEventId(tag: Array): HexKey? { + val value = parseValue(tag) ?: return null + return if (isEventId(value)) value else null + } + + fun parseAddress(tag: Array): Address? { + val value = parseValue(tag) ?: return null + return if (looksLikeAddress(value)) AddressSerializer.parse(value) else null + } + + /** The raw `kind:pubkey:d` value when [tag] points at a coordinate. No parsing, no allocation. */ + fun parseCoordinate(tag: Array): String? { + val value = parseValue(tag) ?: return null + return if (looksLikeAddress(value)) value else null + } + + fun classify(value: String): ParentList { + if (isEventId(value)) return ParentList.EventId(value) + if (looksLikeAddress(value)) { + AddressSerializer.parse(value)?.let { return ParentList.Coordinate(it) } + } + return ParentList.Name(value) + } + + private fun isEventId(value: String) = value.length == 64 && Hex.isHex64(value) + + // Only coordinate-shaped values reach the address parser: it logs a warning for + // everything it rejects, and a plain list name such as "dog" is not an error. + private fun looksLikeAddress(value: String) = CoordinateShape.matches(value) + + fun assemble(pointer: String) = arrayOf(TAG_NAME, pointer) + + fun assemble(parent: ParentList) = assemble(parent.value) + + fun assemble(parent: DecentralizedListEvent) = assemble(parent.listPointer()) + + fun assemble(parents: List) = parents.map { assemble(it) } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/item/tags/SlugTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/item/tags/SlugTag.kt new file mode 100644 index 0000000000..06ecbae8ff --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/item/tags/SlugTag.kt @@ -0,0 +1,42 @@ +/* + * 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.quartz.experimental.decentralizedLists.item.tags + +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.utils.ensure + +/** `["slug", ]`. A URL-friendly identifier for the item. */ +class SlugTag { + companion object { + const val TAG_NAME = "slug" + + fun isTag(tag: Array) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty() + + fun parse(tag: Array): String? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + return tag[1] + } + + fun assemble(value: String) = arrayOf(TAG_NAME, value) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/item/tags/SubsetOfTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/item/tags/SubsetOfTag.kt new file mode 100644 index 0000000000..bfb58b3c2c --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/item/tags/SubsetOfTag.kt @@ -0,0 +1,53 @@ +/* + * 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.quartz.experimental.decentralizedLists.item.tags + +import com.vitorpamplona.quartz.nip01Core.core.Address +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.utils.ensure + +/** + * Class Thread Relationships: `["s", ]` — this set or superset is a subset of that superset. A consumer derives `(parent)-[IS_A_SUPERSET_OF]->(this)`. + * + * Defined for kind 39999 only, child-claims-parent, several allowed. Consumers must attribute + * the derived relationship to a curator's graph only when the event is signed by that curator: + * anyone can publish a tag naming someone else's node. + */ +class SubsetOfTag { + companion object { + const val TAG_NAME = "s" + + fun isTag(tag: Array) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty() + + fun parse(tag: Array): String? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + return tag[1] + } + + fun parseAddress(tag: Array): Address? = parse(tag)?.let { Address.parse(it) } + + fun assemble(parent: String) = arrayOf(TAG_NAME, parent) + + fun assemble(parent: Address) = assemble(parent.toValue()) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/item/tags/TitleTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/item/tags/TitleTag.kt new file mode 100644 index 0000000000..5809c759de --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/item/tags/TitleTag.kt @@ -0,0 +1,42 @@ +/* + * 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.quartz.experimental.decentralizedLists.item.tags + +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.utils.ensure + +/** `["title", ]`. The item's display title. */ +class TitleTag { + companion object { + const val TAG_NAME = "title" + + fun isTag(tag: Array) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty() + + fun parse(tag: Array): String? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + return tag[1] + } + + fun assemble(value: String) = arrayOf(TAG_NAME, value) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/taggings/EventTagging.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/taggings/EventTagging.kt new file mode 100644 index 0000000000..1e157b23e6 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/taggings/EventTagging.kt @@ -0,0 +1,198 @@ +/* + * 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.quartz.experimental.decentralizedLists.taggings + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.experimental.decentralizedLists.description +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.names +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.AddressableListItemEvent +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.itemAddress +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.itemEvent +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.parentList +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.tags.ParentListTag +import com.vitorpamplona.quartz.experimental.decentralizedLists.taggings.tags.Polarity +import com.vitorpamplona.quartz.nip01Core.core.Address +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.fastFirstNotNullOfOrNull +import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.utils.TimeUtils + +/** What an event tagging is about: an addressable event (`a`) or a plain one (`e`). */ +@Immutable +sealed interface TaggingTarget { + /** The first 8 characters used in the assertion `d`. */ + val prefix: String + + @Immutable + data class ByAddress( + val address: Address, + ) : TaggingTarget { + // the author segment of the coordinate, not its kind + override val prefix get() = address.pubKeyHex.take(8) + } + + @Immutable + data class ByEventId( + val eventId: HexKey, + ) : TaggingTarget { + override val prefix get() = eventId.take(8) + } +} + +/** + * Event Taggings: the per-tag *tagging header* — "taggings of events as an Awesome Tag". + * + * A kind 39999 that is at once a list header (`names`, `description`) and an item of the + * deployment's `tagging-with-specific-tag` list, naming its tag with a direct `a`. Assertions + * point at it with a `z`, because their own `a`/`e` slot is taken by the target. + */ +object TaggingHeader { + fun dTag(tagSlug: String) = "tagging:$tagSlug-tagging" + + /** + * @param taggingWithSpecificTagConcept the deployment's `tagging-with-specific-tag` concept. + */ + fun build( + taggingWithSpecificTagConcepts: Collection, + tagElement: Address, + singularName: String, + pluralName: String, + description: String? = null, + createdAt: Long = TimeUtils.now(), + ) = AddressableListItemEvent.build( + parent = taggingWithSpecificTagConcepts.firstNamespace(), + dTag = dTag(tagElement.dTag), + createdAt = createdAt, + ) { + conceptNamespaces(taggingWithSpecificTagConcepts) + names(singularName, pluralName) + description?.let { this.description(it) } + itemAddress(tagElement) + } + + fun build( + taggingWithSpecificTagConcept: String, + tagElement: Address, + singularName: String, + pluralName: String, + description: String? = null, + createdAt: Long = TimeUtils.now(), + ) = build(listOf(taggingWithSpecificTagConcept), tagElement, singularName, pluralName, description, createdAt) +} + +/** + * Event Taggings: "this note is an Awesome Tag" — a kind 39999 carrying the target in `a`/`e` + * and two `z`s, one to the deployment's `nostr-event-tag` list and one to the per-tag + * [TaggingHeader]: + * + * ``` + * ["d", "event-tag---"] + * ["a", ] or ["e", ] + * ["z", ] + * ["z", ] + * ["polarity", "1" | "-1"] + * ``` + * + * An `a`/`e` reference without such a `z` is not a tagging and must not be read as one. + */ +@Immutable +data class EventTagging( + val target: TaggingTarget, + /** The `z`s other than the `nostr-event-tag` one: candidate tagging headers to resolve. */ + val taggingHeaders: List, + val polarity: Polarity, +) { + companion object { + fun dTag( + tagSlug: String, + target: TaggingTarget, + asserter: HexKey, + ) = "event-tag-$tagSlug-${target.prefix}-${asserter.take(8)}" + + /** + * @param nostrEventTagConcepts the `nostr-event-tag` concept of each authority namespace + * to join: the deployment's own, plus any shared one it federates with. + * @param taggingHeader the per-tag tagging header's coordinate. + * @param tagSlug the applied tag's slug, for the deterministic `d`. + * @param asserter the pubkey that will sign, for the deterministic `d`. + */ + fun build( + nostrEventTagConcepts: Collection, + taggingHeader: Address, + tagSlug: String, + target: TaggingTarget, + asserter: HexKey, + apply: Boolean = true, + createdAt: Long = TimeUtils.now(), + ) = AddressableListItemEvent.build( + parent = nostrEventTagConcepts.firstNamespace(), + dTag = dTag(tagSlug, target, asserter), + createdAt = createdAt, + ) { + conceptNamespaces(nostrEventTagConcepts) + parentList(ParentListTag.classify(taggingHeader.toValue())) + when (target) { + is TaggingTarget.ByAddress -> itemAddress(target.address) + is TaggingTarget.ByEventId -> itemEvent(target.eventId) + } + polarity(apply) + } + + fun build( + nostrEventTagConcept: String, + taggingHeader: Address, + tagSlug: String, + target: TaggingTarget, + asserter: HexKey, + apply: Boolean = true, + createdAt: Long = TimeUtils.now(), + ) = build(listOf(nostrEventTagConcept), taggingHeader, tagSlug, target, asserter, apply, createdAt) + + /** + * Null unless [event] joins one of the [honoredNamespaces]' `nostr-event-tag` concepts, + * names a tagging header and a target. Every namespace the reader honors must be passed: + * a federated tagging carries one concept `z` per namespace it joined, and any concept + * `z` the reader did not list would otherwise be taken for a tagging header. + */ + fun parse( + event: AddressableListItemEvent, + honoredNamespaces: Set, + ): EventTagging? { + val pointers = event.parentListPointers() + if (pointers.none { it in honoredNamespaces }) return null + val headers = pointers.filter { it !in honoredNamespaces } + if (headers.isEmpty()) return null + + val target = + event.tags.fastFirstNotNullOfOrNull(ATag::parseAddress)?.let { TaggingTarget.ByAddress(it) } + ?: event.tags.fastFirstNotNullOfOrNull(ETag::parseId)?.let { TaggingTarget.ByEventId(it) } + ?: return null + + return EventTagging(target, headers, event.tags.polarity()) + } + + fun parse( + event: AddressableListItemEvent, + nostrEventTagConcept: String, + ) = parse(event, setOf(nostrEventTagConcept)) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/taggings/PubKeyTagging.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/taggings/PubKeyTagging.kt new file mode 100644 index 0000000000..4654adcaf4 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/taggings/PubKeyTagging.kt @@ -0,0 +1,147 @@ +/* + * 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.quartz.experimental.decentralizedLists.taggings + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.AddressableListItemEvent +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.itemAddress +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.itemEvent +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.itemPubKey +import com.vitorpamplona.quartz.experimental.decentralizedLists.taggings.tags.Polarity +import com.vitorpamplona.quartz.nip01Core.core.Address +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.JsonMapper +import com.vitorpamplona.quartz.nip01Core.core.fastFirstNotNullOfOrNull +import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlinx.serialization.Serializable + +@Immutable +@Serializable +data class NostrUserTagInfo( + val taggedPubkey: HexKey, + val tagEventId: HexKey? = null, +) + +/** The `content` mirror of a pubkey tagging: `{"nostrUserTag": {...}}`. */ +@Immutable +@Serializable +data class NostrUserTagContent( + val nostrUserTag: NostrUserTagInfo, +) + +/** + * Tags & Taggings: "Avi is a Podcaster" — a kind 39999 item asserting that a pubkey belongs to + * a tag, or disputing it. + * + * ``` + * ["d", "profile-tag---"] + * ["p", ] + * ["a", "39999::"] the tag applied: stable identity, scan this + * ["e", ] the tag's version at apply time: provenance only + * ["z", ] + * ["polarity", "1" | "-1"] + * ``` + * + * The deterministic `d` gives each asserter one live stance per (target, tag): republishing + * replaces it, including a flip between apply and dispute. + */ +@Immutable +data class PubKeyTagging( + val target: HexKey, + val tag: Address?, + val tagEventId: HexKey?, + val polarity: Polarity, +) { + companion object { + fun dTag( + tagSlug: String, + target: HexKey, + asserter: HexKey, + ) = "profile-tag-$tagSlug-${target.take(8)}-${asserter.take(8)}" + + /** + * @param userTagConcepts the `nostr-user-tag` concept of each authority namespace to join. + * @param asserter the pubkey that will sign, needed for the deterministic `d`. + */ + fun build( + userTagConcepts: Collection, + tagElement: Address, + tagEventId: HexKey?, + target: HexKey, + asserter: HexKey, + apply: Boolean = true, + createdAt: Long = TimeUtils.now(), + ) = AddressableListItemEvent.build( + parent = userTagConcepts.firstNamespace(), + dTag = dTag(tagElement.dTag, target, asserter), + createdAt = createdAt, + content = JsonMapper.toJson(NostrUserTagContent(NostrUserTagInfo(target, tagEventId))), + ) { + conceptNamespaces(userTagConcepts) + itemPubKey(target) + itemAddress(tagElement) + tagEventId?.let { itemEvent(it) } + polarity(apply) + } + + fun build( + userTagConcept: String, + tagElement: AddressableListItemEvent, + target: HexKey, + asserter: HexKey, + apply: Boolean = true, + createdAt: Long = TimeUtils.now(), + ) = build(listOf(userTagConcept), tagElement.address(), tagElement.id, target, asserter, apply, createdAt) + + fun build( + userTagConcept: String, + tagElement: Address, + tagEventId: HexKey?, + target: HexKey, + asserter: HexKey, + apply: Boolean = true, + createdAt: Long = TimeUtils.now(), + ) = build(listOf(userTagConcept), tagElement, tagEventId, target, asserter, apply, createdAt) + + /** Null unless [event] joins one of the [honoredNamespaces] and names a target pubkey. */ + fun parse( + event: AddressableListItemEvent, + honoredNamespaces: Set, + ): PubKeyTagging? { + if (event.parentListPointers().none { it in honoredNamespaces }) return null + val target = event.tags.fastFirstNotNullOfOrNull(PTag::parseKey) ?: return null + return PubKeyTagging( + target = target, + tag = event.tags.fastFirstNotNullOfOrNull(ATag::parseAddress), + tagEventId = event.tags.fastFirstNotNullOfOrNull(ETag::parseId), + polarity = event.tags.polarity(), + ) + } + + fun parse( + event: AddressableListItemEvent, + userTagConcept: String, + ) = parse(event, setOf(userTagConcept)) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/taggings/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/taggings/TagArrayBuilderExt.kt new file mode 100644 index 0000000000..e4a2295e90 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/taggings/TagArrayBuilderExt.kt @@ -0,0 +1,48 @@ +/* + * 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.quartz.experimental.decentralizedLists.taggings + +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.AddressableListItemEvent +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.tags.ParentList +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.tags.ParentListTag +import com.vitorpamplona.quartz.experimental.decentralizedLists.taggings.tags.CurationMethod +import com.vitorpamplona.quartz.experimental.decentralizedLists.taggings.tags.CurationMethodTag +import com.vitorpamplona.quartz.experimental.decentralizedLists.taggings.tags.PolarityTag +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder + +fun TagArrayBuilder.polarity(apply: Boolean) = addUnique(PolarityTag.assemble(apply)) + +fun TagArrayBuilder.curationMethod(method: CurationMethod) = addUnique(CurationMethodTag.assemble(method)) + +/** Rides alongside the concept-membership `z`, never instead of it. */ +fun TagArrayBuilder.applicabilityHint(hint: TagApplicabilityHint) = addUniqueValueIfNew(ParentListTag.assemble(hint.code)) + +/** + * Federation: one concept `z` per authority namespace the publisher joins (e.g. a shared + * canonical `nostr-event-tag` and the deployment's own). Joining several is opt-in; a reader + * scanning any one of them finds the event. + */ +fun TagArrayBuilder.conceptNamespaces(concepts: Collection) = concepts.forEach { addUniqueValueIfNew(ParentListTag.assemble(it)) } + +internal fun Collection.firstNamespace(): ParentList { + require(isNotEmpty()) { "At least one concept namespace is required" } + return ParentListTag.classify(first()) +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/taggings/TagArrayExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/taggings/TagArrayExt.kt new file mode 100644 index 0000000000..5d4028203c --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/taggings/TagArrayExt.kt @@ -0,0 +1,44 @@ +/* + * 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.quartz.experimental.decentralizedLists.taggings + +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.parentListPointers +import com.vitorpamplona.quartz.experimental.decentralizedLists.taggings.tags.CurationMethodTag +import com.vitorpamplona.quartz.experimental.decentralizedLists.taggings.tags.Polarity +import com.vitorpamplona.quartz.experimental.decentralizedLists.taggings.tags.PolarityTag +import com.vitorpamplona.quartz.nip01Core.core.TagArray +import com.vitorpamplona.quartz.nip01Core.core.fastFirstNotNullOfOrNull +import com.vitorpamplona.quartz.nip01Core.core.fastFirstOrNull + +/** + * How a v1 reader counts this tagging. No `polarity` tag means apply; a tag whose value is + * not a number is not counted, rather than guessed. + */ +fun TagArray.polarity(): Polarity { + val tag = fastFirstOrNull(PolarityTag::isTag) ?: return Polarity.APPLIED + val value = PolarityTag.parseValue(tag) ?: return Polarity.UNCOUNTED + return PolarityTag.bucket(value) +} + +fun TagArray.curationMethod() = fastFirstNotNullOfOrNull(CurationMethodTag::parse) + +/** The applicability hints a tag-element's author recorded. Hints, never gates. */ +fun TagArray.applicabilityHints() = parentListPointers().mapNotNull(TagApplicabilityHint::fromCode) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/taggings/TagElement.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/taggings/TagElement.kt new file mode 100644 index 0000000000..d5daa4b8bd --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/taggings/TagElement.kt @@ -0,0 +1,107 @@ +/* + * 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.quartz.experimental.decentralizedLists.taggings + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.AddressableListItemEvent +import com.vitorpamplona.quartz.nip01Core.core.JsonMapper +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlinx.serialization.Serializable + +/** + * The context a tag-element was created for, recorded as a pubkey-free `z` value alongside + * its concept membership. The author's intent only: a reader must not require a hint for a tag + * to work in a context, nor read its absence as "not applicable". + */ +enum class TagApplicabilityHint( + val code: String, +) { + PUBKEY("tag-for-nostr-pubkey"), + EVENT("tag-for-nostr-event"), + ; + + companion object { + fun fromCode(code: String) = entries.firstOrNull { it.code == code } + } +} + +@Immutable +@Serializable +data class TagInfo( + val slug: String, + val name: String? = null, + val description: String? = null, +) + +/** The `content` of a tag-element: `{"tag": {"slug", "name", "description"}}`. */ +@Immutable +@Serializable +data class TagElementContent( + val tag: TagInfo, +) { + fun toContent() = JsonMapper.toJson(this) + + companion object { + fun parse(content: String): TagElementContent? { + if (content.isBlank()) return null + return runCatching { JsonMapper.fromJson(content) }.getOrNull() + } + } +} + +/** + * Tags & Taggings: a *tag* ("Podcaster is a tag") is a kind 39999 item joining the deployment's + * `tag` concept, with `d` = its slug. It is addressed at `39999::`; tags by + * different authors with the same slug are different tags. + */ +object TagElement { + /** + * @param tagConcept the deployment's `tag` concept address (`39998::tag`). + * Deployment-specific: never hardcode it. + */ + fun build( + tagConcepts: Collection, + slug: String, + name: String, + description: String? = null, + hints: Set = emptySet(), + createdAt: Long = TimeUtils.now(), + ) = AddressableListItemEvent.build( + parent = tagConcepts.firstNamespace(), + dTag = slug, + createdAt = createdAt, + content = TagElementContent(TagInfo(slug, name, description)).toContent(), + ) { + conceptNamespaces(tagConcepts) + hints.forEach { applicabilityHint(it) } + } + + fun build( + tagConcept: String, + slug: String, + name: String, + description: String? = null, + hints: Set = emptySet(), + createdAt: Long = TimeUtils.now(), + ) = build(listOf(tagConcept), slug, name, description, hints, createdAt) + + fun content(event: AddressableListItemEvent) = TagElementContent.parse(event.content) +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/taggings/TagPin.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/taggings/TagPin.kt new file mode 100644 index 0000000000..b0d82202c5 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/taggings/TagPin.kt @@ -0,0 +1,125 @@ +/* + * 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.quartz.experimental.decentralizedLists.taggings + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.AddressableListItemEvent +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.itemAddress +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.itemEvent +import com.vitorpamplona.quartz.experimental.decentralizedLists.taggings.tags.CurationMethod +import com.vitorpamplona.quartz.nip01Core.core.Address +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.JsonMapper +import com.vitorpamplona.quartz.nip01Core.core.fastFirstNotNullOfOrNull +import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlinx.serialization.Serializable + +@Immutable +@Serializable +data class TagPinningInfo( + val tagEventId: HexKey? = null, + val curationMethod: CurationMethod? = null, +) + +/** The `content` mirror of a pin: `{"tagPinning": {...}}`. */ +@Immutable +@Serializable +data class TagPinningContent( + val tagPinning: TagPinningInfo, +) + +/** + * Tags & Taggings: a viewer opting a tag into their personal curated set — a kind 39999 item. + * + * ``` + * ["d", "tag-pin---"] + * ["e", ] the version pinned + * ["a", "39999::"] survives the tag author's later edits + * ["z", ] + * ["curation-method", ""] + * ``` + * + * Unpinning is a plain NIP-09 deletion of the pin; a live pin event means pinned. + */ +@Immutable +data class TagPin( + val tag: Address?, + val tagEventId: HexKey?, + val curationMethod: CurationMethod?, +) { + companion object { + fun dTag( + tagSlug: String, + tagAuthor: HexKey, + viewer: HexKey, + ) = "tag-pin-$tagSlug-${tagAuthor.take(8)}-${viewer.take(8)}" + + /** + * @param tagPinningConcept the deployment's `tag-pinning` concept address. + * @param viewer the pubkey that will sign, needed for the deterministic `d`. + */ + fun build( + tagPinningConcepts: Collection, + tagElement: AddressableListItemEvent, + viewer: HexKey, + curationMethod: CurationMethod, + createdAt: Long = TimeUtils.now(), + ) = AddressableListItemEvent.build( + parent = tagPinningConcepts.firstNamespace(), + dTag = dTag(tagElement.dTag(), tagElement.pubKey, viewer), + createdAt = createdAt, + content = JsonMapper.toJson(TagPinningContent(TagPinningInfo(tagElement.id, curationMethod))), + ) { + conceptNamespaces(tagPinningConcepts) + itemEvent(tagElement.id) + itemAddress(tagElement.address()) + curationMethod(curationMethod) + } + + fun build( + tagPinningConcept: String, + tagElement: AddressableListItemEvent, + viewer: HexKey, + curationMethod: CurationMethod, + createdAt: Long = TimeUtils.now(), + ) = build(listOf(tagPinningConcept), tagElement, viewer, curationMethod, createdAt) + + /** Null unless [event] joins one of the [honoredNamespaces]. */ + fun parse( + event: AddressableListItemEvent, + honoredNamespaces: Set, + ): TagPin? { + if (event.parentListPointers().none { it in honoredNamespaces }) return null + return TagPin( + tag = event.tags.fastFirstNotNullOfOrNull(ATag::parseAddress), + tagEventId = event.tags.fastFirstNotNullOfOrNull(ETag::parseId), + curationMethod = event.tags.curationMethod(), + ) + } + + fun parse( + event: AddressableListItemEvent, + tagPinningConcept: String, + ) = parse(event, setOf(tagPinningConcept)) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/taggings/tags/CurationMethodTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/taggings/tags/CurationMethodTag.kt new file mode 100644 index 0000000000..b73bd5ea52 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/taggings/tags/CurationMethodTag.kt @@ -0,0 +1,64 @@ +/* + * 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.quartz.experimental.decentralizedLists.taggings.tags + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.JsonMapper +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.utils.ensure +import kotlinx.serialization.Serializable + +/** + * A pin's curation intent: whose point of view ranks the tag's members ([observer]), by which + * method (e.g. `nip85:rank`), the rank [cutoff], and whether derived scores go into the + * Trusted Lists published from it. Further method identifiers come by convention. + */ +@Immutable +@Serializable +data class CurationMethod( + val observer: HexKey? = null, + val method: String? = null, + val cutoff: Int? = null, + val includeScoreInTL: Boolean? = null, +) { + companion object { + const val NIP85_RANK = "nip85:rank" + } +} + +/** Tags & Taggings: `["curation-method", ""]` on a pin. */ +class CurationMethodTag { + companion object { + const val TAG_NAME = "curation-method" + + fun isTag(tag: Array) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty() + + fun parse(tag: Array): CurationMethod? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + return runCatching { JsonMapper.fromJson(tag[1]) }.getOrNull() + } + + fun assemble(method: CurationMethod) = arrayOf(TAG_NAME, JsonMapper.toJson(method)) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/taggings/tags/PolarityTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/taggings/tags/PolarityTag.kt new file mode 100644 index 0000000000..b05d573775 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/taggings/tags/PolarityTag.kt @@ -0,0 +1,69 @@ +/* + * 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.quartz.experimental.decentralizedLists.taggings.tags + +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.utils.ensure + +/** How a v1 reader counts a tagging. */ +enum class Polarity { + /** `≥ 0.5`, or no `polarity` tag at all. */ + APPLIED, + + /** `≤ -0.5`. */ + DISPUTED, + + /** + * Strictly between the two, or unreadable. Reserved for a future graded-valence design and + * not counted either way in v1. + */ + UNCOUNTED, +} + +/** + * Tags & Taggings: `["polarity", "1" | "-1"]` — apply or dispute. An absent tag means apply. + * It is multi-letter, so relays cannot filter on it; excluding disputes is a read-time step. + */ +class PolarityTag { + companion object { + const val TAG_NAME = "polarity" + const val APPLY = "1" + const val DISPUTE = "-1" + + fun isTag(tag: Array) = tag.has(1) && tag[0] == TAG_NAME + + /** The raw value, or null when the tag is not a polarity tag or does not hold a number. */ + fun parseValue(tag: Array): Double? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + return tag[1].toDoubleOrNull()?.takeIf { it.isFinite() } + } + + fun bucket(value: Double) = + when { + value >= 0.5 -> Polarity.APPLIED + value <= -0.5 -> Polarity.DISPUTED + else -> Polarity.UNCOUNTED + } + + fun assemble(apply: Boolean) = arrayOf(TAG_NAME, if (apply) APPLY else DISPUTE) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/tags/DescriptionTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/tags/DescriptionTag.kt new file mode 100644 index 0000000000..0f55a54e2b --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/tags/DescriptionTag.kt @@ -0,0 +1,42 @@ +/* + * 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.quartz.experimental.decentralizedLists.tags + +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.utils.ensure + +/** `["description", ]`, carried by both list headers and list items. */ +class DescriptionTag { + companion object { + const val TAG_NAME = "description" + + fun isTag(tag: Array) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty() + + fun parse(tag: Array): String? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + return tag[1] + } + + fun assemble(description: String) = arrayOf(TAG_NAME, description) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/tags/InheritFromTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/tags/InheritFromTag.kt new file mode 100644 index 0000000000..b60a5e1aea --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/tags/InheritFromTag.kt @@ -0,0 +1,119 @@ +/* + * 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.quartz.experimental.decentralizedLists.tags + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.experimental.decentralizedLists.CoordinateShape +import com.vitorpamplona.quartz.nip01Core.core.Address +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.utils.ensure + +/** + * What a `b` tag claims about its target. A closed registry: anything else, including an + * absent third element, reads as [POINTER] — the least-commitment reading, so that an + * underspecified tag never grants live deference. Code that acts on deference must gate on + * [INHERIT] / [INHERIT_ITEMS] explicitly, never on "not pointer". + */ +enum class InheritType( + val code: String, +) { + /** "My object corresponds to that one." No deference, no resolution semantics. */ + POINTER("pointer"), + + /** "My definition is this parent's, unless I state otherwise." Live, followed on read. */ + INHERIT("inherit"), + + /** "My list's items are this parent's items, plus my own." Additive; no definition fields. */ + INHERIT_ITEMS("inherit-items"), + ; + + companion object { + fun fromCode(code: String?) = + when (code) { + INHERIT.code -> INHERIT + INHERIT_ITEMS.code -> INHERIT_ITEMS + else -> POINTER + } + } +} + +/** + * Tapestry Inherit-From: `["b", , ]`, on kinds 39998 and 39999. + * + * Child-claims-parent, and unlike the `n`/`s` class-thread tags it is **not** flipped: the + * derived relationship points child → target for every type. + * + * Order matters only among inherit-typed tags (the first-listed parent wins a field conflict); + * pointer and inherit-items tags carry no meaning in their position. + */ +@Immutable +data class InheritFromTag( + val target: String, + val type: InheritType = InheritType.POINTER, +) { + fun targetAddress() = Address.parse(target) + + fun toTagArray() = assemble(target, type) + + companion object { + const val TAG_NAME = "b" + + /** + * The one reserved non-address value: `["b", "b-tag-deferred"]` marks its carrier as + * *deliberately* unaffiliated ("I considered a shared twin and chose none"). It derives + * nothing — no edge, no target — so [parse] never returns it as a target. + */ + const val UNAFFILIATED = "b-tag-deferred" + + fun isTag(tag: Array) = tag.has(1) && tag[0] == TAG_NAME && CoordinateShape.matches(tag[1]) + + fun isUnaffiliatedMarker(tag: Array) = tag.has(1) && tag[0] == TAG_NAME && tag[1] == UNAFFILIATED + + fun parse(tag: Array): InheritFromTag? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + // The value form is closed: a coordinate, or the reserved marker (which is not a + // target). Anything else derives nothing rather than becoming a bogus parent. + ensure(CoordinateShape.matches(tag[1])) { return null } + return InheritFromTag(tag[1], InheritType.fromCode(tag.getOrNull(2))) + } + + /** The target when [tag] is a `b` of exactly [type]. */ + fun parseTarget( + tag: Array, + type: InheritType, + ): String? = parse(tag)?.takeIf { it.type == type }?.target + + // The type is always written out, pointer included: readers treat a missing one as + // pointer anyway, and an explicit value says the author meant it. + fun assemble( + target: String, + type: InheritType, + ) = arrayOf(TAG_NAME, target, type.code) + + fun assemble( + target: Address, + type: InheritType, + ) = assemble(target.toValue(), type) + + fun assembleUnaffiliated() = arrayOf(TAG_NAME, UNAFFILIATED) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/tags/JsonTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/tags/JsonTag.kt new file mode 100644 index 0000000000..9d11362a2b --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/tags/JsonTag.kt @@ -0,0 +1,48 @@ +/* + * 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.quartz.experimental.decentralizedLists.tags + +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.utils.ensure + +/** + * Tapestry Concepts: `["json", ""]`, the element's structured data. + * + * The object is namespaced by concept slug, so one element can carry data for several concepts + * at once (`{"dog": {...}, "animal": {...}}`). Core nodes use the word-wrapper layout; see + * `concepts.WordWrapper`. `content` stays free for human-readable text. + */ +class JsonTag { + companion object { + const val TAG_NAME = "json" + + fun isTag(tag: Array) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty() + + fun parse(tag: Array): String? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + return tag[1] + } + + fun assemble(json: String) = arrayOf(TAG_NAME, json) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/kinds/KindNames.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/kinds/KindNames.kt index 0a1191cf14..b1ff7a9483 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/kinds/KindNames.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/kinds/KindNames.kt @@ -35,6 +35,10 @@ import com.vitorpamplona.quartz.experimental.citations.PromptCitationEvent import com.vitorpamplona.quartz.experimental.clink.debits.DebitEvent import com.vitorpamplona.quartz.experimental.clink.manage.ManageEvent import com.vitorpamplona.quartz.experimental.clink.offers.OfferEvent +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.AddressableListHeaderEvent +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.ListHeaderEvent +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.AddressableListItemEvent +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.ListItemEvent import com.vitorpamplona.quartz.experimental.decoupling.setup.EncryptionKeyListEvent import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent @@ -644,6 +648,10 @@ object KindNames { EventTrustedListEvent.KIND to KindName("Trusted List of Events", null), AddressableTrustedListEvent.KIND to KindName("Trusted List of Addressables", null), ExternalIdTrustedListEvent.KIND to KindName("Trusted List of External IDs", null), + ListHeaderEvent.KIND to KindName("Decentralized List", null), + AddressableListHeaderEvent.KIND to KindName("Editable Decentralized List", null), + ListItemEvent.KIND to KindName("Decentralized List Item", null), + AddressableListItemEvent.KIND to KindName("Editable Decentralized List Item", null), KeyPackageEvent.KIND to KindName("MLS KeyPackage", null), GitRepositoryStateEvent.KIND to KindName("Git Repo State", "34"), FeedDefinitionEvent.KIND to KindName("Feed Definition", null), diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip50Search/SearchableKinds.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip50Search/SearchableKinds.kt index 278c3b8e44..7c7eb0e954 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip50Search/SearchableKinds.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip50Search/SearchableKinds.kt @@ -113,6 +113,8 @@ object SearchableKinds { 9736, // Bolt12ZapEvent 9737, // Bolt12ZapIntentEvent 9802, // HighlightEvent + 9998, // ListHeaderEvent + 9999, // ListItemEvent 10003, // BookmarkListEvent 10100, // AgentProfileEvent 10154, // PodcastMetadataEvent @@ -194,6 +196,8 @@ object SearchableKinds { 39092, // MediaStarterPackEvent 39307, // TextTrackEvent 39701, // WebBookmarkEvent + 39998, // AddressableListHeaderEvent + 39999, // AddressableListItemEvent 40002, // StreamMessageV2Event 40100, // CanvasEvent 45001, // ForumPostEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt index 7d412edb94..15d8d83388 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt @@ -126,6 +126,10 @@ import com.vitorpamplona.quartz.experimental.citations.PromptCitationEvent import com.vitorpamplona.quartz.experimental.clink.debits.DebitEvent import com.vitorpamplona.quartz.experimental.clink.manage.ManageEvent import com.vitorpamplona.quartz.experimental.clink.offers.OfferEvent +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.AddressableListHeaderEvent +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.ListHeaderEvent +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.AddressableListItemEvent +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.ListItemEvent import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent @@ -846,6 +850,10 @@ class EventFactory { EventTrustedListEvent.KIND -> EventTrustedListEvent(id, pubKey, createdAt, tags, content, sig) AddressableTrustedListEvent.KIND -> AddressableTrustedListEvent(id, pubKey, createdAt, tags, content, sig) ExternalIdTrustedListEvent.KIND -> ExternalIdTrustedListEvent(id, pubKey, createdAt, tags, content, sig) + ListHeaderEvent.KIND -> ListHeaderEvent(id, pubKey, createdAt, tags, content, sig) + AddressableListHeaderEvent.KIND -> AddressableListHeaderEvent(id, pubKey, createdAt, tags, content, sig) + ListItemEvent.KIND -> ListItemEvent(id, pubKey, createdAt, tags, content, sig) + AddressableListItemEvent.KIND -> AddressableListItemEvent(id, pubKey, createdAt, tags, content, sig) RelayAddMemberEvent.KIND -> RelayAddMemberEvent(id, pubKey, createdAt, tags, content, sig) RelayRemoveMemberEvent.KIND -> RelayRemoveMemberEvent(id, pubKey, createdAt, tags, content, sig) RelayMembershipListEvent.KIND -> RelayMembershipListEvent(id, pubKey, createdAt, tags, content, sig) diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/AuditRegressionTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/AuditRegressionTest.kt new file mode 100644 index 0000000000..ab0579b6df --- /dev/null +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/AuditRegressionTest.kt @@ -0,0 +1,120 @@ +/* + * 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.quartz.experimental.decentralizedLists + +import com.vitorpamplona.quartz.experimental.decentralizedLists.assistant.HeaderResolution +import com.vitorpamplona.quartz.experimental.decentralizedLists.concepts.WordWrapper +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.AddressableListHeaderEvent +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.itemKinds +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.tags.ItemKindTag +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.AddressableListItemEvent +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.tags.ParentList +import com.vitorpamplona.quartz.experimental.decentralizedLists.taggings.EventTagging +import com.vitorpamplona.quartz.experimental.decentralizedLists.taggings.TaggingTarget +import com.vitorpamplona.quartz.experimental.decentralizedLists.tags.InheritType +import com.vitorpamplona.quartz.nip01Core.core.Address +import com.vitorpamplona.quartz.utils.EventFactory +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertIs + +/** Regressions found in the audit: each failed before its fix. */ +class AuditRegressionTest { + private val alice = "a".repeat(64) + private val bob = "b".repeat(64) + private val ta = "d".repeat(64) + private val other = "f".repeat(64) + + private fun item(tags: Array>) = assertIs(EventFactory.create("e".repeat(64), alice, 1L, 39999, tags, "", "")) + + // Event Taggings § "Concept namespaces & federation": one nostr-event-tag `z` per namespace. + @Test + fun federatedTaggingsDoNotMistakeOtherNamespacesForTaggingHeaders() { + val tagging = + item( + arrayOf( + arrayOf("d", "event-tag-awesome-tag-cccccccc-aaaaaaaa"), + arrayOf("a", "39999:$bob:good-tag"), + arrayOf("z", "39998:$ta:nostr-event-tag"), + arrayOf("z", "39998:$other:nostr-event-tag"), + arrayOf("z", "39999:$bob:tagging:awesome-tag-tagging"), + ), + ) + + val parsed = EventTagging.parse(tagging, setOf("39998:$ta:nostr-event-tag", "39998:$other:nostr-event-tag"))!! + assertEquals(listOf("39999:$bob:tagging:awesome-tag-tagging"), parsed.taggingHeaders) + } + + @Test + fun federatedTaggingsCanBeBuiltForSeveralNamespaces() { + val template = + EventTagging.build( + nostrEventTagConcepts = listOf("39998:$ta:nostr-event-tag", "39998:$other:nostr-event-tag"), + taggingHeader = Address(39999, bob, "tagging:awesome-tag-tagging"), + tagSlug = "awesome-tag", + target = TaggingTarget.ByEventId("1".repeat(64)), + asserter = alice, + ) + assertEquals( + listOf("39998:$ta:nostr-event-tag", "39998:$other:nostr-event-tag", "39999:$bob:tagging:awesome-tag-tagging"), + template.tags.filter { it[0] == "z" }.map { it[1] }, + ) + } + + // Two versions of the same personal header must resolve to the newest, not whichever came first. + @Test + fun governingHeaderIsTheNewestVersionOfTheWinningAuthor() { + val old = assertIs(EventFactory.create("1".repeat(64), alice, 1L, 39998, arrayOf(arrayOf("d", "dogs")), "", "")) + val new = assertIs(EventFactory.create("2".repeat(64), alice, 5L, 39998, arrayOf(arrayOf("d", "dogs")), "", "")) + assertEquals(new, HeaderResolution.governing(alice, "dogs", null, listOf(old, new))) + } + + // Inherit-From: element 2 is an a-tag; the value form is closed. + @Test + fun bTagsWhoseTargetIsNotACoordinateAreIgnored() { + val node = item(arrayOf(arrayOf("d", "x"), arrayOf("b", "hello", "inherit"), arrayOf("b", "39998:$bob:dogs", "inherit"))) + assertEquals(listOf("39998:$bob:dogs"), node.inheritFromTargets(InheritType.INHERIT)) + } + + @Test + fun itemKindIsTagAgreesWithParse() { + val tag = arrayOf("item-kind", "-1") + assertEquals(ItemKindTag.parse(tag) != null, ItemKindTag.isTag(tag)) + assertEquals(emptyList(), arrayOf(tag).itemKinds()) + } + + // A long list name containing a colon is still a name, and never reaches the address parser. + @Test + fun longNamesWithColonsStayNames() { + val name = "a list of: " + "x".repeat(70) + val node = item(arrayOf(arrayOf("d", "x"), arrayOf("z", name))) + assertIs(node.parentLists().single()) + assertFalse(name in node.linkedAddressIds()) + } + + // Authored JSON: an explicit null in a list field must not make the whole section unreadable. + @Test + fun explicitNullListsInTheWordSectionAreTolerated() { + val wrapper = WordWrapper.parse("""{"word":{"slug":"dogs","wordTypes":null,"coreMemberOf":null}}""")!! + assertEquals("dogs", wrapper.word()?.slug) + } +} diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/DecentralizedListsTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/DecentralizedListsTest.kt new file mode 100644 index 0000000000..4a0e5511f0 --- /dev/null +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/DecentralizedListsTest.kt @@ -0,0 +1,313 @@ +/* + * 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.quartz.experimental.decentralizedLists + +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.AddressableListHeaderEvent +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.ListHeaderEvent +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.recommended +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.required +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.tags.TagRule +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.tags.TagRuleType +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.AddressableListItemEvent +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.ListItemEvent +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.itemPubKey +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.itemString +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.name +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.parentListName +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.tags.ParentList +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip50Search.SearchableEvent +import com.vitorpamplona.quartz.utils.EventFactory +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertIs +import kotlin.test.assertNull +import kotlin.test.assertTrue + +/** + * The Tapestry Decentralized Lists tag layer against the spec's own examples + * (https://github.com/nous-clawds4/tapestry/blob/main/protocols/nips/decentralized-lists.md). + */ +class DecentralizedListsTest { + private val author = "dee45a23c4f1d93f3a2043650c5081e4ac14a778e0acbef03de3768e4f81ac7b" + private val derek = "3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24" + private val headerId = "9d1b6b9562e66f2ecf35eb0a3c2decc736c47fddb13d6fb8f87185a153ea3634" + private val otherHeaderId = "1fc80cf813f1af33d5a435862b7ef7fb96b47e68a48f1abcadf8081f5a545550" + + private fun event( + kind: Int, + tags: Array>, + id: String = "a".repeat(64), + ): Event = EventFactory.create(id, author, 1719888496L, kind, tags, "", "") + + @Test + fun theFactoryBuildsEachKind() { + assertIs(event(9998, emptyArray())) + assertIs(event(39998, emptyArray())) + assertIs(event(9999, emptyArray())) + assertIs(event(39999, emptyArray())) + } + + // Example 1: a list of nostr developers. + @Test + fun theSpecsHeaderExampleParses() { + val header = + assertIs( + event( + 9998, + arrayOf( + arrayOf("names", "nostr developer", "nostr developers"), + arrayOf("description", "This is a list of developers who build within the nostr ecosystem"), + arrayOf("required", "p"), + arrayOf("required", "name"), + ), + id = headerId, + ), + ) + + assertEquals("nostr developer", header.names()?.singular) + assertEquals("nostr developers", header.names()?.plural) + assertEquals("This is a list of developers who build within the nostr ecosystem", header.description()) + assertEquals(listOf("p", "name"), header.requiredTags()) + assertEquals(emptyList(), header.allowedTags()) + assertEquals(headerId, header.listPointer()) + } + + @Test + fun ruleDescriptionsAreTheOptionalThirdElement() { + val header = + assertIs( + event( + 9998, + arrayOf( + arrayOf("names", "endorsement", "endorsements"), + arrayOf("required", "p", "Pubkey of the person being endorsed"), + arrayOf("allowed", "comments", "Optional textual reason"), + arrayOf("disallowed", "e"), + arrayOf("recommended", "title"), + arrayOf("required", ""), + ), + ), + ) + + assertEquals( + listOf( + TagRule(TagRuleType.REQUIRED, "p", "Pubkey of the person being endorsed"), + TagRule(TagRuleType.ALLOWED, "comments", "Optional textual reason"), + TagRule(TagRuleType.DISALLOWED, "e", null), + TagRule(TagRuleType.RECOMMENDED, "title", null), + ), + header.tagRules(), + ) + } + + @Test + fun aNamesTagMissingItsPluralIsDropped() { + val header = assertIs(event(9998, arrayOf(arrayOf("names", "dog")))) + assertNull(header.names()) + } + + // Example 1, item side, and Example 4 (one item on two lists). + @Test + fun theSpecsItemExampleParses() { + val item = + assertIs( + event( + 9999, + arrayOf( + arrayOf("z", headerId), + arrayOf("z", otherHeaderId), + arrayOf("name", "Derek Ross"), + arrayOf("p", derek), + ), + ), + ) + + assertEquals(listOf(ParentList.EventId(headerId), ParentList.EventId(otherHeaderId)), item.parentLists()) + assertEquals("Derek Ross", item.name()) + assertEquals(listOf(derek), item.itemPubKeys().map { it.pubKey }) + assertTrue(headerId in item.linkedEventIds()) + assertFalse(item.declaresList()) + } + + // Example 7: pointing at an editable (39998) header by coordinate. + @Test + fun zTagsPointingAtCoordinatesParseAsAddresses() { + val coordinate = "39998:$author:dogs" + val item = assertIs(event(9999, arrayOf(arrayOf("z", coordinate), arrayOf("t", "Fido")))) + + val parent = assertIs(item.parentLists().single()) + assertEquals(39998, parent.address.kind) + assertEquals(author, parent.address.pubKeyHex) + assertEquals("dogs", parent.address.dTag) + assertEquals(coordinate, parent.value) + assertEquals(listOf("Fido"), item.itemStrings()) + assertTrue(coordinate in item.linkedAddressIds()) + } + + // Example 4, alternate form: undeclared lists named by their singular name. + @Test + fun zTagsWithPlainNamesParseAsNames() { + val item = + assertIs( + event(9999, arrayOf(arrayOf("z", "dog"), arrayOf("z", "animal"), arrayOf("t", "Fido"))), + ) + + assertEquals(listOf(ParentList.Name("dog"), ParentList.Name("animal")), item.parentLists()) + assertEquals(listOf("dog", "animal"), item.parentListPointers()) + } + + // Example 2: the spec puts an naddr in the item's `a` tag. + @Test + fun naddrItemsParseAsAddresses() { + val naddr = + "naddr1qvzqqqr4gupzq4rqjpyzsnf2z5wgma397sxr382z8mg90l80jf7m3z2k628z9wsrqythwumn8ghj7cnfw33k76twv4ezuum0vd5kzmp0qythwumn8ghj7ct5d3shxtnwdaehgu3wd3skuep0qq3kv6tpwskkxatjwfjkucme946xsefdwd5kcetwwskhg6tdv5khg6rfv4nqnxv6fx" + val item = + assertIs( + event( + 9999, + arrayOf( + arrayOf("z", headerId), + arrayOf("a", naddr), + arrayOf("title", "Fiat Currency: The Silent Time Thief"), + ), + ), + ) + + assertEquals(30023, item.itemAddresses().single().kind) + assertEquals("Fiat Currency: The Silent Time Thief", item.title()) + } + + // "Nonstandard methods to declare a list": a 9999 on the list of lists carries header tags. + @Test + fun anItemCanDeclareAListTheNonstandardWay() { + val item = + assertIs( + event( + 9999, + arrayOf( + arrayOf("z", "list"), + arrayOf("names", "dog", "dogs"), + arrayOf("description", "This is a list (by name) of individual dogs."), + arrayOf("required", "t"), + ), + ), + ) + + assertTrue(item.declaresList()) + assertEquals("dogs", item.names()?.plural) + assertEquals(listOf("t"), item.requiredTags()) + } + + @Test + fun theHeaderBuilderWritesTheSpecsShape() { + val template = + ListHeaderEvent.build("dog name", "dog names", "This is a list of commonly used dog names.") { + required("t") + recommended("comments", "Why this name") + } + + assertEquals(9998, template.kind) + assertEquals( + listOf( + listOf("names", "dog name", "dog names"), + listOf("description", "This is a list of commonly used dog names."), + listOf("required", "t"), + listOf("recommended", "comments", "Why this name"), + ), + template.tags.map { it.toList() }, + ) + } + + @Test + fun itemsPointAtRegularHeadersByIdAndAddressableHeadersByCoordinate() { + val header = assertIs(event(9998, arrayOf(arrayOf("names", "dog", "dogs")), id = headerId)) + val byId = ListItemEvent.build(header) { itemString("Fido") } + assertEquals( + listOf(listOf("z", headerId), listOf("t", "Fido")), + byId.tags.map { it.toList() }, + ) + + val editable = + assertIs( + event(39998, arrayOf(arrayOf("d", "dogs"), arrayOf("names", "dog", "dogs"))), + ) + val byCoordinate = AddressableListItemEvent.build(editable, dTag = "fido") { itemString("Fido") } + assertEquals( + listOf(listOf("d", "fido"), listOf("z", "39998:$author:dogs"), listOf("t", "Fido")), + byCoordinate.tags.map { it.toList() }, + ) + } + + @Test + fun itemBuilderAcceptsUndeclaredListsAndPubkeys() { + val template = + ListItemEvent.build(ParentList.Name("nostr developer")) { + parentListName("nostr developer") + name("Derek Ross") + itemPubKey(derek) + } + + // the repeated parent is written once + assertEquals( + listOf(listOf("z", "nostr developer"), listOf("name", "Derek Ross"), listOf("p", derek)), + template.tags.map { it.toList() }, + ) + } + + @Test + fun searchIndexesTheHumanTextAndBothPathsAgree() { + val header = + assertIs( + event( + 9998, + arrayOf( + arrayOf("names", "dog", "dogs"), + arrayOf("titles", "Dog", "Dogs"), + arrayOf("description", "Good boys."), + arrayOf("required", "t"), + ), + ), + ) + assertEquals("dog\ndogs\nDog\nDogs\nGood boys.", header.indexableContent()) + + val item = + assertIs( + event( + 9999, + arrayOf(arrayOf("z", headerId), arrayOf("name", "Fido"), arrayOf("comments", "Very good"), arrayOf("t", "Fido")), + ), + ) + assertEquals("Fido\nVery good\nFido", item.indexableContent()) + + listOf(header, item).forEach { event -> + val fields = mutableListOf() + event.forEachIndexableField { field -> + field?.let { fields.add(it) } + true + } + assertEquals(event.indexableContent(), fields.joinToString(event.indexableSeparator())) + } + + assertEquals("", assertIs(event(9999, emptyArray())).indexableContent()) + } +} diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/TapestryExtensionsTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/TapestryExtensionsTest.kt new file mode 100644 index 0000000000..8e4b8a72a6 --- /dev/null +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/TapestryExtensionsTest.kt @@ -0,0 +1,340 @@ +/* + * 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.quartz.experimental.decentralizedLists + +import com.vitorpamplona.quartz.experimental.decentralizedLists.assistant.CurationCopy +import com.vitorpamplona.quartz.experimental.decentralizedLists.assistant.HeaderResolution +import com.vitorpamplona.quartz.experimental.decentralizedLists.assistant.dListAssistant +import com.vitorpamplona.quartz.experimental.decentralizedLists.assistant.dListCuration +import com.vitorpamplona.quartz.experimental.decentralizedLists.assistant.dListCurations +import com.vitorpamplona.quartz.experimental.decentralizedLists.assistant.replaceDListCuration +import com.vitorpamplona.quartz.experimental.decentralizedLists.assistant.tags.DListCuration +import com.vitorpamplona.quartz.experimental.decentralizedLists.concepts.WordWrapper +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.AddressableListHeaderEvent +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.tags.ItemKind +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.AddressableListItemEvent +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.elementOf +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.subsetOf +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.tags.ParentListTag +import com.vitorpamplona.quartz.experimental.decentralizedLists.taggings.EventTagging +import com.vitorpamplona.quartz.experimental.decentralizedLists.taggings.PubKeyTagging +import com.vitorpamplona.quartz.experimental.decentralizedLists.taggings.TagApplicabilityHint +import com.vitorpamplona.quartz.experimental.decentralizedLists.taggings.TagElement +import com.vitorpamplona.quartz.experimental.decentralizedLists.taggings.TaggingHeader +import com.vitorpamplona.quartz.experimental.decentralizedLists.taggings.TaggingTarget +import com.vitorpamplona.quartz.experimental.decentralizedLists.taggings.applicabilityHints +import com.vitorpamplona.quartz.experimental.decentralizedLists.taggings.polarity +import com.vitorpamplona.quartz.experimental.decentralizedLists.taggings.tags.Polarity +import com.vitorpamplona.quartz.experimental.decentralizedLists.tags.InheritFromTag +import com.vitorpamplona.quartz.experimental.decentralizedLists.tags.InheritType +import com.vitorpamplona.quartz.nip01Core.core.Address +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.toHexKey +import com.vitorpamplona.quartz.utils.EventFactory +import com.vitorpamplona.quartz.utils.sha256.sha256 +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertIs +import kotlin.test.assertNull +import kotlin.test.assertTrue + +/** The Tapestry drafts layered on Decentralized Lists, against their own examples. */ +class TapestryExtensionsTest { + private val alice = "a".repeat(64) + private val bob = "b".repeat(64) + private val assistant = "c".repeat(64) + private val ta = "d".repeat(64) + + private fun event( + kind: Int, + tags: Array>, + pubKey: String = alice, + content: String = "", + id: String = "e".repeat(64), + ): Event = EventFactory.create(id, pubKey, 1L, kind, tags, content, "") + + private fun tagsOf(tags: Array>) = tags.map { it.toList() } + + // Inherit-From + + @Test + fun bTagTypesAndTheirFailSafeDefault() { + val header = + assertIs( + event( + 39998, + arrayOf( + arrayOf("d", "dogs"), + arrayOf("b", "39998:$bob:dogs", "inherit"), + arrayOf("b", "39998:$ta:dogs", "inherit-items"), + arrayOf("b", "39998:$ta:canines"), + arrayOf("b", "39998:$ta:hounds", "something-new"), + ), + ), + ) + + assertEquals( + listOf( + InheritFromTag("39998:$bob:dogs", InheritType.INHERIT), + InheritFromTag("39998:$ta:dogs", InheritType.INHERIT_ITEMS), + InheritFromTag("39998:$ta:canines", InheritType.POINTER), + InheritFromTag("39998:$ta:hounds", InheritType.POINTER), + ), + header.inheritFrom(), + ) + assertEquals(listOf("39998:$bob:dogs"), header.inheritFromTargets(InheritType.INHERIT)) + assertFalse(header.isDeliberatelyUnaffiliated()) + } + + @Test + fun theDeferredSentinelIsAMarkerNotATarget() { + val header = assertIs(event(39998, arrayOf(arrayOf("d", "dogs"), arrayOf("b", "b-tag-deferred")))) + assertTrue(header.isDeliberatelyUnaffiliated()) + assertEquals(emptyList(), header.inheritFrom()) + } + + // Tapestry Concepts + + @Test + fun conceptGraphIsReadOrComputed() { + val without = assertIs(event(39998, arrayOf(arrayOf("d", "dogs")))) + assertEquals("39999:$alice:dogs-concept-graph", without.conceptGraph()) + + val with = assertIs(event(39998, arrayOf(arrayOf("d", "dogs"), arrayOf("concept-graph", "39999:$bob:elsewhere")))) + assertEquals("39999:$bob:elsewhere", with.conceptGraph()) + } + + @Test + fun wordWrapperJsonParses() { + val json = + """{"word":{"slug":"concept-header-for-the-concept-of-dogs","name":"concept header for the concept of dogs","wordTypes":["word","conceptHeader"]},""" + + """"conceptHeader":{"description":"Dog is a concept.","oNames":{"singular":"dog","plural":"dogs"}}}""" + val item = assertIs(event(39999, arrayOf(arrayOf("d", "dogs"), arrayOf("json", json)))) + + val wrapper = item.wordWrapper()!! + assertEquals("concept-header-for-the-concept-of-dogs", wrapper.word()?.slug) + assertEquals(listOf("word", "conceptHeader"), wrapper.word()?.wordTypes) + assertEquals("dogs", wrapper.conceptHeader()?.oNames?.plural) + assertNull(wrapper.section("superset")) + assertNull(WordWrapper.parse("[1,2]")) + assertNull(WordWrapper.parse("not json")) + } + + // Class Thread Relationships + + @Test + fun classThreadTagsRoundTrip() { + val set = Address(39999, alice, "the-set-of-paid-nostr-relays") + val superset = Address(39999, alice, "superset-of-relays") + val template = + AddressableListItemEvent.build(ParentListTag.classify(set.toValue()), dTag = "relay-x") { + elementOf(set) + subsetOf(superset) + } + val signed = assertIs(event(39999, template.tags)) + assertEquals(listOf(set.toValue()), signed.elementOf()) + assertEquals(listOf(superset.toValue()), signed.subsetOf()) + } + + // Cross-NIP Compatibility + + @Test + fun itemKindsWidenTheAcceptedKinds() { + val plain = assertIs(event(39998, arrayOf(arrayOf("d", "communities")))) + assertEquals(listOf(9999, 39999), plain.acceptedItemKinds()) + + val widened = + assertIs( + event( + 39998, + arrayOf( + arrayOf("d", "communities"), + arrayOf("item-kind", "39999"), + arrayOf("item-kind", "34550", "NIP-72 community-definition events"), + arrayOf("item-kind", "not-a-number"), + ), + ), + ) + assertEquals(listOf(ItemKind(39999), ItemKind(34550, "NIP-72 community-definition events")), widened.itemKinds()) + assertEquals(listOf(39999, 34550), widened.acceptedItemKinds()) + } + + @Test + fun foreignKindsCanListThemselves() { + val community = event(34550, arrayOf(arrayOf("d", "lfo"), arrayOf("z", "39998:$ta:communities"))) + assertEquals(listOf("39998:$ta:communities"), community.dListParentPointers()) + } + + // Tags & Taggings + + @Test + fun tagElementsCarryTheirJsonAndHints() { + val template = TagElement.build("39998:$ta:tag", "podcaster", "Podcaster", "Makes podcasts", setOf(TagApplicabilityHint.PUBKEY)) + assertEquals( + listOf(listOf("d", "podcaster"), listOf("z", "39998:$ta:tag"), listOf("z", "tag-for-nostr-pubkey")), + tagsOf(template.tags), + ) + val signed = assertIs(event(39999, template.tags, content = template.content)) + assertEquals("Podcaster", TagElement.content(signed)?.tag?.name) + assertEquals(listOf(TagApplicabilityHint.PUBKEY), signed.tags.applicabilityHints()) + } + + @Test + fun pubKeyTaggingsUseTheDeterministicShape() { + val tag = Address(39999, bob, "podcaster") + val template = PubKeyTagging.build("39998:$ta:nostr-user-tag", tag, "f".repeat(64), target = assistant, asserter = alice, apply = false) + + assertEquals( + listOf( + listOf("d", "profile-tag-podcaster-cccccccc-aaaaaaaa"), + listOf("z", "39998:$ta:nostr-user-tag"), + listOf("p", assistant), + listOf("a", "39999:$bob:podcaster"), + listOf("e", "f".repeat(64)), + listOf("polarity", "-1"), + ), + tagsOf(template.tags), + ) + + val signed = assertIs(event(39999, template.tags)) + val parsed = PubKeyTagging.parse(signed, "39998:$ta:nostr-user-tag")!! + assertEquals(assistant, parsed.target) + assertEquals(tag, parsed.tag) + assertEquals(Polarity.DISPUTED, parsed.polarity) + assertNull(PubKeyTagging.parse(signed, "39998:$ta:some-other-concept")) + } + + @Test + fun polarityBuckets() { + fun p(vararg tags: Array) = arrayOf(*tags).polarity() + assertEquals(Polarity.APPLIED, p()) + assertEquals(Polarity.APPLIED, p(arrayOf("polarity", "1"))) + assertEquals(Polarity.DISPUTED, p(arrayOf("polarity", "-1"))) + assertEquals(Polarity.UNCOUNTED, p(arrayOf("polarity", "0.2"))) + assertEquals(Polarity.UNCOUNTED, p(arrayOf("polarity", "yes"))) + } + + // Event Taggings: the spec's worked example + + @Test + fun eventTaggingsFollowTheWorkedExample() { + val awesomeTag = Address(39999, bob, "awesome-tag") + val header = TaggingHeader.build("39998:$ta:tagging-with-specific-tag", awesomeTag, "Tagging of an event as an Awesome Tag", "Taggings of events as Awesome Tags") + assertEquals("tagging:awesome-tag-tagging", header.tags.first { it[0] == "d" }[1]) + + val headerAddress = Address(39999, bob, "tagging:awesome-tag-tagging") + val goodTag = Address(39999, assistant, "good-tag") + val assertion = EventTagging.build("39998:$ta:nostr-event-tag", headerAddress, "awesome-tag", TaggingTarget.ByAddress(goodTag), asserter = alice) + + assertEquals( + listOf( + listOf("d", "event-tag-awesome-tag-cccccccc-aaaaaaaa"), + listOf("z", "39998:$ta:nostr-event-tag"), + listOf("z", "39999:$bob:tagging:awesome-tag-tagging"), + listOf("a", "39999:$assistant:good-tag"), + listOf("polarity", "1"), + ), + tagsOf(assertion.tags), + ) + + val signed = assertIs(event(39999, assertion.tags)) + val parsed = EventTagging.parse(signed, "39998:$ta:nostr-event-tag")!! + assertEquals(TaggingTarget.ByAddress(goodTag), parsed.target) + assertEquals(listOf("39999:$bob:tagging:awesome-tag-tagging"), parsed.taggingHeaders) + } + + // Assistant Designation + + @Test + fun treasureMapEntriesParseAndStayOutOfEachOther() { + val map = + arrayOf( + arrayOf("30382:rank", ta, "wss://nip85.brainstorm.world"), + arrayOf("39998:dlist-header", assistant, ""), + arrayOf("39998:dogs", assistant, "wss://dcosl.brainstorm.world"), + arrayOf("39999:tagging:with:colons", assistant, "wss://dcosl.brainstorm.world"), + arrayOf("39999:dlist-header", assistant, ""), + ) + + assertEquals(assistant, map.dListAssistant()?.assistant) + assertNull(map.dListAssistant()?.relay) + assertEquals(listOf("dogs", "tagging:with:colons"), map.dListCurations().map { it.dTag }) + assertEquals(Address(39998, assistant, "dogs"), map.dListCuration(39998, "dogs")?.headerAddress()) + + val updated = map.replaceDListCuration(DListCuration(39998, "dogs", bob)) + assertEquals(map.size, updated.size) + assertEquals(bob, updated.dListCuration(39998, "dogs")?.assistant) + assertEquals(listOf("30382:rank", ta, "wss://nip85.brainstorm.world"), updated[0].toList()) + } + + @Test + fun curationCopiesCarryOnlyWhatTheSpecLists() { + val original = + assertIs( + event( + 39999, + arrayOf( + arrayOf("d", "fido"), + arrayOf("z", "39998:$ta:dogs"), + arrayOf("name", "Fido"), + arrayOf("t", "Fido"), + arrayOf("n", "39999:$bob:good-dogs"), + arrayOf("json", "{}"), + ), + pubKey = bob, + content = "a good dog", + ), + ) + val header = Address(39998, assistant, "dogs") + val copy = CurationCopy.build(header, original, null) + + val expectedD = "copy-" + sha256("39998:$assistant:dogs\n39999:$bob:fido".encodeToByteArray()).toHexKey() + assertEquals( + listOf( + listOf("d", expectedD), + listOf("z", "39998:$assistant:dogs"), + listOf("q", "39999:$bob:fido", ""), + listOf("q", original.id, "", bob), + listOf("name", "Fido"), + listOf("t", "Fido"), + ), + tagsOf(copy.tags), + ) + assertEquals("a good dog", copy.content) + } + + @Test + fun personalHeadersWinOverTheAssistantsRegardlessOfAge() { + val map = arrayOf(arrayOf("39998:dlist-header", assistant, "")) + assertEquals( + listOf(Address(39998, alice, "dogs"), Address(39998, assistant, "dogs")), + HeaderResolution.candidates(alice, "dogs", map), + ) + + val personal = assertIs(EventFactory.create("1".repeat(64), alice, 1L, 39998, arrayOf(arrayOf("d", "dogs")), "", "")) + val newerByAssistant = assertIs(EventFactory.create("2".repeat(64), assistant, 99L, 39998, arrayOf(arrayOf("d", "dogs")), "", "")) + + assertEquals(personal, HeaderResolution.governing(alice, "dogs", map, listOf(newerByAssistant, personal))) + assertEquals(newerByAssistant, HeaderResolution.governing(alice, "dogs", map, listOf(newerByAssistant))) + assertNull(HeaderResolution.governing(alice, "dogs", null, listOf(newerByAssistant))) + } +} diff --git a/quartz/src/jvmTest/resources/indexable-content.golden b/quartz/src/jvmTest/resources/indexable-content.golden index a74713c458..ba9a7f11a5 100644 --- a/quartz/src/jvmTest/resources/indexable-content.golden +++ b/quartz/src/jvmTest/resources/indexable-content.golden @@ -58,6 +58,8 @@ 9736 The content body. 9737 The content body. 9802 The Comment\nThe Context\nThe content body. +9998 The Name\nThe Title\nThe Description\nhashtag1\nhashtag2 +9999 The Name\nThe Title\nThe Description\nhashtag1\nhashtag2 10003 The Title 10100 10154 The Title\nThe Description @@ -139,6 +141,8 @@ 39092 The Title\nThe Description 39307 The content body. 39701 The Title\nThe content body. +39998 The Name\nThe Title\nThe Description\nhashtag1\nhashtag2 +39999 The Name\nThe Title\nThe Description\nhashtag1\nhashtag2 40002 The content body. 40100 The content body. 45001 The content body. diff --git a/tools/r8-verify/reflection-contract.txt b/tools/r8-verify/reflection-contract.txt index 4d04998fb8..c84b522f9e 100644 --- a/tools/r8-verify/reflection-contract.txt +++ b/tools/r8-verify/reflection-contract.txt @@ -140,14 +140,14 @@ enum com.vitorpamplona.amethyst.commons.tor.TorType INTERNAL # is exactly why it would never be noticed. enum com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcErrorCode RATE_LIMITED NOT_IMPLEMENTED INSUFFICIENT_BALANCE PAYMENT_FAILED QUOTA_EXCEEDED RESTRICTED UNAUTHORIZED INTERNAL UNSUPPORTED_ENCRYPTION BAD_REQUEST NOT_FOUND EXPIRED UNSUPPORTED_PAYMENT_INSTRUCTION UNSUPPORTED_NETWORK OTHER -enum com.vitorpamplona.amethyst.model.ThemeType SYSTEM LIGHT DARK -enum com.vitorpamplona.amethyst.model.BooleanType ALWAYS NEVER -enum com.vitorpamplona.amethyst.model.ConnectivityType ALWAYS NEVER -enum com.vitorpamplona.amethyst.model.FeatureSetType SIMPLIFIED -enum com.vitorpamplona.amethyst.model.FontFamilyType SYSTEM -enum com.vitorpamplona.amethyst.model.FontSizeType NORMAL -enum com.vitorpamplona.amethyst.model.AccentColorType PURPLE -enum com.vitorpamplona.amethyst.model.ProfileGalleryType CLASSIC +enum com.vitorpamplona.amethyst.commons.model.ThemeType SYSTEM LIGHT DARK +enum com.vitorpamplona.amethyst.commons.model.BooleanType ALWAYS NEVER +enum com.vitorpamplona.amethyst.commons.model.ConnectivityType ALWAYS NEVER +enum com.vitorpamplona.amethyst.commons.model.FeatureSetType SIMPLIFIED +enum com.vitorpamplona.amethyst.commons.model.FontFamilyType SYSTEM +enum com.vitorpamplona.amethyst.commons.model.FontSizeType NORMAL +enum com.vitorpamplona.amethyst.commons.model.AccentColorType PURPLE +enum com.vitorpamplona.amethyst.commons.model.ProfileGalleryType CLASSIC enum com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.NamecoinBackend enum com.vitorpamplona.quartz.nipACWebRtcCalls.tags.CallType enum com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.ChannelExpand