chore(nova): loader on initial data load (#817)

![image.png](https://app.graphite.com/user-attachments/assets/ceb237ae-b8f5-479d-9d77-1ba719799ade.png)
This commit is contained in:
MaheshtheDev 2026-03-31 21:36:42 +00:00
parent 1dbd45427a
commit 9473d64c1d
5 changed files with 102 additions and 33 deletions

View file

@ -845,11 +845,11 @@ function App() {
<div className="text-center py-2">
{authInvalidated ? (
<div className="mb-8">
<div className="p-3 mb-4 bg-red-50 border border-red-200 rounded-lg">
<h2 className="m-0 mb-2 text-sm font-semibold text-red-800 leading-tight">
<div className="p-3 mb-4 rounded-lg border border-red-900/60 bg-red-950/40">
<h2 className="m-0 mb-2 text-sm font-semibold text-red-200 leading-tight">
Session Expired
</h2>
<p className="m-0 text-xs text-red-600 leading-tight">
<p className="m-0 text-xs text-red-300/90 leading-tight">
Logged out since authentication was invalidated. Please
login again.
</p>
@ -857,18 +857,18 @@ function App() {
</div>
) : (
<div className="mb-8">
<h2 className="m-0 mb-4 text-sm font-normal leading-tight">
Login to unlock all chrome extension features
<h2 className="m-0 mb-4 text-sm font-medium leading-snug text-neutral-100">
Login to unlock all Chrome extension features
</h2>
<ul className="list-none p-0 m-0 text-left">
<li className="py-1.5 text-sm text-[#737373] relative pl-5 before:content-['•'] before:absolute before:left-0 before:text-[#737373] before:font-bold">
<li className="py-1.5 text-sm text-neutral-400 relative pl-5 before:content-['•'] before:absolute before:left-0 before:text-neutral-500 before:font-bold">
Save any page to your supermemory
</li>
<li className="py-1.5 text-sm text-[#737373] relative pl-5 before:content-['•'] before:absolute before:left-0 before:text-[#737373] before:font-bold">
<li className="py-1.5 text-sm text-neutral-400 relative pl-5 before:content-['•'] before:absolute before:left-0 before:text-neutral-500 before:font-bold">
Import all your Twitter / X Bookmarks
</li>
<li className="py-1.5 text-sm text-[#737373] relative pl-5 before:content-['•'] before:absolute before:left-0 before:text-[#737373] before:font-bold">
<li className="py-1.5 text-sm text-neutral-400 relative pl-5 before:content-['•'] before:absolute before:left-0 before:text-neutral-500 before:font-bold">
Import your ChatGPT Memories
</li>
</ul>
@ -876,10 +876,10 @@ function App() {
)}
<div className="mt-8">
<p className="m-0 mb-4 text-sm text-[#737373]">
<p className="m-0 mb-4 text-sm text-neutral-400">
Having trouble logging in?{" "}
<button
className="bg-transparent border-none text-blue-500 cursor-pointer underline text-sm p-0 hover:text-blue-700"
className="bg-transparent border-none text-sky-400 cursor-pointer underline text-sm p-0 hover:text-sky-300"
onClick={() => {
window.open("mailto:support@supermemory.ai", "_blank")
}}
@ -890,7 +890,7 @@ function App() {
</p>
<button
className="w-full py-3 px-6 bg-gray-700 text-white border-none rounded-3xl text-base font-medium cursor-pointer transition-colors duration-200 hover:bg-gray-800 disabled:bg-gray-400 disabled:cursor-not-allowed"
className="w-full py-3 px-6 bg-[#2d3f5c] text-white border-none rounded-3xl text-base font-medium cursor-pointer transition-colors duration-200 hover:bg-[#3d5270] disabled:bg-neutral-600 disabled:cursor-not-allowed"
onClick={() => {
chrome.tabs.create({
url: import.meta.env.PROD

View file

@ -4,10 +4,10 @@
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
border: 1px solid #242424;
color-scheme: dark;
color: rgba(255, 255, 255, 0.92);
background-color: #0a0e14;
border: 1px solid #0a0e14;
font-synthesis: none;
text-rendering: optimizeLegibility;
@ -16,11 +16,6 @@
-webkit-text-size-adjust: 100%;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
}
a:hover {
color: #747bff;
}
a:hover {
color: #93c5fd;
}

View file

@ -1,17 +1,35 @@
import path from "node:path"
import { createRequire } from "node:module"
import tailwindcss from "@tailwindcss/vite"
import { defineConfig, type WxtViteConfig } from "wxt"
const require = createRequire(import.meta.url)
function reactPackageRoot(pkg: "react" | "react-dom"): string {
return path.dirname(require.resolve(`${pkg}/package.json`))
}
// See https://wxt.dev/api/config.html
export default defineConfig({
modules: ["@wxt-dev/module-react"],
vite: () =>
({
plugins: [tailwindcss()],
resolve: {
dedupe: ["react", "react-dom"],
alias: {
react: reactPackageRoot("react"),
"react-dom": reactPackageRoot("react-dom"),
},
},
optimizeDeps: {
include: ["react", "react-dom", "@tanstack/react-query"],
},
}) as WxtViteConfig,
manifest: {
name: "supermemory",
homepage_url: "https://supermemory.ai",
version: "6.1.1",
version: "6.1.2",
permissions: ["storage", "activeTab", "webRequest", "tabs"],
host_permissions: [
"*://x.com/*",

View file

@ -9,7 +9,6 @@ import { useQueryState } from "nuqs"
import type { z } from "zod"
import { Masonry, useInfiniteLoader } from "masonic"
import { dmSansClassName } from "@/lib/fonts"
import { SuperLoader } from "@/components/superloader"
import { ErrorBoundary } from "@/components/error-boundary"
import { cn } from "@lib/utils"
import { useProject } from "@/stores"
@ -42,7 +41,7 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from "@ui/components/alert-dialog"
import { CheckIcon, Trash2Icon, XIcon } from "lucide-react"
import { CheckIcon, Loader, Trash2Icon, XIcon } from "lucide-react"
// Document category type
type DocumentCategory =
@ -77,6 +76,55 @@ type OgData = {
const PAGE_SIZE = 100
const MAX_TOTAL = 1000
const MEMORIES_LOADING_LABELS = [
"Getting your supermemories…",
"Fetching your documents…",
"Warming up Nova…",
"Almost there…",
] as const
function useRotatingLoadingLabel(
labels: readonly string[],
intervalMs = 2400,
): string {
const [index, setIndex] = useState(0)
useEffect(() => {
const id = window.setInterval(() => {
setIndex((i) => (i + 1) % labels.length)
}, intervalMs)
return () => window.clearInterval(id)
}, [labels.length, intervalMs])
const label = labels.at(index) ?? labels.at(0)
return label ?? "Loading…"
}
function MemoriesGridLoading() {
const label = useRotatingLoadingLabel(MEMORIES_LOADING_LABELS)
return (
<output
aria-label={label}
className="h-full min-h-[min(50dvh,360px)] w-full flex items-center justify-center gap-4 px-8 py-20"
aria-live="polite"
>
<Loader
className={cn("shrink-0 animate-spin text-sky-400")}
aria-hidden
strokeWidth={2}
/>
<p
className={cn(
dmSansClassName(),
"text-center text-base text-[#A3A3A3] max-w-md leading-relaxed",
)}
>
{label}
</p>
</output>
)
}
// Discriminated union for masonry items
type MasonryItem = { type: "document"; id: string; data: DocumentWithMemories }
@ -135,7 +183,7 @@ export function MemoriesGrid({
emptyStateProps,
}: MemoriesGridProps) {
const [showBulkDeleteConfirm, setShowBulkDeleteConfirm] = useState(false)
const { user } = useAuth()
const { user, isSessionPending } = useAuth()
const { effectiveContainerTags } = useProject()
const isMobile = useIsMobile()
const [selectedCategories, setSelectedCategories] = useQueryState(
@ -343,6 +391,10 @@ export function MemoriesGrid({
[handleCardClick, isSelectionMode, selectedDocumentIds, onToggleSelection],
)
if (isSessionPending) {
return <MemoriesGridLoading />
}
if (!user) {
return (
<div className="flex items-center justify-center h-full">
@ -504,9 +556,7 @@ export function MemoriesGrid({
</div>
</div>
) : isPending ? (
<div className="h-full flex items-center justify-center p-4">
<SuperLoader />
</div>
<MemoriesGridLoading />
) : showNovaEmptyState ? (
<NovaEmptyState
onAddMemory={emptyStateProps.onAddMemory}
@ -558,8 +608,8 @@ export function MemoriesGrid({
/>
{isLoadingMore && (
<div className="py-8 flex items-center justify-center">
<SuperLoader />
<div className="py-10 flex items-center justify-center">
<Loader className="size-10 animate-spin text-sky-400" />
</div>
)}
</div>

View file

@ -24,6 +24,7 @@ interface AuthContextType {
org: Organization | null
organizations: OrganizationListItem[] | null
isRestoring: boolean
isSessionPending: boolean
setActiveOrg: (orgSlug: string) => Promise<void>
updateOrgMetadata: (partial: Record<string, unknown>) => void
refetchOrganizations: () => Promise<unknown>
@ -32,7 +33,7 @@ interface AuthContextType {
const AuthContext = createContext<AuthContextType | undefined>(undefined)
export function AuthProvider({ children }: { children: ReactNode }) {
const { data: session } = useSession()
const { data: session, isPending: isSessionPending } = useSession()
const [org, setOrg] = useState<Organization | null>(null)
const [isRestoring, setIsRestoring] = useState(true)
const {
@ -73,6 +74,10 @@ export function AuthProvider({ children }: { children: ReactNode }) {
}, [])
useEffect(() => {
if (isSessionPending) {
return
}
if (!session?.session) {
setIsRestoring(false)
setOrg(null)
@ -145,7 +150,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
return () => {
cancelled = true
}
}, [session, orgsData, orgsPending, setActiveOrg])
}, [session, isSessionPending, orgsData, orgsPending, setActiveOrg])
useEffect(() => {
if (typeof window === "undefined") return
@ -181,6 +186,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
org,
organizations,
isRestoring,
isSessionPending,
session: session?.session ?? null,
user: session?.user ?? null,
setActiveOrg,