package com.sptmobile.messages import android.app.Application import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope import com.sptmobile.link.HostLinkState import com.sptmobile.link.LinkIo import com.sptmobile.link.LinkNative import com.sptmobile.link.LinkService import com.sptmobile.pairing.HostStore import com.sptmobile.pairing.PairedHost import kotlinx.coroutines.CancellationException import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.withContext // [impl->REQ-GATEWAY-THREAD] /** * The Messages tab (the v1.1.0 batch decision, 2026-07-07): per-gateway in/out * threads. Two levels — the paired-gateway list, then one gateway's UNIFIED * message thread (every endpoint's history, labeled by endpoint, NO digest * content — DESIGN.md ruling 8). A row taps through to that endpoint's digest * view anchored at the message (the screen owns that nav). * * One paired host is one gateway; the thread is that host's history-fetch-all * union. Pure parse/flatten lives in [MessageThread]; this only moves data, * all RPCs through the [LinkService] supervisor's Connected handle on * [LinkIo.dispatcher]. */ class MessagesViewModel(app: Application) : AndroidViewModel(app) { private val store = HostStore(app) /** Paired gateways (= paired hosts), user-ordered (dial order). */ val gateways: StateFlow> = store.hosts.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList()) sealed interface ThreadState { val host: PairedHost data class Loading(override val host: PairedHost) : ThreadState data class Loaded( override val host: PairedHost, val rows: List, ) : ThreadState data class Error(override val host: PairedHost, val message: String) : ThreadState } private val _thread = MutableStateFlow(null) /** The open gateway's thread; null means the gateway list is showing. */ val thread: StateFlow = _thread fun openThread(host: PairedHost) { _thread.value = ThreadState.Loading(host) viewModelScope.launch { load(host) } } /** Re-pull the open gateway's thread (manual refresh). */ fun refresh() { _thread.value?.let { openThread(it.host) } } /** Back to the gateway list. */ fun closeThread() { _thread.value = null } private suspend fun load(host: PairedHost) { val supervisor = LinkService.shared.value val handle = (supervisor?.states?.value?.get(host.node) as? HostLinkState.Connected)?.handle if (handle == null) { _thread.value = ThreadState.Error(host, "gateway link down — no live host") return } try { val reply = withContext(LinkIo.dispatcher) { LinkNative.historyFetchAll(handle, HISTORY_LIMIT) } _thread.value = ThreadState.Loaded(host, MessageThread.parseThread(reply)) } catch (e: RuntimeException) { if (e is CancellationException) throw e // [impl->REQ-HAZARD-STALE-LINK-STALL] a dead-but-cached Connected // handle → tell the supervisor to redial now, don't strand the tab. supervisor.reportStale(host.node, handle, "historyFetchAll: ${e.message}") _thread.value = ThreadState.Error(host, e.message ?: "fetch failed") } } companion object { private const val HISTORY_LIMIT = 200 } }