Declare every persisted local store with synced in its owner module and await their ready promises at boot
This commit is contained in:
parent
602ac09293
commit
5343a28e6c
20 changed files with 103 additions and 103 deletions
|
|
@ -156,7 +156,8 @@ 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`.
|
||||
2. It awaits `.ready` on the `synced` stores boot reads (device, wallet, push and notification
|
||||
settings, `shouldUnwrap`, `needsKeyBackup`).
|
||||
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`,
|
||||
|
|
|
|||
|
|
@ -275,18 +275,27 @@ To persist another kind, add it to `kinds` in `storage.ts`. To persist a new map
|
|||
add a `TABLES` entry and an `init*` method shaped like `initHandles`: load the rows, subscribe
|
||||
to `onItem`, and batch the writes.
|
||||
|
||||
### kv, ss and the two ways to bind them
|
||||
### kv, ss and synced stores
|
||||
|
||||
`kv` wraps Capacitor `Preferences` and `ss` wraps `SecureStorage`. Both are exported from
|
||||
`storage.ts`, queue their writes, and JSON-encode values. Neither is namespaced per user, so
|
||||
anything in them outlives a login and is cleared only by logout. Secrets go in `ss`.
|
||||
|
||||
- `synced({key, storage, defaultValue})` creates a store that persists itself. It emits the
|
||||
default first, and the stored value arrives later (`.ready`). `theme` and `flTheme`
|
||||
(`theme.ts`), `checked` (`notifications.ts`) and `shouldUnwrap` (`sync.ts`) use it.
|
||||
- `sync({key, store, storage})` binds a store that already exists. The root layout awaits it
|
||||
for `device`, `wallet`, `notificationSettings`, `pushState` and `needsKeyBackup` before first
|
||||
render, so boot code sees the restored values. It also binds `shouldUnwrap`, which `synced` already persists.
|
||||
Each persisted store is declared in its owner module with `synced({key, storage, defaultValue})`,
|
||||
or `syncedWithGetter` from `storage.ts` when callers need `.get()` (`withGetter`'s type drops
|
||||
`.ready`). The store emits the default first and loads the stored value in the background, so
|
||||
`.ready` resolves once it has arrived:
|
||||
|
||||
- `theme`, `flTheme` (`theme.ts`), `checked` (`notifications.ts`), `shouldUnwrap` (`sync.ts`),
|
||||
`device` (`device.ts`), `needsKeyBackup` (`healthChecks.ts`) in `kv`
|
||||
- `pushState` (key `notificationState`) and `notificationSettings` in `kv`, both in
|
||||
`push/adapters/common.ts` and re-exported from `@app/push`
|
||||
- `wallet` (`lightning.ts`) in `ss`
|
||||
|
||||
The root layout awaits `.ready` for the stores boot code reads (`device`, `shouldUnwrap`,
|
||||
`wallet`, `notificationSettings`, `pushState`, `needsKeyBackup`) before `Push.sync()` and
|
||||
background sync start. A new store that boot reads joins that list. Never change a key, since
|
||||
it is where existing installs keep their value.
|
||||
|
||||
Raw `localStorage` holds only `theme`, `fl-theme` and `font-size`. The root layout mirrors them
|
||||
there to apply them synchronously before `kv` loads, which avoids a flash of the wrong theme.
|
||||
|
|
@ -305,7 +314,7 @@ Settings are an encrypted app-data event with d-tag `flotilla/settings`, read th
|
|||
|
||||
A preference that should follow the user across devices goes in `SettingsValues` and
|
||||
`defaultSettings`. One that belongs to a device goes in a `kv` store, as push, sound and badge do
|
||||
in `notificationSettings`. Per-space alert preferences are published (`alerts`); the device's
|
||||
in `notificationSettings` (`@app/push`). Per-space alert preferences are published (`alerts`); the device's
|
||||
push permission is not.
|
||||
|
||||
`checked`, the read markers behind badges, lives in `kv`, and `syncCheckedRemote` mirrors it to
|
||||
|
|
@ -413,8 +422,8 @@ NIP-86 calls (`relayManagement.get().forUrl(url)`) are not thunks. They return
|
|||
- **UI signals.** `toast` in `toast.ts`, and `relaysPendingTrust` in `policies.ts`.
|
||||
- **A controller per flow.** `Nip46Controller` (`nip46.ts`) is a class a component instantiates.
|
||||
It holds the writables and actions for a multi-step flow.
|
||||
- **Module-owned values.** `wallet` in `lightning.ts` is a `withGetter(writable(...))` that the
|
||||
root layout persists.
|
||||
- **Module-owned values.** `wallet` in `lightning.ts` persists itself to `ss` with
|
||||
`syncedWithGetter`.
|
||||
|
||||
## Runes and stores
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
import {messagingRelayLists, user} from "@app/core"
|
||||
import {pushModal} from "@app/modal"
|
||||
import {setChecked} from "@app/notifications"
|
||||
import {notificationSettings} from "@app/settings"
|
||||
import {notificationSettings} from "@app/push"
|
||||
|
||||
type Props = {
|
||||
onClick: () => void
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
import {Capacitor} from "@capacitor/core"
|
||||
import {notificationSettings} from "@app/settings"
|
||||
import {onNotification} from "@app/push"
|
||||
import {notificationSettings, onNotification} from "@app/push"
|
||||
|
||||
let audioElement: HTMLAudioElement
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,8 @@
|
|||
import {deriveSpacePermissions} from "@app/management"
|
||||
import {userSpaceUrls} from "@app/rooms"
|
||||
import {deriveSpaceActionItems} from "@app/actionItems"
|
||||
import {notificationSettings, deriveShouldNotify, setSpaceNotifications} from "@app/settings"
|
||||
import {deriveShouldNotify, setSpaceNotifications} from "@app/settings"
|
||||
import {notificationSettings} from "@app/push"
|
||||
import {pushModal, popModal} from "@app/modal"
|
||||
import {makeSpacePath, goToChat} from "@app/routes"
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@
|
|||
import SpaceMenuActions from "@app/components/SpaceMenuActions.svelte"
|
||||
import RelayName from "@app/components/RelayName.svelte"
|
||||
import {deriveSpaceActionItems} from "@app/actionItems"
|
||||
import {notificationSettings, deriveShouldNotify} from "@app/settings"
|
||||
import {deriveShouldNotify} from "@app/settings"
|
||||
import {notificationSettings} from "@app/push"
|
||||
import {pushModal} from "@app/modal"
|
||||
|
||||
type Props = {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import {withGetter} from "@welshman/store"
|
||||
import {writable} from "svelte/store"
|
||||
import {randomId} from "@welshman/lib"
|
||||
import {kv, syncedWithGetter} from "@app/storage"
|
||||
|
||||
export const device = withGetter(writable(randomId()))
|
||||
export const device = syncedWithGetter({key: "device", storage: kv, defaultValue: randomId()})
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import {derived, writable} from "svelte/store"
|
||||
import {derived} from "svelte/store"
|
||||
import {sample} from "@welshman/lib"
|
||||
import {normalizeRelayUrl} from "@welshman/util"
|
||||
import {synced} from "@welshman/store"
|
||||
import {MessagingRelayLists, RelayLists, Relays, SearchRelayLists, projection} from "@welshman/app"
|
||||
import type {Command, IApp, Projection} from "@welshman/app"
|
||||
import {usePlugin} from "@app/core"
|
||||
import {DEFAULT_RELAYS} from "@app/env"
|
||||
import {kv} from "@app/storage"
|
||||
|
||||
export type HealthCheckContext = {
|
||||
readRelays: string[]
|
||||
|
|
@ -93,7 +95,7 @@ const tooManyRelaysCheck = (spec: RelayListSpec): HealthCheck => ({
|
|||
})
|
||||
|
||||
// Set when a key is generated in-app, and cleared once the user has saved a backup of it
|
||||
export const needsKeyBackup = writable(false)
|
||||
export const needsKeyBackup = synced({key: "needsKeyBackup", storage: kv, defaultValue: false})
|
||||
|
||||
export class HealthChecks {
|
||||
context: Projection<HealthCheckContext>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import {writable} from "svelte/store"
|
||||
import {nwc} from "@getalby/sdk"
|
||||
import {bech32ToHex, displayUrl, tryCatch} from "@welshman/lib"
|
||||
import type {Maybe} from "@welshman/lib"
|
||||
import {isNWCWallet, isWebLNWallet, toMsats} from "@welshman/util"
|
||||
import type {Wallet} from "@welshman/util"
|
||||
import {withGetter} from "@welshman/store"
|
||||
import {ss, syncedWithGetter} from "@app/storage"
|
||||
|
||||
/** Both lud06 and lud16 normalize into the bech32 lnurl zapping needs, so decode it before display. */
|
||||
export const displayLnurl = (lnurl: string) => {
|
||||
|
|
@ -20,7 +19,11 @@ export const displayLnurl = (lnurl: string) => {
|
|||
return url ? displayUrl(url) : lnurl
|
||||
}
|
||||
|
||||
export const wallet = withGetter(writable<Maybe<Wallet>>(undefined))
|
||||
export const wallet = syncedWithGetter<Maybe<Wallet>>({
|
||||
key: "wallet",
|
||||
storage: ss,
|
||||
defaultValue: undefined,
|
||||
})
|
||||
|
||||
export const getWebLn = () => (window as any).webln
|
||||
|
||||
|
|
|
|||
|
|
@ -14,27 +14,26 @@ import {
|
|||
makeHttpAuthHeader,
|
||||
} from "@welshman/util"
|
||||
import {Comment} from "@welshman/domain"
|
||||
import {synced, throttled, withGetter} from "@welshman/store"
|
||||
import {throttled} from "@welshman/store"
|
||||
import {Events, Relays} from "@welshman/app"
|
||||
import {app, fromApp, reader} from "@app/core"
|
||||
import {makeRoomPath, makeSpaceChatPath, makeChatPath, makeContentPath} from "@app/routes"
|
||||
import {CONTENT_KINDS, makeCommentFilter} from "@app/content"
|
||||
import {getIsMuted, notificationSettings, userSettingsValues} from "@app/settings"
|
||||
import {getIsMuted, userSettingsValues} from "@app/settings"
|
||||
import {Chats} from "@app/chats"
|
||||
import {dufflepud, DUFFLEPUD_URL} from "@app/env"
|
||||
import {isGroupSpace} from "@app/relays"
|
||||
import {activeSpaceUrls} from "@app/rooms"
|
||||
import {kv} from "@app/storage"
|
||||
import {kv, syncedWithGetter} from "@app/storage"
|
||||
import {notificationSettings} from "@app/push/adapters/common"
|
||||
|
||||
// Checked state
|
||||
|
||||
export const checked = withGetter(
|
||||
synced<Record<string, number>>({
|
||||
key: "checked",
|
||||
defaultValue: {},
|
||||
storage: kv,
|
||||
}),
|
||||
)
|
||||
export const checked = syncedWithGetter<Record<string, number>>({
|
||||
key: "checked",
|
||||
defaultValue: {},
|
||||
storage: kv,
|
||||
})
|
||||
|
||||
export const getChecked = (key: string) => checked.get()[key]
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import {writable} from "svelte/store"
|
||||
import type {Subscriber, Unsubscriber} from "svelte/store"
|
||||
import {Capacitor} from "@capacitor/core"
|
||||
import {call, ms, now, on, parseJson, poll, spec, throttle, uniq} from "@welshman/lib"
|
||||
|
|
@ -15,13 +14,14 @@ import {
|
|||
type Filter,
|
||||
type TrustedEvent,
|
||||
} from "@welshman/util"
|
||||
import {merged, withGetter} from "@welshman/store"
|
||||
import {merged} from "@welshman/store"
|
||||
import {User} from "@welshman/app"
|
||||
import {navigate} from "@app/modal"
|
||||
import {app, messagingRelayLists, network, roomLists} from "@app/core"
|
||||
import {DM_KINDS} from "@app/content"
|
||||
import {getMutedRooms, notificationSettings, shouldNotify, userSettingsValues} from "@app/settings"
|
||||
import {getMutedRooms, shouldNotify, userSettingsValues} from "@app/settings"
|
||||
import {makeEventPath, goToChat, goToSpace} from "@app/routes"
|
||||
import {kv, syncedWithGetter} from "@app/storage"
|
||||
|
||||
export type PushSubscription = {
|
||||
key: string
|
||||
|
|
@ -38,7 +38,26 @@ export type PushState = {
|
|||
export const requiresFallback =
|
||||
Capacitor.getPlatform() === "android" && !Capacitor.isPluginAvailable("PushNotifications")
|
||||
|
||||
export const pushState = withGetter(writable<PushState>({useFallback: requiresFallback}))
|
||||
export const pushState = syncedWithGetter<PushState>({
|
||||
key: "notificationState",
|
||||
storage: kv,
|
||||
defaultValue: {useFallback: requiresFallback},
|
||||
})
|
||||
|
||||
export const notificationSettings = syncedWithGetter({
|
||||
key: "notificationSettings",
|
||||
storage: kv,
|
||||
defaultValue: {
|
||||
push: false,
|
||||
sound: true,
|
||||
badge: false,
|
||||
spaces: true,
|
||||
mentions: true,
|
||||
messages: true,
|
||||
events: true,
|
||||
reminder_minutes: [5],
|
||||
},
|
||||
})
|
||||
|
||||
export interface IPushAdapter {
|
||||
request: (prompt?: boolean) => Promise<string>
|
||||
|
|
|
|||
|
|
@ -3,9 +3,8 @@ import {ellipsize, maybe} from "@welshman/lib"
|
|||
import {hexTags, matchFilter, tagValues, type TrustedEvent} from "@welshman/util"
|
||||
import {DM_KINDS} from "@app/content"
|
||||
import {app} from "@app/core"
|
||||
import {notificationSettings} from "@app/settings"
|
||||
import type {IPushAdapter} from "@app/push/adapters/common"
|
||||
import {onNotification} from "@app/push/adapters/common"
|
||||
import {notificationSettings, onNotification} from "@app/push/adapters/common"
|
||||
import {renderEventAsSummary} from "@app/render"
|
||||
import {goToEvent} from "@app/routes"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
import {Capacitor} from "@capacitor/core"
|
||||
import {notificationSettings, setSpaceNotifications} from "@app/settings"
|
||||
import {setSpaceNotifications} from "@app/settings"
|
||||
import {clearBadges} from "@app/notifications"
|
||||
import {syncEventReminders} from "@app/reminders"
|
||||
import {pushState} from "@app/push/adapters/common"
|
||||
import {notificationSettings, pushState} from "@app/push/adapters/common"
|
||||
import {ElectronNotifications} from "@app/push/adapters/electron"
|
||||
import {WebNotifications} from "@app/push/adapters/web"
|
||||
import {CapacitorNotifications} from "@app/push/adapters/capacitor"
|
||||
import {AndroidFallbackNotifications} from "@app/push/adapters/android"
|
||||
import type {IPushAdapter} from "@app/push/adapters/common"
|
||||
|
||||
export {onNotification} from "@app/push/adapters/common"
|
||||
export {notificationSettings, onNotification} from "@app/push/adapters/common"
|
||||
|
||||
export class Push {
|
||||
static _adapter: IPushAdapter | undefined
|
||||
|
|
|
|||
|
|
@ -6,8 +6,7 @@ import {buildUrl} from "@lib/util"
|
|||
import {getEventStart, getEventTitle, isAttending} from "@app/calendar"
|
||||
import {app, network, reader, roomLists} from "@app/core"
|
||||
import {PUSH_SERVER} from "@app/env"
|
||||
import {pushState} from "@app/push/adapters/common"
|
||||
import {notificationSettings} from "@app/settings"
|
||||
import {notificationSettings, pushState} from "@app/push/adapters/common"
|
||||
|
||||
type Reminder = {
|
||||
at: number
|
||||
|
|
|
|||
|
|
@ -159,19 +159,6 @@ export const shouldNotify = (url: string, h?: string) =>
|
|||
export const deriveShouldNotify = (url: string, h?: string) =>
|
||||
derived(userSettingsValues, $settings => getShouldNotify($settings, url, h))
|
||||
|
||||
export const notificationSettings = withGetter(
|
||||
writable({
|
||||
push: false,
|
||||
sound: true,
|
||||
badge: false,
|
||||
spaces: true,
|
||||
mentions: true,
|
||||
messages: true,
|
||||
events: true,
|
||||
reminder_minutes: [5],
|
||||
}),
|
||||
)
|
||||
|
||||
export const publishSettings = async (params: Partial<SettingsValues>) => {
|
||||
const $app = app.get()
|
||||
const reader = await settings.get().forceLoad(User.require($app).pubkey)
|
||||
|
|
|
|||
|
|
@ -42,7 +42,8 @@ import {
|
|||
verifiedSymbol,
|
||||
} from "@welshman/util"
|
||||
import type {Handle, TrustedEvent} from "@welshman/util"
|
||||
import {withGetter} from "@welshman/store"
|
||||
import {synced, withGetter} from "@welshman/store"
|
||||
import type {SyncedConfig} from "@welshman/store"
|
||||
import type {RepositoryUpdate, WrapItem} from "@welshman/net"
|
||||
import {Relay, Zapper} from "@welshman/domain"
|
||||
import type {RelayInfo, ZapperValues} from "@welshman/domain"
|
||||
|
|
@ -52,6 +53,13 @@ import {IDB} from "@lib/indexeddb"
|
|||
import {appPolicies} from "@app/core"
|
||||
import {DM_KINDS} from "@app/content"
|
||||
|
||||
// withGetter's type drops the ready promise synced adds
|
||||
export const syncedWithGetter = <T>(config: SyncedConfig<T>) => {
|
||||
const store = synced(config)
|
||||
|
||||
return {...withGetter(store), ready: store.ready}
|
||||
}
|
||||
|
||||
export const kv = call(() => {
|
||||
const enqueue = makeQueue()
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ import {
|
|||
} from "@welshman/util"
|
||||
import type {Filter} from "@welshman/util"
|
||||
import type {FollowListReader} from "@welshman/domain"
|
||||
import {merged, synced, throttled, withGetter} from "@welshman/store"
|
||||
import {merged, throttled} from "@welshman/store"
|
||||
import {FollowLists, MessagingRelayLists, RelayLists, SearchRelayLists, Sync} from "@welshman/app"
|
||||
import {
|
||||
app,
|
||||
|
|
@ -57,13 +57,15 @@ import {FEATURED_CONTENT_D} from "@app/featured"
|
|||
import {decodeRelay} from "@app/relays"
|
||||
import {activeSpaceUrls, userSpaceUrls} from "@app/rooms"
|
||||
import {Settings} from "@app/settings"
|
||||
import {kv} from "@app/storage"
|
||||
import {kv, syncedWithGetter} from "@app/storage"
|
||||
import {hasBlossomSupport} from "@app/uploads"
|
||||
|
||||
// Unwrapping is unconditional, so this is what keeps a signer from decrypting the whole DM history.
|
||||
export const shouldUnwrap = withGetter(
|
||||
synced({key: "shouldUnwrap", storage: kv, defaultValue: false}),
|
||||
)
|
||||
export const shouldUnwrap = syncedWithGetter({
|
||||
key: "shouldUnwrap",
|
||||
storage: kv,
|
||||
defaultValue: false,
|
||||
})
|
||||
|
||||
// Utils
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@
|
|||
import {dev} from "$app/environment"
|
||||
import {page} from "$app/stores"
|
||||
import {context as pomadeContext} from "@pomade/core"
|
||||
import {sync} from "@welshman/store"
|
||||
import {setNip55Plugin} from "@welshman/signer"
|
||||
import * as util from "@welshman/util"
|
||||
import * as lib from "@welshman/lib"
|
||||
|
|
@ -28,9 +27,8 @@
|
|||
import {restoreSession} from "@app/session"
|
||||
import {syncSignerAlerts} from "@app/signer"
|
||||
import {wallet} from "@app/lightning"
|
||||
import {kv, ss, storage} from "@app/storage"
|
||||
import {kv, storage} from "@app/storage"
|
||||
import {device} from "@app/device"
|
||||
import {notificationSettings} from "@app/settings"
|
||||
import {needsKeyBackup} from "@app/healthChecks"
|
||||
import {setupShareIntents} from "@app/share"
|
||||
import {shouldUnwrap, syncApplicationData} from "@app/sync"
|
||||
|
|
@ -38,7 +36,7 @@
|
|||
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 {Push, notificationSettings} from "@app/push"
|
||||
import {pushState} from "@app/push/adapters/common"
|
||||
import {setupDeepLinks} from "@app/deepLinks"
|
||||
import {syncKeyboard} from "@app/keyboard"
|
||||
|
|
@ -83,38 +81,14 @@
|
|||
// Set up logging
|
||||
unsubscribers.push(setupLogging())
|
||||
|
||||
// Sync stuff to storage
|
||||
// Boot code reads these, e.g. Push picks its adapter from pushState, so wait for stored values
|
||||
await Promise.all([
|
||||
sync({
|
||||
key: "device",
|
||||
store: device,
|
||||
storage: kv,
|
||||
}),
|
||||
sync({
|
||||
key: "shouldUnwrap",
|
||||
store: shouldUnwrap,
|
||||
storage: kv,
|
||||
}),
|
||||
sync({
|
||||
key: "wallet",
|
||||
store: wallet,
|
||||
storage: ss,
|
||||
}),
|
||||
sync({
|
||||
key: "notificationSettings",
|
||||
store: notificationSettings,
|
||||
storage: kv,
|
||||
}),
|
||||
sync({
|
||||
key: "notificationState",
|
||||
store: pushState,
|
||||
storage: kv,
|
||||
}),
|
||||
sync({
|
||||
key: "needsKeyBackup",
|
||||
store: needsKeyBackup,
|
||||
storage: kv,
|
||||
}),
|
||||
device.ready,
|
||||
shouldUnwrap.ready,
|
||||
wallet.ready,
|
||||
notificationSettings.ready,
|
||||
pushState.ready,
|
||||
needsKeyBackup.ready,
|
||||
])
|
||||
|
||||
// The key backup reminder used to live in a forceHealthChecks record
|
||||
|
|
|
|||
|
|
@ -14,9 +14,8 @@
|
|||
import {append, uniq} from "@welshman/lib"
|
||||
import Chat from "@app/components/Chat.svelte"
|
||||
import {splitChatId} from "@app/chats"
|
||||
import {notificationSettings} from "@app/settings"
|
||||
import {pushToast} from "@app/toast"
|
||||
import {Push} from "@app/push"
|
||||
import {Push, notificationSettings} from "@app/push"
|
||||
import {user} from "@app/core"
|
||||
import type {PageProps} from "./$types"
|
||||
|
||||
|
|
|
|||
|
|
@ -14,8 +14,7 @@
|
|||
import Button from "@lib/components/Button.svelte"
|
||||
import PageContent from "@lib/components/PageContent.svelte"
|
||||
import {pushToast} from "@app/toast"
|
||||
import {saveNotificationSettings} from "@app/push"
|
||||
import {notificationSettings} from "@app/settings"
|
||||
import {notificationSettings, saveNotificationSettings} from "@app/push"
|
||||
|
||||
const reset = () => {
|
||||
settings = {...notificationSettings.get()}
|
||||
|
|
|
|||
Loading…
Reference in a new issue