Move theme, deep link and signer alert boot jobs out of the root layout, with one signerHealth store behind the toast and the status card
This commit is contained in:
parent
9254238d81
commit
e5a270aa5c
7 changed files with 200 additions and 178 deletions
|
|
@ -48,9 +48,9 @@ only gate.
|
|||
and `IconPickerButton.svelte` open app modals. Don't copy them. A lib component that needs app
|
||||
behavior takes it as a prop, or moves to `src/app/components`.
|
||||
- **app → components.** `routes.ts` (`goToChat` opens `ChatEnable`), `share.ts` (`Share`,
|
||||
`ShareEvent`) and `speech.ts` (`OpenRouterEnable`) import a component so they can open a modal
|
||||
mid-flow. `editor/` holds `.svelte` files of its own (suggestion popovers), which `makeEditor`
|
||||
mounts.
|
||||
`ShareEvent`), `deepLinks.ts` (`Search`) and `speech.ts` (`OpenRouterEnable`) import a component
|
||||
so they can open a modal mid-flow. `editor/` holds `.svelte` files of its own (suggestion
|
||||
popovers), which `makeEditor` mounts.
|
||||
- Nothing under `src/app` or `src/lib` imports from `src/routes`.
|
||||
|
||||
## Top-level layout
|
||||
|
|
@ -82,7 +82,7 @@ only gate.
|
|||
- `sync.ts`: `syncApplicationData`, the background sync of user data, spaces and DMs
|
||||
- `settings.ts`: the `Settings` plugin over encrypted app data, plus notification settings
|
||||
- `repository.ts`: the `LatestEvents` plugin, each watched author's most recent event
|
||||
- `thunks.ts` (publish status by event id), `signer.ts` (signer request tracking)
|
||||
- `thunks.ts` (publish status by event id), `signer.ts` (signer request tracking and `signerHealth`)
|
||||
- `env.ts`: every `VITE_` value, parsed
|
||||
- `logger.ts` (log capture and sending), `analytics.ts` (Plausible pageviews), `device.ts` (a
|
||||
device id)
|
||||
|
|
@ -121,7 +121,7 @@ only gate.
|
|||
(prompts for missing inbox/outbox relays)
|
||||
|
||||
**Platform and voice**
|
||||
- `push/` (notification adapters), `share.ts`, `keyboard.ts`
|
||||
- `push/` (notification adapters), `share.ts`, `deepLinks.ts`, `keyboard.ts`
|
||||
- `dictation.ts` (speech-to-text) and `speech.ts` (read aloud), both through OpenRouter
|
||||
|
||||
A feature gets a `src/app/<feature>.ts` only when it has non-UI logic to hold. Polls, goals,
|
||||
|
|
@ -148,14 +148,17 @@ client.
|
|||
|
||||
`src/routes/+layout.svelte` runs the boot sequence. It imports `@app/policies` for its side effect,
|
||||
and `@app/storage`, which registers `storagePolicy` the same way, so every `AppPolicy` is on
|
||||
`appPolicies` before anything calls `app.get()`. Then, in order:
|
||||
`appPolicies` before anything calls `app.get()`. `applySavedTheme()` stamps the last theme and font
|
||||
size from `localStorage` before first paint. Then, in order:
|
||||
|
||||
1. `restoreSession()` restores the saved session, if there is one, which builds a user-scoped
|
||||
`App` through `login`.
|
||||
2. The device, wallet and notification stores sync to `kv`/`ss`.
|
||||
3. It waits for storage, then handles a cold-start deep link.
|
||||
3. It waits for storage, then `setupDeepLinks()` listens for warm-start links and handles a
|
||||
cold-start one.
|
||||
4. Each long-running subscription goes onto one `unsubscribers` list: `setupHistory`,
|
||||
`syncApplicationData`, `setupShareIntents`, `syncKeyboard`, badges, `Push.sync()`.
|
||||
`syncApplicationData`, `setupShareIntents`, `syncKeyboard`, badges, `Push.sync()`,
|
||||
`syncSignerAlerts`, and `syncTheme`, which mirrors the theme stores back to `localStorage`.
|
||||
|
||||
When login swaps in a new `App`, the layout runs `syncApplicationData` again. Routes render inside
|
||||
`AppContainer`, behind the login gate, and `ModalContainer` renders outside it. `flotilla-state`
|
||||
|
|
@ -195,7 +198,8 @@ Flotilla's own native code:
|
|||
without FCM), and `ShareIntentPlugin`. `MainActivity.java` registers them, and JS binds them with
|
||||
`registerPlugin` (`push/adapters/android.ts`, `share.ts`).
|
||||
- `ios/App/ShareExtension/`: the extension can't call into the app, so it opens a
|
||||
`flotilla://share` URL. `handleDeepLink` in the root layout passes that to `shareFromNative`.
|
||||
`flotilla://share` URL. `handleDeepLink` in `src/app/deepLinks.ts` passes that to
|
||||
`shareFromNative`.
|
||||
|
||||
`Push` in `src/app/push/index.ts` chooses an adapter at runtime: the Android fallback, Capacitor
|
||||
`PushNotifications` (FCM/APNs through `PUSH_SERVER`), or web notifications.
|
||||
|
|
|
|||
|
|
@ -110,9 +110,9 @@ mobile. Pages outside a space use `Page`, `PageBar` and `PageContent` themselves
|
|||
and a new event detail route needs an `eventRoutes` entry and a branch in `getPageTitle`.
|
||||
Without one the tab shows only `PLATFORM_NAME`.
|
||||
|
||||
Deep links arrive in `handleDeepLink` in the root layout: push-notification links (`?relay=&id=`),
|
||||
the iOS share extension (the `share` host), signer returns (`x-callback-url`), and otherwise a
|
||||
plain path. Nostr entities go through `/[bech32]`, which sends profiles to `makeProfilePath`, and
|
||||
Deep links arrive in `handleDeepLink` in `src/app/deepLinks.ts`: push-notification links
|
||||
(`?relay=&id=`), the iOS share extension (the `share` host), signer returns (`x-callback-url`), and
|
||||
otherwise a plain path. Nostr entities go through `/[bech32]`, which sends profiles to `makeProfilePath`, and
|
||||
loads events before calling `goToEvent`.
|
||||
|
||||
### Adding a space content page
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import {avg, spec} from "@welshman/lib"
|
||||
import {spec} from "@welshman/lib"
|
||||
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
|
||||
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
|
||||
import ClockCircle from "@assets/icons/clock-circle.svg?dataurl"
|
||||
|
|
@ -9,21 +9,13 @@
|
|||
import LogOut from "@app/components/LogOut.svelte"
|
||||
import {pushModal} from "@app/modal"
|
||||
import {session} from "@app/core"
|
||||
import {signerRequests} from "@app/signer"
|
||||
import {signerHealth, signerRequests} from "@app/signer"
|
||||
import PomadeSessions from "@app/components/PomadeSessions.svelte"
|
||||
|
||||
const finished = $derived($signerRequests.filter(request => request.finishedAt))
|
||||
const pending = $derived($signerRequests.filter(request => !request.finishedAt))
|
||||
const success = $derived(finished.filter(spec({ok: true})))
|
||||
const failure = $derived(finished.filter(spec({ok: false})))
|
||||
const cutoff = $derived(Date.now() - 10_000)
|
||||
const recentCompleted = $derived(finished.filter(request => request.finishedAt! > cutoff))
|
||||
const recentAvg = $derived(avg(recentCompleted.map(r => r.finishedAt! - r.startedAt)))
|
||||
const recentFailure = $derived(recentCompleted.filter(request => !request.ok))
|
||||
const recentSuccess = $derived(recentCompleted.filter(request => request.ok))
|
||||
const isDisconnected = $derived(
|
||||
recentCompleted.length > 0 && recentFailure.length === recentCompleted.length,
|
||||
)
|
||||
|
||||
const logout = () => pushModal(LogOut)
|
||||
</script>
|
||||
|
|
@ -34,14 +26,12 @@
|
|||
<div class="flex items-center justify-between">
|
||||
<span class="text-xl font-bold">Signer Status</span>
|
||||
<span class="flex items-center gap-2">
|
||||
{#if isDisconnected}
|
||||
{#if $signerHealth.disconnected}
|
||||
<Icon icon={CloseCircle} class="text-error" size={4} /> Disconnected
|
||||
{:else if recentFailure.length > 3}
|
||||
{:else if $signerHealth.failing}
|
||||
<Icon icon={Danger} class="text-warning" size={4} /> Partial Failure
|
||||
{:else if recentAvg > 1000 || pending.length > 10}
|
||||
{:else if $signerHealth.slow}
|
||||
<Icon icon={ClockCircle} class="text-warning" size={4} /> Slow connection
|
||||
{:else if recentSuccess.length === 0 && recentFailure.length > 0}
|
||||
<Icon icon={Danger} class="text-warning" size={4} /> Partial Failure
|
||||
{:else}
|
||||
<Icon icon={CheckCircle} class="text-success" size={4} /> Ok
|
||||
{/if}
|
||||
|
|
@ -67,7 +57,7 @@
|
|||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{#if isDisconnected}
|
||||
{#if $signerHealth.disconnected}
|
||||
<Button class="button button-error" onclick={logout}>Logout to Reconnect</Button>
|
||||
{:else}
|
||||
<PomadeSessions />
|
||||
|
|
|
|||
73
src/app/deepLinks.ts
Normal file
73
src/app/deepLinks.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import {App} from "@capacitor/app"
|
||||
import type {URLOpenListenerEvent} from "@capacitor/app"
|
||||
import {goto} from "$app/navigation"
|
||||
import {app} from "@app/core"
|
||||
import {clearModals, getModal, navigate, pushModal} from "@app/modal"
|
||||
import {goToChat, goToHome} from "@app/routes"
|
||||
import {shareFromNative} from "@app/share"
|
||||
import {pushToast} from "@app/toast"
|
||||
import {onPushNotificationAction} from "@app/push/adapters/common"
|
||||
import Search from "@app/components/Search.svelte"
|
||||
|
||||
const openShortcut = (pathname: string) => {
|
||||
switch (pathname) {
|
||||
case "/messages":
|
||||
goToChat()
|
||||
break
|
||||
case "/search": {
|
||||
const modal = getModal()
|
||||
|
||||
if (modal?.component !== Search) {
|
||||
pushModal(Search, {}, {replaceState: Boolean(modal)})
|
||||
}
|
||||
break
|
||||
}
|
||||
case "/spaces":
|
||||
navigate("/spaces")
|
||||
break
|
||||
case "/inbox":
|
||||
clearModals()
|
||||
goToHome()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeepLink = (rawUrl: string) => {
|
||||
const url = new URL(rawUrl)
|
||||
const relay = url.searchParams.get("relay")
|
||||
const id = url.searchParams.get("id")
|
||||
|
||||
if (relay && id) {
|
||||
onPushNotificationAction({notification: {data: {relay, id}}})
|
||||
} else if (url.protocol === "flotilla:" && url.host === "shortcut") {
|
||||
if (app.get().user) {
|
||||
openShortcut(url.pathname)
|
||||
}
|
||||
} else if (url.host === "share") {
|
||||
// The iOS share extension has no channel to the app, so it posts a flotilla://share url.
|
||||
shareFromNative(Object.fromEntries(url.searchParams))
|
||||
} else if (url.host === "x-callback-url" && url.pathname === "/authError") {
|
||||
pushToast({
|
||||
theme: "error",
|
||||
message: url.searchParams.get("errorMessage") || "Signer authorization failed.",
|
||||
})
|
||||
} else if (url.host !== "x-callback-url" || url.pathname !== "/authSuccess") {
|
||||
goto(`${url.pathname}${url.search}${url.hash}`, {replaceState: false, noScroll: false})
|
||||
}
|
||||
}
|
||||
|
||||
export const setupDeepLinks = async () => {
|
||||
// Capacitor emits this from onNewIntent only, and retains it until a listener is attached.
|
||||
const listener = await App.addListener("appUrlOpen", (event: URLOpenListenerEvent) =>
|
||||
handleDeepLink(event.url),
|
||||
)
|
||||
|
||||
// A link launching the app from a killed state arrives via onCreate, which emits no appUrlOpen.
|
||||
const launch = await App.getLaunchUrl()
|
||||
|
||||
if (launch?.url) {
|
||||
handleDeepLink(launch.url)
|
||||
}
|
||||
|
||||
return () => listener.remove()
|
||||
}
|
||||
|
|
@ -1,7 +1,10 @@
|
|||
import {derived} from "svelte/store"
|
||||
import {derived, get, readable} from "svelte/store"
|
||||
import {ago, avg, ms, now, spec} from "@welshman/lib"
|
||||
import {Logger} from "@welshman/app"
|
||||
import type {LogMessage} from "@welshman/app"
|
||||
import {fromApp} from "@app/core"
|
||||
import {navigate} from "@app/modal"
|
||||
import {pushToast, toast} from "@app/toast"
|
||||
|
||||
export type SignerRequest = {
|
||||
id: string
|
||||
|
|
@ -41,3 +44,42 @@ export const signerRequests = derived(
|
|||
return Array.from(requestsById.values())
|
||||
},
|
||||
)
|
||||
|
||||
const clock = readable(now(), set => {
|
||||
const interval = setInterval(() => set(now()), ms(1))
|
||||
|
||||
return () => clearInterval(interval)
|
||||
})
|
||||
|
||||
// Logger timestamps are in milliseconds
|
||||
export const signerHealth = derived([signerRequests, clock], ([$requests]) => {
|
||||
const pending = $requests.filter(request => !request.finishedAt)
|
||||
const stale = pending.filter(request => request.startedAt < ms(ago(30)))
|
||||
const recent = $requests.filter(request => request.finishedAt && request.finishedAt > ms(ago(10)))
|
||||
const recentFailures = recent.filter(spec({ok: false}))
|
||||
const disconnected = recent.length > 0 && recentFailures.length === recent.length
|
||||
|
||||
return {
|
||||
disconnected,
|
||||
failing: recentFailures.length > 3,
|
||||
slow:
|
||||
avg(recent.map(request => request.finishedAt! - request.startedAt)) > 1000 ||
|
||||
pending.length > 10,
|
||||
unresponsive: stale.length > 10 || (recent.length > 5 && disconnected),
|
||||
}
|
||||
})
|
||||
|
||||
export const syncSignerAlerts = () =>
|
||||
derived(signerHealth, $signerHealth => $signerHealth.unresponsive).subscribe(unresponsive => {
|
||||
if (unresponsive && !get(toast)) {
|
||||
pushToast({
|
||||
theme: "error",
|
||||
timeout: 60_000,
|
||||
message: "Your signer isn't responding.",
|
||||
action: {
|
||||
message: "Details",
|
||||
onclick: () => navigate("/settings/profile"),
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import twColors from "tailwindcss/colors"
|
||||
import {derived, readable} from "svelte/store"
|
||||
import {hash} from "@welshman/lib"
|
||||
import {debounce} from "throttle-debounce"
|
||||
import {derived, get, readable} from "svelte/store"
|
||||
import {call, hash} from "@welshman/lib"
|
||||
import {synced} from "@welshman/store"
|
||||
import {FL_THEME} from "@app/env"
|
||||
import {kv} from "@app/storage"
|
||||
import {userSettingsValues} from "@app/settings"
|
||||
|
||||
const colors = [
|
||||
twColors.amber[600],
|
||||
|
|
@ -61,3 +63,41 @@ export const activeTheme = derived([theme, prefersDark], ([$theme, $prefersDark]
|
|||
|
||||
return $theme
|
||||
})
|
||||
|
||||
// The stores syncTheme mirrors load well after first paint, so stamp their last values first.
|
||||
export const applySavedTheme = () => {
|
||||
const savedTheme = localStorage.getItem("theme")
|
||||
const initialTheme =
|
||||
savedTheme === "light" || savedTheme === "dark"
|
||||
? savedTheme
|
||||
: get(prefersDark)
|
||||
? "dark"
|
||||
: "light"
|
||||
|
||||
document.documentElement.style.fontSize = `${localStorage.getItem("font-size") || 1.1}rem`
|
||||
document.documentElement.style.colorScheme = initialTheme
|
||||
document.body.setAttribute("data-fl-theme", localStorage.getItem("fl-theme") || FL_THEME)
|
||||
document.body.setAttribute("data-theme", initialTheme)
|
||||
}
|
||||
|
||||
export const syncTheme = () => {
|
||||
const unsubscribers = [
|
||||
activeTheme.subscribe($activeTheme => {
|
||||
localStorage.setItem("theme", $activeTheme)
|
||||
document.body.setAttribute("data-theme", $activeTheme)
|
||||
document.documentElement.style.colorScheme = $activeTheme
|
||||
}),
|
||||
flTheme.subscribe($flTheme => {
|
||||
localStorage.setItem("fl-theme", $flTheme)
|
||||
document.body.setAttribute("data-fl-theme", $flTheme)
|
||||
}),
|
||||
userSettingsValues.subscribe(
|
||||
debounce(100, $settings => {
|
||||
localStorage.setItem("font-size", String($settings.font_size))
|
||||
document.documentElement.style.fontSize = `${$settings.font_size}rem`
|
||||
}),
|
||||
),
|
||||
]
|
||||
|
||||
return () => unsubscribers.forEach(call)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,16 @@
|
|||
<script lang="ts">
|
||||
import "@lib/components/theme.css"
|
||||
import "@welshman/editor/index.css"
|
||||
import {debounce} from "throttle-debounce"
|
||||
import * as nip19 from "nostr-tools/nip19"
|
||||
import type {Unsubscriber} from "svelte/store"
|
||||
import {get} from "svelte/store"
|
||||
import {App, type URLOpenListenerEvent} from "@capacitor/app"
|
||||
import {App} from "@capacitor/app"
|
||||
import {Capacitor} from "@capacitor/core"
|
||||
import {NostrSignerPlugin} from "nostr-signer-capacitor-plugin"
|
||||
import {dev} from "$app/environment"
|
||||
import {goto} from "$app/navigation"
|
||||
import {page} from "$app/stores"
|
||||
import {context as pomadeContext} from "@pomade/core"
|
||||
import {sync, throttled} from "@welshman/store"
|
||||
import {sync} from "@welshman/store"
|
||||
import {setNip55Plugin} from "@welshman/signer"
|
||||
import * as util from "@welshman/util"
|
||||
import * as lib from "@welshman/lib"
|
||||
|
|
@ -22,45 +20,34 @@
|
|||
import Startup from "@app/components/Startup.svelte"
|
||||
import ModalContainer from "@app/components/ModalContainer.svelte"
|
||||
import * as core from "@app/core"
|
||||
import {goToChat, goToHome, setupHistory} from "@app/routes"
|
||||
import Search from "@app/components/Search.svelte"
|
||||
import {clearModals, getModal, navigate, pushModal} from "@app/modal"
|
||||
import {app} from "@app/core"
|
||||
import {setupHistory} from "@app/routes"
|
||||
import {setupAnalytics} from "@app/analytics"
|
||||
import {setupLogging} from "@app/logger"
|
||||
import "@app/policies"
|
||||
import {restoreSession} from "@app/session"
|
||||
import {signerRequests} from "@app/signer"
|
||||
import {syncSignerAlerts} from "@app/signer"
|
||||
import {wallet} from "@app/lightning"
|
||||
import {kv, ss, storage} from "@app/storage"
|
||||
import {device} from "@app/device"
|
||||
import {userSettingsValues, notificationSettings} from "@app/settings"
|
||||
import {notificationSettings} from "@app/settings"
|
||||
import {needsKeyBackup} from "@app/healthChecks"
|
||||
import {setupShareIntents, shareFromNative} from "@app/share"
|
||||
import {setupShareIntents} from "@app/share"
|
||||
import {shouldUnwrap, syncApplicationData} from "@app/sync"
|
||||
import * as env from "@app/env"
|
||||
import {activeTheme, flTheme, theme} from "@app/theme"
|
||||
import {toast, pushToast} from "@app/toast"
|
||||
import {applySavedTheme, flTheme, syncTheme, theme} from "@app/theme"
|
||||
import * as notifications from "@app/notifications"
|
||||
import {notificationCount, backgroundNotificationCount} from "@app/notifications"
|
||||
import {Push} from "@app/push"
|
||||
import {onPushNotificationAction, pushState} from "@app/push/adapters/common"
|
||||
import {pushState} from "@app/push/adapters/common"
|
||||
import {setupDeepLinks} from "@app/deepLinks"
|
||||
import {syncKeyboard} from "@app/keyboard"
|
||||
import {getPageTitle} from "@app/title"
|
||||
import NewNotificationSound from "@src/app/components/NewNotificationSound.svelte"
|
||||
|
||||
const {children} = $props()
|
||||
|
||||
// The stores these mirror are in indexeddb, which loads well after first paint.
|
||||
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
const savedTheme = localStorage.getItem("theme")
|
||||
const initialTheme =
|
||||
savedTheme === "light" || savedTheme === "dark" ? savedTheme : prefersDark ? "dark" : "light"
|
||||
|
||||
// @ts-ignore
|
||||
document.documentElement.style["font-size"] = `${localStorage.getItem("font-size") || 1.1}rem`
|
||||
document.documentElement.style.colorScheme = initialTheme
|
||||
document.body.setAttribute("data-fl-theme", localStorage.getItem("fl-theme") || env.FL_THEME)
|
||||
document.body.setAttribute("data-theme", initialTheme)
|
||||
applySavedTheme()
|
||||
|
||||
// Add stuff to window for convenience
|
||||
Object.assign(window, {get, nip19, theme, ...plugins, ...lib, ...util, ...core})
|
||||
|
|
@ -73,72 +60,6 @@
|
|||
setNip55Plugin(NostrSignerPlugin)
|
||||
}
|
||||
|
||||
// Handles warm-start links from appUrlOpen and cold-start links from getLaunchUrl.
|
||||
const handleDeepLink = (rawUrl: string) => {
|
||||
const url = new URL(rawUrl)
|
||||
const relay = url.searchParams.get("relay")
|
||||
const id = url.searchParams.get("id")
|
||||
|
||||
if (relay && id) {
|
||||
onPushNotificationAction({notification: {data: {relay, id}}})
|
||||
return
|
||||
}
|
||||
|
||||
if (url.protocol === "flotilla:" && url.host === "shortcut") {
|
||||
if (core.app.get().user) {
|
||||
switch (url.pathname) {
|
||||
case "/messages":
|
||||
goToChat()
|
||||
break
|
||||
case "/search": {
|
||||
const modal = getModal()
|
||||
|
||||
if (modal?.component !== Search) {
|
||||
pushModal(Search, {}, {replaceState: Boolean(modal)})
|
||||
}
|
||||
break
|
||||
}
|
||||
case "/spaces":
|
||||
navigate("/spaces")
|
||||
break
|
||||
case "/inbox":
|
||||
clearModals()
|
||||
goToHome()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// The iOS share extension has no channel to the app, so it posts a flotilla://share url.
|
||||
if (url.host === "share") {
|
||||
shareFromNative(Object.fromEntries(url.searchParams))
|
||||
return
|
||||
}
|
||||
|
||||
if (url.host === "x-callback-url") {
|
||||
if (url.pathname === "/authError") {
|
||||
const errorMessage = url.searchParams.get("errorMessage")
|
||||
|
||||
pushToast({
|
||||
theme: "error",
|
||||
message: errorMessage || "Signer authorization failed.",
|
||||
})
|
||||
}
|
||||
|
||||
if (["/authSuccess", "/authError"].includes(url.pathname)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const target = `${url.pathname}${url.search}${url.hash}`
|
||||
goto(target, {replaceState: false, noScroll: false})
|
||||
}
|
||||
|
||||
// Capacitor emits this from onNewIntent only, so it covers a warm start and not a cold one.
|
||||
App.addListener("appUrlOpen", (event: URLOpenListenerEvent) => handleDeepLink(event.url))
|
||||
|
||||
// Handle back button on mobile
|
||||
App.addListener("backButton", () => {
|
||||
if (window.history.length > 1) {
|
||||
|
|
@ -207,12 +128,8 @@
|
|||
// Wait for critical storage data only
|
||||
await storage.get()?.ready
|
||||
|
||||
// A link launching the app from a killed state arrives via onCreate, which emits no appUrlOpen.
|
||||
const launch = await App.getLaunchUrl()
|
||||
|
||||
if (launch?.url) {
|
||||
handleDeepLink(launch.url)
|
||||
}
|
||||
// Handle warm and cold start deep links
|
||||
unsubscribers.push(await setupDeepLinks())
|
||||
|
||||
// Close the database connection on reload
|
||||
unsubscribers.push(closeStorage)
|
||||
|
|
@ -239,7 +156,7 @@
|
|||
unsubscribers.push(Push.sync())
|
||||
|
||||
// Wait for the new app's storage the way startup does, so sync reconciles against cached events.
|
||||
let currentApp = core.app.get()
|
||||
let currentApp = app.get()
|
||||
|
||||
const resync = async () => {
|
||||
await storage.get()?.ready
|
||||
|
|
@ -248,62 +165,19 @@
|
|||
}
|
||||
|
||||
unsubscribers.push(
|
||||
core.app.subscribe($app => {
|
||||
if ($app !== currentApp) {
|
||||
currentApp = $app
|
||||
app.subscribe(nextApp => {
|
||||
if (nextApp !== currentApp) {
|
||||
currentApp = nextApp
|
||||
resync()
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
// Listen for signer errors, report to user via toast
|
||||
unsubscribers.push(
|
||||
throttled(10_000, signerRequests).subscribe($requests => {
|
||||
if ($toast) {
|
||||
return
|
||||
}
|
||||
|
||||
const longCutoff = Date.now() - 30_000
|
||||
const shortCutoff = Date.now() - 10_000
|
||||
const pending = $requests.filter(r => !r.finishedAt && r.startedAt < longCutoff)
|
||||
const completed = $requests.filter(r => r.finishedAt && r.finishedAt > shortCutoff)
|
||||
const showPendingError = pending.length > 10
|
||||
const showCompletedError = completed.length > 5 && completed.filter(r => r.ok).length === 0
|
||||
|
||||
if (showPendingError || showCompletedError) {
|
||||
pushToast({
|
||||
theme: "error",
|
||||
timeout: 60_000,
|
||||
message: "Your signer isn't responding.",
|
||||
action: {
|
||||
message: "Details",
|
||||
onclick: () => goto("/settings/profile"),
|
||||
},
|
||||
})
|
||||
}
|
||||
}),
|
||||
)
|
||||
unsubscribers.push(syncSignerAlerts())
|
||||
|
||||
// Sync theme and font size
|
||||
unsubscribers.push(
|
||||
activeTheme.subscribe($activeTheme => {
|
||||
localStorage.setItem("theme", $activeTheme)
|
||||
document.body.setAttribute("data-theme", $activeTheme)
|
||||
document.documentElement.style.colorScheme = $activeTheme
|
||||
}),
|
||||
flTheme.subscribe($flTheme => {
|
||||
localStorage.setItem("fl-theme", $flTheme)
|
||||
document.body.setAttribute("data-fl-theme", $flTheme)
|
||||
}),
|
||||
userSettingsValues.subscribe(
|
||||
debounce(100, $settings => {
|
||||
localStorage.setItem("font-size", String($settings.font_size))
|
||||
|
||||
// @ts-ignore
|
||||
document.documentElement.style["font-size"] = `${$settings.font_size}rem`
|
||||
}),
|
||||
),
|
||||
)
|
||||
unsubscribers.push(syncTheme())
|
||||
|
||||
return () => unsubscribers.forEach(lib.call)
|
||||
})
|
||||
|
|
@ -315,8 +189,7 @@
|
|||
})
|
||||
|
||||
$effect(() => {
|
||||
const {user} = core.app.get()
|
||||
const title = getPageTitle({page: $page, pubkey: user?.pubkey})
|
||||
const title = getPageTitle({page: $page, pubkey: $app.user?.pubkey})
|
||||
const count = $documentActive ? $notificationCount : $backgroundNotificationCount
|
||||
|
||||
document.title = count > 0 ? `(${count}) ${title}` : title
|
||||
|
|
|
|||
Loading…
Reference in a new issue