app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

ShellOverlays.kt (8587B)


      1 package org.harvestcircle.application
      2 
      3 sealed interface FoundationOverlay {
      4     data class ConfirmAction(
      5         val title: String,
      6         val explanation: String,
      7         val actionLabel: String,
      8         val action: ConfirmationAction,
      9         val phase: ConfirmationPhase = ConfirmationPhase.Ready,
     10     ) : FoundationOverlay {
     11         val busy: Boolean
     12             get() = phase != ConfirmationPhase.Ready
     13     }
     14 
     15     data class Status(
     16         val key: StatusOverlayKey,
     17     ) : FoundationOverlay
     18 
     19     data class OpenNostrReference(
     20         val input: String = "",
     21         val result: ReferenceResult? = null,
     22     ) : FoundationOverlay
     23 }
     24 
     25 sealed interface ConfirmationAction {
     26     data class RemoveLocalIdentity(
     27         val identityId: IdentityId,
     28         val requestId: RemovalRequestId,
     29     ) : ConfirmationAction
     30 }
     31 
     32 enum class ConfirmationPhase { Ready, Submitting, Dismissing }
     33 
     34 enum class StatusOverlayKey { Signer, Sync }
     35 
     36 data class OverlayState(
     37     val current: FoundationOverlay? = null,
     38     val returnFocus: ShellFocusTarget? = null,
     39     val restoreFocus: ShellFocusTarget? = null,
     40 )
     41 
     42 sealed interface ShellFocusTarget {
     43     data object RouteFallback : ShellFocusTarget
     44 
     45     data object BootstrapFallback : ShellFocusTarget
     46 
     47     data object TodayReference : ShellFocusTarget
     48 
     49     data object TopBarReference : ShellFocusTarget
     50 
     51     data object TopBarSync : ShellFocusTarget
     52 
     53     data object TopBarSigner : ShellFocusTarget
     54 
     55     data class IdentityRow(
     56         val publicKeyHex: String,
     57     ) : ShellFocusTarget
     58 }
     59 
     60 sealed interface OverlayIntent {
     61     data class Open(
     62         val overlay: FoundationOverlay,
     63         val returnFocus: ShellFocusTarget = ShellFocusTarget.RouteFallback,
     64     ) : OverlayIntent
     65 
     66     data class OpenReference(
     67         val returnFocus: ShellFocusTarget = ShellFocusTarget.RouteFallback,
     68     ) : OverlayIntent
     69 
     70     class EditReference(
     71         val value: String,
     72     ) : OverlayIntent {
     73         override fun toString(): String = "EditReference(value=[REDACTED])"
     74     }
     75 
     76     data object SubmitReference : OverlayIntent
     77 
     78     data class ApplyReferenceResult(
     79         val result: ReferenceResult,
     80         val clearInput: Boolean = false,
     81     ) : OverlayIntent
     82 
     83     data class Confirm(
     84         val action: ConfirmationAction,
     85     ) : OverlayIntent
     86 
     87     data class DismissConfirmation(
     88         val action: ConfirmationAction,
     89     ) : OverlayIntent
     90 
     91     data object Close : OverlayIntent
     92 
     93     data class Escape(
     94         val confirmation: ConfirmationAction? = null,
     95     ) : OverlayIntent
     96 }
     97 
     98 sealed interface ShellEffect {
     99     data class DispatchIdentity(
    100         val intent: HarvestCircleIntent,
    101     ) : ShellEffect
    102 }
    103 
    104 data class OverlayTransition(
    105     val state: HarvestCircleShellState,
    106     val effects: List<ShellEffect> = emptyList(),
    107 )
    108 
    109 object OverlayReducer {
    110     fun transition(
    111         state: HarvestCircleShellState,
    112         intent: OverlayIntent,
    113     ): OverlayTransition =
    114         when (intent) {
    115             is OverlayIntent.Open ->
    116                 if (intent.overlay is FoundationOverlay.OpenNostrReference) {
    117                     OverlayTransition(state)
    118                 } else {
    119                     state.openOverlay(intent.overlay, intent.returnFocus)
    120                 }
    121             is OverlayIntent.OpenReference ->
    122                 state.openOverlay(FoundationOverlay.OpenNostrReference(), intent.returnFocus)
    123             is OverlayIntent.EditReference -> applyReferenceEdit(state, intent.value)
    124             OverlayIntent.SubmitReference -> OverlayTransition(state)
    125             is OverlayIntent.ApplyReferenceResult -> applyReferenceResult(state, intent.result, intent.clearInput)
    126             is OverlayIntent.Confirm -> admitConfirmation(state, intent.action, submitting = true)
    127             is OverlayIntent.DismissConfirmation -> admitConfirmation(state, intent.action, submitting = false)
    128             OverlayIntent.Close ->
    129                 if (state.overlays.current is FoundationOverlay.ConfirmAction) {
    130                     OverlayTransition(state)
    131                 } else {
    132                     state.closeOverlay()
    133                 }
    134             is OverlayIntent.Escape ->
    135                 when (val current = state.overlays.current) {
    136                     is FoundationOverlay.ConfirmAction -> {
    137                         val expected = intent.confirmation
    138                         if (expected == null) OverlayTransition(state) else admitConfirmation(state, expected, submitting = false)
    139                     }
    140                     null -> OverlayTransition(state)
    141                     else -> state.closeOverlay()
    142                 }
    143         }
    144 
    145     private fun applyReferenceEdit(
    146         state: HarvestCircleShellState,
    147         raw: String,
    148     ): OverlayTransition {
    149         val overlay = state.overlays.current as? FoundationOverlay.OpenNostrReference ?: return OverlayTransition(state)
    150         val updated =
    151             when (val admission = ReferenceInputPolicy.admit(raw)) {
    152                 is ReferenceInputAdmission.Accepted -> overlay.copy(input = admission.value, result = null)
    153                 ReferenceInputAdmission.PrivateKeyShaped -> overlay.copy(input = "", result = ReferenceResult.PrivateKeyRejected)
    154                 ReferenceInputAdmission.TooLarge -> overlay.copy(input = "", result = ReferenceResult.Invalid)
    155                 ReferenceInputAdmission.AmbiguousHex -> overlay.copy(input = "", result = ReferenceResult.AmbiguousHex)
    156             }
    157         return state.updateOverlay(updated)
    158     }
    159 
    160     private fun admitConfirmation(
    161         state: HarvestCircleShellState,
    162         expected: ConfirmationAction,
    163         submitting: Boolean,
    164     ): OverlayTransition {
    165         val current = state.overlays.current as? FoundationOverlay.ConfirmAction ?: return OverlayTransition(state)
    166         val removal = expected as? ConfirmationAction.RemoveLocalIdentity ?: return OverlayTransition(state)
    167         val admitted = state.identity.removalConfirmation ?: return OverlayTransition(state)
    168         if (current.action != expected ||
    169             current.phase != ConfirmationPhase.Ready ||
    170             state.identity.removalStatus != RemovalStatus.AWAITING_CONFIRMATION ||
    171             admitted.identityId != removal.identityId ||
    172             admitted.requestId != removal.requestId
    173         ) {
    174             return OverlayTransition(state)
    175         }
    176         val phase = if (submitting) ConfirmationPhase.Submitting else ConfirmationPhase.Dismissing
    177         val identityIntent =
    178             if (submitting) {
    179                 HarvestCircleIntent.ConfirmIdentityRemoval(removal.identityId, removal.requestId)
    180             } else {
    181                 HarvestCircleIntent.CancelIdentityRemoval(removal.identityId, removal.requestId)
    182             }
    183         return OverlayTransition(
    184             state.copy(overlays = state.overlays.copy(current = current.copy(phase = phase))),
    185             listOf(ShellEffect.DispatchIdentity(identityIntent)),
    186         )
    187     }
    188 
    189     private fun applyReferenceResult(
    190         state: HarvestCircleShellState,
    191         result: ReferenceResult,
    192         clearInput: Boolean,
    193     ): OverlayTransition {
    194         val overlay = state.overlays.current as? FoundationOverlay.OpenNostrReference ?: return OverlayTransition(state)
    195         return state.updateOverlay(overlay.copy(input = if (clearInput) "" else overlay.input, result = result))
    196     }
    197 
    198     private fun HarvestCircleShellState.openOverlay(
    199         overlay: FoundationOverlay,
    200         returnFocus: ShellFocusTarget,
    201     ): OverlayTransition = OverlayTransition(copy(overlays = overlays.opened(overlay, returnFocus)))
    202 
    203     private fun HarvestCircleShellState.updateOverlay(overlay: FoundationOverlay): OverlayTransition =
    204         OverlayTransition(copy(overlays = overlays.copy(current = overlay)))
    205 
    206     private fun HarvestCircleShellState.closeOverlay(): OverlayTransition = OverlayTransition(copy(overlays = overlays.closed()))
    207 }
    208 
    209 internal fun OverlayState.opened(
    210     overlay: FoundationOverlay,
    211     returnFocus: ShellFocusTarget,
    212 ): OverlayState = copy(current = overlay, returnFocus = returnFocus, restoreFocus = null)
    213 
    214 internal fun OverlayState.closed(): OverlayState = copy(current = null, returnFocus = null, restoreFocus = returnFocus)
    215 
    216 enum class ReferenceResult(
    217     val message: String,
    218 ) {
    219     Invalid("This reference is not valid."),
    220     PrivateKeyRejected("Private-key references cannot be opened."),
    221     AmbiguousHex(
    222         "Bare hexadecimal references are not accepted because they can also be private keys.\n\n" +
    223             "Use a note1 or nevent1 reference.",
    224     ),
    225     Unsupported("This Nostr reference is not supported by this build."),
    226 }