aboutsummaryrefslogtreecommitdiff
path: root/extension/src/features
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/src/features
parent2ba94602a87c87b47f566745bdab40ce75e0e879 (diff)
feature and internal api polish
Diffstat (limited to 'extension/src/features')
-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
11 files changed, 218 insertions, 250 deletions
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