- Blue highlight lines for both column and row inserts are now clipped
to the table boundaries via a relative overflow-hidden wrapper div
around the table, so they no longer extend beyond the table edges.
- Insert-column + button is now absolutely positioned with
left-1/2 top-1/2 -translate-x/y-1/2 for pixel-perfect centering
on the column border (previously used flex centering which was
slightly off due to Tooltip wrapper).
- Added ml-1 margin before the column delete button so it doesn't
overlap with the insert-column + button hover zone.
- Added a second '+ Add row' button just below the table (above the
footer), in addition to the one in the footer bar.
- Added '+ Add column' button in the footer bar, right next to the
'+ Add row' button.
Co-authored-by: milvasic <milvasic@users.noreply.github.com>
Insert zone improvements:
- Row insert: hover zone now only covers the row-number area (left edge)
instead of spanning the full row width, matching the column insert
pattern where the zone is between header cells. The + button is
positioned at the intersection of the horizontal row border and the
vertical first-column border (translate-x-1/2 on the right edge of
the row-number cell).
- Column insert: blue highlight line now extends through the entire
table (header + all data rows) using bottom: -200rem with
pointer-events-none so it doesn't block cell interactions.
- Row insert: blue highlight line extends across the full table width
using width: 200rem with pointer-events-none for the same reason.
- Removed the spacer <tr> approach for row inserts; zones are now
directly inside the row-number <td> with absolute positioning.
Visual changes:
- Headers are now square (removed rounded-tl-md from header cells and
rounded-bl-md from last-row cells).
- All buttons have explicit cursor-pointer class (sort, delete column,
delete row, add column, add row, insert column, insert row, cancel,
confirm, and the + insert buttons).
Co-authored-by: milvasic <milvasic@users.noreply.github.com>
Insert column/row zones:
- Expanded hover area to 32px wide (columns) and 20px tall (rows) so
the insert trigger is much easier to reach
- Column + button is centered right above the vertical border between
two columns, inside the header cells with absolute positioning
- Row + button is centered on the horizontal border between two rows,
rendered via a zero-height spacer <tr> with an absolutely positioned
hover zone
- z-index set to z-30 (above the z-20 sticky header) so column insert
buttons render on top of everything
- On hover, a 3px blue highlight line appears at the exact border where
the new column/row will be inserted, giving clear visual feedback
- The entire zone is clickable (not just the button) for easier use
Sticky header improvements:
- Added a solid bg-background mask row at the top of the sticky thead
that hides table cells scrolling underneath the header area
- All header cells including the row-number spacer and add-column button
now have explicit bg-background so nothing bleeds through
- Header cell background (bg-accent/50) now wraps the full cell content
including sort and delete buttons (moved bg from input to the
containing div), giving the header a cohesive look
Row insert zones use dedicated spacer <tr> elements between data rows
(instead of absolutely positioned elements inside cells), which is more
reliable across different table widths and avoids clipping issues.
Co-authored-by: milvasic <milvasic@users.noreply.github.com>
Table editor improvements:
1. Insert column between columns: Hovering over the border area above
two adjacent columns reveals a circular '+' button. Clicking it
inserts a new empty column at that position. The buttons appear at
70% opacity on hover over the gutter zone and full opacity on direct
hover.
2. Insert row between rows: Hovering over the border between two data
rows reveals a circular '+' button on the first cell. Clicking it
inserts a new empty row at that position.
3. Row delete button moved to end: The trash button for deleting a row
is now at the right end of the row (matching the column delete button
size at size-7) instead of the left side next to the row number.
4. Empty cell placeholder removed: Cell inputs no longer show '...' as
placeholder text when empty.
5. Add row button moved to footer: The 'Add row' button is now in the
footer bar next to the column/row count, alongside Cancel and Confirm
buttons, instead of floating below the table.
6. Sticky table header: The thead is now sticky (top-0, z-20) with a
background color, so column names remain visible when scrolling
through large tables.
Co-authored-by: milvasic <milvasic@users.noreply.github.com>
Five improvements:
1. Delete table button: A trash icon appears to the left of the edit
pencil on rendered tables (on hover). Clicking it opens a confirmation
dialog before removing the entire table from the memo content.
2. SSE connection status indicator: A small colored dot in the sidebar
navigation (above the user menu) shows the live-refresh connection
status:
- Green = connected, live updates active
- Yellow (pulsing) = connecting
- Red = disconnected, updates not live
Hover tooltip explains the current state. Uses useSyncExternalStore
for efficient re-renders from a singleton status store.
3. Always-visible action buttons: Sort and delete buttons in the table
editor are now always visible at 40% opacity (previously hidden until
hover). They become fully opaque on hover for better discoverability.
4. Larger table editor dialog: Fixed size of 56rem x 44rem (capped to
viewport) so the dialog is spacious regardless of table dimensions.
The table area scrolls within the fixed frame.
5. Monospace font in table editor: All cell inputs use Fira Code with
fallbacks to Fira Mono, JetBrains Mono, Cascadia Code, Consolas,
and system monospace for better alignment when editing tabular data.
Co-authored-by: milvasic <milvasic@users.noreply.github.com>
Add a dialog-based table editor that makes creating and editing markdown
tables much easier than manipulating raw pipe-delimited text.
Features:
- Visual grid of input fields for editing headers and cells
- Add and remove rows and columns
- Sort columns ascending/descending (supports both text and numeric)
- Tab key navigation between cells (auto-creates new rows at the end)
- Properly formatted/aligned markdown output on confirm
- Row numbers with hover-to-delete interaction
- Column sort indicators and remove buttons
Integration points:
1. Toolbar: New 'Table' button in the InsertMenu (+) dropdown opens the
dialog for creating new tables from the editor
2. Slash command: /table now opens the dialog instead of inserting raw
markdown, via new Command.action callback support
3. Rendered tables: Edit pencil icon appears on hover over rendered tables
in MemoContent, opens dialog pre-populated with parsed table data,
and saves changes directly via updateMemo mutation (same pattern as
TaskListItem checkbox toggling)
New files:
- utils/markdown-table.ts: Parse, serialize, find/replace markdown tables
- components/TableEditorDialog.tsx: Reusable table editor dialog component
Modified:
- Extended Command interface with optional action callback for dialogs
- SlashCommands handles action-based commands (skips text insertion)
- Editor accepts custom commands via props
- EditorContent creates commands with table editor wired in
- MemoEditor manages table dialog state shared between slash cmd and toolbar
- InsertMenu includes Table entry and its own dialog for toolbar flow
- Table.tsx (MemoContent) adds edit button and dialog for rendered tables
Co-authored-by: milvasic <milvasic@users.noreply.github.com>
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