diff options
author | vnugent <public@vaughnnugent.com> | 2023-11-22 15:07:08 -0500 |
---|---|---|
committer | vnugent <public@vaughnnugent.com> | 2023-11-22 15:07:08 -0500 |
commit | e272adcc3f32e31fe7668551453b8e34bc823c3e (patch) | |
tree | 680c695184ddbc27227578afa9f169d98a69f55a /extension/src/features | |
parent | 2ba94602a87c87b47f566745bdab40ce75e0e879 (diff) |
feature and internal api polish
Diffstat (limited to 'extension/src/features')
-rw-r--r-- | extension/src/features/account-api.ts | 48 | ||||
-rw-r--r-- | extension/src/features/auth-api.ts | 11 | ||||
-rw-r--r-- | extension/src/features/identity-api.ts | 29 | ||||
-rw-r--r-- | extension/src/features/index.ts | 3 | ||||
-rw-r--r-- | extension/src/features/nip07allow-api.ts | 59 | ||||
-rw-r--r-- | extension/src/features/nostr-api.ts | 6 | ||||
-rw-r--r-- | extension/src/features/server-api/index.ts | 13 | ||||
-rw-r--r-- | extension/src/features/settings.ts | 96 | ||||
-rw-r--r-- | extension/src/features/tagfilter-api.ts | 50 | ||||
-rw-r--r-- | extension/src/features/types.ts | 70 | ||||
-rw-r--r-- | extension/src/features/util.ts | 83 |
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 |