app

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

HarvestCircleShellPresenter.kt (6811B)


      1 package org.harvestcircle.application
      2 
      3 import kotlinx.coroutines.CoroutineScope
      4 import kotlinx.coroutines.Job
      5 import kotlinx.coroutines.flow.MutableStateFlow
      6 import kotlinx.coroutines.flow.StateFlow
      7 import kotlinx.coroutines.flow.asStateFlow
      8 import kotlinx.coroutines.flow.collect
      9 import kotlinx.coroutines.flow.update
     10 import kotlinx.coroutines.launch
     11 import org.harvestcircle.appearance.AppearanceState
     12 import org.harvestcircle.appearance.MotionPreference
     13 import org.harvestcircle.appearance.TextSizePreference
     14 import org.harvestcircle.appearance.ThemePreference
     15 import org.harvestcircle.navigation.AppRoute
     16 import org.harvestcircle.navigation.NavigationIntent
     17 import org.harvestcircle.product.ScreenKey
     18 
     19 interface IdentityPresentationPort {
     20     val state: StateFlow<HarvestCirclePresenterState>
     21 
     22     fun dispatch(intent: HarvestCircleIntent)
     23 }
     24 
     25 data class HarvestCircleShellState(
     26     val identity: HarvestCirclePresenterState,
     27     val buildInfo: BuildInfo,
     28     val session: ShellSessionState = ShellSessionState(),
     29     val localUsability: LocalUsability = deriveLocalUsability(identity.snapshot),
     30     val root: ShellRoot = deriveShellRoot(identity, session),
     31     val appearance: AppearanceState = AppearanceState(),
     32     val overlays: OverlayState = OverlayState(),
     33 )
     34 
     35 sealed interface HarvestCircleShellIntent {
     36     data class Identity(
     37         val intent: HarvestCircleIntent,
     38     ) : HarvestCircleShellIntent
     39 
     40     data object EnterReadOnly : HarvestCircleShellIntent
     41 
     42     data object ManageIdentity : HarvestCircleShellIntent
     43 
     44     data class Navigate(
     45         val screenKey: ScreenKey,
     46     ) : HarvestCircleShellIntent
     47 
     48     data class Navigation(
     49         val intent: NavigationIntent,
     50     ) : HarvestCircleShellIntent
     51 
     52     data class Overlay(
     53         val intent: OverlayIntent,
     54     ) : HarvestCircleShellIntent
     55 
     56     data class SetTheme(
     57         val theme: ThemePreference,
     58     ) : HarvestCircleShellIntent
     59 
     60     data class SetTextSize(
     61         val textSize: TextSizePreference,
     62     ) : HarvestCircleShellIntent
     63 
     64     data class SetMotion(
     65         val motion: MotionPreference,
     66     ) : HarvestCircleShellIntent
     67 }
     68 
     69 class HarvestCircleShellPresenter(
     70     private val identityPresenter: IdentityPresentationPort,
     71     buildInfo: BuildInfo,
     72     scope: CoroutineScope,
     73     private val referenceParser: NostrReferenceParser = RejectingNostrReferenceParser,
     74 ) {
     75     private val mutableState = MutableStateFlow(HarvestCircleShellState(identityPresenter.state.value, buildInfo))
     76     val state: StateFlow<HarvestCircleShellState> = mutableState.asStateFlow()
     77     private val observation: Job =
     78         scope.launch {
     79             identityPresenter.state.collect { identity -> updateIdentity(identity) }
     80         }
     81 
     82     fun dispatch(intent: HarvestCircleShellIntent) {
     83         when (intent) {
     84             is HarvestCircleShellIntent.Identity -> identityPresenter.dispatch(intent.intent)
     85             HarvestCircleShellIntent.EnterReadOnly -> reduce(ShellEvent.EnterReadOnly)
     86             HarvestCircleShellIntent.ManageIdentity -> reduce(ShellEvent.ManageIdentity)
     87             is HarvestCircleShellIntent.Navigate -> reduce(ShellEvent.Navigate(intent.screenKey))
     88             is HarvestCircleShellIntent.Navigation -> reduce(ShellEvent.Navigation(intent.intent))
     89             is HarvestCircleShellIntent.Overlay -> dispatchOverlay(intent.intent)
     90             is HarvestCircleShellIntent.SetTheme -> reduce(ShellEvent.SetTheme(intent.theme))
     91             is HarvestCircleShellIntent.SetTextSize -> reduce(ShellEvent.SetTextSize(intent.textSize))
     92             is HarvestCircleShellIntent.SetMotion -> reduce(ShellEvent.SetMotion(intent.motion))
     93         }
     94     }
     95 
     96     fun close() {
     97         observation.cancel()
     98     }
     99 
    100     private fun updateIdentity(identity: HarvestCirclePresenterState) {
    101         reduce(ShellEvent.IdentityObserved(identity))
    102     }
    103 
    104     private fun dispatchOverlay(intent: OverlayIntent) {
    105         when (intent) {
    106             OverlayIntent.SubmitReference -> {
    107                 val overlay = mutableState.value.overlays.current as? FoundationOverlay.OpenNostrReference ?: return
    108                 val admitted = ReferenceInputPolicy.admit(overlay.input)
    109                 if (admitted == ReferenceInputAdmission.PrivateKeyShaped) {
    110                     applyReferenceResult(ReferenceResult.PrivateKeyRejected, clearInput = true)
    111                     return
    112                 }
    113                 if (admitted == ReferenceInputAdmission.TooLarge) {
    114                     applyReferenceResult(ReferenceResult.Invalid, clearInput = true)
    115                     return
    116                 }
    117                 if (admitted == ReferenceInputAdmission.AmbiguousHex) {
    118                     applyReferenceResult(ReferenceResult.AmbiguousHex, clearInput = true)
    119                     return
    120                 }
    121                 val parsed = referenceParser.parse((admitted as ReferenceInputAdmission.Accepted).value)
    122                 when (parsed.classification) {
    123                     NostrReferenceClassification.Invalid -> applyReferenceResult(ReferenceResult.Invalid)
    124                     NostrReferenceClassification.PrivateKeyRejected ->
    125                         applyReferenceResult(ReferenceResult.PrivateKeyRejected, clearInput = true)
    126                     NostrReferenceClassification.EventId,
    127                     NostrReferenceClassification.PublicKey,
    128                     NostrReferenceClassification.Profile,
    129                     NostrReferenceClassification.Note,
    130                     NostrReferenceClassification.Event,
    131                     NostrReferenceClassification.Address,
    132                     -> applyReferenceResult(ReferenceResult.Unsupported)
    133                 }
    134                 return
    135             }
    136             else -> Unit
    137         }
    138         admitOverlay(intent)
    139     }
    140 
    141     private fun admitOverlay(intent: OverlayIntent) {
    142         while (true) {
    143             val current = mutableState.value
    144             val transition = OverlayReducer.transition(current, intent)
    145             if (transition.state == current && transition.effects.isEmpty()) return
    146             if (!mutableState.compareAndSet(current, transition.state)) continue
    147             transition.effects.forEach(::execute)
    148             return
    149         }
    150     }
    151 
    152     private fun execute(effect: ShellEffect) {
    153         when (effect) {
    154             is ShellEffect.DispatchIdentity -> identityPresenter.dispatch(effect.intent)
    155         }
    156     }
    157 
    158     private fun applyReferenceResult(
    159         result: ReferenceResult,
    160         clearInput: Boolean = false,
    161     ) {
    162         admitOverlay(OverlayIntent.ApplyReferenceResult(result, clearInput))
    163     }
    164 
    165     private fun reduce(event: ShellEvent) {
    166         mutableState.update { current -> ShellReducer.reduce(current, event) }
    167     }
    168 }
    169 
    170 val HarvestCircleShellState.currentRoute: AppRoute?
    171     get() = (root as? ShellRoot.Dashboard)?.navigation?.current