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.
This commit is contained in:
Jamie Pine
2026-01-20 10:35:48 -08:00
parent 7c3e977025
commit ebe7d36ed5
14 changed files with 176 additions and 36 deletions

44
TODO
View File

@@ -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)

View File

@@ -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}

View File

@@ -134,7 +134,7 @@ export function BrowseScreen() {
);
return (
<View className="flex-1 bg-sidebar">
<View className="flex-1 bg-app">
<ScrollView
ref={scrollViewRef}
horizontal

View File

@@ -56,7 +56,7 @@ function FileCard({ file, onPress }: { file: File; onPress: () => void }) {
<View className="rounded-lg p-2 items-center justify-center aspect-square mb-2">
<Image
source={iconSource}
className="w-16 h-16"
className="w-20 h-20"
style={{ resizeMode: "contain" }}
/>
</View>

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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

View File

@@ -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 {

View File

@@ -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

View File

@@ -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: (
<div className="flex items-center justify-center h-full text-ink">
Recents (coming soon)
</div>
),
element: <RecentsView />,
},
{
path: "file-kinds",

View File

@@ -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,

View File

@@ -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<FileSearchInput | null>(() => {
@@ -80,6 +81,35 @@ export function useExplorerFiles(): ExplorerFilesResult {
};
}, [isSearchMode, mode, currentPath, sortBy]);
// Build recents query input
const recentsQueryInput = useMemo<FileSearchInput | null>(() => {
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<FileSearchInput, FileSearchOutput>({
wireMethod: "query:search.files",
@@ -92,6 +122,14 @@ export function useExplorerFiles(): ExplorerFilesResult {
enabled: isSearchMode && !!searchQueryInput && searchQueryInput.query.length >= 2,
});
// Recents query
const recentsQuery = useNormalizedQuery<FileSearchInput, FileSearchOutput>({
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 };
}

View File

@@ -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 <GridView />;
case 'list':
return <ListView />;
case 'media':
return <MediaView />;
case 'column':
return <ColumnView />;
case 'size':
return <SizeView />;
case 'knowledge':
return <KnowledgeView />;
default:
return <GridView />;
}
}

View File

@@ -0,0 +1 @@
export { RecentsView } from './RecentsView';