feat(ui): app sessions overlay the current page in every display mode
Launching an app (or opening a tx in mempool from Home/wallet/channels) used to router.push the app-session page in overlay/fullscreen modes, swapping the page underneath — confusing and lossy. All display modes are now store-driven: panel renders beside the page, overlay/fullscreen render above it (teleported to body), and the route never changes. Deep-link paths (/tx/<hash>) ride along via the launcher's panelPath instead of a route query; closing always returns exactly where the user was. The app-session route stays for direct links. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="app-session-root">
|
||||
<Teleport to="body" :disabled="isInlinePanel && !isMobile">
|
||||
<Teleport to="body" :disabled="inlinePanelMode">
|
||||
<div
|
||||
:class="backdropClasses"
|
||||
@click.self="handleBackdropClick"
|
||||
@@ -92,7 +92,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'
|
||||
import { ref, computed, nextTick, onMounted, onBeforeUnmount, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useAppLauncherStore } from '@/stores/appLauncher'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
@@ -110,10 +110,12 @@ import { useAppIdentity } from './appSession/useAppIdentity'
|
||||
import { useNostrBridge } from './appSession/useNostrBridge'
|
||||
import { openExternalUrl, openInAppOrNewTab } from '@/utils/openExternal'
|
||||
import { useElectrsSync } from '@/composables/useElectrsSync'
|
||||
import { IS_DEMO, isDemoExternal } from '@/composables/useDemoIntro'
|
||||
import { IS_DEMO, isDemoApp, isDemoExternal } from '@/composables/useDemoIntro'
|
||||
|
||||
const props = defineProps<{
|
||||
appIdProp?: string
|
||||
/** Deep-link path inside the app (store-driven sessions), e.g. /tx/<hash> */
|
||||
pathProp?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -167,10 +169,12 @@ const blockedTitle = computed(() => appId.value === 'fedimint' || appId.value ==
|
||||
// viewport (and match the CSS `md` breakpoint) instead of a stale one-shot read.
|
||||
const isMobile = ref(typeof window !== 'undefined' && window.innerWidth < 768)
|
||||
function updateIsMobile() { isMobile.value = window.innerWidth < 768 }
|
||||
// In the demo, apps backed by a real external site that blocks iframing
|
||||
// (mempool.space) open in a new tab rather than the in-app session frame.
|
||||
// In the demo, apps backed by a real external site that blocks iframing open
|
||||
// in a new tab rather than the in-app session frame. Demoable apps are served
|
||||
// same-origin by the mock backend, so the prod new-tab list doesn't apply.
|
||||
const mustOpenNewTab = computed(() =>
|
||||
NEW_TAB_APPS.has(appId.value) || (IS_DEMO && isDemoExternal(appId.value))
|
||||
(NEW_TAB_APPS.has(appId.value) && !(IS_DEMO && isDemoApp(appId.value))) ||
|
||||
(IS_DEMO && isDemoExternal(appId.value))
|
||||
)
|
||||
|
||||
// ElectrumX shows a sync screen before its real UI (the Electrum server only
|
||||
@@ -200,7 +204,8 @@ const screensaverSuppressedApps = new Set([
|
||||
|
||||
const appUrl = computed(() => {
|
||||
const runtimeUrl = store.data?.['package-data']?.[appId.value]?.installed?.['interface-addresses']?.main?.['lan-address'] || undefined
|
||||
return resolveAppUrl(appId.value, route.query.path as string | undefined, runtimeUrl)
|
||||
const deepPath = props.pathProp ?? (route.query.path as string | undefined)
|
||||
return resolveAppUrl(appId.value, deepPath, runtimeUrl)
|
||||
})
|
||||
|
||||
function closeRouteSession() {
|
||||
@@ -227,16 +232,8 @@ function setMode(mode: DisplayMode) {
|
||||
displayMode.value = mode
|
||||
localStorage.setItem(DISPLAY_MODE_KEY, mode)
|
||||
|
||||
// Switch from inline panel to route-based overlay/fullscreen
|
||||
if (isInlinePanel.value && mode !== 'panel') {
|
||||
const id = appId.value
|
||||
emit('close')
|
||||
const returnTo = route.fullPath.startsWith('/dashboard') ? route.fullPath : '/dashboard/apps'
|
||||
router.push({ name: 'app-session', params: { appId: id }, query: { returnTo } })
|
||||
return
|
||||
}
|
||||
|
||||
// Switch from route-based to inline panel
|
||||
// Route-based sessions (deep links) hand off to the store-driven session so
|
||||
// the app keeps floating above the dashboard instead of owning the route.
|
||||
if (!isInlinePanel.value && mode === 'panel') {
|
||||
const id = appId.value
|
||||
const launcher = useAppLauncherStore()
|
||||
@@ -250,22 +247,24 @@ function setMode(mode: DisplayMode) {
|
||||
return
|
||||
}
|
||||
|
||||
if (mode === 'fullscreen' && sessionRef.value && !document.fullscreenElement) {
|
||||
sessionRef.value.requestFullscreen().catch(() => {})
|
||||
}
|
||||
}
|
||||
|
||||
// Reactive classes based on display mode. On mobile the store-driven panel
|
||||
// renders as a full-screen overlay (teleported to body) so it covers the nav
|
||||
// and the underlying page never changes — desktop keeps the inline panel.
|
||||
// Reactive classes based on display mode. The store-driven session honors the
|
||||
// selected display mode in place: panel renders inline beside the page,
|
||||
// overlay/fullscreen render above it (teleported to body) — the underlying
|
||||
// route never changes. Mobile always uses the full overlay.
|
||||
const inlinePanelMode = computed(() =>
|
||||
isInlinePanel.value && !isMobile.value && displayMode.value === 'panel'
|
||||
)
|
||||
|
||||
const backdropClasses = computed(() => {
|
||||
if (isInlinePanel.value && !isMobile.value) return 'app-session-backdrop-inline'
|
||||
if (inlinePanelMode.value) return 'app-session-backdrop-inline'
|
||||
return 'app-session-backdrop-overlay'
|
||||
})
|
||||
|
||||
const panelClasses = computed(() => {
|
||||
const base = 'app-session-panel glass-card'
|
||||
if (isInlinePanel.value && !isMobile.value) return `${base} app-session-inline`
|
||||
if (inlinePanelMode.value) return `${base} app-session-inline`
|
||||
if (displayMode.value === 'fullscreen' && !isMobile.value) return `${base} app-session-fullscreen`
|
||||
return `${base} app-session-overlay`
|
||||
})
|
||||
@@ -380,9 +379,14 @@ function onMessage(e: MessageEvent) {
|
||||
|
||||
// Enter fullscreen on mount if mode is fullscreen
|
||||
watch(displayMode, (mode) => {
|
||||
if (mode === 'fullscreen' && sessionRef.value && !document.fullscreenElement) {
|
||||
sessionRef.value.requestFullscreen().catch(() => {})
|
||||
}
|
||||
if (mode !== 'fullscreen') return
|
||||
// The panel may teleport to <body> on this mode change — request fullscreen
|
||||
// after the DOM settles so we grab the element at its new location.
|
||||
void nextTick(() => {
|
||||
if (displayMode.value === 'fullscreen' && sessionRef.value && !document.fullscreenElement) {
|
||||
sessionRef.value.requestFullscreen().catch(() => {})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
Reference in New Issue
Block a user