diff --git a/apps/browser-extension/entrypoints/content.ts b/apps/browser-extension/entrypoints/content.ts index 385beaec..97f5fc44 100644 --- a/apps/browser-extension/entrypoints/content.ts +++ b/apps/browser-extension/entrypoints/content.ts @@ -1,4 +1,9 @@ -import { DOMAINS, ELEMENT_IDS, MESSAGE_TYPES } from "../utils/constants" +import { + DOMAINS, + ELEMENT_IDS, + MESSAGE_TYPES, + STORAGE_KEYS, +} from "../utils/constants" import { createChatGPTInputBarElement, createSaveTweetElement, @@ -261,8 +266,8 @@ export default defineContentScript({ isTwitterImportOpen = true // Check if user is authenticated - browser.storage.local.get(["bearerToken"], ({ bearerToken }) => { - const isAuthenticated = !!bearerToken + browser.storage.local.get([STORAGE_KEYS.BEARER_TOKEN], (result) => { + const isAuthenticated = !!result[STORAGE_KEYS.BEARER_TOKEN] twitterImportUI = createTwitterImportUI( hideTwitterImportUI, @@ -297,10 +302,10 @@ export default defineContentScript({ }) { if (!isTwitterImportOpen || !twitterImportUI) return - const statusDiv = twitterImportUI.querySelector("#twitter-import-status") - const button = twitterImportUI.querySelector("#twitter-import-button") + const statusDiv = twitterImportUI.querySelector(`#${ELEMENT_IDS.TWITTER_IMPORT_STATUS}`) + const button = twitterImportUI.querySelector(`#${ELEMENT_IDS.TWITTER_IMPORT_BTN}`) - if (message.type === "import-update") { + if (message.type === MESSAGE_TYPES.IMPORT_UPDATE) { if (statusDiv) { statusDiv.innerHTML = `
@@ -315,7 +320,7 @@ export default defineContentScript({ } } - if (message.type === "import-done") { + if (message.type === MESSAGE_TYPES.IMPORT_DONE) { if (statusDiv) { statusDiv.innerHTML = `
@@ -449,7 +454,9 @@ export default defineContentScript({ return } - chrome.storage.local.set({ bearerToken }, () => {}) + chrome.storage.local.set({ + [STORAGE_KEYS.BEARER_TOKEN]: bearerToken, + }, () => {}) } }) }, diff --git a/apps/browser-extension/entrypoints/popup/App.tsx b/apps/browser-extension/entrypoints/popup/App.tsx index 4d533a47..2f42747a 100644 --- a/apps/browser-extension/entrypoints/popup/App.tsx +++ b/apps/browser-extension/entrypoints/popup/App.tsx @@ -1,41 +1,40 @@ +import { useQueryClient } from "@tanstack/react-query" import { useEffect, useState } from "react" import "./App.css" +import { STORAGE_KEYS } from "../../utils/constants" import { - getDefaultProject, - getProjects, - setDefaultProject, -} from "../../utils/api" + useDefaultProject, + useProjects, + useSetDefaultProject, +} from "../../utils/query-hooks" import type { Project } from "../../utils/types" function App() { const [userSignedIn, setUserSignedIn] = useState(false) const [loading, setLoading] = useState(true) - const [projects, setProjects] = useState([]) - const [defaultProject, setDefaultProjectState] = useState( - null, - ) - const [loadingProjects, setLoadingProjects] = useState(false) const [showProjectSelector, setShowProjectSelector] = useState(false) const [currentUrl, setCurrentUrl] = useState("") const [currentTitle, setCurrentTitle] = useState("") const [saving, setSaving] = useState(false) const [activeTab, setActiveTab] = useState<"save" | "imports">("save") + const queryClient = useQueryClient() + const { data: projects = [], isLoading: loadingProjects } = useProjects({ + enabled: userSignedIn, + }) + const { data: defaultProject } = useDefaultProject({ + enabled: userSignedIn, + }) + const setDefaultProjectMutation = useSetDefaultProject() + useEffect(() => { const checkAuthStatus = async () => { try { - const result = await chrome.storage.local.get(["bearerToken"]) - const isSignedIn = !!result.bearerToken + const result = await chrome.storage.local.get([ + STORAGE_KEYS.BEARER_TOKEN, + ]) + const isSignedIn = !!result[STORAGE_KEYS.BEARER_TOKEN] setUserSignedIn(isSignedIn) - - if (isSignedIn) { - try { - const defaultProj = await getDefaultProject() - setDefaultProjectState(defaultProj) - } catch (error) { - console.error("Error loading default project:", error) - } - } } catch (error) { console.error("Error checking auth status:", error) setUserSignedIn(false) @@ -63,44 +62,28 @@ function App() { getCurrentTab() }, []) - const loadProjects = async () => { - setLoadingProjects(true) - try { - const projectsList = await getProjects() - setProjects(projectsList) - console.log("Projects:", projectsList) - console.log("Default project:", defaultProject) - // If no default project is set and projects are available, set first as default - if (!defaultProject && projectsList.length > 0) { - const firstProject = projectsList[0] - await setDefaultProject(firstProject) - setDefaultProjectState(firstProject) - } - } catch (error) { - console.error("Error loading projects:", error) - } finally { - setLoadingProjects(false) - } - } - - const handleProjectSelect = async (project: Project) => { - try { - await setDefaultProject(project) - setDefaultProjectState(project) - setShowProjectSelector(false) - } catch (error) { - console.error("Error setting default project:", error) - } + const handleProjectSelect = (project: Project) => { + setDefaultProjectMutation.mutate(project, { + onSuccess: () => { + setShowProjectSelector(false) + }, + onError: (error) => { + console.error("Error setting default project:", error) + }, + }) } const handleShowProjectSelector = () => { - console.log("handleShowProjectSelector, projects.length:", projects.length) - if (projects.length === 0) { - loadProjects() - } setShowProjectSelector(true) } + useEffect(() => { + if (!defaultProject && projects.length > 0) { + const firstProject = projects[0] + setDefaultProjectMutation.mutate(firstProject) + } + }, [defaultProject, projects, setDefaultProjectMutation]) + const handleSaveCurrentPage = async () => { setSaving(true) try { @@ -122,10 +105,9 @@ function App() { const handleSignOut = async () => { try { - await chrome.storage.local.remove(["bearerToken"]) + await chrome.storage.local.remove([STORAGE_KEYS.BEARER_TOKEN]) setUserSignedIn(false) - setDefaultProjectState(null) - setProjects([]) + queryClient.clear() } catch (error) { console.error("Error signing out:", error) } diff --git a/apps/browser-extension/entrypoints/popup/main.tsx b/apps/browser-extension/entrypoints/popup/main.tsx index bfa6ad76..746e1018 100644 --- a/apps/browser-extension/entrypoints/popup/main.tsx +++ b/apps/browser-extension/entrypoints/popup/main.tsx @@ -1,5 +1,7 @@ +import { QueryClientProvider } from "@tanstack/react-query" import React from "react" import ReactDOM from "react-dom/client" +import { queryClient } from "../../utils/query-client" import App from "./App.js" import "./style.css" @@ -7,7 +9,9 @@ const rootElement = document.getElementById("root") if (rootElement) { ReactDOM.createRoot(rootElement).render( - + + + , ) } diff --git a/apps/browser-extension/entrypoints/welcome/main.tsx b/apps/browser-extension/entrypoints/welcome/main.tsx index 0b54490a..2df8dbf1 100644 --- a/apps/browser-extension/entrypoints/welcome/main.tsx +++ b/apps/browser-extension/entrypoints/welcome/main.tsx @@ -1,5 +1,7 @@ +import { QueryClientProvider } from "@tanstack/react-query" import React from "react" import ReactDOM from "react-dom/client" +import { queryClient } from "../../utils/query-client" import Welcome from "./Welcome" import "./welcome.css" @@ -7,7 +9,9 @@ const rootElement = document.getElementById("root") if (rootElement) { ReactDOM.createRoot(rootElement).render( - + + + , ) } diff --git a/apps/browser-extension/package.json b/apps/browser-extension/package.json index 4c9d1402..9d5b3b51 100644 --- a/apps/browser-extension/package.json +++ b/apps/browser-extension/package.json @@ -15,6 +15,7 @@ "postinstall": "wxt prepare" }, "dependencies": { + "@tanstack/react-query": "^5.85.5", "react": "^19.1.0", "react-dom": "^19.1.0" }, diff --git a/apps/browser-extension/public/dark-mode-icon.png b/apps/browser-extension/public/dark-mode-icon.png deleted file mode 100644 index 6c9dd28a..00000000 Binary files a/apps/browser-extension/public/dark-mode-icon.png and /dev/null differ diff --git a/apps/browser-extension/public/light-mode-icon.png b/apps/browser-extension/public/light-mode-icon.png deleted file mode 100644 index 66df9ad7..00000000 Binary files a/apps/browser-extension/public/light-mode-icon.png and /dev/null differ diff --git a/apps/browser-extension/public/logo-trademark.svg b/apps/browser-extension/public/logo-trademark.svg deleted file mode 100644 index 8407cbd4..00000000 --- a/apps/browser-extension/public/logo-trademark.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/apps/browser-extension/utils/api.ts b/apps/browser-extension/utils/api.ts index 1b544c9a..858e81b1 100644 --- a/apps/browser-extension/utils/api.ts +++ b/apps/browser-extension/utils/api.ts @@ -1,5 +1,5 @@ /** - * API service for Supermemory browser extension + * API service for supermemory browser extension */ import { API_ENDPOINTS, STORAGE_KEYS } from "./constants" import { @@ -70,47 +70,6 @@ export async function fetchProjects(): Promise { } } -/** - * Get projects from cache or fetch fresh - */ -export async function getProjects(useCache = true): Promise { - if (useCache) { - try { - const cached = await chrome.storage.local.get([ - STORAGE_KEYS.PROJECTS_CACHE, - ]) - const cachedData = cached[STORAGE_KEYS.PROJECTS_CACHE] - - if (cachedData?.timestamp && cachedData.projects) { - // Cache for 5 minutes - const cacheAge = Date.now() - cachedData.timestamp - if (cacheAge < 5 * 60 * 1000) { - return cachedData.projects - } - } - } catch (error) { - console.warn("Failed to read projects cache:", error) - } - } - - // Fetch fresh data - const projects = await fetchProjects() - - // Cache the results - try { - await chrome.storage.local.set({ - [STORAGE_KEYS.PROJECTS_CACHE]: { - projects, - timestamp: Date.now(), - }, - }) - } catch (error) { - console.warn("Failed to cache projects:", error) - } - - return projects -} - /** * Get default project from storage */ diff --git a/apps/browser-extension/utils/constants.ts b/apps/browser-extension/utils/constants.ts index d21db8a4..b499a359 100644 --- a/apps/browser-extension/utils/constants.ts +++ b/apps/browser-extension/utils/constants.ts @@ -14,29 +14,27 @@ export const API_ENDPOINTS = { * Storage Keys */ export const STORAGE_KEYS = { - BEARER_TOKEN: "bearerToken", - TWITTER_AUTH: "twitterAuth", - TOKENS_LOGGED: "tokens_logged", - TWITTER_COOKIE: "cookie", - TWITTER_CSRF: "csrf", - TWITTER_AUTH_TOKEN: "auth", - DEFAULT_PROJECT: "defaultProject", - PROJECTS_CACHE: "projectsCache", + BEARER_TOKEN: "bearer-token", + TOKENS_LOGGED: "tokens-logged", + TWITTER_COOKIE: "twitter-cookie", + TWITTER_CSRF: "twitter-csrf", + TWITTER_AUTH_TOKEN: "twitter-auth-token", + DEFAULT_PROJECT: "sm-default-project", } as const /** * DOM Element IDs */ export const ELEMENT_IDS = { - TWITTER_IMPORT_BUTTON: "supermemory-twitter-import-button", - TWITTER_IMPORT_STATUS: "twitter-import-status", - TWITTER_CLOSE_BTN: "twitter-close-btn", - TWITTER_IMPORT_BTN: "twitter-import-button", - TWITTER_SIGNIN_BTN: "twitter-signin-btn", - SUPERMEMORY_TOAST: "supermemory-toast", - SUPERMEMORY_SAVE_BUTTON: "supermemory-save-button", - SAVE_TWEET_ELEMENT: "supermemory-save-tweet-element", - CHATGPT_INPUT_BAR_ELEMENT: "supermemory-chatgpt-input-bar-element", + TWITTER_IMPORT_BUTTON: "sm-twitter-import-button", + TWITTER_IMPORT_STATUS: "sm-twitter-import-status", + TWITTER_CLOSE_BTN: "sm-twitter-close-btn", + TWITTER_IMPORT_BTN: "sm-twitter-import-btn", + TWITTER_SIGNIN_BTN: "sm-twitter-signin-btn", + SUPERMEMORY_TOAST: "sm-toast", + SUPERMEMORY_SAVE_BUTTON: "sm-save-button", + SAVE_TWEET_ELEMENT: "sm-save-tweet-element", + CHATGPT_INPUT_BAR_ELEMENT: "sm-chatgpt-input-bar-element", } as const /** @@ -70,19 +68,14 @@ export const CONTAINER_TAGS = { * Message Types for extension communication */ export const MESSAGE_TYPES = { - SAVE_MEMORY: "saveMemory", - SHOW_TOAST: "showToast", - BATCH_IMPORT_ALL: "batchImportAll", - IMPORT_UPDATE: "import-update", - IMPORT_DONE: "import-done", - GET_RELATED_MEMORIES: "getRelatedMemories", + SAVE_MEMORY: "sm-save-memory", + SHOW_TOAST: "sm-show-toast", + BATCH_IMPORT_ALL: "sm-batch-import-all", + IMPORT_UPDATE: "sm-import-update", + IMPORT_DONE: "sm-import-done", + GET_RELATED_MEMORIES: "sm-get-related-memories", } as const export const CONTEXT_MENU_IDS = { - SAVE_TO_SUPERMEMORY: "save-to-supermemory", -} as const - -export const CSS_CLASSES = { - TOAST_STYLES_ID: "supermemory-toast-styles", - SPINNER_STYLES_ID: "supermemory-spinner-styles", + SAVE_TO_SUPERMEMORY: "sm-save-to-supermemory", } as const diff --git a/apps/browser-extension/utils/query-client.ts b/apps/browser-extension/utils/query-client.ts new file mode 100644 index 00000000..c1839691 --- /dev/null +++ b/apps/browser-extension/utils/query-client.ts @@ -0,0 +1,24 @@ +/** + * React Query configuration for supermemory browser extension + */ +import { QueryClient } from "@tanstack/react-query" + +export const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 5 * 60 * 1000, // 5 minutes + gcTime: 10 * 60 * 1000, // 10 minutes (previously cacheTime) + retry: (failureCount, error) => { + // Don't retry on authentication errors + if (error?.constructor?.name === "AuthenticationError") { + return false + } + return failureCount < 3 + }, + refetchOnWindowFocus: false, + }, + mutations: { + retry: 1, + }, + }, +}) diff --git a/apps/browser-extension/utils/query-hooks.ts b/apps/browser-extension/utils/query-hooks.ts new file mode 100644 index 00000000..721a68ad --- /dev/null +++ b/apps/browser-extension/utils/query-hooks.ts @@ -0,0 +1,64 @@ +/** + * React Query hooks for supermemory API + */ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" +import { + fetchProjects, + getDefaultProject, + saveMemory, + searchMemories, + setDefaultProject, +} from "./api" +import type { MemoryPayload } from "./types" + +// Query Keys +export const queryKeys = { + projects: ["projects"] as const, + defaultProject: ["defaultProject"] as const, +} + +// Projects Query +export function useProjects(options?: { enabled?: boolean }) { + return useQuery({ + queryKey: queryKeys.projects, + queryFn: fetchProjects, + staleTime: 5 * 60 * 1000, // 5 minutes + enabled: options?.enabled ?? true, + }) +} + +// Default Project Query +export function useDefaultProject(options?: { enabled?: boolean }) { + return useQuery({ + queryKey: queryKeys.defaultProject, + queryFn: getDefaultProject, + staleTime: 2 * 60 * 1000, // 2 minutes + enabled: options?.enabled ?? true, + }) +} + +// Set Default Project Mutation +export function useSetDefaultProject() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: setDefaultProject, + onSuccess: (_, project) => { + queryClient.setQueryData(queryKeys.defaultProject, project) + }, + }) +} + +// Save Memory Mutation +export function useSaveMemory() { + return useMutation({ + mutationFn: (payload: MemoryPayload) => saveMemory(payload), + }) +} + +// Search Memories Mutation +export function useSearchMemories() { + return useMutation({ + mutationFn: (query: string) => searchMemories(query), + }) +} diff --git a/apps/browser-extension/utils/twitter-auth.ts b/apps/browser-extension/utils/twitter-auth.ts index 8734d144..3dfc50f6 100644 --- a/apps/browser-extension/utils/twitter-auth.ts +++ b/apps/browser-extension/utils/twitter-auth.ts @@ -2,6 +2,7 @@ * Twitter Authentication Module * Handles token capture and storage for Twitter API access */ +import { STORAGE_KEYS } from "./constants" export interface TwitterAuthTokens { cookie: string @@ -34,17 +35,17 @@ export function captureTwitterTokens( ) if (authHeader?.value && cookieHeader?.value && csrfHeader?.value) { - chrome.storage.session.get(["tokens_logged"], (result) => { - if (!result.tokens_logged) { + chrome.storage.session.get([STORAGE_KEYS.TOKENS_LOGGED], (result) => { + if (!result[STORAGE_KEYS.TOKENS_LOGGED]) { console.log("Twitter auth tokens captured successfully") - chrome.storage.session.set({ tokens_logged: true }) + chrome.storage.session.set({ [STORAGE_KEYS.TOKENS_LOGGED]: true }) } }) chrome.storage.session.set({ - cookie: cookieHeader.value, - csrf: csrfHeader.value, - auth: authHeader.value, + [STORAGE_KEYS.TWITTER_COOKIE]: cookieHeader.value, + [STORAGE_KEYS.TWITTER_CSRF]: csrfHeader.value, + [STORAGE_KEYS.TWITTER_AUTH_TOKEN]: authHeader.value, }) return true @@ -58,16 +59,24 @@ export function captureTwitterTokens( * @returns Promise resolving to tokens or null if not available */ export async function getTwitterTokens(): Promise { - const result = await chrome.storage.session.get(["cookie", "csrf", "auth"]) + const result = await chrome.storage.session.get([ + STORAGE_KEYS.TWITTER_COOKIE, + STORAGE_KEYS.TWITTER_CSRF, + STORAGE_KEYS.TWITTER_AUTH_TOKEN, + ]) - if (!result.cookie || !result.csrf || !result.auth) { + if ( + !result[STORAGE_KEYS.TWITTER_COOKIE] || + !result[STORAGE_KEYS.TWITTER_CSRF] || + !result[STORAGE_KEYS.TWITTER_AUTH_TOKEN] + ) { return null } return { - cookie: result.cookie, - csrf: result.csrf, - auth: result.auth, + cookie: result[STORAGE_KEYS.TWITTER_COOKIE], + csrf: result[STORAGE_KEYS.TWITTER_CSRF], + auth: result[STORAGE_KEYS.TWITTER_AUTH_TOKEN], } } diff --git a/apps/browser-extension/utils/ui-components.ts b/apps/browser-extension/utils/ui-components.ts index 917e0ca9..7fbf098d 100644 --- a/apps/browser-extension/utils/ui-components.ts +++ b/apps/browser-extension/utils/ui-components.ts @@ -165,7 +165,7 @@ export function createTwitterImportButton(onClick: () => void): HTMLElement { transition: all 0.2s ease; ` - const iconUrl = browser.runtime.getURL("/light-mode-icon.png") + const iconUrl = browser.runtime.getURL("/icon-16.png") button.innerHTML = ` Save to Memory ` diff --git a/apps/browser-extension/wxt.config.ts b/apps/browser-extension/wxt.config.ts index 9143866a..579f2485 100644 --- a/apps/browser-extension/wxt.config.ts +++ b/apps/browser-extension/wxt.config.ts @@ -24,10 +24,7 @@ export default defineConfig({ ], web_accessible_resources: [ { - resources: [ - "icon-16.png", - "fonts/*.ttf", - ], + resources: ["icon-16.png", "fonts/*.ttf"], matches: [""], }, ], diff --git a/bun.lockb b/bun.lockb index 8454fa3d..322ea574 100755 Binary files a/bun.lockb and b/bun.lockb differ