remove old icon picker
This commit is contained in:
@@ -21,7 +21,7 @@
|
|||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
import WalletAsReceivingAddress from "@app/components/WalletAsReceivingAddress.svelte"
|
import WalletAsReceivingAddress from "@app/components/WalletAsReceivingAddress.svelte"
|
||||||
import Divider from "@src/lib/components/Divider.svelte"
|
import Divider from "@lib/components/Divider.svelte"
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
|
|||||||
@@ -93,7 +93,8 @@ import {
|
|||||||
userBlossomServers,
|
userBlossomServers,
|
||||||
shouldUnwrap,
|
shouldUnwrap,
|
||||||
} from "@welshman/app"
|
} from "@welshman/app"
|
||||||
import {compressFile} from "@src/lib/html"
|
import {compressFile} from "@lib/html"
|
||||||
|
import {kv, db} from "@app/core/storage"
|
||||||
import type {SettingsValues, Alert} from "@app/core/state"
|
import type {SettingsValues, Alert} from "@app/core/state"
|
||||||
import {
|
import {
|
||||||
SETTINGS,
|
SETTINGS,
|
||||||
@@ -111,7 +112,6 @@ import {
|
|||||||
} from "@app/core/state"
|
} from "@app/core/state"
|
||||||
import {loadAlertStatuses} from "@app/core/requests"
|
import {loadAlertStatuses} from "@app/core/requests"
|
||||||
import {platform, platformName, getPushInfo} from "@app/util/push"
|
import {platform, platformName, getPushInfo} from "@app/util/push"
|
||||||
import {preferencesStorageProvider, Collection} from "@src/lib/storage"
|
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
|
|
||||||
@@ -156,8 +156,8 @@ export const logout = async () => {
|
|||||||
|
|
||||||
localStorage.clear()
|
localStorage.clear()
|
||||||
|
|
||||||
await preferencesStorageProvider.clear()
|
await kv.clear()
|
||||||
await Collection.clearAll()
|
await db.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Synchronization
|
// Synchronization
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import {reject, call, identity} from "@welshman/lib"
|
||||||
|
import {Preferences} from "@capacitor/preferences"
|
||||||
|
import {Encoding, Filesystem, Directory} from "@capacitor/filesystem"
|
||||||
|
import {IDB} from "@lib/indexeddb"
|
||||||
|
|
||||||
|
export const kv = call(() => {
|
||||||
|
let p = Promise.resolve()
|
||||||
|
|
||||||
|
const get = async <T>(key: string): Promise<T | undefined> => {
|
||||||
|
const result = await Preferences.get({key})
|
||||||
|
if (!result.value) return undefined
|
||||||
|
try {
|
||||||
|
return JSON.parse(result.value)
|
||||||
|
} catch (e) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const set = async <T>(key: string, value: T): Promise<void> => {
|
||||||
|
p = p.then(() => Preferences.set({key, value: JSON.stringify(value)}))
|
||||||
|
|
||||||
|
await p
|
||||||
|
}
|
||||||
|
|
||||||
|
const clear = async () => {
|
||||||
|
p = p.then(() => Preferences.clear())
|
||||||
|
|
||||||
|
await p
|
||||||
|
}
|
||||||
|
|
||||||
|
return {get, set, clear}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const db = new IDB({name: "flotilla-9gl", version: 1})
|
||||||
|
|
||||||
|
// Migration - we used to use capacitor's filesystem for storage, clear it out since we're
|
||||||
|
// going back to indexeddb
|
||||||
|
call(async () => {
|
||||||
|
const res = await Filesystem.readdir({
|
||||||
|
path: "",
|
||||||
|
directory: Directory.Data,
|
||||||
|
})
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
res.files.map(file =>
|
||||||
|
Filesystem.deleteFile({
|
||||||
|
path: file.name,
|
||||||
|
directory: Directory.Data,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import {derived, get} from "svelte/store"
|
import {derived, get} from "svelte/store"
|
||||||
|
import {Badge} from "@capawesome/capacitor-badge"
|
||||||
import {synced, throttled} from "@welshman/store"
|
import {synced, throttled} from "@welshman/store"
|
||||||
import {pubkey, relaysByUrl} from "@welshman/app"
|
import {pubkey, relaysByUrl} from "@welshman/app"
|
||||||
import {prop, spec, identity, now, groupBy} from "@welshman/lib"
|
import {prop, spec, identity, now, groupBy} from "@welshman/lib"
|
||||||
@@ -23,15 +24,14 @@ import {
|
|||||||
getSpaceUrlsFromGroupSelections,
|
getSpaceUrlsFromGroupSelections,
|
||||||
getSpaceRoomsFromGroupSelections,
|
getSpaceRoomsFromGroupSelections,
|
||||||
} from "@app/core/state"
|
} from "@app/core/state"
|
||||||
import {preferencesStorageProvider} from "@src/lib/storage"
|
import {kv} from "@app/core/storage"
|
||||||
import {Badge} from "@capawesome/capacitor-badge"
|
|
||||||
|
|
||||||
// Checked state
|
// Checked state
|
||||||
|
|
||||||
export const checked = synced<Record<string, number>>({
|
export const checked = synced<Record<string, number>>({
|
||||||
key: "checked",
|
key: "checked",
|
||||||
defaultValue: {},
|
defaultValue: {},
|
||||||
storage: preferencesStorageProvider,
|
storage: kv,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const deriveChecked = (key: string) => derived(checked, prop(key))
|
export const deriveChecked = (key: string) => derived(checked, prop(key))
|
||||||
|
|||||||
+189
-164
@@ -48,35 +48,16 @@ import {
|
|||||||
onZapper,
|
onZapper,
|
||||||
onHandle,
|
onHandle,
|
||||||
wrapManager,
|
wrapManager,
|
||||||
|
onRelay,
|
||||||
} from "@welshman/app"
|
} from "@welshman/app"
|
||||||
import {Collection} from "@lib/storage"
|
|
||||||
import {isMobile} from "@lib/html"
|
import {isMobile} from "@lib/html"
|
||||||
|
import type {IDBTable} from "@lib/indexeddb"
|
||||||
|
|
||||||
const syncEvents = async () => {
|
const kinds = {
|
||||||
const collection = new Collection<TrustedEvent>({table: "events", getId: prop("id")})
|
meta: [PROFILE, FOLLOWS, MUTES, RELAYS, BLOSSOM_SERVERS, INBOX_RELAYS, APP_DATA, ROOMS],
|
||||||
|
alert: [ALERT_STATUS, ALERT_EMAIL, ALERT_WEB, ALERT_IOS, ALERT_ANDROID],
|
||||||
const initialEvents = await collection.get()
|
space: [RELAY_ADD_MEMBER, RELAY_REMOVE_MEMBER, RELAY_MEMBERS, RELAY_JOIN, RELAY_LEAVE],
|
||||||
|
room: [
|
||||||
// Mark events verified to avoid re-verification of signatures
|
|
||||||
for (const event of initialEvents) {
|
|
||||||
event[verifiedSymbol] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
repository.load(initialEvents)
|
|
||||||
|
|
||||||
const metaKinds = [
|
|
||||||
PROFILE,
|
|
||||||
FOLLOWS,
|
|
||||||
MUTES,
|
|
||||||
RELAYS,
|
|
||||||
BLOSSOM_SERVERS,
|
|
||||||
INBOX_RELAYS,
|
|
||||||
APP_DATA,
|
|
||||||
ROOMS,
|
|
||||||
]
|
|
||||||
const alertKinds = [ALERT_STATUS, ALERT_EMAIL, ALERT_WEB, ALERT_IOS, ALERT_ANDROID]
|
|
||||||
const spaceKinds = [RELAY_ADD_MEMBER, RELAY_REMOVE_MEMBER, RELAY_MEMBERS, RELAY_JOIN, RELAY_LEAVE]
|
|
||||||
const roomKinds = [
|
|
||||||
ROOM_META,
|
ROOM_META,
|
||||||
ROOM_DELETE,
|
ROOM_DELETE,
|
||||||
ROOM_ADMINS,
|
ROOM_ADMINS,
|
||||||
@@ -84,178 +65,222 @@ const syncEvents = async () => {
|
|||||||
ROOM_ADD_MEMBER,
|
ROOM_ADD_MEMBER,
|
||||||
ROOM_REMOVE_MEMBER,
|
ROOM_REMOVE_MEMBER,
|
||||||
ROOM_CREATE_PERMISSION,
|
ROOM_CREATE_PERMISSION,
|
||||||
]
|
],
|
||||||
const contentKinds = [EVENT_TIME, THREAD, MESSAGE, ZAP_GOAL, DIRECT_MESSAGE, DIRECT_MESSAGE_FILE]
|
content: [EVENT_TIME, THREAD, MESSAGE, ZAP_GOAL, DIRECT_MESSAGE, DIRECT_MESSAGE_FILE],
|
||||||
|
}
|
||||||
|
|
||||||
const rankEvent = (event: TrustedEvent) => {
|
const rankEvent = (event: TrustedEvent) => {
|
||||||
if (metaKinds.includes(event.kind)) return 9
|
if (kinds.meta.includes(event.kind)) return 9
|
||||||
if (alertKinds.includes(event.kind)) return 8
|
if (kinds.alert.includes(event.kind)) return 8
|
||||||
if (spaceKinds.includes(event.kind)) return 7
|
if (kinds.space.includes(event.kind)) return 7
|
||||||
if (roomKinds.includes(event.kind)) return 6
|
if (kinds.room.includes(event.kind)) return 6
|
||||||
if (!isMobile && contentKinds.includes(event.kind)) return 5
|
if (!isMobile && kinds.content.includes(event.kind)) return 5
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
return on(
|
const eventsAdapter = {
|
||||||
repository,
|
name: "events",
|
||||||
"update",
|
keyPath: ["id"],
|
||||||
batch(3000, async (updates: RepositoryUpdate[]) => {
|
init: async (table: IDBTable<TrustedEvent>) => {
|
||||||
const add: TrustedEvent[] = []
|
const initialEvents = await table.getAll()
|
||||||
const remove = new Set<string>()
|
|
||||||
|
|
||||||
for (const update of updates) {
|
// Mark events verified to avoid re-verification of signatures
|
||||||
for (const event of update.added) {
|
for (const event of initialEvents) {
|
||||||
if (rankEvent(event) > 0) {
|
event[verifiedSymbol] = true
|
||||||
add.push(event)
|
}
|
||||||
remove.delete(event.id)
|
|
||||||
|
repository.load(initialEvents)
|
||||||
|
|
||||||
|
return on(
|
||||||
|
repository,
|
||||||
|
"update",
|
||||||
|
batch(3000, async (updates: RepositoryUpdate[]) => {
|
||||||
|
const add: TrustedEvent[] = []
|
||||||
|
const remove = new Set<string>()
|
||||||
|
|
||||||
|
for (const update of updates) {
|
||||||
|
for (const event of update.added) {
|
||||||
|
if (rankEvent(event) > 0) {
|
||||||
|
add.push(event)
|
||||||
|
remove.delete(event.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const id of update.removed) {
|
||||||
|
remove.add(id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const id of update.removed) {
|
if (add.length > 0) {
|
||||||
remove.add(id)
|
await table.bulkPut(add)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (remove.size > 0) {
|
||||||
|
await table.bulkDelete(remove)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
type TrackerItem = {id: string; relays: string[]}
|
||||||
|
|
||||||
|
const trackerAdapter = {
|
||||||
|
name: "tracker",
|
||||||
|
keyPath: ["id"],
|
||||||
|
init: async (table: IDBTable<TrackerItem>) => {
|
||||||
|
const relaysById = new Map<string, Set<string>>()
|
||||||
|
|
||||||
|
for (const {id, relays} of await table.getAll()) {
|
||||||
|
relaysById.set(id, new Set(relays))
|
||||||
|
}
|
||||||
|
|
||||||
|
tracker.load(relaysById)
|
||||||
|
|
||||||
|
const _onAdd = async (ids: Iterable<string>) => {
|
||||||
|
const items: TrackerItem[] = []
|
||||||
|
|
||||||
|
for (const id of ids) {
|
||||||
|
const event = repository.getEvent(id)
|
||||||
|
|
||||||
|
if (!event || rankEvent(event) === 0) continue
|
||||||
|
|
||||||
|
const relays = Array.from(tracker.getRelays(id))
|
||||||
|
|
||||||
|
if (relays.length === 0) continue
|
||||||
|
|
||||||
|
items.push({id, relays})
|
||||||
}
|
}
|
||||||
|
|
||||||
await collection.update({add, remove})
|
await table.bulkPut(items)
|
||||||
}),
|
}
|
||||||
)
|
|
||||||
|
const _onRemove = async (ids: Iterable<string>) => {
|
||||||
|
await table.bulkDelete(Array.from(ids))
|
||||||
|
}
|
||||||
|
|
||||||
|
const onAdd = batch(3000, _onAdd)
|
||||||
|
|
||||||
|
const onRemove = batch(3000, _onRemove)
|
||||||
|
|
||||||
|
const onLoad = () => _onAdd(tracker.relaysById.keys())
|
||||||
|
|
||||||
|
const onClear = () => _onRemove(tracker.relaysById.keys())
|
||||||
|
|
||||||
|
tracker.on("add", onAdd)
|
||||||
|
tracker.on("remove", onRemove)
|
||||||
|
tracker.on("load", onLoad)
|
||||||
|
tracker.on("clear", onClear)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
tracker.off("add", onAdd)
|
||||||
|
tracker.off("remove", onRemove)
|
||||||
|
tracker.off("load", onLoad)
|
||||||
|
tracker.off("clear", onClear)
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
type TrackerItem = [string, string[]]
|
const relaysAdapter = {
|
||||||
|
name: "relays",
|
||||||
|
keyPath: ["url"],
|
||||||
|
init: async (table: IDBTable<RelayProfile>) => {
|
||||||
|
relays.set(await table.getAll())
|
||||||
|
|
||||||
const syncTracker = async () => {
|
return onRelay(batch(3000, table.bulkPut))
|
||||||
const collection = new Collection<TrackerItem>({
|
},
|
||||||
table: "tracker",
|
|
||||||
getId: (item: TrackerItem) => item[0],
|
|
||||||
})
|
|
||||||
|
|
||||||
const relaysById = new Map<string, Set<string>>()
|
|
||||||
|
|
||||||
for (const [id, relays] of await collection.get()) {
|
|
||||||
relaysById.set(id, new Set(relays))
|
|
||||||
}
|
|
||||||
|
|
||||||
tracker.load(relaysById)
|
|
||||||
|
|
||||||
const updateOne = batch(3000, (ids: string[]) => {
|
|
||||||
collection.add(ids.map(id => [id, Array.from(tracker.getRelays(id))]))
|
|
||||||
})
|
|
||||||
|
|
||||||
const updateAll = throttle(3000, () => {
|
|
||||||
collection.set(
|
|
||||||
Array.from(tracker.relaysById.entries()).map(([id, relays]) => [id, Array.from(relays)]),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
tracker.on("add", updateOne)
|
|
||||||
tracker.on("remove", updateOne)
|
|
||||||
tracker.on("load", updateAll)
|
|
||||||
tracker.on("clear", updateAll)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
tracker.off("add", updateOne)
|
|
||||||
tracker.off("remove", updateOne)
|
|
||||||
tracker.off("load", updateAll)
|
|
||||||
tracker.off("clear", updateAll)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const syncRelays = async () => {
|
const relayStatsAdapter = {
|
||||||
const collection = new Collection<RelayProfile>({table: "relays", getId: prop("url")})
|
name: "relayStats",
|
||||||
|
keyPath: ["url"],
|
||||||
|
init: async (table: IDBTable<RelayStats>) => {
|
||||||
|
relayStats.set(await table.getAll())
|
||||||
|
|
||||||
relays.set(await collection.get())
|
return throttled(3000, relayStats).subscribe(table.bulkPut)
|
||||||
|
},
|
||||||
return throttled(3000, relays).subscribe(collection.set)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const syncRelayStats = async () => {
|
const handlesAdapter = {
|
||||||
const collection = new Collection<RelayStats>({table: "relayStats", getId: prop("url")})
|
name: "handles",
|
||||||
|
keyPath: ["nip05"],
|
||||||
|
init: async (table: IDBTable<Handle>) => {
|
||||||
|
handles.set(await table.getAll())
|
||||||
|
|
||||||
relayStats.set(await collection.get())
|
return onHandle(batch(3000, table.bulkPut))
|
||||||
|
},
|
||||||
return throttled(3000, relayStats).subscribe(collection.set)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const syncHandles = async () => {
|
const zappersAdapter = {
|
||||||
const collection = new Collection<Handle>({table: "handles", getId: prop("nip05")})
|
name: "zappers",
|
||||||
|
keyPath: ["lnurl"],
|
||||||
|
init: async (table: IDBTable<Zapper>) => {
|
||||||
|
zappers.set(await table.getAll())
|
||||||
|
|
||||||
handles.set(await collection.get())
|
return onZapper(batch(3000, table.bulkPut))
|
||||||
|
},
|
||||||
return onHandle(batch(3000, collection.add))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const syncZappers = async () => {
|
type FreshnessItem = {key: string; value: number}
|
||||||
const collection = new Collection<Zapper>({table: "zappers", getId: prop("lnurl")})
|
|
||||||
|
|
||||||
zappers.set(await collection.get())
|
const freshnessAdapter = {
|
||||||
|
name: "freshness",
|
||||||
|
keyPath: ["key"],
|
||||||
|
init: async (table: IDBTable<FreshnessItem>) => {
|
||||||
|
const initialRecords = await table.getAll()
|
||||||
|
|
||||||
return onZapper(batch(3000, collection.add))
|
freshness.set(fromPairs(initialRecords.map(({key, value}) => [key, value])))
|
||||||
|
|
||||||
|
return throttled(3000, freshness).subscribe($freshness => {
|
||||||
|
table.bulkPut(Object.entries($freshness).map(([key, value]) => ({key, value})))
|
||||||
|
})
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
type FreshnessItem = [string, number]
|
type PlaintextItem = {key: string; value: string}
|
||||||
|
|
||||||
const syncFreshness = async () => {
|
const plaintextAdapter = {
|
||||||
const collection = new Collection<FreshnessItem>({
|
name: "plaintext",
|
||||||
table: "freshness",
|
keyPath: ["key"],
|
||||||
getId: (item: FreshnessItem) => item[0],
|
init: async (table: IDBTable<PlaintextItem>) => {
|
||||||
})
|
const initialRecords = await table.getAll()
|
||||||
|
|
||||||
freshness.set(fromPairs(await collection.get()))
|
plaintext.set(fromPairs(initialRecords.map(({key, value}) => [key, value])))
|
||||||
|
|
||||||
return throttled(3000, freshness).subscribe($freshness => {
|
return throttled(3000, plaintext).subscribe($plaintext => {
|
||||||
collection.set(Object.entries($freshness))
|
table.bulkPut(Object.entries($plaintext).map(([key, value]) => ({key, value})))
|
||||||
})
|
})
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
type PlaintextItem = [string, string]
|
const wrapManagerAdapter = {
|
||||||
|
name: "wrapManager",
|
||||||
|
keyPath: ["id"],
|
||||||
|
init: async (table: IDBTable<WrapItem>) => {
|
||||||
|
wrapManager.load(await table.getAll())
|
||||||
|
|
||||||
const syncPlaintext = async () => {
|
const addOne = batch(3000, table.bulkPut)
|
||||||
const collection = new Collection<PlaintextItem>({
|
|
||||||
table: "plaintext",
|
|
||||||
getId: (item: PlaintextItem) => item[0],
|
|
||||||
})
|
|
||||||
|
|
||||||
plaintext.set(fromPairs(await collection.get()))
|
const removeOne = throttle(3000, table.bulkDelete)
|
||||||
|
|
||||||
return throttled(3000, plaintext).subscribe($plaintext => {
|
wrapManager.on("add", addOne)
|
||||||
collection.set(Object.entries($plaintext))
|
wrapManager.on("remove", removeOne)
|
||||||
})
|
|
||||||
|
return () => {
|
||||||
|
wrapManager.off("add", addOne)
|
||||||
|
wrapManager.off("remove", removeOne)
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const syncWrapManager = async () => {
|
export const adapters = [
|
||||||
const collection = new Collection<WrapItem>({table: "wraps", getId: prop("id")})
|
eventsAdapter,
|
||||||
|
trackerAdapter,
|
||||||
wrapManager.load(await collection.get())
|
relaysAdapter,
|
||||||
|
relayStatsAdapter,
|
||||||
const addOne = batch(3000, (wrapItems: WrapItem[]) => collection.add(wrapItems))
|
handlesAdapter,
|
||||||
|
zappersAdapter,
|
||||||
const updateAll = throttle(3000, () => collection.set(wrapManager.dump()))
|
freshnessAdapter,
|
||||||
|
plaintextAdapter,
|
||||||
wrapManager.on("add", addOne)
|
wrapManagerAdapter,
|
||||||
wrapManager.on("remove", updateAll)
|
]
|
||||||
|
|
||||||
return () => {
|
|
||||||
wrapManager.off("add", addOne)
|
|
||||||
wrapManager.off("remove", updateAll)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const syncDataStores = async () => {
|
|
||||||
const promises = [
|
|
||||||
syncEvents(),
|
|
||||||
syncTracker(),
|
|
||||||
syncRelays(),
|
|
||||||
syncHandles(),
|
|
||||||
syncZappers(),
|
|
||||||
syncPlaintext(),
|
|
||||||
syncWrapManager(),
|
|
||||||
]
|
|
||||||
|
|
||||||
if (!isMobile) {
|
|
||||||
promises.push(syncFreshness(), syncRelayStats())
|
|
||||||
}
|
|
||||||
|
|
||||||
const unsubscribers = await Promise.all(promises)
|
|
||||||
|
|
||||||
return () => unsubscribers.forEach(call)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import {preferencesStorageProvider} from "@src/lib/storage"
|
import {kv} from "@app/core/storage"
|
||||||
import {synced} from "@welshman/store"
|
import {synced} from "@welshman/store"
|
||||||
|
|
||||||
export const theme = synced({
|
export const theme = synced({
|
||||||
key: "theme",
|
key: "theme",
|
||||||
defaultValue: window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light",
|
defaultValue: window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light",
|
||||||
storage: preferencesStorageProvider,
|
storage: kv,
|
||||||
})
|
})
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,170 @@
|
|||||||
|
import {openDB, deleteDB} from "idb"
|
||||||
|
import type {IDBPDatabase} from "idb"
|
||||||
|
import {writable} from "svelte/store"
|
||||||
|
import type {Unsubscriber} from "svelte/store"
|
||||||
|
import {call, defer} from "@welshman/lib"
|
||||||
|
import type {Maybe} from "@welshman/lib"
|
||||||
|
import {withGetter} from "@welshman/store"
|
||||||
|
|
||||||
|
export type IDBAdapter = {
|
||||||
|
name: string
|
||||||
|
keyPath: string[]
|
||||||
|
init: (table: IDBTable<any>) => Promise<Unsubscriber>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IDBAdapters = IDBAdapter[]
|
||||||
|
|
||||||
|
export enum IDBStatus {
|
||||||
|
Ready = "ready",
|
||||||
|
Closed = "closed",
|
||||||
|
Opening = "opening",
|
||||||
|
Closing = "closing",
|
||||||
|
Initial = "initial",
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IDBOptions = {
|
||||||
|
name: string
|
||||||
|
version: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export class IDB {
|
||||||
|
idbp: Maybe<Promise<IDBPDatabase>>
|
||||||
|
ready: Maybe<Promise<void>>
|
||||||
|
unsubscribers: Maybe<Unsubscriber[]>
|
||||||
|
status = IDBStatus.Initial
|
||||||
|
|
||||||
|
constructor(readonly options: IDBOptions) {}
|
||||||
|
|
||||||
|
init(adapters: IDBAdapters) {
|
||||||
|
if (this.status !== IDBStatus.Initial) {
|
||||||
|
throw new Error(`Database re-initialized while ${this.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.status = IDBStatus.Opening
|
||||||
|
|
||||||
|
this.idbp = openDB(this.options.name, this.options.version, {
|
||||||
|
upgrade(idbDb: IDBPDatabase) {
|
||||||
|
const names = new Set(adapters.map(a => a.name))
|
||||||
|
|
||||||
|
for (const table of idbDb.objectStoreNames) {
|
||||||
|
if (!names.has(table)) {
|
||||||
|
idbDb.deleteObjectStore(table)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const {name, keyPath} of adapters) {
|
||||||
|
try {
|
||||||
|
idbDb.createObjectStore(name, {keyPath})
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
blocked() {},
|
||||||
|
blocking() {},
|
||||||
|
})
|
||||||
|
|
||||||
|
this.ready = this.idbp.then(async idbp => {
|
||||||
|
window.addEventListener("beforeunload", () => idbp.close())
|
||||||
|
|
||||||
|
this.unsubscribers = await Promise.all(adapters.map(({name, init}) => init(this.table(name))))
|
||||||
|
|
||||||
|
this.status = IDBStatus.Ready
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
table = <T>(name: string) => new IDBTable<T>(this, name)
|
||||||
|
|
||||||
|
_withIDBP = async <T>(f: (db: IDBPDatabase) => Promise<T>) => {
|
||||||
|
if (this.status === IDBStatus.Initial) {
|
||||||
|
throw new Error("Database was accessed in initial state")
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're closing, ignore any lingering requests
|
||||||
|
if ([IDBStatus.Closed, IDBStatus.Closing].includes(this.status)) return
|
||||||
|
|
||||||
|
return f(await this.idbp)
|
||||||
|
}
|
||||||
|
|
||||||
|
getAll = async <T>(table: string): Promise<T[]> =>
|
||||||
|
this._withIDBP(async idbp => {
|
||||||
|
const tx = idbp.transaction(table, "readwrite")
|
||||||
|
const store = tx.objectStore(table)
|
||||||
|
const result = await store.getAll()
|
||||||
|
|
||||||
|
await tx.done
|
||||||
|
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
|
||||||
|
bulkPut = async <T>(table: string, data: Iterable<T>) =>
|
||||||
|
this._withIDBP(async idbp => {
|
||||||
|
const tx = idbp.transaction(table, "readwrite")
|
||||||
|
const store = tx.objectStore(table)
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
Array.from(data).map(item => {
|
||||||
|
try {
|
||||||
|
store.put(item)
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e, item)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
await tx.done
|
||||||
|
})
|
||||||
|
|
||||||
|
bulkDelete = async (table: string, ids: Iterable<string>) =>
|
||||||
|
this._withIDBP(async idbp => {
|
||||||
|
const tx = idbp.transaction(table, "readwrite")
|
||||||
|
const store = tx.objectStore(table)
|
||||||
|
|
||||||
|
await Promise.all(Array.from(ids).map(id => store.delete(id)))
|
||||||
|
await tx.done
|
||||||
|
})
|
||||||
|
|
||||||
|
close = () =>
|
||||||
|
this._withIDBP(async idbp => {
|
||||||
|
this.unsubscribers!.forEach(call)
|
||||||
|
this.status = IDBStatus.Closing
|
||||||
|
|
||||||
|
await idbp.close()
|
||||||
|
|
||||||
|
// Allow the caller to call reset and re-init immediately
|
||||||
|
if (this.status === IDBStatus.Closing) {
|
||||||
|
this.idbp = undefined
|
||||||
|
this.ready = undefined
|
||||||
|
this.unsubscribers = undefined
|
||||||
|
this.status = IDBStatus.Closed
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
clear = async () => {
|
||||||
|
await this.close()
|
||||||
|
await deleteDB(this.options.name, {
|
||||||
|
blocked() {},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
reset = () => {
|
||||||
|
if (![IDBStatus.Closing, IDBStatus.Closed].includes(this.status)) {
|
||||||
|
throw new Error("Database reset when not closed")
|
||||||
|
}
|
||||||
|
|
||||||
|
this.status = IDBStatus.Initial
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class IDBTable<T> {
|
||||||
|
constructor(
|
||||||
|
readonly db: IDB,
|
||||||
|
readonly name: string,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
getAll = () => this.db.getAll<T>(this.name)
|
||||||
|
|
||||||
|
bulkPut = (data: Iterable<T>) => this.db.bulkPut(this.name, data)
|
||||||
|
|
||||||
|
bulkDelete = (ids: Iterable<string>) => this.db.bulkDelete(this.name, ids)
|
||||||
|
}
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
import {reject, identity} from "@welshman/lib"
|
|
||||||
import {type StorageProvider} from "@welshman/store"
|
|
||||||
import {Preferences} from "@capacitor/preferences"
|
|
||||||
import {Encoding, Filesystem, Directory} from "@capacitor/filesystem"
|
|
||||||
|
|
||||||
export class PreferencesStorageProvider implements StorageProvider {
|
|
||||||
p = Promise.resolve()
|
|
||||||
|
|
||||||
get = async <T>(key: string): Promise<T | undefined> => {
|
|
||||||
const result = await Preferences.get({key})
|
|
||||||
if (!result.value) return undefined
|
|
||||||
try {
|
|
||||||
return JSON.parse(result.value)
|
|
||||||
} catch (e) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
set = async <T>(key: string, value: T): Promise<void> => {
|
|
||||||
this.p = this.p.then(() => Preferences.set({key, value: JSON.stringify(value)}))
|
|
||||||
|
|
||||||
await this.p
|
|
||||||
}
|
|
||||||
|
|
||||||
clear = async () => {
|
|
||||||
this.p = this.p.then(() => Preferences.clear())
|
|
||||||
|
|
||||||
await this.p
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const preferencesStorageProvider = new PreferencesStorageProvider()
|
|
||||||
|
|
||||||
export type CollectionOptions<T> = {
|
|
||||||
table: string
|
|
||||||
getId: (item: T) => string
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Collection<T> {
|
|
||||||
constructor(readonly options: CollectionOptions<T>) {}
|
|
||||||
|
|
||||||
static clearAll = async (): Promise<void> => {
|
|
||||||
const res = await Filesystem.readdir({
|
|
||||||
path: "",
|
|
||||||
directory: Directory.Data,
|
|
||||||
})
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
res.files.map(file =>
|
|
||||||
Filesystem.deleteFile({
|
|
||||||
path: file.name,
|
|
||||||
directory: Directory.Data,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#path = () => `collection_${this.options.table}.json`
|
|
||||||
|
|
||||||
get = async (): Promise<T[]> => {
|
|
||||||
try {
|
|
||||||
const file = await Filesystem.readFile({
|
|
||||||
path: this.#path(),
|
|
||||||
directory: Directory.Data,
|
|
||||||
encoding: Encoding.UTF8,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Speed things up by parsing only once
|
|
||||||
return JSON.parse("[" + file.data.toString().split("\n").filter(identity).join(",") + "]")
|
|
||||||
} catch (err) {
|
|
||||||
// file doesn't exist, or isn't valid json
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
set = (items: T[]) =>
|
|
||||||
Filesystem.writeFile({
|
|
||||||
path: this.#path(),
|
|
||||||
directory: Directory.Data,
|
|
||||||
encoding: Encoding.UTF8,
|
|
||||||
data: items.map(v => JSON.stringify(v)).join("\n"),
|
|
||||||
})
|
|
||||||
|
|
||||||
add = (items: T[]) =>
|
|
||||||
Filesystem.appendFile({
|
|
||||||
path: this.#path(),
|
|
||||||
directory: Directory.Data,
|
|
||||||
encoding: Encoding.UTF8,
|
|
||||||
data: "\n" + items.map(v => JSON.stringify(v)).join("\n"),
|
|
||||||
})
|
|
||||||
|
|
||||||
remove = async (ids: Set<string>) =>
|
|
||||||
this.set(reject(item => ids.has(this.options.getId(item)), await this.get()))
|
|
||||||
|
|
||||||
update = async ({add, remove}: {add?: T[]; remove?: Set<string>}) => {
|
|
||||||
if (remove && remove.size > 0) {
|
|
||||||
const items = reject(item => remove.has(this.options.getId(item)), await this.get())
|
|
||||||
|
|
||||||
if (add) {
|
|
||||||
items.push(...add)
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.set(items)
|
|
||||||
} else if (add && add.length > 0) {
|
|
||||||
await this.add(add)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -20,21 +20,21 @@
|
|||||||
import * as welshmanSigner from "@welshman/signer"
|
import * as welshmanSigner from "@welshman/signer"
|
||||||
import * as net from "@welshman/net"
|
import * as net from "@welshman/net"
|
||||||
import * as app from "@welshman/app"
|
import * as app from "@welshman/app"
|
||||||
import {preferencesStorageProvider} from "@lib/storage"
|
|
||||||
import AppContainer from "@app/components/AppContainer.svelte"
|
import AppContainer from "@app/components/AppContainer.svelte"
|
||||||
import ModalContainer from "@app/components/ModalContainer.svelte"
|
import ModalContainer from "@app/components/ModalContainer.svelte"
|
||||||
import {setupHistory} from "@app/util/history"
|
import {setupHistory} from "@app/util/history"
|
||||||
import {setupTracking} from "@app/util/tracking"
|
import {setupTracking} from "@app/util/tracking"
|
||||||
import {setupAnalytics} from "@app/util/analytics"
|
import {setupAnalytics} from "@app/util/analytics"
|
||||||
import {authPolicy, trustPolicy, mostlyRestrictedPolicy} from "@app/util/policies"
|
import {authPolicy, trustPolicy, mostlyRestrictedPolicy} from "@app/util/policies"
|
||||||
|
import {kv, db} from "@app/core/storage"
|
||||||
import {userSettingsValues} from "@app/core/state"
|
import {userSettingsValues} from "@app/core/state"
|
||||||
import {syncApplicationData} from "@app/core/sync"
|
import {syncApplicationData} from "@app/core/sync"
|
||||||
import {theme} from "@app/util/theme"
|
|
||||||
import {toast, pushToast} from "@app/util/toast"
|
|
||||||
import {initializePushNotifications} from "@app/util/push"
|
|
||||||
import * as commands from "@app/core/commands"
|
import * as commands from "@app/core/commands"
|
||||||
import * as requests from "@app/core/requests"
|
import * as requests from "@app/core/requests"
|
||||||
import * as appState from "@app/core/state"
|
import * as appState from "@app/core/state"
|
||||||
|
import {theme} from "@app/util/theme"
|
||||||
|
import {toast, pushToast} from "@app/util/toast"
|
||||||
|
import {initializePushNotifications} from "@app/util/push"
|
||||||
import * as notifications from "@app/util/notifications"
|
import * as notifications from "@app/util/notifications"
|
||||||
import * as storage from "@app/util/storage"
|
import * as storage from "@app/util/storage"
|
||||||
import NewNotificationSound from "@src/app/components/NewNotificationSound.svelte"
|
import NewNotificationSound from "@src/app/components/NewNotificationSound.svelte"
|
||||||
@@ -96,22 +96,28 @@
|
|||||||
sync({
|
sync({
|
||||||
key: "pubkey",
|
key: "pubkey",
|
||||||
store: pubkey,
|
store: pubkey,
|
||||||
storage: preferencesStorageProvider,
|
storage: kv,
|
||||||
}),
|
}),
|
||||||
sync({
|
sync({
|
||||||
key: "sessions",
|
key: "sessions",
|
||||||
store: sessions,
|
store: sessions,
|
||||||
storage: preferencesStorageProvider,
|
storage: kv,
|
||||||
}),
|
}),
|
||||||
sync({
|
sync({
|
||||||
key: "shouldUnwrap",
|
key: "shouldUnwrap",
|
||||||
store: shouldUnwrap,
|
store: shouldUnwrap,
|
||||||
storage: preferencesStorageProvider,
|
storage: kv,
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
// Wait until data storage is initialized before syncing other stuff
|
// Wait until data storage is initialized before syncing other stuff
|
||||||
unsubscribers.push(await storage.syncDataStores())
|
await db.init(storage.adapters)
|
||||||
|
|
||||||
|
// Close DB and restart when we're done
|
||||||
|
unsubscribers.push(() => {
|
||||||
|
db.close()
|
||||||
|
db.reset()
|
||||||
|
})
|
||||||
|
|
||||||
// Add our extra policies now that we're set up
|
// Add our extra policies now that we're set up
|
||||||
defaultSocketPolicies.push(...policies)
|
defaultSocketPolicies.push(...policies)
|
||||||
|
|||||||
Reference in New Issue
Block a user