refactor(avm): factory lambda, shared pow label and icon types, HTTP helpers out

- LoggedInPage builds AccountViewModel through a viewModel { }
  initializer; the Class<T>-based Factory, the VM's last JVM-only
  member, is gone.
- powKindLabelRes (compose resources only) moves to commonsUI; its
  R.string twin for the mining foreground service stays in the app.
- MarmotGroupIconUpload and MarmotGroupIconChange are plain data and
  move to commons/marmot, out of the Android uploader's file.
- urlPreview and checkVideoIsOnline go through the app's OkHttp clients
  and become app-side extensions beside saveMediaToGallery, with the
  same call shape.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S7FuNBSKiyVecARSoE4B9P
This commit is contained in:
Claude
2026-09-28 13:32:12 +00:00
parent b8387e55d4
commit 6da2de7d2b
17 changed files with 169 additions and 125 deletions
@@ -21,7 +21,6 @@
package com.vitorpamplona.amethyst.service.pow
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.boost
import com.vitorpamplona.amethyst.commons.resources.post
import com.vitorpamplona.amethyst.commons.resources.pow_kind_chat_message
@@ -30,6 +29,7 @@ import com.vitorpamplona.amethyst.commons.resources.private_message
import com.vitorpamplona.amethyst.commons.resources.reaction
import com.vitorpamplona.amethyst.commons.resources.voice_post
import com.vitorpamplona.amethyst.commons.resources.voice_reply
import com.vitorpamplona.amethyst.commons.service.pow.powKindLabelRes
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
@@ -39,24 +39,6 @@ import com.vitorpamplona.quartz.nip56Reports.ReportEvent
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceEvent
import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent
import org.jetbrains.compose.resources.StringResource
/**
* The one user-facing label for "what is being mined": shared by the
* broadcast banner, the mining foreground notification, and failure toasts
* so a job is described the same way everywhere it appears.
*/
fun powKindLabelRes(kind: Int): StringResource =
when (kind) {
ReactionEvent.KIND -> Res.string.reaction
RepostEvent.KIND, GenericRepostEvent.KIND -> Res.string.boost
VoiceEvent.KIND -> Res.string.voice_post
VoiceReplyEvent.KIND -> Res.string.voice_reply
ReportEvent.KIND -> Res.string.pow_kind_report
GiftWrapEvent.KIND -> Res.string.private_message
ChannelMessageEvent.KIND, LiveActivitiesChatMessageEvent.KIND -> Res.string.pow_kind_chat_message
else -> Res.string.post
}
/**
* Android-resource twin of [powKindLabelRes], for [com.vitorpamplona.amethyst.service.pow.PowMiningForegroundService].
@@ -100,6 +100,7 @@ import com.vitorpamplona.amethyst.commons.service.broadcast.BroadcastStatus
import com.vitorpamplona.amethyst.commons.service.broadcast.RelayResult
import com.vitorpamplona.amethyst.commons.service.pow.PoWEstimator
import com.vitorpamplona.amethyst.commons.service.pow.PoWJobState
import com.vitorpamplona.amethyst.commons.service.pow.powKindLabelRes
import com.vitorpamplona.amethyst.commons.ui.pluralStringRes
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
@@ -107,7 +108,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.allGoodColor
import com.vitorpamplona.amethyst.commons.ui.theme.warningColor
import com.vitorpamplona.amethyst.service.pow.deviceHashesPerSecond
import com.vitorpamplona.amethyst.service.pow.formatTimeLeft
import com.vitorpamplona.amethyst.service.pow.powKindLabelRes
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
@@ -36,6 +36,7 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.theme.HalfVertPadding
import com.vitorpamplona.amethyst.model.UrlCachedPreviewer
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.urlPreview
@Composable
fun LoadUrlPreview(
@@ -46,6 +46,7 @@ import com.vitorpamplona.amethyst.commons.richtext.UrlParser
import com.vitorpamplona.amethyst.model.MediaAspectRatioCache
import com.vitorpamplona.amethyst.model.UrlCachedPreviewer
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.urlPreview
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip14Subject.subject
@@ -54,6 +54,7 @@ import com.vitorpamplona.amethyst.ui.components.DisplayUrlWithLoadingSymbol
import com.vitorpamplona.amethyst.ui.components.WaitAndDisplay
import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.urlPreview
@Composable
fun PreviewUrl(
@@ -25,12 +25,12 @@ import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.commons.audio.VisualizerStyle
import com.vitorpamplona.amethyst.commons.cashu.ops.describeMintError
import com.vitorpamplona.amethyst.commons.feeds.CardFeedState
import com.vitorpamplona.amethyst.commons.feeds.FeedState
import com.vitorpamplona.amethyst.commons.marmot.MarmotGroupIconChange
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.model.AddressableNote
import com.vitorpamplona.amethyst.commons.model.CombinedZap
@@ -88,13 +88,12 @@ import com.vitorpamplona.amethyst.commons.resources.signer_not_found_exception_d
import com.vitorpamplona.amethyst.commons.resources.unauthorized_exception
import com.vitorpamplona.amethyst.commons.resources.unauthorized_exception_description
import com.vitorpamplona.amethyst.commons.resources.user_x_does_not_have_a_lightning_address_setup_to_receive_sats
import com.vitorpamplona.amethyst.commons.service.OnlineChecker
import com.vitorpamplona.amethyst.commons.service.broadcast.BroadcastTracker
import com.vitorpamplona.amethyst.commons.service.http.IRoleBasedHttpClientBuilder
import com.vitorpamplona.amethyst.commons.service.pow.PoWCategory
import com.vitorpamplona.amethyst.commons.service.pow.powKindLabelRes
import com.vitorpamplona.amethyst.commons.state.UiSettingsState
import com.vitorpamplona.amethyst.commons.tor.TorSettingsFlow
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.state.GenericBaseCache
@@ -104,16 +103,12 @@ import com.vitorpamplona.amethyst.commons.util.logTime
import com.vitorpamplona.amethyst.commons.util.showAmount
import com.vitorpamplona.amethyst.commons.util.showAmountInteger
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModelHost
import com.vitorpamplona.amethyst.model.UrlCachedPreviewer
import com.vitorpamplona.amethyst.model.privacyOptions.RoleBasedHttpClientBuilder
import com.vitorpamplona.amethyst.service.ClinkDebitPayer
import com.vitorpamplona.amethyst.service.V4VPaymentHandler
import com.vitorpamplona.amethyst.service.ZapPaymentHandler
import com.vitorpamplona.amethyst.service.cashu.melt.MeltProcessor
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
import com.vitorpamplona.amethyst.service.pow.powKindLabelRes
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.RelaySubscriptionsCoordinator
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.send.MarmotGroupIconChange
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.markRoomNoteAsRead
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.rowHasUnread
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.eventsync.EventSync
@@ -143,7 +138,6 @@ import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent
import com.vitorpamplona.quartz.nip05DnsIdentifiers.INip05Client
import com.vitorpamplona.quartz.nip05DnsIdentifiers.Nip05Client
import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
import com.vitorpamplona.quartz.nip17Dm.base.NIP17Group
@@ -2156,15 +2150,6 @@ class AccountViewModel(
account.deleteStatus(LocalCache.getOrCreateAddressableNote(address))
}
fun urlPreview(
url: String,
onResult: suspend (UrlPreviewState) -> Unit,
) {
viewModelScope.launch(Dispatchers.IO) {
UrlCachedPreviewer.previewInfo(url, httpClientBuilder::okHttpClientForPreview, onResult)
}
}
fun loadReactionTo(note: Note?): String? {
if (note == null) return null
@@ -2262,11 +2247,6 @@ class AccountViewModel(
.mapNotNull { hex -> checkGetOrCreateUser(hex) }
.sortedByDescending { account.isKnown(it) }
suspend fun checkVideoIsOnline(videoUrl: String): Boolean =
withContext(Dispatchers.IO) {
OnlineChecker.isOnline(videoUrl, httpClientBuilder::okHttpClientForVideo)
}
fun loadAndMarkAsRead(
routeForLastRead: String,
createdAt: Long?,
@@ -2313,28 +2293,6 @@ class AccountViewModel(
}
}
class Factory(
val account: Account,
val settings: UiSettingsState,
val torSettings: TorSettingsFlow,
val dataSources: RelaySubscriptionsCoordinator,
val okHttpClient: RoleBasedHttpClientBuilder,
val nip05ClientBuilder: () -> Nip05Client,
val host: AccountViewModelHost,
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T =
AccountViewModel(
account,
settings,
torSettings,
dataSources,
okHttpClient,
nip05ClientBuilder,
host,
) as T
}
init {
Log.d("AccountViewModel", "Init")
viewModelScope.launch(Dispatchers.IO) {
@@ -27,17 +27,36 @@ import android.os.Looper
import android.widget.Toast
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.commons.marmot.MarmotGroupIconUpload
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.failed_to_save_the_video
import com.vitorpamplona.amethyst.commons.resources.video_saved_to_the_gallery
import com.vitorpamplona.amethyst.commons.service.OnlineChecker
import com.vitorpamplona.amethyst.commons.ui.components.UrlPreviewState
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
import com.vitorpamplona.amethyst.model.UrlCachedPreviewer
import com.vitorpamplona.amethyst.ui.actions.MediaSaverToDisk
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.send.MarmotGroupIconUpload
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.send.MarmotGroupIconUploader
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
// Account actions that read a content Uri or write to the device's media store, which only the
// Android app can do. They stay out of AccountViewModel so the view model does not need Android.
// Account actions that need Android (a content Uri, the media store) or the JVM HTTP stack the
// app's OkHttp clients come from. They stay out of AccountViewModel so the view model needs neither.
fun AccountViewModel.urlPreview(
url: String,
onResult: suspend (UrlPreviewState) -> Unit,
) {
viewModelScope.launch(Dispatchers.IO) {
UrlCachedPreviewer.previewInfo(url, httpClientBuilder::okHttpClientForPreview, onResult)
}
}
suspend fun AccountViewModel.checkVideoIsOnline(videoUrl: String): Boolean =
withContext(Dispatchers.IO) {
OnlineChecker.isOnline(videoUrl, httpClientBuilder::okHttpClientForVideo)
}
/**
* Encrypt + upload a picked image as a group avatar (canonical `marmot-group-image-v1` scheme).
@@ -69,19 +69,17 @@ fun LoggedInPage(
accountSessionManager: AccountSessionManager,
) {
val accountViewModel: AccountViewModel =
viewModel(
key = "AccountViewModel",
factory =
AccountViewModel.Factory(
account = account,
settings = Amethyst.instance.uiState,
torSettings = Amethyst.instance.torPrefs.value,
dataSources = Amethyst.instance.sources,
okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder,
nip05ClientBuilder = { Amethyst.instance.nip05Client },
host = AndroidAccountViewModelHost(Amethyst.instance),
),
)
viewModel(key = "AccountViewModel") {
AccountViewModel(
account = account,
settings = Amethyst.instance.uiState,
torSettings = Amethyst.instance.torPrefs.value,
dataSources = Amethyst.instance.sources,
httpClientBuilder = Amethyst.instance.roleBasedHttpClientBuilder,
nip05ClientBuilder = { Amethyst.instance.nip05Client },
host = AndroidAccountViewModelHost(Amethyst.instance),
)
}
LaunchedEffect(Unit) {
accountViewModel.firstRoute = route
@@ -44,6 +44,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.commons.marmot.MarmotGroupIconChange
import com.vitorpamplona.amethyst.commons.marmot.ui.MarmotRetentionChoice
import com.vitorpamplona.amethyst.commons.marmot.ui.MarmotRetentionPicker
import com.vitorpamplona.amethyst.commons.model.navigation.Route
@@ -65,7 +66,6 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.CreatingTopBar
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.send.MarmotGroupIconChange
import com.vitorpamplona.amethyst.ui.screen.loggedIn.uploadMarmotGroupIcon
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.utils.RandomInstance
@@ -43,6 +43,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.marmot.MarmotGroupIconChange
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.description
import com.vitorpamplona.amethyst.commons.resources.marmot_avatar_url
@@ -63,7 +64,6 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.ActionTopBar
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.send.MarmotGroupIconChange
import com.vitorpamplona.amethyst.ui.screen.loggedIn.uploadMarmotGroupIcon
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import kotlinx.coroutines.Dispatchers
@@ -58,7 +58,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
* icon is [removeRequested], or the group's current (decrypted) avatar. Tapping the
* avatar opens the system photo picker; a text button below removes the current icon.
* All selection state is hoisted so the parent screen can turn it into a
* [com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.send.MarmotGroupIconChange]
* [com.vitorpamplona.amethyst.commons.marmot.MarmotGroupIconChange]
* at save time.
*/
@Composable
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.send
import android.content.Context
import android.net.Uri
import com.vitorpamplona.amethyst.commons.marmot.MarmotGroupIconUpload
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
@@ -31,53 +32,11 @@ import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator
import com.vitorpamplona.amethyst.service.uploads.UploadingState
import com.vitorpamplona.quartz.marmot.mip01Groups.MarmotGroupImageCipher
import com.vitorpamplona.quartz.marmot.mip01Groups.MarmotGroupImageEncryption
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import com.vitorpamplona.quartz.utils.Log
import java.io.File
/**
* The parameters produced by encrypting + uploading a new Marmot group avatar,
* ready to be folded into a [com.vitorpamplona.quartz.marmot.mip01Groups.MarmotGroupData]
* via [com.vitorpamplona.quartz.marmot.mip01Groups.MarmotGroupData.withImage].
*/
class MarmotGroupIconUpload(
/** SHA-256 (hex) of the encrypted blob = its Blossom content hash. */
val imageHash: HexKey,
/** 32-byte HKDF seed for the image AEAD key (MIP-01 v2). */
val imageKey: ByteArray,
/** 12-byte nonce. */
val imageNonce: ByteArray,
/** 32-byte HKDF seed for the Blossom-auth keypair (MIP-01 v2). */
val imageUploadKey: ByteArray,
/**
* Media type of the DECRYPTED image.
*
* MIP-01's blob never carried one, but the current profile's `0x8002`
* component requires it on a present image — and it is bound into the
* AEAD's AAD there, so a receiver cannot be steered into decoding the
* plaintext as a different type than the uploader meant.
*/
val mediaType: String,
)
/**
* How a metadata update should treat the group icon.
*/
sealed class MarmotGroupIconChange {
/** Leave the existing icon (if any) untouched. */
data object Keep : MarmotGroupIconChange()
/** Remove the current icon. */
data object Clear : MarmotGroupIconChange()
/** Replace the icon with a freshly-uploaded one. */
class Set(
val upload: MarmotGroupIconUpload,
) : MarmotGroupIconChange()
}
/**
* Encrypts a picked image with the MIP-01 v2 scheme (see [MarmotGroupImageEncryption])
* and uploads the ciphertext to Blossom, signing the upload authorization with the
@@ -105,6 +105,7 @@ import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar
import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.backups.BackupConflictCards
import com.vitorpamplona.amethyst.ui.screen.loggedIn.checkVideoIsOnline
import com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.NewGeoPostButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.NewHashtagPostButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.HomeFilterAssemblerSubscription
@@ -34,6 +34,7 @@ import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel
import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel
import com.vitorpamplona.amethyst.commons.service.OnlineChecker
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.checkVideoIsOnline
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent
import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.Dispatchers
@@ -107,6 +107,7 @@ import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar
import com.vitorpamplona.amethyst.ui.screen.SaveableFeedState
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.urlPreview
import com.vitorpamplona.quartz.nipB0WebBookmarks.WebBookmarkEvent
import kotlinx.coroutines.launch
@@ -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.amethyst.commons.marmot
import com.vitorpamplona.quartz.nip01Core.core.HexKey
/**
* The parameters produced by encrypting + uploading a new Marmot group avatar,
* ready to be folded into a [com.vitorpamplona.quartz.marmot.mip01Groups.MarmotGroupData]
* via [com.vitorpamplona.quartz.marmot.mip01Groups.MarmotGroupData.withImage].
*/
class MarmotGroupIconUpload(
/** SHA-256 (hex) of the encrypted blob = its Blossom content hash. */
val imageHash: HexKey,
/** 32-byte HKDF seed for the image AEAD key (MIP-01 v2). */
val imageKey: ByteArray,
/** 12-byte nonce. */
val imageNonce: ByteArray,
/** 32-byte HKDF seed for the Blossom-auth keypair (MIP-01 v2). */
val imageUploadKey: ByteArray,
/**
* Media type of the DECRYPTED image.
*
* MIP-01's blob never carried one, but the current profile's `0x8002`
* component requires it on a present image — and it is bound into the
* AEAD's AAD there, so a receiver cannot be steered into decoding the
* plaintext as a different type than the uploader meant.
*/
val mediaType: String,
)
/**
* How a metadata update should treat the group icon.
*/
sealed class MarmotGroupIconChange {
/** Leave the existing icon (if any) untouched. */
data object Keep : MarmotGroupIconChange()
/** Remove the current icon. */
data object Clear : MarmotGroupIconChange()
/** Replace the icon with a freshly-uploaded one. */
class Set(
val upload: MarmotGroupIconUpload,
) : MarmotGroupIconChange()
}
@@ -0,0 +1,58 @@
/*
* 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.service.pow
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.boost
import com.vitorpamplona.amethyst.commons.resources.post
import com.vitorpamplona.amethyst.commons.resources.pow_kind_chat_message
import com.vitorpamplona.amethyst.commons.resources.pow_kind_report
import com.vitorpamplona.amethyst.commons.resources.private_message
import com.vitorpamplona.amethyst.commons.resources.reaction
import com.vitorpamplona.amethyst.commons.resources.voice_post
import com.vitorpamplona.amethyst.commons.resources.voice_reply
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
import com.vitorpamplona.quartz.nip56Reports.ReportEvent
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceEvent
import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent
import org.jetbrains.compose.resources.StringResource
/**
* The one user-facing label for "what is being mined": shared by the
* broadcast banner, the mining foreground notification, and failure toasts
* so a job is described the same way everywhere it appears.
*/
fun powKindLabelRes(kind: Int): StringResource =
when (kind) {
ReactionEvent.KIND -> Res.string.reaction
RepostEvent.KIND, GenericRepostEvent.KIND -> Res.string.boost
VoiceEvent.KIND -> Res.string.voice_post
VoiceReplyEvent.KIND -> Res.string.voice_reply
ReportEvent.KIND -> Res.string.pow_kind_report
GiftWrapEvent.KIND -> Res.string.private_message
ChannelMessageEvent.KIND, LiveActivitiesChatMessageEvent.KIND -> Res.string.pow_kind_chat_message
else -> Res.string.post
}