Merge remote-tracking branch 'gitea-ai/main'
All checks were successful
Demo images / Build & push demo images (push) Successful in 2m51s
All checks were successful
Demo images / Build & push demo images (push) Successful in 2m51s
This commit is contained in:
commit
0dfc3a7cfb
@ -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
|
||||
|
||||
@ -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 = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user