Add SSE event broadcasting for reaction changes so that when a user
adds or removes a reaction on one device, all other open instances
see the update in real-time.
Backend:
- Rename MemoEvent/MemoEventType to SSEEvent/SSEEventType for generality
- Add reaction.upserted and reaction.deleted event types
- Broadcast events from UpsertMemoReaction and DeleteMemoReaction,
using the reaction's ContentID (memo name) as the event name
Frontend:
- Handle reaction.upserted and reaction.deleted SSE events by
invalidating the affected memo detail cache and memo lists
- Rename internal handler to handleSSEEvent to reflect broader scope
Co-authored-by: milvasic <milvasic@users.noreply.github.com>
Implement real-time memo synchronization across all open browser instances
using Server-Sent Events (SSE). When a memo is created, updated, or
deleted on one device, all other connected clients receive the change
notification and automatically refresh their data.
Backend changes:
- Add SSEHub (pub/sub) for broadcasting memo change events to connected clients
- Add SSE HTTP endpoint at /api/v1/sse with Bearer token authentication
(supports both Authorization header and query parameter for EventSource)
- Broadcast memo.created, memo.updated, and memo.deleted events from
the memo service after successful operations
- Include SSEHub in APIV1Service and wire it into server initialization
- Update test helper to include SSEHub to prevent nil pointer panics
Frontend changes:
- Add useLiveMemoRefresh hook that connects to SSE endpoint using fetch
ReadableStream (supports custom auth headers unlike native EventSource)
- Automatically invalidate React Query caches on received events:
- memo.created: invalidate memo lists + user stats
- memo.updated: invalidate specific memo detail + memo lists
- memo.deleted: remove memo from cache + invalidate lists + user stats
- Exponential backoff reconnection (1s to 30s) on connection failures
- Integrate hook in AppInitializer for app-wide live refresh
- Add SSE-specific Vite dev proxy config with no timeout for streaming
Co-authored-by: milvasic <milvasic@users.noreply.github.com>
Fixes#5589
When the page returns from background to foreground after the JWT
token expires (~15 min), React Query's automatic refetch-on-focus
triggers multiple API calls simultaneously. These all fail with 401
Unauthorized, leaving the user with empty content.
Solution:
- Add useTokenRefreshOnFocus hook that listens to visibilitychange
- Proactively refresh token BEFORE React Query refetches
- Uses 2-minute buffer to catch expiring tokens early
- Graceful error handling - logs error but doesn't block
Changes:
- Created web/src/hooks/useTokenRefreshOnFocus.ts
- Updated isTokenExpired() to accept optional buffer parameter
- Exported refreshAccessToken() for use by the hook
- Integrated hook into AppInitializer (only when user authenticated)
Removed the hide-scrollbar CSS class and all its usages throughout the
codebase. Hiding scrollbars can hurt UX by making it unclear when
content is scrollable.
Changes:
- Removed hide-scrollbar CSS definition from index.css
- Removed hide-scrollbar class from Navigation component (2 instances)
- Removed hide-scrollbar class from MemoDetailSidebar (2 instances)
- Removed hide-scrollbar class from TagsSection
- Removed hide-scrollbar class from ShortcutsSection
Components now use standard browser scrollbar behavior, which provides
better visual feedback to users about scrollable content. Modern
browsers already handle scrollbar appearance elegantly.
Fixed issue #5579 where the calendar selection dialog was very laggy.
The root cause was rendering ~365 individual Tooltip components when
opening the year calendar view (one per day with activity). This created
a huge number of DOM nodes and event listeners that caused significant
performance issues.
Changes:
- Added disableTooltips prop to MonthCalendar and CalendarCell components
- Disabled tooltips in YearCalendar's small month views
- Removed unnecessary TooltipProvider wrapper in YearCalendar
- Tooltips remain enabled in the default month calendar view
Performance improvements:
- Eliminates ~365 tooltip instances when dialog opens
- Reduces initial render time significantly
- Makes dialog interactions smooth and responsive
Users can still click on dates to drill down for details if needed.
Fixed issue #5576 where clicking the edit button on a shortcut would
incorrectly open a create dialog instead of an edit dialog.
The root cause was an incorrect useEffect that was watching the shortcut
state itself instead of the initialShortcut prop. When the dialog was
opened for editing, the state wasn't properly reinitializing with the
existing shortcut data.
Changed the useEffect to:
- Watch initialShortcut as the dependency
- Reinitialize the shortcut state when initialShortcut changes
- Properly distinguishes between create (initialShortcut undefined) and
edit (initialShortcut has data) modes
- Add validation check for loading state before allowing save
- Prevents false "Content, attachment, or file required" error
- Occurs when user presses CTRL+Enter immediately after opening edit mode
- Editor state may still be loading when keyboard shortcut fires
Closes#5581
- Fix nested task lists not showing proper indentation
- Use simple CSS cascade with [&_ul.contains-task-list]:ml-6
- Fix checkbox clicks toggling wrong tasks in nested lists
- Search from memo root container for global task indexing
- Remove complex selectors in favor of standard approach
- Match behavior of GitHub, Notion, and other platforms
Closes#5575
Fixes issue where OAuth sign-in fails with 'Cannot read properties of
undefined (reading 'digest')' when accessing Memos over HTTP.
The crypto.subtle API is only available in secure contexts (HTTPS or
localhost), but PKCE (RFC 7636) is optional per OAuth 2.0 standards.
Changes:
- Make PKCE generation optional with graceful fallback
- Use PKCE when crypto.subtle available (HTTPS/localhost)
- Fall back to standard OAuth flow when unavailable (HTTP)
- Log warning to console when PKCE unavailable
- Only include code_challenge in auth URL when PKCE enabled
The backend already supports optional PKCE (empty codeVerifier), so no
backend changes needed. This fix aligns frontend behavior with backend.
Benefits:
- OAuth sign-in works on HTTP deployments (reverse proxy scenarios)
- Enhanced security (PKCE) still used when HTTPS available
- Backward compatible with OAuth providers that don't support PKCE
Fixes#5570
- Add workflow_dispatch trigger for manual binary builds
- Build for linux (amd64, arm64, arm/v7), darwin (amd64, arm64), windows (amd64)
- Package as tar.gz (Unix) and zip (Windows)
- Generate build summary with artifact sizes
- Remove unused showNsfwContent prop (was only used in MemoDetail to pre-reveal NSFW, which defeats the purpose)
- Inline useNsfwContent hook logic directly into MemoView.tsx (only 3 lines, no reusability benefit)
- Delete web/src/components/MemoView/hooks/useNsfwContent.ts
- NSFW content now consistently starts hidden across all pages
- Maintains same user experience: click to reveal, no toggle back
This removes unnecessary indirection and prop threading while preserving functionality.
Root cause: enabled={isInitialized && !!user} prevented displaying cached
data when user auth state transitioned during token refresh.
Changes:
- Remove !!user check from Home page enabled condition
- Add clearAccessToken() in redirectOnAuthFailure for clean logout
Fixes#5565
Add custom memos_unicode_lower() SQLite function to enable proper
case-insensitive text search for non-English languages (Cyrillic,
Greek, CJK, etc.).
Previously, SQLite's LOWER() only worked for ASCII characters due to
modernc.org/sqlite lacking ICU extension. This caused searches for
non-English text to be case-sensitive (e.g., searching 'блины' wouldn't
find 'Блины').
Changes:
- Add store/db/sqlite/functions.go with Unicode case folding function
- Register custom function using golang.org/x/text/cases.Fold()
- Update filter renderer to use custom function for SQLite dialect
- Add test for Unicode case-insensitive search
- Make golang.org/x/text a direct dependency
Fixes#5559
Fixes#5551
The Docker image now runs as non-root (UID 10001) for security, but this
breaks upgrades from 0.25.3 where data files were owned by root.
Changes:
- Dockerfile: Keep USER as root, install su-exec
- entrypoint.sh: Fix ownership of /var/opt/memos, then drop to non-root
- Supports custom MEMOS_UID/MEMOS_GID env vars for flexibility
This allows seamless upgrades without manual chown on the host.
Defense-in-depth fix: Add missing nil check before accessing
currentUser.ID and currentUser.Role in DeleteUser function.
While the auth interceptor should block unauthenticated requests,
this check prevents potential nil pointer panic if fetchCurrentUser
returns (nil, nil).
- Add serveMediaStream() to stream video/audio without loading into memory
- Use http.ServeFile for local files (zero-copy, handles range requests)
- Redirect to S3 presigned URLs for S3-stored media files
- Refactor for better maintainability:
- Extract constants and pre-compile lookup maps
- Consolidate duplicated S3 client creation logic
- Split authentication into focused helper methods
- Group code by responsibility with section comments
- Add setSecurityHeaders() and setMediaHeaders() helpers
- Remove menu item and dialog from MemoActionMenu
- Remove removeCompletedTasks() and hasCompletedTasks() utilities
- Remove translation keys from all 34 locale files
- Feature was not aligned with standard note-taking UX patterns
- Add same value check before updating createTime/updateTime
- Skip request if new timestamp equals current timestamp
- Simplify callback handlers and improve code readability
- Use .some() instead of .filter().length for cleaner code
- Changed InstanceProfile to include admin user field
- Updated GetInstanceProfile method to retrieve admin user
- Modified related tests to reflect changes in admin user retrieval
- Removed owner cache logic and tests, introducing new admin cache tests