aboutsummaryrefslogtreecommitdiff
path: root/extension
diff options
context:
space:
mode:
authorLibravatar vnugent <public@vaughnnugent.com>2023-11-22 15:07:08 -0500
committerLibravatar vnugent <public@vaughnnugent.com>2023-11-22 15:07:08 -0500
commite272adcc3f32e31fe7668551453b8e34bc823c3e (patch)
tree680c695184ddbc27227578afa9f169d98a69f55a /extension
parent2ba94602a87c87b47f566745bdab40ce75e0e879 (diff)
feature and internal api polish
Diffstat (limited to 'extension')
-rw-r--r--extension/src/entries/contentScript/nostr-shim.js123
-rw-r--r--extension/src/entries/contentScript/primary/components/PromptPopup.vue10
-rw-r--r--extension/src/entries/contentScript/primary/main.js10
-rw-r--r--extension/src/entries/contentScript/util.ts138
-rw-r--r--extension/src/entries/nostr-provider.js6
-rw-r--r--extension/src/entries/options/components/Identities.vue9
-rw-r--r--extension/src/entries/options/components/SiteSettings.vue5
-rw-r--r--extension/src/entries/store/allowedOrigins.ts10
-rw-r--r--extension/src/entries/store/features.ts4
-rw-r--r--extension/src/entries/store/identity.ts4
-rw-r--r--extension/src/features/account-api.ts48
-rw-r--r--extension/src/features/auth-api.ts11
-rw-r--r--extension/src/features/identity-api.ts29
-rw-r--r--extension/src/features/index.ts3
-rw-r--r--extension/src/features/nip07allow-api.ts59
-rw-r--r--extension/src/features/nostr-api.ts6
-rw-r--r--extension/src/features/server-api/index.ts13
-rw-r--r--extension/src/features/settings.ts96
-rw-r--r--extension/src/features/tagfilter-api.ts50
-rw-r--r--extension/src/features/types.ts70
-rw-r--r--extension/src/features/util.ts83
21 files changed, 383 insertions, 404 deletions
diff --git a/extension/src/entries/contentScript/nostr-shim.js b/extension/src/entries/contentScript/nostr-shim.js
deleted file mode 100644
index 418b9c1..0000000
--- a/extension/src/entries/contentScript/nostr-shim.js
+++ /dev/null
@@ -1,123 +0,0 @@
-// Copyright (C) 2023 Vaughn Nugent
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero General Public License as
-// published by the Free Software Foundation, either version 3 of the
-// License, or (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-import { runtime } from "webextension-polyfill"
-import { isEqual, isNil, isEmpty } from 'lodash'
-import { apiCall } from '@vnuge/vnlib.browser'
-import { useScriptTag, watchOnce } from "@vueuse/core"
-import { useStore } from '../store'
-import { storeToRefs } from 'pinia'
-
-const _promptHandler = (() => {
- let _handler = undefined;
- return{
- invoke: (event) => _handler(event),
- set: (handler) => _handler = handler
- }
-})()
-
-export const usePrompt = (callback) => _promptHandler.set(callback);
-
-
-export const onLoad = async () =>{
-
- const store = useStore()
- const { nostr } = store.plugins
- const { isTabAllowed, selectedKey } = storeToRefs(store)
-
- const injectHandler = () => {
-
- //Setup listener for the content script to process nostr messages
- const ext = '@vnuge/nvault-extension'
-
- const scriptUrl = runtime.getURL('src/entries/nostr-provider.js')
-
- //setup script tag
- useScriptTag(scriptUrl, undefined, { manual: false, defer: true })
-
- //Only listen for messages if injection is enabled
- window.addEventListener('message', async ({ source, data, origin }) => {
-
- const invokePrompt = async (cb) => {
- //await propmt for user to allow the request
- const allow = await _promptHandler.invoke({ ...data, origin })
- //send request to background
- return response = allow ? await cb() : { error: 'User denied permission' }
- }
-
- //Confirm the message format is correct
- if (!isEqual(source, window) || isEmpty(data) || isNil(data.type)) {
- return
- }
- //Confirm extension is for us
- if (!isEqual(data.ext, ext)) {
- return
- }
-
- //clean any junk/methods with json parse/stringify
- data = JSON.parse(JSON.stringify(data))
-
- // pass on to background
- var response;
- await apiCall(async () => {
- switch (data.type) {
- case 'getPublicKey':
- return invokePrompt(async () => selectedKey.value.PublicKey)
- case 'signEvent':
- return invokePrompt(async () => {
- const event = data.payload.event
-
- //Set key id to selected key
- event.KeyId = selectedKey.value.Id
- event.pubkey = selectedKey.value.PublicKey;
-
- return await nostr.signEvent(event);
- })
- //Check the public key against selected key
- case 'getRelays':
- return invokePrompt(async () => await nostr.getRelays())
- case 'nip04.encrypt':
- return invokePrompt(async () => await nostr.nip04Encrypt({
- pubkey: data.payload.peer,
- content: data.payload.plaintext,
- //Set selected key id as our desired decryption key
- KeyId: selectedKey.value.Id
- }))
- case 'nip04.decrypt':
- return invokePrompt(async () => await nostr.nip04Decrypt({
- pubkey: data.payload.peer,
- content: data.payload.ciphertext,
- //Set selected key id as our desired decryption key
- KeyId: selectedKey.value.Id
- }))
- default:
- throw new Error('Unknown nostr message type')
- }
- })
- // return response message, must have the same id as the request
- window.postMessage({ ext, id: data.id, response }, origin);
- });
- }
-
- //Make sure the origin is allowed
- if (store.isTabAllowed === false){
- //If not allowed yet, wait for the store to update
- watchOnce(isTabAllowed, val => val ? injectHandler() : undefined);
- }
- else{
- injectHandler();
- }
-
-} \ No newline at end of file
diff --git a/extension/src/entries/contentScript/primary/components/PromptPopup.vue b/extension/src/entries/contentScript/primary/components/PromptPopup.vue
index b8b7cab..381f7b3 100644
--- a/extension/src/entries/contentScript/primary/components/PromptPopup.vue
+++ b/extension/src/entries/contentScript/primary/components/PromptPopup.vue
@@ -75,7 +75,7 @@
<script setup lang="ts">
import { ref } from 'vue'
-import { usePrompt } from '../../nostr-shim.js'
+import { usePrompt, type UserPermissionRequest } from '../../util'
import { computed } from 'vue';
import { Popover, PopoverButton, PopoverPanel } from '@headlessui/vue'
import { clone, first } from 'lodash';
@@ -88,11 +88,7 @@ const keyName = computed(() => selectedKey.value?.UserName)
const prompt = ref(null)
-interface PopupEvent{
- type: string
- msg: string
- origin: string
- data: any
+interface PopupEvent extends UserPermissionRequest {
allow: () => void
close: () => void
}
@@ -117,7 +113,7 @@ const allow = () => {
}
//Listen for events
-usePrompt(async (ev: PopupEvent) => {
+usePrompt((ev: UserPermissionRequest):Promise<boolean> => {
ev = clone(ev)
diff --git a/extension/src/entries/contentScript/primary/main.js b/extension/src/entries/contentScript/primary/main.js
index e73923d..dbfa07b 100644
--- a/extension/src/entries/contentScript/primary/main.js
+++ b/extension/src/entries/contentScript/primary/main.js
@@ -13,7 +13,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
+import { runtime } from "webextension-polyfill";
import { createApp } from "vue";
import { createPinia } from 'pinia';
import { useBackgroundPiniaPlugin, identityPlugin, originPlugin } from '../../store'
@@ -25,7 +25,7 @@ import '@fontsource/noto-sans-masaram-gondi'
//We need inline styles to inject into the shadow dom
import tw from "~/assets/all.scss?inline";
import localStyle from './style.scss?inline'
-import { onLoad } from "../nostr-shim";
+import { onLoad } from "../util";
import { defer } from "lodash";
/* FONT AWESOME CONFIG */
@@ -35,6 +35,10 @@ import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
library.add(faCircleInfo)
+//The extension name, same as nostr-provider script path
+const ext = '@vnuge/nvault-extension'
+const scriptUrl = runtime.getURL('src/entries/nostr-provider.js')
+
renderContent([], (appRoot, shadowRoot) => {
//Create the background feature wiring
@@ -62,5 +66,5 @@ renderContent([], (appRoot, shadowRoot) => {
.mount(appRoot);
//Load the nostr shim
- defer(onLoad)
+ defer(() => onLoad(ext, scriptUrl))
}); \ No newline at end of file
diff --git a/extension/src/entries/contentScript/util.ts b/extension/src/entries/contentScript/util.ts
new file mode 100644
index 0000000..aa6aac3
--- /dev/null
+++ b/extension/src/entries/contentScript/util.ts
@@ -0,0 +1,138 @@
+// Copyright (C) 2023 Vaughn Nugent
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+import { isEqual, isNil, isEmpty } from 'lodash'
+import { apiCall } from '@vnuge/vnlib.browser'
+import { Store, storeToRefs } from 'pinia'
+import { useScriptTag, watchOnce } from "@vueuse/core"
+import { useStore } from '../store'
+
+export type PromptHandler = (request: UserPermissionRequest) => Promise<boolean>
+
+export interface UserPermissionRequest {
+ type: string
+ msg: string
+ origin: string
+ data: any
+}
+
+const _promptHandler = (() => {
+ let _handler: PromptHandler | undefined = undefined;
+ return {
+ invoke(event:UserPermissionRequest){
+ if (!_handler) {
+ throw new Error('No prompt handler set')
+ }
+ return _handler(event)
+ },
+ set(handler: PromptHandler) {
+ _handler = handler
+ }
+ }
+})()
+
+
+const registerWindowHandler = (store: Store, extName: string) => {
+
+ const { selectedKey } = storeToRefs(store)
+ const { nostr } = store.plugins;
+
+ //Only listen for messages if injection is enabled
+ window.addEventListener('message', async ({ source, data, origin }) => {
+
+ //clean any junk/methods with json parse/stringify
+ data = JSON.parse(JSON.stringify(data))
+
+ const invokePrompt = async (cb:(...args:any) => Promise<any>) => {
+ //await propmt for user to allow the request
+ const allow = await _promptHandler.invoke({ ...data, origin })
+ //send request to background
+ return response = allow ? await cb() : { error: 'User denied permission' }
+ }
+
+ //Confirm the message format is correct
+ if (!isEqual(source, window) || isEmpty(data) || isNil(data.type)) {
+ return
+ }
+ //Confirm extension is for us
+ if (!isEqual(data.ext, extName)) {
+ return
+ }
+
+ // pass on to background
+ var response;
+ await apiCall(async () => {
+ switch (data.type) {
+ case 'getPublicKey':
+ return invokePrompt(async () => selectedKey.value?.PublicKey)
+ case 'signEvent':
+ return invokePrompt(async () => {
+ const event = data.payload.event
+
+ //Set key id to selected key
+ event.KeyId = selectedKey.value!.Id
+ event.pubkey = selectedKey.value!.PublicKey;
+
+ return await nostr.signEvent(event);
+ })
+ //Check the public key against selected key
+ case 'getRelays':
+ return invokePrompt(async () => await nostr.getRelays())
+ case 'nip04.encrypt':
+ return invokePrompt(async () => await nostr.nip04Encrypt({
+ pubkey: data.payload.peer,
+ content: data.payload.plaintext,
+ //Set selected key id as our desired decryption key
+ KeyId: selectedKey.value!.Id
+ }))
+ case 'nip04.decrypt':
+ return invokePrompt(async () => await nostr.nip04Decrypt({
+ pubkey: data.payload.peer,
+ content: data.payload.ciphertext,
+ //Set selected key id as our desired decryption key
+ KeyId: selectedKey.value!.Id
+ }))
+ default:
+ throw new Error('Unknown nostr message type')
+ }
+ })
+ // return response message, must have the same id as the request
+ window.postMessage({ ext: extName, id: data.id, response }, origin);
+ });
+}
+
+export const usePrompt = (callback: PromptHandler) => _promptHandler.set(callback);
+
+export const onLoad = async (extName: string, scriptUrl: string) => {
+
+ const store = useStore()
+ const { isTabAllowed } = storeToRefs(store)
+
+ const injectHandler = () => {
+ //inject the nostr provider script into the page
+ useScriptTag(scriptUrl, undefined, { manual: false, defer: true })
+ //Regsiter listener for messages from the injected script
+ registerWindowHandler(store, extName)
+ }
+
+ //Make sure the origin is allowed
+ if (isTabAllowed.value === false) {
+ //If not allowed yet, wait for the store to update
+ watchOnce(isTabAllowed, val => val ? injectHandler() : undefined);
+ }
+ else {
+ injectHandler();
+ }
+} \ No newline at end of file
diff --git a/extension/src/entries/nostr-provider.js b/extension/src/entries/nostr-provider.js
index 9fa3bb7..7d8f3c5 100644
--- a/extension/src/entries/nostr-provider.js
+++ b/extension/src/entries/nostr-provider.js
@@ -74,9 +74,9 @@ window.nostr = {
} ,
async signEvent(event){
- const { event:ev } = await sendMessage('signEvent', { event })
- debugLog("Signed event", ev);
- return ev
+ const signed = await sendMessage('signEvent', { event })
+ debugLog("Signed event", signed);
+ return signed
},
getRelays(){
diff --git a/extension/src/entries/options/components/Identities.vue b/extension/src/entries/options/components/Identities.vue
index e3af74e..0e3e79d 100644
--- a/extension/src/entries/options/components/Identities.vue
+++ b/extension/src/entries/options/components/Identities.vue
@@ -158,11 +158,10 @@ const onNip05Download = () => {
//create file blob
const blob = new Blob([JSON.stringify({ names:nip05 })], { type: 'application/json' })
- //Download the file
- downloadAnchor.value!.href = URL.createObjectURL(blob);
- downloadAnchor.value?.setAttribute('download', 'nostr.json')
- downloadAnchor.value?.click();
-
+ const anchor = get(downloadAnchor);
+ anchor!.href = URL.createObjectURL(blob);
+ anchor!.setAttribute('download', 'nip05.json')
+ anchor!.click();
})
}
diff --git a/extension/src/entries/options/components/SiteSettings.vue b/extension/src/entries/options/components/SiteSettings.vue
index c0c2e93..b31bb9c 100644
--- a/extension/src/entries/options/components/SiteSettings.vue
+++ b/extension/src/entries/options/components/SiteSettings.vue
@@ -7,7 +7,7 @@
Extension settings
</h3>
<div class="my-6">
- <fieldset :disabled="waiting.value">
+ <fieldset :disabled="waiting">
<div class="">
<div class="w-full">
<div class="flex flex-row w-full">
@@ -139,7 +139,7 @@ const { apply, data, buffer, modified, update } = useDataBuffer(settings.value,
})
//Watch for store settings changes and apply them
-watch(settings, v => apply(v.value))
+watch(settings, v => apply(v))
const originProtection = computed({
get: () => isOriginProtectionOn.value,
@@ -196,7 +196,6 @@ const onSave = async () => {
toggleEdit();
}
-
const testConnection = async () =>{
return await apiCall(async ({axios, toaster}) =>{
try{
diff --git a/extension/src/entries/store/allowedOrigins.ts b/extension/src/entries/store/allowedOrigins.ts
index d6de42d..661dd43 100644
--- a/extension/src/entries/store/allowedOrigins.ts
+++ b/extension/src/entries/store/allowedOrigins.ts
@@ -3,15 +3,15 @@ import 'pinia'
import { } from 'lodash'
import { PiniaPluginContext } from 'pinia'
import { computed, shallowRef } from 'vue';
-import { onWatchableChange } from '../../features/types';
+import { onWatchableChange } from '../../features';
import { type AllowedOriginStatus } from '../../features/nip07allow-api';
declare module 'pinia' {
export interface PiniaCustomProperties {
- isTabAllowed: boolean;
- currentOrigin: string | undefined;
- allowedOrigins: Array<string>;
- isOriginProtectionOn: boolean;
+ readonly isTabAllowed: boolean;
+ readonly currentOrigin: string | undefined;
+ readonly allowedOrigins: Array<string>;
+ readonly isOriginProtectionOn: boolean;
allowOrigin(origin?:string): Promise<void>;
dissallowOrigin(origin?:string): Promise<void>;
disableOriginProtection(): Promise<void>;
diff --git a/extension/src/entries/store/features.ts b/extension/src/entries/store/features.ts
index 219386f..c619b0e 100644
--- a/extension/src/entries/store/features.ts
+++ b/extension/src/entries/store/features.ts
@@ -13,10 +13,10 @@ import {
useSettingsApi,
useForegoundFeatures,
useEventTagFilterApi,
- useInjectAllowList
+ useInjectAllowList,
+ onWatchableChange
} from "../../features"
-import { onWatchableChange } from '../../features/types'
import { ChannelContext } from '../../messaging'
export type BgPlugins = ReturnType<typeof usePlugins>
diff --git a/extension/src/entries/store/identity.ts b/extension/src/entries/store/identity.ts
index 320263f..b1635f2 100644
--- a/extension/src/entries/store/identity.ts
+++ b/extension/src/entries/store/identity.ts
@@ -2,9 +2,8 @@
import 'pinia'
import { } from 'lodash'
import { PiniaPluginContext } from 'pinia'
-import { NostrPubKey } from '../../features'
+import { onWatchableChange, type NostrPubKey } from '../../features'
import { shallowRef } from 'vue';
-import { onWatchableChange } from '../../features/types';
declare module 'pinia' {
export interface PiniaCustomStateProperties {
@@ -17,7 +16,6 @@ declare module 'pinia' {
}
}
-
export const identityPlugin = ({ store }: PiniaPluginContext) => {
const { identity } = store.plugins
diff --git a/extension/src/features/account-api.ts b/extension/src/features/account-api.ts
index 9c701c3..96948c4 100644
--- a/extension/src/features/account-api.ts
+++ b/extension/src/features/account-api.ts
@@ -13,16 +13,15 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
-import { useMfaConfig, usePkiConfig, PkiPublicKey, debugLog } from "@vnuge/vnlib.browser";
+import { useMfaConfig, usePkiConfig, type PkiPublicKey } from "@vnuge/vnlib.browser";
import { ArrayToHexString, Base64ToUint8Array } from "@vnuge/vnlib.browser/dist/binhelpers";
import { JsonObject } from "type-fest";
-import { useSingleSlotStorage } from "./types";
import { computed, watch } from "vue";
-import { storage } from "webextension-polyfill";
import { JWK, SignJWT, importJWK } from "jose";
-import { cloneDeep } from "lodash";
+import { clone } from "lodash";
import { FeatureApi, BgRuntime, IFeatureExport, exportForegroundApi, optionsOnly, popupAndOptionsOnly } from "./framework";
import { AppSettings } from "./settings";
+import { set, toRefs } from "@vueuse/core";
export interface EcKeyParams extends JsonObject {
@@ -83,7 +82,7 @@ export const usePkiApi = (): IFeatureExport<AppSettings, PkiApi> => {
interface PkiSettings {
userName: string,
- privateKey?:JWK
+ privateKey:JWK | undefined
}
export interface LocalPkiApi extends FeatureApi {
@@ -97,17 +96,17 @@ export const useLocalPki = (): IFeatureExport<AppSettings, LocalPkiApi> => {
return{
//Setup registration
background: ({ state } : BgRuntime<AppSettings>) =>{
- const { get, set } = useSingleSlotStorage<PkiSettings>(storage.local, 'pki-settings')
+ const store = state.useStorageSlot<PkiSettings>('pki-settings', { userName: '', privateKey: undefined })
+ const { userName, privateKey } = toRefs(store)
const getPubKey = async (): Promise<PkiPubKey | undefined> => {
- const setting = await get()
- if (!setting?.privateKey) {
+ if (!privateKey.value) {
return undefined
}
//Clone the private key, remove the private parts
- const c = cloneDeep(setting.privateKey)
+ const c = clone(privateKey.value)
delete c.d
delete c.p
@@ -118,12 +117,12 @@ export const useLocalPki = (): IFeatureExport<AppSettings, LocalPkiApi> => {
return {
...c,
- userName: setting.userName
+ userName: userName.value
} as PkiPubKey
}
return{
- regenerateKey: optionsOnly(async (userName:string, params:EcKeyParams) => {
+ regenerateKey: optionsOnly(async (uname:string, params:EcKeyParams) => {
const p = {
...params,
name: "ECDSA",
@@ -133,46 +132,47 @@ export const useLocalPki = (): IFeatureExport<AppSettings, LocalPkiApi> => {
const key = await window.crypto.subtle.generateKey(p, true, ['sign', 'verify'])
//Convert to jwk
- const privateKey = await window.crypto.subtle.exportKey('jwk', key.privateKey) as JWK;
+ const newKey = await window.crypto.subtle.exportKey('jwk', key.privateKey) as JWK;
//Convert to base64 so we can hash it easier
- const b = btoa(privateKey.x! + privateKey.y!);
+ const b = btoa(newKey.x! + newKey.y!);
//take sha256 of the binary version of the coords
const digest = await crypto.subtle.digest('SHA-256', Base64ToUint8Array(b));
//Set the kid
- privateKey.kid = ArrayToHexString(digest);
+ newKey.kid = ArrayToHexString(digest);
//Serial number is random hex
const serial = new Uint8Array(32)
crypto.getRandomValues(serial)
- privateKey.serial = ArrayToHexString(serial);
+ newKey.serial = ArrayToHexString(serial);
- //Save the key
- await set({ userName, privateKey })
+ //Set the username
+ set(userName, uname)
+ set(privateKey, newKey)
}),
getPubKey: optionsOnly(getPubKey),
generateOtp: optionsOnly(async () =>{
- const setting = await get()
- if (!setting?.privateKey) {
+
+ if (!privateKey.value) {
throw new Error('No key found')
}
- const privKey = await importJWK(setting.privateKey as JWK)
+ const privKey = await importJWK(privateKey.value as JWK)
const random = new Uint8Array(32)
crypto.getRandomValues(random)
const jwt = new SignJWT({
- 'sub': setting.userName,
+ 'sub': userName.value,
'n': ArrayToHexString(random),
- keyid: setting.privateKey.kid,
- serial: privKey.serial
+ keyid: privateKey.value.kid,
+ serial: (privKey as any).serial
});
const token = await jwt.setIssuedAt()
- .setProtectedHeader({ alg: setting.privateKey.alg! })
+ .setProtectedHeader({ alg: privateKey.value.alg! })
.setIssuer(state.currentConfig.value.apiUrl)
.setExpirationTime('30s')
.sign(privKey)
diff --git a/extension/src/features/auth-api.ts b/extension/src/features/auth-api.ts
index 81bf6ea..e3f6c21 100644
--- a/extension/src/features/auth-api.ts
+++ b/extension/src/features/auth-api.ts
@@ -14,13 +14,14 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import { AxiosInstance } from "axios";
-import { get, watchOnce } from "@vueuse/core";
+import { get } from "@vueuse/core";
import { computed } from "vue";
import { usePkiAuth, useSession, useUser } from "@vnuge/vnlib.browser";
-import { type FeatureApi, type BgRuntime, type IFeatureExport, exportForegroundApi, popupAndOptionsOnly, popupOnly } from "./framework";
import type { ClientStatus } from "./types";
import type { AppSettings } from "./settings";
import type { JsonObject } from "type-fest";
+import { type FeatureApi, type BgRuntime, type IFeatureExport, exportForegroundApi, popupAndOptionsOnly, popupOnly } from "./framework";
+import { waitForChangeFn } from "./util";
export interface ProectedHandler<T extends JsonObject> {
(message: T): Promise<any>
@@ -77,14 +78,13 @@ export const useAuthApi = (): IFeatureExport<AppSettings, UserApi> => {
onInstalled(() => {
//Configure interval to run every 5 minutes to update the status
setInterval(runHeartbeat, 60 * 1000);
-
//Run immediately
runHeartbeat();
-
return Promise.resolve();
})
return {
+ waitForChange: waitForChangeFn([currentConfig, loggedIn, userName]),
login: popupOnly(async (token: string): Promise<boolean> => {
//Perform login
await login(token)
@@ -107,9 +107,6 @@ export const useAuthApi = (): IFeatureExport<AppSettings, UserApi> => {
userName: get(userName),
} as ClientStatus
},
- async waitForChange(){
- return new Promise((resolve) => watchOnce([currentConfig, loggedIn] as any, () => resolve()))
- }
}
},
foreground: exportForegroundApi<UserApi>([
diff --git a/extension/src/features/identity-api.ts b/extension/src/features/identity-api.ts
index d7db5ff..a8ac4e6 100644
--- a/extension/src/features/identity-api.ts
+++ b/extension/src/features/identity-api.ts
@@ -13,7 +13,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
-import { Endpoints, useServerApi } from "./server-api";
+import { Endpoints } from "./server-api";
import { NostrPubKey, Watchable } from "./types";
import {
type FeatureApi,
@@ -26,8 +26,9 @@ import {
import { AppSettings } from "./settings";
import { shallowRef, watch } from "vue";
import { useSession } from "@vnuge/vnlib.browser";
-import { set, useToggle, watchOnce } from "@vueuse/core";
+import { set, useToggle } from "@vueuse/core";
import { defer, isArray } from "lodash";
+import { waitForChange, waitForChangeFn } from "./util";
export interface IdentityApi extends FeatureApi, Watchable {
createIdentity: (identity: NostrPubKey) => Promise<NostrPubKey>
@@ -41,13 +42,13 @@ export interface IdentityApi extends FeatureApi, Watchable {
export const useIdentityApi = (): IFeatureExport<AppSettings, IdentityApi> => {
return{
background: ({ state }: BgRuntime<AppSettings>) =>{
- const { execRequest } = useServerApi(state);
+ const { execRequest } = state.useServerApi();
const { loggedIn } = useSession();
//Get the current selected key
const selectedKey = shallowRef<NostrPubKey | undefined>();
const allKeys = shallowRef<NostrPubKey[]>([]);
- const [ onTriggered , triggerChange ] = useToggle()
+ const [ onKeyUpdateTriggered , triggerKeyUpdate ] = useToggle()
const keyLoadWatchLoop = async () => {
while(true){
@@ -62,7 +63,7 @@ export const useIdentityApi = (): IFeatureExport<AppSettings, IdentityApi> => {
}
//Wait for changes to trigger a new key-load
- await new Promise((resolve) => watchOnce([onTriggered, loggedIn] as any, () => resolve(null)))
+ await waitForChange([loggedIn, onKeyUpdateTriggered])
}
}
@@ -74,16 +75,18 @@ export const useIdentityApi = (): IFeatureExport<AppSettings, IdentityApi> => {
return {
//Identity is only available in options context
createIdentity: optionsOnly(async (id: NostrPubKey) => {
- await execRequest(Endpoints.CreateId, id)
- triggerChange()
+ const newKey = await execRequest(Endpoints.CreateId, id)
+ triggerKeyUpdate()
+ return newKey
}),
updateIdentity: optionsOnly(async (id: NostrPubKey) => {
- await execRequest(Endpoints.UpdateId, id)
- triggerChange()
+ const updated = await execRequest(Endpoints.UpdateId, id)
+ triggerKeyUpdate()
+ return updated
}),
deleteIdentity: optionsOnly(async (key: NostrPubKey) => {
await execRequest(Endpoints.DeleteKey, key);
- triggerChange()
+ triggerKeyUpdate()
}),
selectKey: popupAndOptionsOnly((key: NostrPubKey): Promise<void> => {
set(selectedKey, key);
@@ -95,10 +98,8 @@ export const useIdentityApi = (): IFeatureExport<AppSettings, IdentityApi> => {
getPublicKey: (): Promise<NostrPubKey | undefined> => {
return Promise.resolve(selectedKey.value);
},
- waitForChange: () => {
- return new Promise((resolve) => watchOnce([selectedKey, loggedIn, onTriggered] as any, () => resolve()))
- }
- }
+ waitForChange: waitForChangeFn([selectedKey, loggedIn, allKeys])
+ }
},
foreground: exportForegroundApi([
'createIdentity',
diff --git a/extension/src/features/index.ts b/extension/src/features/index.ts
index 320ea1c..c146cef 100644
--- a/extension/src/features/index.ts
+++ b/extension/src/features/index.ts
@@ -30,4 +30,5 @@ export { useNostrApi } from './nostr-api'
export { useSettingsApi, useAppSettings } from './settings'
export { useHistoryApi } from './history'
export { useEventTagFilterApi } from './tagfilter-api'
-export { useInjectAllowList } from './nip07allow-api' \ No newline at end of file
+export { useInjectAllowList } from './nip07allow-api'
+export { onWatchableChange } from './util' \ No newline at end of file
diff --git a/extension/src/features/nip07allow-api.ts b/extension/src/features/nip07allow-api.ts
index 0612b66..8c08d5e 100644
--- a/extension/src/features/nip07allow-api.ts
+++ b/extension/src/features/nip07allow-api.ts
@@ -13,13 +13,14 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
-import { storage, tabs, type Tabs } from "webextension-polyfill";
-import { Watchable, useSingleSlotStorage } from "./types";
+import { tabs, type Tabs } from "webextension-polyfill";
+import { Watchable } from "./types";
import { defaultTo, filter, includes, isEqual } from "lodash";
import { BgRuntime, FeatureApi, IFeatureExport, exportForegroundApi, popupAndOptionsOnly } from "./framework";
import { AppSettings } from "./settings";
-import { set, get, watchOnce, useToggle } from "@vueuse/core";
+import { set, get, toRefs } from "@vueuse/core";
import { computed, shallowRef } from "vue";
+import { waitForChangeFn } from "./util";
interface AllowedSites{
origins: string[];
@@ -41,14 +42,10 @@ export interface InjectAllowlistApi extends FeatureApi, Watchable {
export const useInjectAllowList = (): IFeatureExport<AppSettings, InjectAllowlistApi> => {
return {
- background: ({ }: BgRuntime<AppSettings>) => {
+ background: ({ state }: BgRuntime<AppSettings>) => {
- const store = useSingleSlotStorage<AllowedSites>(storage.local, 'nip07-allowlist', { origins: [], enabled: true });
-
- //watch current tab
- const allowedOrigins = shallowRef<string[]>([])
- const protectionEnabled = shallowRef<boolean>(true)
- const [manullyTriggered, trigger] = useToggle()
+ const store = state.useStorageSlot<AllowedSites>('nip07-allowlist', { origins: [], enabled: true });
+ const { origins, enabled } = toRefs(store)
const { currentOrigin, currentTab } = (() => {
@@ -72,19 +69,9 @@ export const useInjectAllowList = (): IFeatureExport<AppSettings, InjectAllowlis
return { currentTab, currentOrigin }
})()
- const writeChanges = async () => {
- await store.set({ origins: get(allowedOrigins), enabled: get(protectionEnabled) })
- }
-
- //Initial load
- store.get().then((data) => {
- allowedOrigins.value = data.origins
- protectionEnabled.value = data.enabled
- })
-
const isOriginAllowed = (origin?: string): boolean => {
//If protection is not enabled, allow all
- if(protectionEnabled.value == false){
+ if(enabled.value == false){
return true;
}
//if no origin specified, use current origin
@@ -97,7 +84,7 @@ export const useInjectAllowList = (): IFeatureExport<AppSettings, InjectAllowlis
//Default to origin only
const originOnly = new URL(origin).origin
- return includes(allowedOrigins.value, originOnly)
+ return includes(origins.value, originOnly)
}
const addOrigin = async (origin?: string): Promise<void> => {
@@ -110,13 +97,9 @@ export const useInjectAllowList = (): IFeatureExport<AppSettings, InjectAllowlis
const originOnly = new URL(newOrigin).origin
//See if origin is already in the list
- if (!includes(allowedOrigins.value, originOnly)) {
+ if (!includes(origins.value, originOnly)) {
//Add to the list
- allowedOrigins.value.push(originOnly);
- trigger();
-
- //Save changes
- await writeChanges()
+ origins.value.push(originOnly);
//If current tab was added, reload the tab
if (!origin) {
@@ -134,40 +117,32 @@ export const useInjectAllowList = (): IFeatureExport<AppSettings, InjectAllowlis
//Get origin part of url
const delOriginOnly = new URL(delOrigin).origin
- const allowList = get(allowedOrigins)
+ const allowList = get(origins)
//Remove the origin
- allowedOrigins.value = filter(allowList, (o) => !isEqual(o, delOriginOnly));
- trigger();
-
- await writeChanges()
+ origins.value = filter(allowList, (o) => !isEqual(o, delOriginOnly));
//If current tab was removed, reload the tab
if (!origin) {
await tabs.reload(currentTab.value?.id)
}
}
-
return {
+ waitForChange: waitForChangeFn([currentTab, enabled, origins]),
addOrigin: popupAndOptionsOnly(addOrigin),
removeOrigin: popupAndOptionsOnly(removeOrigin),
enable: popupAndOptionsOnly(async (value: boolean): Promise<void> => {
- set(protectionEnabled, value)
- await writeChanges()
+ set(enabled, value)
}),
async getStatus(): Promise<AllowedOriginStatus> {
return{
- allowedOrigins: get(allowedOrigins),
- enabled: get(protectionEnabled),
+ allowedOrigins: get(origins),
+ enabled: get(enabled),
currentOrigin: get(currentOrigin),
isAllowed: isOriginAllowed()
}
},
- async waitForChange() {
- //Wait for the trigger to change
- await new Promise((resolve) => watchOnce([currentTab, protectionEnabled, manullyTriggered] as any, () => resolve(null)));
- },
}
},
foreground: exportForegroundApi([
diff --git a/extension/src/features/nostr-api.ts b/extension/src/features/nostr-api.ts
index 743f8f1..81ef0c6 100644
--- a/extension/src/features/nostr-api.ts
+++ b/extension/src/features/nostr-api.ts
@@ -13,7 +13,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
-import { Endpoints, useServerApi } from "./server-api";
+import { Endpoints } from "./server-api";
import { type FeatureApi, type BgRuntime, type IFeatureExport, optionsOnly, exportForegroundApi } from "./framework";
import { type AppSettings } from "./settings";
import { useTagFilter } from "./tagfilter-api";
@@ -38,8 +38,8 @@ export const useNostrApi = (): IFeatureExport<AppSettings, NostrApi> => {
return{
background: ({ state }: BgRuntime<AppSettings>) =>{
- const { execRequest } = useServerApi(state);
- const { filterTags } = useTagFilter()
+ const { execRequest } = state.useServerApi();
+ const { filterTags } = useTagFilter(state)
return {
getRelays: async (): Promise<NostrRelay[]> => {
diff --git a/extension/src/features/server-api/index.ts b/extension/src/features/server-api/index.ts
index 6637aaa..fd8e65e 100644
--- a/extension/src/features/server-api/index.ts
+++ b/extension/src/features/server-api/index.ts
@@ -14,12 +14,11 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
-import { computed } from "vue"
+import { Ref } from "vue"
import { get } from '@vueuse/core'
import { type WebMessage, type UserProfile } from "@vnuge/vnlib.browser"
import { initEndponts } from "./endpoints"
import { cloneDeep } from "lodash"
-import { type AppSettings } from "../settings"
import type { EncryptionRequest, NostrEvent, NostrPubKey, NostrRelay } from "../types"
export enum Endpoints {
@@ -48,12 +47,12 @@ export interface ExecRequestHandler{
(id: Endpoints.UpdateProfile, profile: UserProfile):Promise<string>
}
-export const useServerApi = (settings: AppSettings): { execRequest: ExecRequestHandler } => {
- const { registerEndpoint, execRequest } = initEndponts()
+export interface ServerApi{
+ execRequest: ExecRequestHandler
+}
- //ref to nostr endpoint url
- const nostrUrl = computed(() => settings.currentConfig.value.nostrEndpoint || '/nostr');
- const accUrl = computed(() => settings.currentConfig.value.accountBasePath || '/account');
+export const useServerApi = (nostrUrl: Ref<string>, accUrl: Ref<string>): ServerApi => {
+ const { registerEndpoint, execRequest } = initEndponts()
registerEndpoint({
id: Endpoints.GetKeys,
diff --git a/extension/src/features/settings.ts b/extension/src/features/settings.ts
index 059c2d2..9a3c32d 100644
--- a/extension/src/features/settings.ts
+++ b/extension/src/features/settings.ts
@@ -14,13 +14,15 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import { storage } from "webextension-polyfill"
-import { isEmpty, merge } from 'lodash'
+import { } from 'lodash'
import { configureApi, debugLog } from '@vnuge/vnlib.browser'
-import { readonly, ref, Ref } from "vue";
+import { MaybeRefOrGetter, readonly, Ref, shallowRef, watch } from "vue";
import { JsonObject } from "type-fest";
-import { Watchable, useSingleSlotStorage } from "./types";
+import { Watchable } from "./types";
import { BgRuntime, FeatureApi, optionsOnly, IFeatureExport, exportForegroundApi, popupAndOptionsOnly } from './framework'
-import { get, watchOnce } from "@vueuse/core";
+import { get, set, toRefs } from "@vueuse/core";
+import { waitForChangeFn, useStorage } from "./util";
+import { ServerApi, useServerApi } from "./server-api";
export interface PluginConfig extends JsonObject {
readonly apiUrl: string;
@@ -42,9 +44,9 @@ const defaultConfig : PluginConfig = {
};
export interface AppSettings{
- getCurrentConfig: () => Promise<PluginConfig>;
- restoreApiSettings: () => Promise<void>;
- saveConfig: (config: PluginConfig) => Promise<void>;
+ saveConfig(config: PluginConfig): void;
+ useStorageSlot<T>(slot: string, defaultValue: MaybeRefOrGetter<T>): Ref<T>;
+ useServerApi(): ServerApi,
readonly currentConfig: Readonly<Ref<PluginConfig>>;
}
@@ -56,27 +58,11 @@ export interface SettingsApi extends FeatureApi, Watchable {
}
export const useAppSettings = (): AppSettings => {
- const currentConfig = ref<PluginConfig>({} as PluginConfig);
- const store = useSingleSlotStorage<PluginConfig>(storage.local, 'siteConfig', defaultConfig);
- const getCurrentConfig = async () => {
-
- const siteConfig = await store.get()
-
- //Store a default config if none exists
- if (isEmpty(siteConfig)) {
- await store.set(defaultConfig);
- }
-
- //Merge the default config with the site config
- return merge(defaultConfig, siteConfig)
- }
-
- const restoreApiSettings = async () => {
- //Set the current config
- const current = await getCurrentConfig();
- currentConfig.value = current;
+ const _storageBackend = storage.local;
+ const store = useStorage<PluginConfig>(_storageBackend, 'siteConfig', defaultConfig);
+ watch(store, (config, _) => {
//Configure the vnlib api
configureApi({
session: {
@@ -84,74 +70,58 @@ export const useAppSettings = (): AppSettings => {
browserIdSize: 32,
},
user: {
- accountBasePath: current.accountBasePath,
+ accountBasePath: config.accountBasePath,
},
axios: {
- baseURL: current.apiUrl,
+ baseURL: config.apiUrl,
tokenHeader: import.meta.env.VITE_WEB_TOKEN_HEADER,
},
storage: localStorage
})
- }
- const saveConfig = async (config: PluginConfig) => {
- //Save the config and update the current config
- await store.set(config);
- currentConfig.value = config;
- }
+ }, { deep: true })
+
+ //Save the config and update the current config
+ const saveConfig = (config: PluginConfig) => set(store, config);
+
+ //Reactive urls for server api
+ const { accountBasePath, nostrEndpoint } = toRefs(store)
+ const serverApi = useServerApi(nostrEndpoint, accountBasePath)
return {
- getCurrentConfig,
- restoreApiSettings,
saveConfig,
- currentConfig:readonly(currentConfig)
+ currentConfig: readonly(store),
+ useStorageSlot: <T>(slot: string, defaultValue: MaybeRefOrGetter<T>) => {
+ return useStorage<T>(_storageBackend, slot, defaultValue)
+ },
+ useServerApi: () => serverApi
}
}
export const useSettingsApi = () : IFeatureExport<AppSettings, SettingsApi> =>{
return{
- background: ({ state, onConnected, onInstalled }: BgRuntime<AppSettings>) => {
-
- const _darkMode = ref(false);
+ background: ({ state }: BgRuntime<AppSettings>) => {
- onInstalled(async () => {
- await state.restoreApiSettings();
- debugLog('Server settings restored from storage');
- })
-
- onConnected(async () => {
- //refresh the config on connect
- await state.restoreApiSettings();
- })
+ const _darkMode = shallowRef(false);
return {
-
- getSiteConfig: () => state.getCurrentConfig(),
-
+ waitForChange: waitForChangeFn([state.currentConfig, _darkMode]),
+ getSiteConfig: () => Promise.resolve(state.currentConfig.value),
setSiteConfig: optionsOnly(async (config: PluginConfig): Promise<PluginConfig> => {
//Save the config
- await state.saveConfig(config);
-
- //Restore the api settings
- await state.restoreApiSettings();
+ state.saveConfig(config);
debugLog('Config settings saved!');
//Return the config
- return state.currentConfig.value
+ return get(state.currentConfig)
}),
-
setDarkMode: popupAndOptionsOnly(async (darkMode: boolean) => {
- console.log('Setting dark mode to', darkMode, 'from', _darkMode.value)
_darkMode.value = darkMode
}),
getDarkMode: async () => get(_darkMode),
-
- waitForChange: () => {
- return new Promise((resolve) => watchOnce([state.currentConfig, _darkMode] as any, () => resolve()))
- },
}
},
foreground: exportForegroundApi([
diff --git a/extension/src/features/tagfilter-api.ts b/extension/src/features/tagfilter-api.ts
index 75f4b2a..f5f1b6c 100644
--- a/extension/src/features/tagfilter-api.ts
+++ b/extension/src/features/tagfilter-api.ts
@@ -13,37 +13,40 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
-import { storage } from "webextension-polyfill";
-import { NostrEvent, TaggedNostrEvent, useSingleSlotStorage } from "./types";
+import { TaggedNostrEvent, Watchable } from "./types";
import { filter, isEmpty, isEqual, isRegExp } from "lodash";
import { BgRuntime, FeatureApi, IFeatureExport, exportForegroundApi } from "./framework";
import { AppSettings } from "./settings";
+import { get, toRefs } from "@vueuse/core";
+import { waitForChangeFn } from "./util";
interface EventTagFilteStorage {
filters: string[];
+ enabled: boolean;
}
-export interface EventTagFilterApi extends FeatureApi {
+export interface EventTagFilterApi extends FeatureApi, Watchable {
filterTags(event: TaggedNostrEvent): Promise<void>;
addFilter(tag: string): Promise<void>;
removeFilter(tag: string): Promise<void>;
addFilters(tags: string[]): Promise<void>;
+ isEnabled(): Promise<boolean>;
+ enable(value:boolean): Promise<void>;
}
-export const useTagFilter = () => {
+export const useTagFilter = (settings: AppSettings): EventTagFilterApi => {
//use storage
- const { get, set } = useSingleSlotStorage<EventTagFilteStorage>(storage.local, 'tag-filter-struct', { filters: [] });
+ const store = settings.useStorageSlot<EventTagFilteStorage>('tag-filter-struct', { filters: [], enabled: false });
+ const { filters, enabled } = toRefs(store)
return {
+ waitForChange: waitForChangeFn([filters, enabled]),
filterTags: async (event: TaggedNostrEvent): Promise<void> => {
if(!event.tags){
return;
}
- //Load latest filter list
- const data = await get();
-
if(isEmpty(event.tags)){
return;
}
@@ -58,13 +61,13 @@ export const useTagFilter = () => {
return false;
}
- if(!data.filters.length){
+ if(!filters.value.length){
return true;
}
const asString = tagName.toString();
- for (const filter of data.filters) {
+ for (const filter of get(filters)) {
//if the filter is a regex, test it, if it fails, its allowed
if (isRegExp(filter)) {
if (filter.test(asString)) {
@@ -85,35 +88,32 @@ export const useTagFilter = () => {
event.tags = allowedTags;
},
addFilter: async (tag: string) => {
- const data = await get();
//add new filter to list
- data.filters.push(tag);
- //save new config
- await set(data);
+ filters.value.push(tag);
},
removeFilter: async (tag: string) => {
- const data = await get();
//remove filter from list
- data.filters = filter(data.filters, (t) => !isEqual(t, tag));
- //save new config
- await set(data);
+ filters.value = filter(filters.value, t => !isEqual(t, tag));
},
addFilters: async (tags: string[]) => {
- const data = await get();
//add new filters to list
- data.filters.push(...tags);
- //save new config
- await set(data);
+ filters.value.push(...tags);
+ },
+ isEnabled: async () => {
+ return enabled.value;
+ },
+ enable: async (value:boolean) => {
+ enabled.value = value;
}
}
}
export const useEventTagFilterApi = (): IFeatureExport<AppSettings, EventTagFilterApi> => {
return{
- background: ({ }: BgRuntime<AppSettings>) => {
+ background: ({ state }: BgRuntime<AppSettings>) => {
return{
- ...useTagFilter()
- }
+ ...useTagFilter(state)
+ }
},
foreground: exportForegroundApi([
'filterTags',
diff --git a/extension/src/features/types.ts b/extension/src/features/types.ts
index a64be93..856b95a 100644
--- a/extension/src/features/types.ts
+++ b/extension/src/features/types.ts
@@ -13,7 +13,6 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
-import { defer } from "lodash";
import { JsonObject } from "type-fest";
export interface NostrPubKey extends JsonObject {
@@ -90,68 +89,11 @@ export interface LoginMessage extends JsonObject {
}
export interface Watchable{
+ /**
+ * Waits for a change to the state of the
+ * watchable object. This is a one-shot promise
+ * that will resolve when the state changes. This
+ * must be called again to listen for the next change.
+ */
waitForChange(): Promise<void>;
-}
-
-export const useStorage = (storage: any & chrome.storage.StorageArea) => {
- const get = async <T>(key: string): Promise<T | undefined> => {
- const value = await storage.get(key)
- return value[key] as T;
- }
-
- const set = async <T>(key: string, value: T): Promise<void> => {
- await storage.set({ [key]: value });
- }
-
- const remove = async (key: string): Promise<void> => {
- await storage.remove(key);
- }
-
- return { get, set, remove }
-}
-
-export interface SingleSlotStorage<T>{
- get(): Promise<T | undefined>;
- set(value: T): Promise<void>;
- remove(): Promise<void>;
-}
-
-export interface DefaultSingleSlotStorage<T>{
- get(): Promise<T>;
- set(value: T): Promise<void>;
- remove(): Promise<void>;
-}
-
-export interface UseSingleSlotStorage{
- <T>(storage: any & chrome.storage.StorageArea, key: string): SingleSlotStorage<T>;
- <T>(storage: any & chrome.storage.StorageArea, key: string, defaultValue: T): DefaultSingleSlotStorage<T>;
-}
-
-const _useSingleSlotStorage = <T>(storage: any & chrome.storage.StorageArea, key: string, defaultValue?: T) => {
- const s = useStorage(storage);
-
- const get = async (): Promise<T | undefined> => {
- return await s.get<T>(key) || defaultValue;
- }
-
- const set = (value: T): Promise<void> => s.set(key, value);
- const remove = (): Promise<void> => s.remove(key);
-
- return { get, set, remove }
-}
-
-export const useSingleSlotStorage: UseSingleSlotStorage = _useSingleSlotStorage;
-
-export const onWatchableChange = (watchable: Watchable, onChangeCallback: () => Promise<any>, controls? : { immediate: boolean}) => {
-
- defer(async () => {
- if (controls?.immediate) {
- await onChangeCallback();
- }
-
- while (true) {
- await watchable.waitForChange();
- await onChangeCallback();
- }
- })
} \ No newline at end of file
diff --git a/extension/src/features/util.ts b/extension/src/features/util.ts
new file mode 100644
index 0000000..e9147bc
--- /dev/null
+++ b/extension/src/features/util.ts
@@ -0,0 +1,83 @@
+// Copyright (C) 2023 Vaughn Nugent
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+
+import { defer } from "lodash";
+import { RemovableRef, SerializerAsync, StorageLikeAsync, useStorageAsync, watchOnce } from "@vueuse/core";
+import { type MaybeRefOrGetter, type WatchSource, isProxy, toRaw } from "vue";
+import type { Watchable } from "./types";
+
+export const waitForChange = <T extends Readonly<WatchSource<unknown>[]>>(source: [...T]):Promise<void> => {
+ return new Promise((resolve) => watchOnce(source, () => resolve()))
+}
+
+export const waitForChangeFn = <T extends Readonly<WatchSource<unknown>[]>>(source: [...T]) => {
+ return (): Promise<void> => {
+ return new Promise((resolve) => watchOnce(source, () => resolve()))
+ }
+}
+
+export const useStorage = <T>(storage: any & chrome.storage.StorageArea, key: string, initialValue: MaybeRefOrGetter<T>): RemovableRef<T> => {
+
+ const wrapper: StorageLikeAsync = {
+
+ async getItem(key: string): Promise<any | undefined> {
+ const value = await storage.get(key)
+ //pass the raw value to the serializer
+ return value[key] as T;
+ },
+ async setItem(key: string, value: any): Promise<void> {
+ //pass the raw value to storage
+ await storage.set({ [key]: value });
+ },
+ async removeItem(key: string): Promise<void> {
+ await storage.remove(key);
+ }
+ }
+
+ /**
+ * Custom sealizer that passes the raw
+ * values to the storage, the storage
+ * wrapper above will store the raw values
+ * as is.
+ */
+ const serializer: SerializerAsync<T> = {
+ async read(value: any) {
+ return value as T
+ },
+ async write(value: any) {
+ if (isProxy(value)) {
+ return toRaw(value)
+ }
+ return value;
+ }
+ }
+
+ return useStorageAsync<T>(key, initialValue, wrapper, { serializer, deep: true, shallow: true });
+}
+
+export const onWatchableChange = (watchable: Watchable, onChangeCallback: () => Promise<any>, controls?: { immediate: boolean }) => {
+
+ defer(async () => {
+ if (controls?.immediate) {
+ await onChangeCallback();
+ }
+
+ while (true) {
+ await watchable.waitForChange();
+ await onChangeCallback();
+ }
+ })
+} \ No newline at end of file