From ebe7d36ed50720cf5f244b4269563f3ec9ee7bb3 Mon Sep 17 00:00:00 2001 From: Jamie Pine Date: Tue, 20 Jan 2026 10:35:48 -0800 Subject: [PATCH] feat(recents): add RecentsView for displaying recently indexed files This commit introduces the RecentsView component, which allows users to view recently indexed files sorted by the indexed_at timestamp. The view integrates seamlessly with existing components, ensuring consistent interactions such as keyboard navigation and drag-to-select. Additionally, it updates the Explorer context to support entering and exiting recents mode, enhancing the overall file browsing experience. --- TODO | 44 +++++++----- .../src/components/primitive/SettingsRow.tsx | 2 +- .../src/screens/browse/BrowseScreen.tsx | 2 +- .../src/screens/explorer/views/GridView.tsx | 2 +- core/src/ops/search/ephemeral_search.rs | 1 + core/src/ops/search/filters.rs | 1 + core/src/ops/search/input.rs | 2 + core/src/ops/search/query.rs | 2 + core/src/ops/search/sorting.rs | 3 + packages/interface/src/router.tsx | 7 +- .../interface/src/routes/explorer/context.tsx | 31 ++++++++- .../routes/explorer/hooks/useExplorerFiles.ts | 69 ++++++++++++++++--- .../views/RecentsView/RecentsView.tsx | 45 ++++++++++++ .../explorer/views/RecentsView/index.ts | 1 + 14 files changed, 176 insertions(+), 36 deletions(-) create mode 100644 packages/interface/src/routes/explorer/views/RecentsView/RecentsView.tsx create mode 100644 packages/interface/src/routes/explorer/views/RecentsView/index.ts diff --git a/TODO b/TODO index f72acfce0..9d79c4021 100644 --- a/TODO +++ b/TODO @@ -22,36 +22,48 @@ Journey to v2.0.0-pre.1: ✔ Get all desktop release CI working @done(25-12-16 23:51) -☐ Remote file access on demand @critical +☐ Remote file access on demand ☐ Ensure updater and changelog are working @today -☐ Ephemeral sidecars @critical +☐ Ephemeral sidecars ☐ Fix tag on file sync -☐ Make a separate data folder for dev builds @critical -☐ Sometimes quick preview reporting file not found @today (happens in the second column of column view) -☐ Connection info on device panel (lan/relay) +☐ Make a separate data folder for dev builds +☐ Default path for paste to device (paste to /Downloads) +☐ Mobile explorer + ☐ search + ☐ inspector + ☐ context menu + ☐ view settings + ☐ job manager + ☐ preview + ☐ share +✔ Sometimes quick preview reporting file not found @today (happens in the second column of column view) @done(26-01-20 09:09) +✔ Connection info on device panel (lan/relay) @done(26-01-20 09:09) ☐ Improve job panel data display, layout and ordering @today ☐ Pop out job viewer -☐ More metadata in file copy jobs, to show more visually appealing transfer job @today +✔ More metadata in file copy jobs, to show more visually appealing transfer job @today @done(26-01-20 09:09) ☐ Drag between columns in column view ✔ Rename files, create folders, new folder with items @done(25-12-24 09:08) ✔ Inspector multi-select @done(26-01-10 22:22) -✔ Open files with default app (cross platform) @critical @done(25-12-25 08:17) +✔ Open files with default app (cross platform) @done(25-12-25 08:17) ✔ Grid view render bug, shows as column for split second on first render of results @done(25-12-22 07:49) ✔ Job sound: make copy sound a varient, not play also @done(25-12-24 07:24) ✔ Sidebar active based on Explorer path @today @done(25-12-20 07:59) -☐ Fix three device sync @critical +☐ Fix three device sync ☐ Run now button in Location Inspector doesn't work well @today ☐ Delete location UX improvement -☐ Delete jobs +☐ Fix unique bytes calculation +☐ Choose custom device name on mobile setup flow +☐ Device state tombstone sync +✔ Delete jobs @done(26-01-20 09:09) ☐ Drop external items INTO spacedrive explorer @today -☐ Eject volume button -✔ Fix explorer arrow key navigation @critical @done(25-12-22 06:13) +✔ Eject volume button @done(26-01-20 09:09) +✔ Fix explorer arrow key navigation @done(25-12-22 06:13) ✔ Complete Spaces Sidebar with customize palette @done(25-12-20 09:56) # It has an issue where it conflicts with the tab focus navigation and is out of order often ☐ Device owned data count mismatch reconciliation / improve sync testing ☐ Improve sync testing, synced directories seem to be missing relationships to form tree (sometimes) ✔ Drag selection area + command to add to selection @today @done(25-12-25 08:18) -✔ Back/forward button navigation not working @critical @done(25-12-22 05:43) +✔ Back/forward button navigation not working @done(25-12-22 05:43) # Investigate sync integrity ✔ Refetch all queries when window focus @today @done(25-12-20 09:55) ✔ Fix job reactivity @today @done(25-12-21 06:14) @@ -62,19 +74,17 @@ Journey to v2.0.0-pre.1: ✔ Explorer should render results for a device, show volumes @today @done(25-12-20 09:55) ✔ Tags are not reactive, anywhere side from Inspector @today @done(26-01-10 22:21) ☐ Tags should support ephemeral -✔ Starting up screen when Core/daemon is initializing @critical @done(25-12-25 08:18) +✔ Starting up screen when Core/daemon is initializing @done(25-12-25 08:18) # This is complex because ephemeral entries do not have a content identity -☐ Fix unique bytes calculation ✔ Disable knowledge view @today @done(25-12-25 09:24) ✔ Fix zoom on videos / images @done(25-12-22 07:49) -☐ Show alternate paths in inspector +✔ Show alternate paths in inspector @done(26-01-20 09:10) ☐ Make default home folders SpaceItems -☐ Choose custom device name on mobile setup flow # Decide how this works in the context of sync, likely organize by device ✔ New Space Item resource event not working @done(25-12-24 07:25) ✔ Can't drag and drop onto space items to copy paste @done(25-12-16 23:51) ☐ Sometimes device shows as online even if its not -☐ Stale detection background discovery reindex @critical +☐ Stale detection background discovery reindex # Looks like cache state issue, even though the devices due ✔ Index mode not showing in location inspector @done(26-01-10 22:23) ✔ Space Item by entry UUID, not entry id and should have constraints against duplicates, for both entries and categories like Overview and Favorites @done(26-01-10 22:23) diff --git a/apps/mobile/src/components/primitive/SettingsRow.tsx b/apps/mobile/src/components/primitive/SettingsRow.tsx index 301776576..af3680904 100644 --- a/apps/mobile/src/components/primitive/SettingsRow.tsx +++ b/apps/mobile/src/components/primitive/SettingsRow.tsx @@ -33,7 +33,7 @@ export function SettingsRow({ "flex-row items-center px-6 py-3 bg-app-box min-h-[56px]", isFirst && "rounded-t-[32px]", isLast && "rounded-b-[32px]", - onPress && "active:bg-app-hover", + onPress && "active:bg-app-selected", className )} {...props} diff --git a/apps/mobile/src/screens/browse/BrowseScreen.tsx b/apps/mobile/src/screens/browse/BrowseScreen.tsx index 9e44251fc..c553567b5 100644 --- a/apps/mobile/src/screens/browse/BrowseScreen.tsx +++ b/apps/mobile/src/screens/browse/BrowseScreen.tsx @@ -134,7 +134,7 @@ export function BrowseScreen() { ); return ( - + void }) { diff --git a/core/src/ops/search/ephemeral_search.rs b/core/src/ops/search/ephemeral_search.rs index 8fb8ea342..bc6bf7690 100644 --- a/core/src/ops/search/ephemeral_search.rs +++ b/core/src/ops/search/ephemeral_search.rs @@ -178,6 +178,7 @@ fn passes_ephemeral_filters( DateField::ModifiedAt => metadata.modified, DateField::CreatedAt => metadata.created, DateField::AccessedAt => metadata.accessed, + DateField::IndexedAt => None, // Ephemeral search doesn't have indexed_at }; if let Some(system_time) = system_time_opt { diff --git a/core/src/ops/search/filters.rs b/core/src/ops/search/filters.rs index 339713849..c947252f7 100644 --- a/core/src/ops/search/filters.rs +++ b/core/src/ops/search/filters.rs @@ -43,6 +43,7 @@ impl FilterBuilder { DateField::CreatedAt => crate::infra::db::entities::entry::Column::CreatedAt, DateField::ModifiedAt => crate::infra::db::entities::entry::Column::ModifiedAt, DateField::AccessedAt => crate::infra::db::entities::entry::Column::AccessedAt, + DateField::IndexedAt => crate::infra::db::entities::entry::Column::IndexedAt, }; if let Some(start) = range.start { diff --git a/core/src/ops/search/input.rs b/core/src/ops/search/input.rs index 3776b2653..bcd5f4a1a 100644 --- a/core/src/ops/search/input.rs +++ b/core/src/ops/search/input.rs @@ -87,6 +87,7 @@ pub enum DateField { CreatedAt, ModifiedAt, AccessedAt, + IndexedAt, } /// Filter for file size in bytes @@ -111,6 +112,7 @@ pub enum SortField { Size, ModifiedAt, CreatedAt, + IndexedAt, } /// Sort direction diff --git a/core/src/ops/search/query.rs b/core/src/ops/search/query.rs index 744324c87..e154ac275 100644 --- a/core/src/ops/search/query.rs +++ b/core/src/ops/search/query.rs @@ -594,6 +594,7 @@ impl FileSearchQuery { crate::ops::search::input::DateField::CreatedAt => entry::Column::CreatedAt, crate::ops::search::input::DateField::ModifiedAt => entry::Column::ModifiedAt, crate::ops::search::input::DateField::AccessedAt => entry::Column::AccessedAt, + crate::ops::search::input::DateField::IndexedAt => entry::Column::IndexedAt, }; if let Some(start) = date_range.start { @@ -988,6 +989,7 @@ impl FileSearchQuery { crate::ops::search::input::DateField::CreatedAt => Some(entry_model.created_at), crate::ops::search::input::DateField::ModifiedAt => Some(entry_model.modified_at), crate::ops::search::input::DateField::AccessedAt => entry_model.accessed_at, + crate::ops::search::input::DateField::IndexedAt => entry_model.indexed_at, }; if let Some(date) = date_to_check { diff --git a/core/src/ops/search/sorting.rs b/core/src/ops/search/sorting.rs index a9b094d86..57986ce74 100644 --- a/core/src/ops/search/sorting.rs +++ b/core/src/ops/search/sorting.rs @@ -42,6 +42,9 @@ impl SortBuilder { SortField::CreatedAt => { self.order.push(("created_at".to_string(), direction)); } + SortField::IndexedAt => { + self.order.push(("indexed_at".to_string(), direction)); + } } self diff --git a/packages/interface/src/router.tsx b/packages/interface/src/router.tsx index cda2b5830..3bceff66e 100644 --- a/packages/interface/src/router.tsx +++ b/packages/interface/src/router.tsx @@ -6,6 +6,7 @@ import { JobsScreen } from "./components/JobManager"; import { DaemonManager } from "./routes/daemon"; import { TagView } from "./routes/tag"; import { FileKindsView } from "./routes/file-kinds"; +import { RecentsView } from "./routes/explorer/views/RecentsView"; /** * Router routes configuration (without router instance) @@ -33,11 +34,7 @@ export const explorerRoutes = [ }, { path: "recents", - element: ( -
- Recents (coming soon) -
- ), + element: , }, { path: "file-kinds", diff --git a/packages/interface/src/routes/explorer/context.tsx b/packages/interface/src/routes/explorer/context.tsx index 8a52e9b8c..202e4b1ff 100644 --- a/packages/interface/src/routes/explorer/context.tsx +++ b/packages/interface/src/routes/explorer/context.tsx @@ -60,7 +60,8 @@ export interface SearchFilters { export type ExplorerMode = | { type: "browse" } - | { type: "search"; query: string; scope: SearchScope }; + | { type: "search"; query: string; scope: SearchScope } + | { type: "recents" }; export type NavigationTarget = | { type: "path"; path: SdPath } @@ -190,6 +191,8 @@ type UIAction = | { type: "SET_TAG_MODE"; active: boolean } | { type: "ENTER_SEARCH_MODE"; query: string; scope: SearchScope } | { type: "EXIT_SEARCH_MODE" } + | { type: "ENTER_RECENTS_MODE" } + | { type: "EXIT_RECENTS_MODE" } | { type: "SET_SEARCH_FILTERS"; filters: SearchFilters } | { type: "LOAD_PREFERENCES"; @@ -245,6 +248,18 @@ function uiReducer(state: UIState, action: UIAction): UIState { searchFilters: {}, }; + case "ENTER_RECENTS_MODE": + return { + ...state, + mode: { type: "recents" }, + }; + + case "EXIT_RECENTS_MODE": + return { + ...state, + mode: { type: "browse" }, + }; + case "SET_SEARCH_FILTERS": return { ...state, @@ -395,6 +410,8 @@ interface ExplorerContextValue { mode: ExplorerMode; enterSearchMode: (query: string, scope?: SearchScope) => void; exitSearchMode: () => void; + enterRecentsMode: () => void; + exitRecentsMode: () => void; searchFilters: SearchFilters; setSearchFilters: (filters: SearchFilters) => void; @@ -712,6 +729,14 @@ export function ExplorerProvider({ uiDispatch({ type: "EXIT_SEARCH_MODE" }); }, []); + const enterRecentsMode = useCallback(() => { + uiDispatch({ type: "ENTER_RECENTS_MODE" }); + }, []); + + const exitRecentsMode = useCallback(() => { + uiDispatch({ type: "EXIT_RECENTS_MODE" }); + }, []); + const setSearchFilters = useCallback((filters: SearchFilters) => { uiDispatch({ type: "SET_SEARCH_FILTERS", filters }); }, []); @@ -767,6 +792,8 @@ export function ExplorerProvider({ mode: uiState.mode, enterSearchMode, exitSearchMode, + enterRecentsMode, + exitRecentsMode, searchFilters: uiState.searchFilters, setSearchFilters, devices, @@ -808,6 +835,8 @@ export function ExplorerProvider({ uiState.mode, enterSearchMode, exitSearchMode, + enterRecentsMode, + exitRecentsMode, uiState.searchFilters, setSearchFilters, devices, diff --git a/packages/interface/src/routes/explorer/hooks/useExplorerFiles.ts b/packages/interface/src/routes/explorer/hooks/useExplorerFiles.ts index 148cc9ae5..08f4e43b3 100644 --- a/packages/interface/src/routes/explorer/hooks/useExplorerFiles.ts +++ b/packages/interface/src/routes/explorer/hooks/useExplorerFiles.ts @@ -5,7 +5,7 @@ import { useExplorer } from "../context"; import type { SearchScope } from "../context"; import { useVirtualListing } from "./useVirtualListing"; -export type FileSource = "search" | "virtual" | "directory"; +export type FileSource = "search" | "virtual" | "directory" | "recents"; export interface ExplorerFilesResult { files: File[]; @@ -30,6 +30,7 @@ export function useExplorerFiles(): ExplorerFilesResult { // Check for search mode const isSearchMode = mode.type === "search"; + const isRecentsMode = mode.type === "recents"; // Build search query input const searchQueryInput = useMemo(() => { @@ -80,6 +81,35 @@ export function useExplorerFiles(): ExplorerFilesResult { }; }, [isSearchMode, mode, currentPath, sortBy]); + // Build recents query input + const recentsQueryInput = useMemo(() => { + if (!isRecentsMode) return null; + + return { + query: "", // Empty query to match all files + scope: "Library", + filters: { + file_types: null, + tags: null, + date_range: null, + size_range: null, + locations: null, + content_types: null, + include_hidden: null, + include_archived: null, + }, + mode: "Fast", // Fast mode since we're just sorting by indexed_at + sort: { + field: "IndexedAt", // Sort by when files were indexed + direction: "Desc", // Most recent first + }, + pagination: { + limit: 100, // Reasonable limit for recents screen + offset: 0, + }, + }; + }, [isRecentsMode]); + // Search query const searchQuery = useNormalizedQuery({ wireMethod: "query:search.files", @@ -92,6 +122,14 @@ export function useExplorerFiles(): ExplorerFilesResult { enabled: isSearchMode && !!searchQueryInput && searchQueryInput.query.length >= 2, }); + // Recents query + const recentsQuery = useNormalizedQuery({ + wireMethod: "query:search.files", + input: recentsQueryInput!, + resourceType: "file", + enabled: isRecentsMode && !!recentsQueryInput, + }); + // Directory query const directoryQuery = useNormalizedQuery({ wireMethod: "query:files.directory_listing", @@ -105,14 +143,23 @@ export function useExplorerFiles(): ExplorerFilesResult { } : null!, resourceType: "file", - enabled: !!currentPath && !isVirtualView && !isSearchMode, + enabled: !!currentPath && !isVirtualView && !isSearchMode && !isRecentsMode, pathScope: currentPath ?? undefined, }); - // Determine source and files with priority: search > virtual > directory - const source: FileSource = isSearchMode ? "search" : isVirtualView ? "virtual" : "directory"; + // Determine source and files with priority: recents > search > virtual > directory + const source: FileSource = isRecentsMode + ? "recents" + : isSearchMode + ? "search" + : isVirtualView + ? "virtual" + : "directory"; const files = useMemo(() => { + if (isRecentsMode) { + return (recentsQuery.data as FileSearchOutput | undefined)?.files || []; + } if (isSearchMode) { return (searchQuery.data as FileSearchOutput | undefined)?.files || []; } @@ -120,13 +167,15 @@ export function useExplorerFiles(): ExplorerFilesResult { return virtualFiles || []; } return (directoryQuery.data as any)?.files || []; - }, [isSearchMode, isVirtualView, searchQuery.data, virtualFiles, directoryQuery.data]); + }, [isRecentsMode, isSearchMode, isVirtualView, recentsQuery.data, searchQuery.data, virtualFiles, directoryQuery.data]); - const isLoading = isSearchMode - ? searchQuery.isLoading - : isVirtualView - ? false - : directoryQuery.isLoading; + const isLoading = isRecentsMode + ? recentsQuery.isLoading + : isSearchMode + ? searchQuery.isLoading + : isVirtualView + ? false + : directoryQuery.isLoading; return { files, isLoading, source }; } diff --git a/packages/interface/src/routes/explorer/views/RecentsView/RecentsView.tsx b/packages/interface/src/routes/explorer/views/RecentsView/RecentsView.tsx new file mode 100644 index 000000000..f4faf8951 --- /dev/null +++ b/packages/interface/src/routes/explorer/views/RecentsView/RecentsView.tsx @@ -0,0 +1,45 @@ +import { useEffect } from 'react'; +import { useExplorer } from '../../context'; +import { GridView } from '../GridView'; +import { ListView } from '../ListView'; +import { MediaView } from '../MediaView'; +import { ColumnView } from '../ColumnView'; +import { SizeView } from '../SizeView'; +import { KnowledgeView } from '../KnowledgeView'; + +/** + * RecentsView displays recently indexed files sorted by indexed_at timestamp. + * + * Similar to SearchView, it delegates to existing view components which automatically + * read from useExplorerFiles. This ensures recents has the same interactions as normal + * browsing: keyboard navigation, drag-to-select, context menus, etc. + */ +export function RecentsView() { + const explorer = useExplorer(); + const { viewMode, enterRecentsMode, exitRecentsMode } = explorer; + + // Enter recents mode on mount, exit on unmount + useEffect(() => { + enterRecentsMode(); + return () => exitRecentsMode(); + }, [enterRecentsMode, exitRecentsMode]); + + // Route to the appropriate view based on viewMode + // The views will automatically use recents results via useExplorerFiles + switch (viewMode) { + case 'grid': + return ; + case 'list': + return ; + case 'media': + return ; + case 'column': + return ; + case 'size': + return ; + case 'knowledge': + return ; + default: + return ; + } +} diff --git a/packages/interface/src/routes/explorer/views/RecentsView/index.ts b/packages/interface/src/routes/explorer/views/RecentsView/index.ts new file mode 100644 index 000000000..34451a368 --- /dev/null +++ b/packages/interface/src/routes/explorer/views/RecentsView/index.ts @@ -0,0 +1 @@ +export { RecentsView } from './RecentsView';