([]);
- const instanceGeneralSetting = instanceStore.state.generalSetting;
+ const { generalSetting: instanceGeneralSetting } = useInstance();
// Redirect to root page if already signed in.
useEffect(() => {
- if (currentUser) {
+ if (currentUser?.name) {
window.location.href = Routes.ROOT;
}
- }, []);
+ }, [currentUser]);
// Prepare identity provider list.
useEffect(() => {
@@ -79,7 +78,7 @@ const SignIn = observer(() => {
{!instanceGeneralSetting.disallowPasswordAuth ? (
) : (
- identityProviderList.length == 0 && Password auth is not allowed.
+ identityProviderList.length === 0 && Password auth is not allowed.
)}
{!instanceGeneralSetting.disallowUserRegistration && !instanceGeneralSetting.disallowPasswordAuth && (
@@ -117,6 +116,6 @@ const SignIn = observer(() => {
);
-});
+};
export default SignIn;
diff --git a/web/src/pages/SignUp.tsx b/web/src/pages/SignUp.tsx
index 07eb60349..6574874a4 100644
--- a/web/src/pages/SignUp.tsx
+++ b/web/src/pages/SignUp.tsx
@@ -1,7 +1,6 @@
import { create } from "@bufbuild/protobuf";
import { timestampDate } from "@bufbuild/protobuf/wkt";
import { LoaderIcon } from "lucide-react";
-import { observer } from "mobx-react-lite";
import { useState } from "react";
import { toast } from "react-hot-toast";
import { Link } from "react-router-dom";
@@ -10,20 +9,21 @@ import AuthFooter from "@/components/AuthFooter";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { authServiceClient, userServiceClient } from "@/connect";
+import { useAuth } from "@/contexts/AuthContext";
+import { useInstance } from "@/contexts/InstanceContext";
import useLoading from "@/hooks/useLoading";
import useNavigateTo from "@/hooks/useNavigateTo";
-import { instanceStore } from "@/store";
-import { initialUserStore } from "@/store/user";
import { User_Role, UserSchema } from "@/types/proto/api/v1/user_service_pb";
import { useTranslate } from "@/utils/i18n";
-const SignUp = observer(() => {
+const SignUp = () => {
const t = useTranslate();
const navigateTo = useNavigateTo();
const actionBtnLoadingState = useLoading(false);
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
- const instanceGeneralSetting = instanceStore.state.generalSetting;
+ const { generalSetting: instanceGeneralSetting, profile } = useInstance();
+ const { initialize } = useAuth();
const handleUsernameInputChanged = (e: React.ChangeEvent) => {
const text = e.target.value as string;
@@ -67,7 +67,7 @@ const SignUp = observer(() => {
if (response.accessToken) {
setAccessToken(response.accessToken, response.accessTokenExpiresAt ? timestampDate(response.accessTokenExpiresAt) : undefined);
}
- await initialUserStore();
+ await initialize();
navigateTo("/");
} catch (error: unknown) {
console.error(error);
@@ -131,7 +131,7 @@ const SignUp = observer(() => {
) : (
Sign up is not allowed.
)}
- {!instanceStore.state.profile.owner ? (
+ {!profile.owner ? (
{t("auth.host-tip")}
) : (
@@ -145,6 +145,6 @@ const SignUp = observer(() => {
);
-});
+};
export default SignUp;
diff --git a/web/src/pages/UserProfile.tsx b/web/src/pages/UserProfile.tsx
index 5b2cf057a..54e5185ad 100644
--- a/web/src/pages/UserProfile.tsx
+++ b/web/src/pages/UserProfile.tsx
@@ -1,7 +1,5 @@
import copy from "copy-to-clipboard";
import { ExternalLinkIcon } from "lucide-react";
-import { observer } from "mobx-react-lite";
-import { useEffect, useState } from "react";
import { toast } from "react-hot-toast";
import { useParams } from "react-router-dom";
import { MemoRenderContext } from "@/components/MasonryView";
@@ -10,36 +8,29 @@ import PagedMemoList from "@/components/PagedMemoList";
import UserAvatar from "@/components/UserAvatar";
import { Button } from "@/components/ui/button";
import { useMemoFilters, useMemoSorting } from "@/hooks";
-import useLoading from "@/hooks/useLoading";
-import { userStore } from "@/store";
+import { useUser } from "@/hooks/useUserQueries";
import { State } from "@/types/proto/api/v1/common_pb";
import { Memo } from "@/types/proto/api/v1/memo_service_pb";
-import { User } from "@/types/proto/api/v1/user_service_pb";
import { useTranslate } from "@/utils/i18n";
-const UserProfile = observer(() => {
+const UserProfile = () => {
const t = useTranslate();
const params = useParams();
- const loadingState = useLoading();
- const [user, setUser] = useState();
+ const username = params.username;
- useEffect(() => {
- const username = params.username;
- if (!username) {
- throw new Error("username is required");
- }
+ // Fetch user with React Query
+ const {
+ data: user,
+ isLoading,
+ error,
+ } = useUser(`users/${username}`, {
+ enabled: !!username,
+ });
- userStore
- .getOrFetchUser(`users/${username}`)
- .then((user) => {
- setUser(user);
- loadingState.setFinish();
- })
- .catch((error) => {
- console.error(error);
- toast.error(t("message.user-not-found"));
- });
- }, [params.username]);
+ // Handle errors
+ if (error && !isLoading) {
+ toast.error(t("message.user-not-found"));
+ }
// Build filter using unified hook (no shortcuts, but includes pinned)
const memoFilter = useMemoFilters({
@@ -65,7 +56,7 @@ const UserProfile = observer(() => {
return (
- {!loadingState.isLoading &&
+ {!isLoading &&
(user ? (
<>
{/* User profile header - centered with max width */}
@@ -107,6 +98,6 @@ const UserProfile = observer(() => {
))}
);
-});
+};
export default UserProfile;
diff --git a/web/src/store/README.md b/web/src/store/README.md
deleted file mode 100644
index c3b3d09e4..000000000
--- a/web/src/store/README.md
+++ /dev/null
@@ -1,277 +0,0 @@
-# Store Architecture
-
-This directory contains the application's state management implementation using MobX.
-
-## Overview
-
-The store architecture follows a clear separation of concerns:
-
-- **Server State Stores**: Manage data fetched from the backend API
-- **Client State Stores**: Manage UI preferences and transient state
-
-## Store Files
-
-### Server State Stores (API Data)
-
-| Store | File | Purpose |
-|-------|------|---------|
-| `memoStore` | `memo.ts` | Memo CRUD operations, optimistic updates |
-| `userStore` | `user.ts` | User authentication, settings, stats |
-| `instanceStore` | `instance.ts` | Instance profile and settings |
-| `attachmentStore` | `attachment.ts` | File attachment management |
-
-**Features:**
-- ✅ Request deduplication (prevents duplicate API calls)
-- ✅ Structured error handling with `StoreError`
-- ✅ Computed property memoization for performance
-- ✅ Optimistic updates (immediate UI feedback)
-- ✅ Automatic caching
-
-### Client State Stores (UI State)
-
-| Store | File | Purpose | Persistence |
-|-------|------|---------|-------------|
-| `viewStore` | `view.ts` | Display preferences (sort, layout) | localStorage |
-| `memoFilterStore` | `memoFilter.ts` | Active search filters | URL params |
-
-**Features:**
-- ✅ No API calls (instant updates)
-- ✅ localStorage persistence (viewStore)
-- ✅ URL synchronization (memoFilterStore - shareable links)
-
-### Utilities
-
-| File | Purpose |
-|------|---------|
-| `base-store.ts` | Base classes and factory functions |
-| `store-utils.ts` | Request deduplication, error handling, optimistic updates |
-| `config.ts` | MobX configuration |
-| `common.ts` | Shared constants and utilities |
-| `index.ts` | Centralized exports |
-
-## Usage Examples
-
-### Basic Store Usage
-
-```typescript
-import { memoStore, userStore, viewStore } from "@/store";
-import { observer } from "mobx-react-lite";
-
-const MyComponent = observer(() => {
- // Access state
- const memos = memoStore.state.memos;
- const currentUser = userStore.state.currentUser;
- const sortOrder = viewStore.state.orderByTimeAsc;
-
- // Call actions
- const handleCreate = async () => {
- await memoStore.createMemo({ content: "Hello" });
- };
-
- const toggleSort = () => {
- viewStore.toggleSortOrder();
- };
-
- return ...
;
-});
-```
-
-### Server Store Pattern
-
-```typescript
-// Fetch data with automatic deduplication
-const memo = await memoStore.getOrFetchMemoByName("memos/123");
-
-// Update with optimistic UI updates
-await memoStore.updateMemo({ name: "memos/123", content: "Updated" }, ["content"]);
-
-// Errors are wrapped in StoreError
-try {
- await memoStore.deleteMemo("memos/123");
-} catch (error) {
- if (error instanceof StoreError) {
- console.error(error.code, error.message);
- }
-}
-```
-
-### Client Store Pattern
-
-```typescript
-// View preferences (persisted to localStorage)
-viewStore.setLayout("MASONRY");
-viewStore.toggleSortOrder();
-
-// Filters (synced to URL)
-memoFilterStore.addFilter({ factor: "tagSearch", value: "work" });
-memoFilterStore.removeFiltersByFactor("tagSearch");
-memoFilterStore.clearAllFilters();
-```
-
-## Creating New Stores
-
-### Server State Store
-
-```typescript
-import { StandardState, createServerStore } from "./base-store";
-import { createRequestKey, StoreError } from "./store-utils";
-
-class MyState extends StandardState {
- dataMap: Record = {};
-
- get items() {
- return Object.values(this.dataMap);
- }
-}
-
-const myStore = (() => {
- const base = createServerStore(new MyState(), {
- name: "myStore",
- enableDeduplication: true,
- });
-
- const { state, executeRequest } = base;
-
- const fetchItems = async () => {
- return executeRequest(
- createRequestKey("fetchItems"),
- async () => {
- const items = await api.fetchItems();
- state.setPartial({ dataMap: items });
- return items;
- },
- "FETCH_ITEMS_FAILED"
- );
- };
-
- return { state, fetchItems };
-})();
-```
-
-### Client State Store
-
-```typescript
-import { StandardState } from "./base-store";
-
-class MyState extends StandardState {
- preference: string = "default";
-
- setPartial(partial: Partial) {
- Object.assign(this, partial);
- // Optional: persist to localStorage
- localStorage.setItem("my-preference", JSON.stringify(this));
- }
-}
-
-const myStore = (() => {
- const state = new MyState();
-
- const setPreference = (value: string) => {
- state.setPartial({ preference: value });
- };
-
- return { state, setPreference };
-})();
-```
-
-## Best Practices
-
-### ✅ Do
-
-- Use `observer()` HOC for components that access store state
-- Call store actions from event handlers
-- Use computed properties for derived state
-- Handle errors from async store operations
-- Keep stores focused on a single domain
-
-### ❌ Don't
-
-- Don't mutate store state directly - use `setPartial()` or action methods
-- Don't call async store methods during render
-- Don't mix server and client state in the same store
-- Don't access stores outside of React components (except initialization)
-
-## Performance Tips
-
-1. **Computed Properties**: Use getters for derived state - they're memoized by MobX
-2. **Request Deduplication**: Automatic for server stores - prevents wasted API calls
-3. **Optimistic Updates**: Used in `updateMemo` - immediate UI feedback
-4. **Fine-grained Reactivity**: MobX only re-renders components that access changed properties
-
-## Testing
-
-```typescript
-import { memoStore } from "@/store";
-
-describe("memoStore", () => {
- it("should fetch memos", async () => {
- const memos = await memoStore.fetchMemos({ filter: "..." });
- expect(memos).toBeDefined();
- });
-
- it("should cache memos", () => {
- const memo = memoStore.getMemoByName("memos/123");
- expect(memo).toBeDefined();
- });
-});
-```
-
-## Migration Guide
-
-If you're migrating from old store patterns:
-
-1. **Replace direct state mutations** with `setPartial()`:
- ```typescript
- // Before
- store.state.value = 5;
-
- // After
- store.state.setPartial({ value: 5 });
- ```
-
-2. **Wrap API calls** with `executeRequest()`:
- ```typescript
- // Before
- const data = await api.fetch();
- state.data = data;
-
- // After
- return executeRequest("fetchData", async () => {
- const data = await api.fetch();
- state.setPartial({ data });
- return data;
- }, "FETCH_FAILED");
- ```
-
-3. **Use StandardState** for new stores:
- ```typescript
- // Before
- class State {
- constructor() { makeAutoObservable(this); }
- }
-
- // After
- class State extends StandardState {
- // makeAutoObservable() called automatically
- }
- ```
-
-## Troubleshooting
-
-**Q: Component not re-rendering when state changes?**
-A: Make sure you wrapped it with `observer()` from `mobx-react-lite`.
-
-**Q: Getting "Cannot modify state outside of actions" error?**
-A: Use `state.setPartial()` instead of direct mutations.
-
-**Q: API calls firing multiple times?**
-A: Check that your store uses `createServerStore()` with deduplication enabled.
-
-**Q: localStorage not persisting?**
-A: Ensure your client store overrides `setPartial()` to call `localStorage.setItem()`.
-
-## Resources
-
-- [MobX Documentation](https://mobx.js.org/)
-- [mobx-react-lite](https://github.com/mobxjs/mobx-react-lite)
-- [Store Pattern Guide](./base-store.ts)
diff --git a/web/src/store/attachment.ts b/web/src/store/attachment.ts
deleted file mode 100644
index 772fc7bf2..000000000
--- a/web/src/store/attachment.ts
+++ /dev/null
@@ -1,143 +0,0 @@
-// Attachment Store - manages file attachment state including uploads and metadata
-import { computed, makeObservable, observable } from "mobx";
-import { attachmentServiceClient } from "@/connect";
-import { Attachment, CreateAttachmentRequest, UpdateAttachmentRequest } from "@/types/proto/api/v1/attachment_service_pb";
-import { createServerStore, StandardState } from "./base-store";
-import { createRequestKey } from "./store-utils";
-
-class AttachmentState extends StandardState {
- // Map of attachments indexed by resource name (e.g., "attachments/123")
- attachmentMapByName: Record = {};
-
- constructor() {
- super();
- makeObservable(this, {
- attachmentMapByName: observable,
- attachments: computed,
- size: computed,
- });
- }
-
- get attachments(): Attachment[] {
- return Object.values(this.attachmentMapByName);
- }
-
- get size(): number {
- return Object.keys(this.attachmentMapByName).length;
- }
-}
-
-const attachmentStore = (() => {
- const base = createServerStore(new AttachmentState(), {
- name: "attachment",
- enableDeduplication: true,
- });
-
- const { state, executeRequest } = base;
-
- const fetchAttachmentByName = async (name: string): Promise => {
- const requestKey = createRequestKey("fetchAttachment", { name });
-
- return executeRequest(
- requestKey,
- async () => {
- const attachment = await attachmentServiceClient.getAttachment({ name });
-
- // Update cache
- state.setPartial({
- attachmentMapByName: {
- ...state.attachmentMapByName,
- [attachment.name]: attachment,
- },
- });
-
- return attachment;
- },
- "FETCH_ATTACHMENT_FAILED",
- );
- };
-
- const getAttachmentByName = (name: string): Attachment | undefined => {
- return state.attachmentMapByName[name];
- };
-
- const getOrFetchAttachmentByName = async (name: string): Promise => {
- const cached = getAttachmentByName(name);
- if (cached) {
- return cached;
- }
- return fetchAttachmentByName(name);
- };
-
- const createAttachment = async (attachment: Attachment): Promise => {
- return executeRequest(
- "", // No deduplication for creates
- async () => {
- const result = await attachmentServiceClient.createAttachment({ attachment });
-
- // Add to cache
- state.setPartial({
- attachmentMapByName: {
- ...state.attachmentMapByName,
- [result.name]: result,
- },
- });
-
- return result;
- },
- "CREATE_ATTACHMENT_FAILED",
- );
- };
-
- const updateAttachment = async (request: UpdateAttachmentRequest): Promise => {
- return executeRequest(
- "", // No deduplication for updates
- async () => {
- const attachment = await attachmentServiceClient.updateAttachment(request);
-
- // Update cache
- state.setPartial({
- attachmentMapByName: {
- ...state.attachmentMapByName,
- [attachment.name]: attachment,
- },
- });
-
- return attachment;
- },
- "UPDATE_ATTACHMENT_FAILED",
- );
- };
-
- const deleteAttachment = async (name: string): Promise => {
- return executeRequest(
- "", // No deduplication for deletes
- async () => {
- await attachmentServiceClient.deleteAttachment({ name });
-
- // Remove from cache
- const attachmentMap = { ...state.attachmentMapByName };
- delete attachmentMap[name];
- state.setPartial({ attachmentMapByName: attachmentMap });
- },
- "DELETE_ATTACHMENT_FAILED",
- );
- };
-
- const clearCache = (): void => {
- state.setPartial({ attachmentMapByName: {} });
- };
-
- return {
- state,
- fetchAttachmentByName,
- getAttachmentByName,
- getOrFetchAttachmentByName,
- createAttachment,
- updateAttachment,
- deleteAttachment,
- clearCache,
- };
-})();
-
-export default attachmentStore;
diff --git a/web/src/store/base-store.ts b/web/src/store/base-store.ts
deleted file mode 100644
index 20da1cace..000000000
--- a/web/src/store/base-store.ts
+++ /dev/null
@@ -1,96 +0,0 @@
-// Base store classes and utilities for consistent store patterns
-// - BaseServerStore: For stores that fetch data from APIs
-// - BaseClientStore: For stores that manage UI/client state
-import { action, makeObservable } from "mobx";
-import { RequestDeduplicator, StoreError } from "./store-utils";
-
-export interface BaseState {
- setPartial(partial: Partial): void;
-}
-
-export interface ServerStoreConfig {
- enableDeduplication?: boolean;
- name: string;
-}
-
-export function createServerStore(state: TState, config: ServerStoreConfig) {
- const deduplicator = config.enableDeduplication !== false ? new RequestDeduplicator() : null;
-
- return {
- state,
- deduplicator,
- name: config.name,
-
- async executeRequest(key: string, operation: () => Promise, errorCode?: string): Promise {
- try {
- if (deduplicator && key) {
- return await deduplicator.execute(key, operation);
- }
- return await operation();
- } catch (error) {
- if (StoreError.isAbortError(error)) {
- throw error;
- }
- throw StoreError.wrap(errorCode || `${config.name.toUpperCase()}_OPERATION_FAILED`, error);
- }
- },
- };
-}
-
-export interface ClientStoreConfig {
- name: string;
- persistence?: {
- key: string;
- serialize?: (state: any) => string;
- deserialize?: (data: string) => any;
- };
-}
-
-export function createClientStore(state: TState, config: ClientStoreConfig) {
- // Load from localStorage if enabled
- if (config.persistence) {
- try {
- const cached = localStorage.getItem(config.persistence.key);
- if (cached) {
- const data = config.persistence.deserialize ? config.persistence.deserialize(cached) : JSON.parse(cached);
- Object.assign(state, data);
- }
- } catch (error) {
- console.warn(`Failed to load ${config.name} from localStorage:`, error);
- }
- }
-
- return {
- state,
- name: config.name,
-
- persist(): void {
- if (config.persistence) {
- try {
- const data = config.persistence.serialize ? config.persistence.serialize(state) : JSON.stringify(state);
- localStorage.setItem(config.persistence.key, data);
- } catch (error) {
- console.warn(`Failed to persist ${config.name}:`, error);
- }
- }
- },
-
- clearPersistence(): void {
- if (config.persistence) {
- localStorage.removeItem(config.persistence.key);
- }
- },
- };
-}
-
-export abstract class StandardState implements BaseState {
- constructor() {
- makeObservable(this, {
- setPartial: action,
- });
- }
-
- setPartial(partial: Partial): void {
- Object.assign(this, partial);
- }
-}
diff --git a/web/src/store/config.ts b/web/src/store/config.ts
deleted file mode 100644
index 7ea3baa13..000000000
--- a/web/src/store/config.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-// MobX configuration for strict state management
-// Enforces best practices: state changes must happen in actions, computed values cannot have side effects
-import { configure } from "mobx";
-
-configure({
- // Enforce that all state mutations happen within actions (start permissive, can upgrade later)
- enforceActions: "never",
- // Use Proxies for better performance and ES6 compatibility (required for makeAutoObservable)
- useProxies: "always",
- // Isolate global state to prevent accidental sharing between tests
- isolateGlobalState: true,
- // Disable error boundaries so errors propagate normally
- disableErrorBoundaries: false,
-});
-
-export function enableStrictMode() {
- if (import.meta.env.DEV) {
- configure({
- enforceActions: "observed",
- computedRequiresReaction: false,
- reactionRequiresObservable: false,
- });
- console.info("✓ MobX strict mode enabled");
- }
-}
-
-export function enableProductionMode() {
- configure({
- enforceActions: "never",
- disableErrorBoundaries: false,
- });
-}
diff --git a/web/src/store/index.ts b/web/src/store/index.ts
deleted file mode 100644
index 25b57b6ee..000000000
--- a/web/src/store/index.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-// Store Module - exports all application stores and their types
-// Server State Stores (fetch/cache backend data): memoStore, userStore, instanceStore, attachmentStore
-// Client State Stores (UI preferences): viewStore, memoFilterStore
-import attachmentStore from "./attachment";
-import instanceStore from "./instance";
-import memoStore from "./memo";
-// Client State Stores
-import memoFilterStore from "./memoFilter";
-import userStore from "./user";
-import viewStore from "./view";
-
-export type { BaseState, ClientStoreConfig, ServerStoreConfig } from "./base-store";
-export { createClientStore, createServerStore, StandardState } from "./base-store";
-// Re-export common utilities
-export {
- activityNamePrefix,
- extractIdentityProviderIdFromName,
- extractMemoIdFromName,
- extractUserIdFromName,
- identityProviderNamePrefix,
- instanceSettingNamePrefix,
- memoNamePrefix,
- userNamePrefix,
-} from "./common";
-// Re-export filter types
-export type { FilterFactor, MemoFilter } from "./memoFilter";
-export { getMemoFilterKey, parseFilterQuery, stringifyFilters } from "./memoFilter";
-// Utilities and Types
-export { createRequestKey, RequestDeduplicator, StoreError } from "./store-utils";
-// Re-export view types
-export type { LayoutMode } from "./view";
-
-// Export store instances
-export {
- // Server state stores
- memoStore,
- userStore,
- instanceStore,
- attachmentStore,
- // Client state stores
- memoFilterStore,
- viewStore,
-};
-
-export const stores = {
- // Server state
- server: {
- memo: memoStore,
- user: userStore,
- instance: instanceStore,
- attachment: attachmentStore,
- },
-
- // Client state
- client: {
- memoFilter: memoFilterStore,
- view: viewStore,
- },
-} as const;
diff --git a/web/src/store/instance.ts b/web/src/store/instance.ts
deleted file mode 100644
index d9d9a834a..000000000
--- a/web/src/store/instance.ts
+++ /dev/null
@@ -1,137 +0,0 @@
-// Instance Store - manages instance-level configuration and settings
-import { create } from "@bufbuild/protobuf";
-import { uniqBy } from "lodash-es";
-import { computed } from "mobx";
-import { instanceServiceClient } from "@/connect";
-import {
- InstanceProfile,
- InstanceProfileSchema,
- InstanceSetting,
- InstanceSetting_GeneralSetting,
- InstanceSetting_GeneralSettingSchema,
- InstanceSetting_Key,
- InstanceSetting_MemoRelatedSetting,
- InstanceSetting_MemoRelatedSettingSchema,
- InstanceSettingSchema,
-} from "@/types/proto/api/v1/instance_service_pb";
-import { createServerStore, StandardState } from "./base-store";
-import { buildInstanceSettingName, getInstanceSettingKeyName, instanceSettingNamePrefix } from "./common";
-import { createRequestKey } from "./store-utils";
-
-class InstanceState extends StandardState {
- profile: InstanceProfile = create(InstanceProfileSchema, {});
- settings: InstanceSetting[] = [];
-
- // Computed property for general settings (memoized)
- get generalSetting(): InstanceSetting_GeneralSetting {
- return computed(() => {
- const setting = this.settings.find((s) => s.name === `${instanceSettingNamePrefix}GENERAL`);
- if (setting?.value.case === "generalSetting") {
- return setting.value.value;
- }
- return create(InstanceSetting_GeneralSettingSchema, {});
- }).get();
- }
-
- // Computed property for memo-related settings (memoized)
- get memoRelatedSetting(): InstanceSetting_MemoRelatedSetting {
- return computed(() => {
- const setting = this.settings.find((s) => s.name === `${instanceSettingNamePrefix}MEMO_RELATED`);
- if (setting?.value.case === "memoRelatedSetting") {
- return setting.value.value;
- }
- return create(InstanceSetting_MemoRelatedSettingSchema, {});
- }).get();
- }
-}
-
-const instanceStore = (() => {
- const base = createServerStore(new InstanceState(), {
- name: "instance",
- enableDeduplication: true,
- });
-
- const { state, executeRequest } = base;
-
- const fetchInstanceSetting = async (settingKey: InstanceSetting_Key): Promise => {
- const requestKey = createRequestKey("fetchInstanceSetting", { key: settingKey });
-
- return executeRequest(
- requestKey,
- async () => {
- const setting = await instanceServiceClient.getInstanceSetting({
- name: buildInstanceSettingName(settingKey),
- });
-
- // Merge into settings array, avoiding duplicates
- state.setPartial({
- settings: uniqBy([setting, ...state.settings], "name"),
- });
- },
- "FETCH_INSTANCE_SETTING_FAILED",
- );
- };
-
- const upsertInstanceSetting = async (setting: InstanceSetting): Promise => {
- return executeRequest(
- "", // No deduplication for updates
- async () => {
- await instanceServiceClient.updateInstanceSetting({ setting });
-
- // Update local state
- state.setPartial({
- settings: uniqBy([setting, ...state.settings], "name"),
- });
- },
- "UPDATE_INSTANCE_SETTING_FAILED",
- );
- };
-
- const getInstanceSettingByKey = (settingKey: InstanceSetting_Key): InstanceSetting => {
- const setting = state.settings.find((s) => s.name === buildInstanceSettingName(settingKey));
- return setting || create(InstanceSettingSchema, {});
- };
-
- const fetchInstanceProfile = async (): Promise => {
- const requestKey = createRequestKey("fetchInstanceProfile");
-
- return executeRequest(
- requestKey,
- async () => {
- const profile = await instanceServiceClient.getInstanceProfile({});
- state.setPartial({ profile });
- return profile;
- },
- "FETCH_INSTANCE_PROFILE_FAILED",
- );
- };
-
- return {
- state,
- fetchInstanceSetting,
- fetchInstanceProfile,
- upsertInstanceSetting,
- getInstanceSettingByKey,
- };
-})();
-
-// Initialize the instance store - called once at app startup
-export const initialInstanceStore = async (): Promise => {
- try {
- // Fetch instance profile
- const instanceProfile = await instanceStore.fetchInstanceProfile();
-
- // Fetch required settings
- await Promise.all([
- instanceStore.fetchInstanceSetting(InstanceSetting_Key.GENERAL),
- instanceStore.fetchInstanceSetting(InstanceSetting_Key.MEMO_RELATED),
- ]);
-
- // Apply settings to state
- Object.assign(instanceStore.state, { profile: instanceProfile });
- } catch (error) {
- console.error("Failed to initialize instance store:", error);
- }
-};
-
-export default instanceStore;
diff --git a/web/src/store/memo.ts b/web/src/store/memo.ts
deleted file mode 100644
index cdd1bf497..000000000
--- a/web/src/store/memo.ts
+++ /dev/null
@@ -1,189 +0,0 @@
-import { create } from "@bufbuild/protobuf";
-import { FieldMaskSchema } from "@bufbuild/protobuf/wkt";
-import { uniqueId } from "lodash-es";
-import { makeAutoObservable } from "mobx";
-import { memoServiceClient } from "@/connect";
-import { CreateMemoRequest, ListMemosRequest, ListMemosRequestSchema, Memo, MemoSchema } from "@/types/proto/api/v1/memo_service_pb";
-import { createRequestKey, RequestDeduplicator, StoreError } from "./store-utils";
-import userStore from "./user";
-
-class LocalState {
- stateId: string = uniqueId();
- memoMapByName: Record = {};
- currentRequest: AbortController | null = null;
-
- constructor() {
- makeAutoObservable(this);
- }
-
- setPartial(partial: Partial) {
- Object.assign(this, partial);
- }
-
- updateStateId() {
- this.stateId = uniqueId();
- }
-
- get memos() {
- return Object.values(this.memoMapByName);
- }
-
- get size() {
- return Object.keys(this.memoMapByName).length;
- }
-}
-
-const memoStore = (() => {
- const state = new LocalState();
- const deduplicator = new RequestDeduplicator();
-
- const fetchMemos = async (request: Partial) => {
- // Deduplicate requests with the same parameters
- const requestKey = createRequestKey("fetchMemos", request as Record);
-
- return deduplicator.execute(requestKey, async () => {
- if (state.currentRequest) {
- state.currentRequest.abort();
- }
-
- const controller = new AbortController();
- state.setPartial({ currentRequest: controller });
-
- try {
- const { memos, nextPageToken } = await memoServiceClient.listMemos(
- create(ListMemosRequestSchema, request as Record),
- { signal: controller.signal },
- );
-
- if (!controller.signal.aborted) {
- const memoMap = request.pageToken ? { ...state.memoMapByName } : {};
- for (const memo of memos) {
- memoMap[memo.name] = memo;
- }
- state.setPartial({
- stateId: uniqueId(),
- memoMapByName: memoMap,
- });
- return { memos, nextPageToken };
- }
- } catch (error: any) {
- if (StoreError.isAbortError(error)) {
- return;
- }
- throw StoreError.wrap("FETCH_MEMOS_FAILED", error);
- } finally {
- if (state.currentRequest === controller) {
- state.setPartial({ currentRequest: null });
- }
- }
- });
- };
-
- const getOrFetchMemoByName = async (name: string, options?: { skipCache?: boolean; skipStore?: boolean }) => {
- const memoCache = state.memoMapByName[name];
- if (memoCache && !options?.skipCache) {
- return memoCache;
- }
-
- const memo = await memoServiceClient.getMemo({
- name,
- });
-
- if (!options?.skipStore) {
- const memoMap = { ...state.memoMapByName };
- memoMap[name] = memo;
- state.setPartial({
- stateId: uniqueId(),
- memoMapByName: memoMap,
- });
- }
-
- return memo;
- };
-
- const getMemoByName = (name: string) => {
- return state.memoMapByName[name];
- };
-
- const createMemo = async (memoToCreate: Memo) => {
- const memo = await memoServiceClient.createMemo({ memo: memoToCreate });
- const memoMap = { ...state.memoMapByName };
- memoMap[memo.name] = memo;
- state.setPartial({
- stateId: uniqueId(),
- memoMapByName: memoMap,
- });
- // Refresh user stats to update tag counts
- userStore.fetchUserStats().catch(console.error);
- return memo;
- };
-
- const updateMemo = async (update: Partial, updateMask: string[]) => {
- // Optimistic update: immediately update the UI
- const previousMemo = state.memoMapByName[update.name!];
- const optimisticMemo = { ...previousMemo, ...update };
-
- // Apply optimistic update
- const memoMap = { ...state.memoMapByName };
- memoMap[update.name!] = optimisticMemo;
- state.setPartial({
- stateId: uniqueId(),
- memoMapByName: memoMap,
- });
-
- try {
- // Perform actual server update
- const memo = await memoServiceClient.updateMemo({
- memo: create(MemoSchema, update as Record),
- updateMask: create(FieldMaskSchema, { paths: updateMask }),
- });
-
- // Confirm with server response
- const confirmedMemoMap = { ...state.memoMapByName };
- confirmedMemoMap[memo.name] = memo;
- state.setPartial({
- stateId: uniqueId(),
- memoMapByName: confirmedMemoMap,
- });
- // Refresh user stats to update tag counts
- userStore.fetchUserStats().catch(console.error);
- return memo;
- } catch (error) {
- // Rollback on error
- const rollbackMemoMap = { ...state.memoMapByName };
- rollbackMemoMap[update.name!] = previousMemo;
- state.setPartial({
- stateId: uniqueId(),
- memoMapByName: rollbackMemoMap,
- });
- throw StoreError.wrap("UPDATE_MEMO_FAILED", error);
- }
- };
-
- const deleteMemo = async (name: string) => {
- await memoServiceClient.deleteMemo({
- name,
- });
-
- const memoMap = { ...state.memoMapByName };
- delete memoMap[name];
- state.setPartial({
- stateId: uniqueId(),
- memoMapByName: memoMap,
- });
- // Refresh user stats to update tag counts
- userStore.fetchUserStats().catch(console.error);
- };
-
- return {
- state,
- fetchMemos,
- getOrFetchMemoByName,
- getMemoByName,
- createMemo,
- updateMemo,
- deleteMemo,
- };
-})();
-
-export default memoStore;
diff --git a/web/src/store/memoFilter.ts b/web/src/store/memoFilter.ts
deleted file mode 100644
index 49e3a884e..000000000
--- a/web/src/store/memoFilter.ts
+++ /dev/null
@@ -1,138 +0,0 @@
-// Memo Filter Store - manages active memo filters and search state
-// This is a client state store that syncs with URL query parameters
-import { uniqBy } from "lodash-es";
-import { action, computed, makeObservable, observable } from "mobx";
-import { StandardState } from "./base-store";
-
-export type FilterFactor =
- | "tagSearch"
- | "visibility"
- | "contentSearch"
- | "displayTime"
- | "pinned"
- | "property.hasLink"
- | "property.hasTaskList"
- | "property.hasCode";
-
-export interface MemoFilter {
- factor: FilterFactor;
- value: string;
-}
-
-export const getMemoFilterKey = (filter: MemoFilter): string => `${filter.factor}:${filter.value}`;
-
-export const parseFilterQuery = (query: string | null): MemoFilter[] => {
- if (!query) return [];
-
- try {
- return query.split(",").map((filterStr) => {
- const [factor, value] = filterStr.split(":");
- return {
- factor: factor as FilterFactor,
- value: decodeURIComponent(value || ""),
- };
- });
- } catch (error) {
- console.error("Failed to parse filter query:", error);
- return [];
- }
-};
-
-export const stringifyFilters = (filters: MemoFilter[]): string => {
- return filters.map((filter) => `${filter.factor}:${encodeURIComponent(filter.value)}`).join(",");
-};
-
-class MemoFilterState extends StandardState {
- filters: MemoFilter[] = [];
- shortcut?: string = undefined;
-
- constructor() {
- super();
- makeObservable(this, {
- filters: observable,
- shortcut: observable,
- hasActiveFilters: computed,
- setFilters: action,
- addFilter: action,
- removeFilter: action,
- removeFiltersByFactor: action,
- clearAllFilters: action,
- setShortcut: action,
- });
- this.initFromURL();
- }
-
- private initFromURL(): void {
- try {
- const searchParams = new URLSearchParams(window.location.search);
- this.filters = parseFilterQuery(searchParams.get("filter"));
- } catch (error) {
- console.warn("Failed to parse filters from URL:", error);
- this.filters = [];
- }
- }
-
- getFiltersByFactor(factor: FilterFactor): MemoFilter[] {
- return this.filters.filter((f) => f.factor === factor);
- }
-
- setFilters(filters: MemoFilter[]): void {
- this.filters = filters;
- }
-
- addFilter(filter: MemoFilter): void {
- this.filters = uniqBy([...this.filters, filter], getMemoFilterKey);
- }
-
- removeFilter(predicate: (f: MemoFilter) => boolean): void {
- this.filters = this.filters.filter((f) => !predicate(f));
- }
-
- removeFiltersByFactor(factor: FilterFactor): void {
- this.filters = this.filters.filter((f) => f.factor !== factor);
- }
-
- clearAllFilters(): void {
- this.filters = [];
- this.shortcut = undefined;
- }
-
- setShortcut(shortcut?: string): void {
- this.shortcut = shortcut;
- }
-
- hasFilter(filter: MemoFilter): boolean {
- return this.filters.some((f) => getMemoFilterKey(f) === getMemoFilterKey(filter));
- }
-
- get hasActiveFilters(): boolean {
- return this.filters.length > 0 || this.shortcut !== undefined;
- }
-}
-
-const memoFilterStore = (() => {
- const state = new MemoFilterState();
-
- return {
- state,
- get filters(): MemoFilter[] {
- return state.filters;
- },
- get shortcut(): string | undefined {
- return state.shortcut;
- },
- get hasActiveFilters(): boolean {
- return state.hasActiveFilters;
- },
- getFiltersByFactor: (factor: FilterFactor): MemoFilter[] => state.getFiltersByFactor(factor),
- setFilters: (filters: MemoFilter[]): void => state.setFilters(filters),
- addFilter: (filter: MemoFilter): void => state.addFilter(filter),
- removeFilter: (predicate: (f: MemoFilter) => boolean): void => state.removeFilter(predicate),
- removeFiltersByFactor: (factor: FilterFactor): void => state.removeFiltersByFactor(factor),
- clearAllFilters: (): void => state.clearAllFilters(),
- setShortcut: (shortcut?: string): void => state.setShortcut(shortcut),
- hasFilter: (filter: MemoFilter): boolean => state.hasFilter(filter),
- };
-})();
-
-export default memoFilterStore;
diff --git a/web/src/store/store-utils.ts b/web/src/store/store-utils.ts
deleted file mode 100644
index dff7c3d47..000000000
--- a/web/src/store/store-utils.ts
+++ /dev/null
@@ -1,106 +0,0 @@
-// Store utilities for MobX stores
-// Provides request deduplication, error handling, and other common patterns
-
-export class StoreError extends Error {
- constructor(
- public readonly code: string,
- message: string,
- public readonly originalError?: unknown,
- ) {
- super(message);
- this.name = "StoreError";
- }
-
- static isAbortError(error: unknown): boolean {
- return error instanceof Error && error.name === "AbortError";
- }
-
- static wrap(code: string, error: unknown, customMessage?: string): StoreError {
- if (error instanceof StoreError) {
- return error;
- }
-
- const message = customMessage || (error instanceof Error ? error.message : "Unknown error");
- return new StoreError(code, message, error);
- }
-}
-
-// Request deduplication manager - prevents multiple identical requests
-export class RequestDeduplicator {
- private pendingRequests = new Map>();
-
- async execute(key: string, requestFn: () => Promise): Promise {
- // Check if this request is already pending
- if (this.pendingRequests.has(key)) {
- return this.pendingRequests.get(key) as Promise;
- }
-
- // Create new request
- const promise = requestFn().finally(() => {
- // Clean up after request completes (success or failure)
- this.pendingRequests.delete(key);
- });
-
- // Store the pending request
- this.pendingRequests.set(key, promise);
-
- return promise;
- }
-
- clear(): void {
- this.pendingRequests.clear();
- }
-
- isPending(key: string): boolean {
- return this.pendingRequests.has(key);
- }
-
- get size(): number {
- return this.pendingRequests.size;
- }
-}
-
-export function createRequestKey(prefix: string, params?: Record): string {
- if (!params) {
- return prefix;
- }
-
- // Sort keys for consistent hashing
- const sortedParams = Object.keys(params)
- .sort()
- .reduce(
- (acc, key) => {
- acc[key] = params[key];
- return acc;
- },
- {} as Record,
- );
-
- return `${prefix}:${JSON.stringify(sortedParams)}`;
-}
-
-// Optimistic update helper with rollback on error
-export class OptimisticUpdate {
- constructor(
- private getCurrentState: () => T,
- private setState: (state: T) => void,
- ) {}
-
- async execute(optimisticState: T, updateFn: () => Promise): Promise {
- const previousState = this.getCurrentState();
-
- try {
- // Apply optimistic update immediately
- this.setState(optimisticState);
-
- // Perform actual update
- const result = await updateFn();
-
- return result;
- } catch (error) {
- // Rollback on error
- this.setState(previousState);
- throw error;
- }
- }
-}
diff --git a/web/src/store/user.ts b/web/src/store/user.ts
deleted file mode 100644
index b0d7433a1..000000000
--- a/web/src/store/user.ts
+++ /dev/null
@@ -1,375 +0,0 @@
-import { create } from "@bufbuild/protobuf";
-import { FieldMaskSchema } from "@bufbuild/protobuf/wkt";
-import { uniqueId } from "lodash-es";
-import { computed, makeAutoObservable } from "mobx";
-import { clearAccessToken, setAccessToken } from "@/auth-state";
-import { authServiceClient, shortcutServiceClient, userServiceClient } from "@/connect";
-import { Shortcut } from "@/types/proto/api/v1/shortcut_service_pb";
-import {
- User,
- UserNotification,
- UserSetting,
- UserSetting_GeneralSetting,
- UserSetting_Key,
- UserSetting_WebhooksSetting,
- UserSettingSchema,
- UserStats,
-} from "@/types/proto/api/v1/user_service_pb";
-import { buildUserSettingName } from "./common";
-import { createRequestKey, RequestDeduplicator, StoreError } from "./store-utils";
-
-// Helper to extract setting value from UserSetting oneof
-function getSettingValue(setting: UserSetting, caseType: string): T | undefined {
- if (setting.value.case === caseType) {
- return setting.value.value as T;
- }
- return undefined;
-}
-
-class LocalState {
- currentUser?: string;
- userGeneralSetting?: UserSetting_GeneralSetting;
- userWebhooksSetting?: UserSetting_WebhooksSetting;
- shortcuts: Shortcut[] = [];
- notifications: UserNotification[] = [];
- userMapByName: Record = {};
- userStatsByName: Record = {};
-
- // The state id of user stats map.
- statsStateId = uniqueId();
-
- // Computed property that aggregates tag counts across all users (memoized)
- get tagCount() {
- return computed(() => {
- const tagCount: Record = {};
- for (const stats of Object.values(this.userStatsByName)) {
- for (const tag of Object.keys(stats.tagCount)) {
- tagCount[tag] = (tagCount[tag] || 0) + stats.tagCount[tag];
- }
- }
- return tagCount;
- }).get();
- }
-
- get currentUserStats() {
- if (!this.currentUser) {
- return undefined;
- }
- // Backend returns stats with key "users/{id}/stats"
- return this.userStatsByName[`${this.currentUser}/stats`];
- }
-
- constructor() {
- makeAutoObservable(this);
- }
-
- setPartial(partial: Partial) {
- Object.assign(this, partial);
- }
-}
-
-const userStore = (() => {
- const state = new LocalState();
- const deduplicator = new RequestDeduplicator();
-
- const getOrFetchUser = async (name: string) => {
- const userMap = state.userMapByName;
- if (userMap[name]) {
- return userMap[name] as User;
- }
- const requestKey = createRequestKey("getOrFetchUser", { name });
- return deduplicator.execute(requestKey, async () => {
- // Double-check cache in case another request finished first
- if (state.userMapByName[name]) {
- return state.userMapByName[name] as User;
- }
- const user = await userServiceClient.getUser({
- name: name,
- });
- state.setPartial({
- userMapByName: {
- ...state.userMapByName,
- [name]: user,
- },
- });
- return user;
- });
- };
-
- const getUserByName = (name: string) => {
- return state.userMapByName[name];
- };
-
- const fetchUsers = async () => {
- const requestKey = createRequestKey("fetchUsers");
- return deduplicator.execute(requestKey, async () => {
- try {
- const { users } = await userServiceClient.listUsers({});
- const userMap = state.userMapByName;
- for (const user of users) {
- userMap[user.name] = user;
- }
- state.setPartial({
- userMapByName: userMap,
- });
- return users;
- } catch (error) {
- throw StoreError.wrap("FETCH_USERS_FAILED", error);
- }
- });
- };
-
- const updateUser = async (user: Partial, updateMaskPaths: string[]) => {
- const updatedUser = await userServiceClient.updateUser({
- user: user as User,
- updateMask: create(FieldMaskSchema, { paths: updateMaskPaths }),
- });
- state.setPartial({
- userMapByName: {
- ...state.userMapByName,
- [updatedUser.name]: updatedUser,
- },
- });
- };
-
- const deleteUser = async (name: string) => {
- await userServiceClient.deleteUser({ name });
- const userMap = state.userMapByName;
- delete userMap[name];
- state.setPartial({
- userMapByName: userMap,
- });
- };
-
- const updateUserGeneralSetting = async (generalSetting: Partial, updateMaskPaths: string[]) => {
- if (!state.currentUser) {
- throw new Error("No current user");
- }
-
- const settingName = buildUserSettingName(state.currentUser, UserSetting_Key.GENERAL);
- const userSetting = create(UserSettingSchema, {
- name: settingName,
- value: {
- case: "generalSetting",
- value: generalSetting as UserSetting_GeneralSetting,
- },
- });
-
- const updatedUserSetting = await userServiceClient.updateUserSetting({
- setting: userSetting,
- updateMask: create(FieldMaskSchema, { paths: updateMaskPaths }),
- });
-
- state.setPartial({
- userGeneralSetting: getSettingValue(updatedUserSetting, "generalSetting"),
- });
- };
-
- const getUserGeneralSetting = async () => {
- if (!state.currentUser) {
- throw new Error("No current user");
- }
-
- const settingName = buildUserSettingName(state.currentUser, UserSetting_Key.GENERAL);
- const userSetting = await userServiceClient.getUserSetting({ name: settingName });
- const generalSetting = getSettingValue(userSetting, "generalSetting");
-
- state.setPartial({
- userGeneralSetting: generalSetting,
- });
-
- return generalSetting;
- };
-
- const fetchUserSettings = async () => {
- if (!state.currentUser) {
- return;
- }
-
- // Fetch settings and shortcuts in parallel for better performance
- const [{ settings }, { shortcuts }] = await Promise.all([
- userServiceClient.listUserSettings({ parent: state.currentUser }),
- shortcutServiceClient.listShortcuts({ parent: state.currentUser }),
- ]);
-
- // Extract and store each setting type using the oneof pattern
- const generalSetting = settings.find((s) => s.value.case === "generalSetting");
- const webhooksSetting = settings.find((s) => s.value.case === "webhooksSetting");
-
- state.setPartial({
- userGeneralSetting: generalSetting ? getSettingValue(generalSetting, "generalSetting") : undefined,
- userWebhooksSetting: webhooksSetting ? getSettingValue(webhooksSetting, "webhooksSetting") : undefined,
- shortcuts: shortcuts,
- });
- };
-
- // Note: fetchShortcuts is now handled by fetchUserSettings
- // The shortcuts are extracted from the user shortcuts setting
-
- const fetchNotifications = async () => {
- if (!state.currentUser) {
- throw new Error("No current user available");
- }
-
- const { notifications } = await userServiceClient.listUserNotifications({
- parent: state.currentUser,
- });
-
- state.setPartial({
- notifications,
- });
- };
-
- const updateNotification = async (notification: Partial, updateMaskPaths: string[]) => {
- const updatedNotification = await userServiceClient.updateUserNotification({
- notification: notification as UserNotification,
- updateMask: create(FieldMaskSchema, { paths: updateMaskPaths }),
- });
- state.setPartial({
- notifications: state.notifications.map((n) => {
- if (n.name === updatedNotification.name) {
- return updatedNotification;
- }
- return n;
- }),
- });
- return updatedNotification;
- };
-
- const deleteNotification = async (name: string) => {
- await userServiceClient.deleteUserNotification({ name });
- state.setPartial({
- notifications: state.notifications.filter((n) => n.name !== name),
- });
- };
-
- const fetchUserStats = async (user?: string) => {
- const requestKey = createRequestKey("fetchUserStats", { user });
- return deduplicator.execute(requestKey, async () => {
- try {
- const userStatsByName: Record = {};
- if (!user) {
- const { stats } = await userServiceClient.listAllUserStats({});
- for (const userStats of stats) {
- userStatsByName[userStats.name] = userStats;
- }
- } else {
- const userStats = await userServiceClient.getUserStats({ name: user });
- userStatsByName[userStats.name] = userStats; // Use userStats.name as key for consistency
- }
- state.setPartial({
- userStatsByName: {
- ...state.userStatsByName,
- ...userStatsByName,
- },
- statsStateId: uniqueId(), // Update state ID to trigger reactivity
- });
- } catch (error) {
- throw StoreError.wrap("FETCH_USER_STATS_FAILED", error);
- }
- });
- };
-
- const setStatsStateId = (id = uniqueId()) => {
- state.statsStateId = id;
- };
-
- return {
- state,
- getOrFetchUser,
- getUserByName,
- fetchUsers,
- updateUser,
- deleteUser,
- updateUserGeneralSetting,
- getUserGeneralSetting,
- fetchUserSettings,
- fetchNotifications,
- updateNotification,
- deleteNotification,
- fetchUserStats,
- setStatsStateId,
- };
-})();
-
-// Initializes the user store with proper sequencing:
-// 1. Fetch current authenticated user session
-// 2. Set current user in store (required for subsequent calls)
-// 3. Fetch user settings (depends on currentUser being set)
-//
-// Auth flow:
-// - On first call, GetCurrentSession has no access token
-// - The interceptor will automatically call RefreshToken using the HttpOnly refresh cookie
-// - If refresh succeeds, GetCurrentSession is retried with the new access token
-// - If refresh fails (no cookie or expired), user needs to login
-export const initialUserStore = async () => {
- try {
- // Step 1: Authenticate and get current user
- // The interceptor will handle token refresh if needed
- const { user: currentUser } = await authServiceClient.getCurrentUser({});
-
- if (!currentUser) {
- // No authenticated user - clear state
- clearAccessToken();
- userStore.state.setPartial({
- currentUser: undefined,
- userGeneralSetting: undefined,
- userMapByName: {},
- });
- return;
- }
-
- // Step 2: Set current user in store
- // CRITICAL: This must happen before fetchUserSettings() is called
- // because fetchUserSettings() depends on state.currentUser being set
- userStore.state.setPartial({
- currentUser: currentUser.name,
- userMapByName: {
- [currentUser.name]: currentUser,
- },
- });
-
- // Step 3: Fetch user settings and stats
- // CRITICAL: This must happen after currentUser is set in step 2
- // The fetchUserSettings() and fetchUserStats() methods check state.currentUser internally
- await Promise.all([userStore.fetchUserSettings(), userStore.fetchUserStats()]);
- } catch (error: any) {
- // Auth failed (no refresh token, expired, or other error)
- // Clear state and let user login again
- console.error("Failed to initialize user store:", error);
- clearAccessToken();
- userStore.state.setPartial({
- currentUser: undefined,
- userGeneralSetting: undefined,
- userMapByName: {},
- });
- }
-};
-
-// Logout function that clears tokens and state
-// This calls DeleteSession which:
-// 1. Revokes the refresh token in the database
-// 2. Clears both session and refresh token cookies
-// We then clear the in-memory access token and reset the store state
-export const logout = async () => {
- try {
- await authServiceClient.signOut({});
- } catch (error) {
- // Log error but continue with local cleanup
- console.error("Failed to delete session on server:", error);
- } finally {
- // Always clear local state, even if server call fails
- clearAccessToken();
- userStore.state.setPartial({
- currentUser: undefined,
- userGeneralSetting: undefined,
- userWebhooksSetting: undefined,
- shortcuts: [],
- notifications: [],
- userMapByName: {},
- userStatsByName: {},
- });
- }
-};
-
-export default userStore;
diff --git a/web/src/store/view.ts b/web/src/store/view.ts
deleted file mode 100644
index 9c72f5954..000000000
--- a/web/src/store/view.ts
+++ /dev/null
@@ -1,97 +0,0 @@
-import { makeObservable, observable } from "mobx";
-import { StandardState } from "./base-store";
-
-const LOCAL_STORAGE_KEY = "memos-view-setting";
-
-export type LayoutMode = "LIST" | "MASONRY";
-
-class ViewState extends StandardState {
- // Sort order: true = ascending (oldest first), false = descending (newest first)
- orderByTimeAsc: boolean = false;
- // Display layout mode: LIST (vertical list) or MASONRY (Pinterest-style grid)
- layout: LayoutMode = "LIST";
-
- constructor() {
- super();
- makeObservable(this, {
- orderByTimeAsc: observable,
- layout: observable,
- });
- }
-
- setPartial(partial: Partial): void {
- // Validate layout if provided
- if (partial.layout !== undefined && !["LIST", "MASONRY"].includes(partial.layout)) {
- console.warn(`Invalid layout "${partial.layout}", ignoring`);
- return;
- }
-
- Object.assign(this, partial);
-
- // Persist to localStorage
- try {
- localStorage.setItem(
- LOCAL_STORAGE_KEY,
- JSON.stringify({
- orderByTimeAsc: this.orderByTimeAsc,
- layout: this.layout,
- }),
- );
- } catch (error) {
- console.warn("Failed to persist view settings:", error);
- }
- }
-}
-
-const viewStore = (() => {
- const state = new ViewState();
-
- // Load from localStorage on initialization
- try {
- const cached = localStorage.getItem(LOCAL_STORAGE_KEY);
- if (cached) {
- const data = JSON.parse(cached);
-
- // Validate and restore orderByTimeAsc
- if (Object.hasOwn(data, "orderByTimeAsc")) {
- state.orderByTimeAsc = Boolean(data.orderByTimeAsc);
- }
-
- // Validate and restore layout
- if (Object.hasOwn(data, "layout") && ["LIST", "MASONRY"].includes(data.layout)) {
- state.layout = data.layout as LayoutMode;
- }
- }
- } catch (error) {
- console.warn("Failed to load view settings from localStorage:", error);
- }
-
- const toggleSortOrder = (): void => {
- state.setPartial({ orderByTimeAsc: !state.orderByTimeAsc });
- };
-
- const setLayout = (layout: LayoutMode): void => {
- state.setPartial({ layout });
- };
-
- const resetToDefaults = (): void => {
- state.setPartial({
- orderByTimeAsc: false,
- layout: "LIST",
- });
- };
-
- const clearStorage = (): void => {
- localStorage.removeItem(LOCAL_STORAGE_KEY);
- };
-
- return {
- state,
- toggleSortOrder,
- setLayout,
- resetToDefaults,
- clearStorage,
- };
-})();
-
-export default viewStore;