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 = `
`
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