Merge remote-tracking branch 'gitea-ai/main'
All checks were successful
Demo images / Build & push demo images (push) Successful in 2m51s

This commit is contained in:
archipelago 2026-07-24 06:35:29 -04:00
commit 0dfc3a7cfb
6 changed files with 122 additions and 31 deletions

View File

@ -11,8 +11,8 @@ android {
applicationId = "com.archipelago.app"
minSdk = 26
targetSdk = 35
versionCode = 30
versionName = "0.5.10"
versionCode = 31
versionName = "0.5.11"
vectorDrawables {
useSupportLibrary = true

View File

@ -23,20 +23,26 @@ import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.windowInsetsBottomHeight
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.layout.windowInsetsTopHeight
import androidx.compose.foundation.shape.RoundedCornerShape
@ -66,6 +72,7 @@ import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
@ -928,7 +935,13 @@ private fun InAppBrowser(
modifier = Modifier
.fillMaxSize()
.background(SurfaceBlack)
.windowInsetsPadding(WindowInsets.safeDrawing),
// Bottom inset handled by the touch-shield strip below the bar —
// NOT by padding: a padded area only paints, it doesn't consume,
// so taps in the gesture strip fell straight THROUGH this overlay
// into the kiosk's tab bar behind it (accidental AIUI-tab hits).
.windowInsetsPadding(
WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal + WindowInsetsSides.Top)
),
) {
// WebView + loading overlay fill the area above the bottom control bar.
Box(modifier = Modifier.weight(1f).fillMaxWidth()) {
@ -1117,6 +1130,21 @@ private fun InAppBrowser(
)
}
}
// Touch-shield over the gesture-nav strip: solid black AND consumes
// taps — stray touches below the control bar landed on the kiosk's
// tab bar behind this overlay (opening the AIUI chat by accident).
Box(
Modifier
.fillMaxWidth()
.windowInsetsBottomHeight(WindowInsets.navigationBars)
.background(Color.Black)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = {},
),
)
}
}

View File

@ -522,8 +522,10 @@ const cloudStorageDisplay = computed(() => cloudStorageUsed.value !== null ? for
const cloudFolderDisplay = computed(() => cloudFolderCount.value !== null ? String(cloudFolderCount.value) : '...')
onMounted(async () => {
try { const usage = await fileBrowserClient.getUsage(); cloudStorageUsed.value = usage.totalSize; cloudFolderCount.value = usage.folderCount } catch { /* not running */ }
// Paint last-known wallet figures BEFORE any network round-trip.
hydrateWalletSnapshot()
loadSystemStats(); systemStatsInterval = setInterval(loadSystemStats, 10000); checkUpdateStatus(); loadWeb5Status()
try { const usage = await fileBrowserClient.getUsage(); cloudStorageUsed.value = usage.totalSize; cloudFolderCount.value = usage.folderCount } catch { /* not running */ }
// Poll wallet balances/transactions like Web5.vue does without this a
// pending on-chain receive (or a fresh instant payment) only shows up
// after a manual wallet action or a remount.
@ -583,25 +585,78 @@ function ecashToWalletTransaction(tx: EcashTransaction): WalletTransaction {
}
}
// Last-known wallet snapshot, hydrated before ANY network round-trip so the
// card paints real figures instantly (app-launch-speed doctrine: over the
// mesh every serialized RPC costs a full RTT never make the user watch it).
const WALLET_SNAPSHOT_KEY = 'archy-wallet-snapshot-v1'
function hydrateWalletSnapshot() {
try {
const raw = localStorage.getItem(WALLET_SNAPSHOT_KEY)
if (!raw) return
const s = JSON.parse(raw)
walletOnchain.value = s.onchain ?? 0
walletLightning.value = s.lightning ?? 0
walletEcash.value = s.ecash ?? 0
walletFedimint.value = s.fedimint ?? 0
walletArk.value = s.ark ?? 0
walletConnected.value = s.connected === true
if (Array.isArray(s.transactions)) walletTransactions.value = s.transactions
} catch { /* corrupt/absent snapshot — fresh load fills in */ }
}
function persistWalletSnapshot() {
try {
localStorage.setItem(WALLET_SNAPSHOT_KEY, JSON.stringify({
onchain: walletOnchain.value,
lightning: walletLightning.value,
ecash: walletEcash.value,
fedimint: walletFedimint.value,
ark: walletArk.value,
connected: walletConnected.value,
// Enough for the Transactions modal's first paint; refresh replaces it.
transactions: walletTransactions.value.slice(0, 50),
}))
} catch { /* storage full — snapshot is best-effort */ }
}
async function loadWeb5Status() {
// A transient RPC timeout must NOT flash the balance to 0 ("wallet says 0 when
// there is a balance"). On failure keep the last-known value the refs start
// at 0, so only the very first load before any success shows 0.
try { const res = await rpcClient.call<{ balance_sats: number; channel_balance_sats: number }>({ method: 'lnd.getinfo', timeout: 5000 }); walletOnchain.value = res.balance_sats || 0; walletLightning.value = res.channel_balance_sats || 0; walletConnected.value = true } catch { walletConnected.value = false }
try { const res = await rpcClient.call<{ balance_sats: number }>({ method: 'wallet.ecash-balance', timeout: 5000 }); walletEcash.value = res.balance_sats ?? 0 } catch { /* keep last-known balance */ }
try { const res = await rpcClient.call<{ balance_sats: number }>({ method: 'wallet.fedimint-balance', timeout: 5000 }); walletFedimint.value = res.balance_sats ?? 0 } catch { /* keep last-known balance */ }
try { const res = await rpcClient.call<{ spendable_sats: number }>({ method: 'wallet.ark-balance', timeout: 5000 }); walletArk.value = res.spendable_sats ?? 0 } catch { /* keep last-known balance */ }
// from the persisted snapshot, so 0 only ever shows on a genuinely fresh node.
//
// All seven calls are independent fire them TOGETHER. Serialized, this
// block cost 7 × (mesh RTT + backend time); parallel it costs one slowest
// call, which is what makes the card feel like an app launch.
const balances = Promise.allSettled([
rpcClient.call<{ balance_sats: number; channel_balance_sats: number }>({ method: 'lnd.getinfo', timeout: 5000 })
.then(res => { walletOnchain.value = res.balance_sats || 0; walletLightning.value = res.channel_balance_sats || 0; walletConnected.value = true })
.catch(() => { walletConnected.value = false }),
rpcClient.call<{ balance_sats: number }>({ method: 'wallet.ecash-balance', timeout: 5000 })
.then(res => { walletEcash.value = res.balance_sats ?? 0 }).catch(() => { /* keep last-known */ }),
rpcClient.call<{ balance_sats: number }>({ method: 'wallet.fedimint-balance', timeout: 5000 })
.then(res => { walletFedimint.value = res.balance_sats ?? 0 }).catch(() => { /* keep last-known */ }),
rpcClient.call<{ spendable_sats: number }>({ method: 'wallet.ark-balance', timeout: 5000 })
.then(res => { walletArk.value = res.spendable_sats ?? 0 }).catch(() => { /* keep last-known */ }),
])
// Merge LND transactions with ecash/Fedimint history (wallet.ecash-history
// already unifies both) previously only LND transactions were fetched
// here, so any Cashu or Fedimint receive (e.g. a TollGate payment) never
// appeared in the Transactions modal even though the balance included it.
let lndTxs: WalletTransaction[] = []
try { const res = await rpcClient.call<{ transactions: WalletTransaction[]; incoming_pending_count: number }>({ method: 'lnd.gettransactions', timeout: 5000 }); lndTxs = (res.transactions || []).map(tx => ({ ...tx, kind: 'onchain' as const })) } catch { /* keep last-known transactions */ }
let lightningTxs: WalletTransaction[] = []
try { const res = await rpcClient.call<{ transactions: WalletTransaction[] }>({ method: 'lnd.lightning-history', timeout: 5000 }); lightningTxs = res.transactions || [] } catch { /* keep last-known transactions */ }
let ecashTxs: WalletTransaction[] = []
try { const res = await rpcClient.call<{ transactions: EcashTransaction[] }>({ method: 'wallet.ecash-history', timeout: 5000 }); ecashTxs = (res.transactions || []).map(ecashToWalletTransaction) } catch { /* keep last-known transactions */ }
walletTransactions.value = [...lndTxs, ...lightningTxs, ...ecashTxs].sort((a, b) => b.time_stamp - a.time_stamp)
// already unifies both) so Cashu/Fedimint receives appear in the modal.
const histories = Promise.allSettled([
rpcClient.call<{ transactions: WalletTransaction[]; incoming_pending_count: number }>({ method: 'lnd.gettransactions', timeout: 5000 })
.then(res => (res.transactions || []).map(tx => ({ ...tx, kind: 'onchain' as const }))).catch(() => [] as WalletTransaction[]),
rpcClient.call<{ transactions: WalletTransaction[] }>({ method: 'lnd.lightning-history', timeout: 5000 })
.then(res => res.transactions || []).catch(() => [] as WalletTransaction[]),
rpcClient.call<{ transactions: EcashTransaction[] }>({ method: 'wallet.ecash-history', timeout: 5000 })
.then(res => (res.transactions || []).map(ecashToWalletTransaction)).catch(() => [] as WalletTransaction[]),
]).then((results) => {
const merged = results.flatMap(r => (r.status === 'fulfilled' ? r.value : []))
// Keep last-known list when every history call failed this round.
if (merged.length > 0 || results.some(r => r.status === 'fulfilled')) {
walletTransactions.value = merged.sort((a, b) => b.time_stamp - a.time_stamp)
}
})
await Promise.allSettled([balances, histories])
persistWalletSnapshot()
}
// System stats

View File

@ -794,15 +794,19 @@ function goBack() {
onMounted(async () => {
if (props.peerId) {
// Find the peer by onion address
try {
const result = await rpcClient.federationListNodes()
const peers = result?.nodes ?? []
currentPeer.value = peers.find((p: PeerNode) => p.onion === props.peerId) || null
} catch {
// Continue with just the onion address
}
await Promise.all([loadCatalog(), loadOwned()])
// The peer-name lookup is cosmetic the catalog only needs the onion we
// already have. Serialized, it added a full mesh round-trip before the
// files even started loading.
await Promise.all([
rpcClient.federationListNodes()
.then((result) => {
const peers = result?.nodes ?? []
currentPeer.value = peers.find((p: PeerNode) => p.onion === props.peerId) || null
})
.catch(() => { /* continue with just the onion address */ }),
loadCatalog(),
loadOwned(),
])
} else {
loading.value = false
}

View File

@ -350,13 +350,17 @@ async function loadPeers() {
const hadPeers = peers.value.length > 0 || observers.value.length > 0
loadingPeers.value = true
try {
const res = await rpcClient.listPeers()
// Independent RPCs fetched together (serialized they stacked two full
// mesh round-trips before anything rendered).
const [res, fedSettled] = await Promise.all([
rpcClient.listPeers(),
rpcClient.federationListNodes().catch(() => null),
])
const peerList = res.peers || []
const observerList: Peer[] = []
try {
const fedRes = await rpcClient.federationListNodes()
const fedNodes = fedRes.nodes || []
const fedNodes = fedSettled?.nodes || []
for (const n of fedNodes) {
if (!n.onion || n.trust_level === 'untrusted') {
continue