From 2cd0108cc71347423fc0f422b979cb2f8be5d6ef Mon Sep 17 00:00:00 2001 From: Owais Jamil Date: Tue, 7 Apr 2026 13:40:17 -0500 Subject: [PATCH] refactor: controller pattern --- package.json | 1 + pnpm-lock.yaml | 76 +++++++++++ pnpm-workspace.yaml | 1 + src/components/deck/AddColumnPanel.tsx | 4 +- .../notifications/notification-grouping.ts | 4 +- src/components/saved/SavedPostsPanel.test.tsx | 8 +- src/components/saved/SavedPostsPanel.tsx | 14 +- .../search/EmbeddingsSettings.test.tsx | 10 +- src/components/search/EmbeddingsSettings.tsx | 2 +- src/components/search/HashtagPanel.test.tsx | 2 +- src/components/search/HashtagPanel.tsx | 7 +- .../search/LocalPostResultsList.tsx | 2 +- src/components/search/PostSearchFilters.tsx | 2 +- src/components/search/SearchPanel.test.tsx | 12 +- src/components/search/SearchPanel.tsx | 41 +++--- .../search/SyncStatusPanel.test.tsx | 8 +- src/components/search/SyncStatusPanel.tsx | 11 +- .../settings/SettingsInlineFeedback.tsx | 2 +- src/components/shared/Icon.tsx | 2 +- src/contexts/app-preferences.tsx | 17 +-- src/lib/api/drafts.ts | 8 +- src/lib/api/media.ts | 2 +- src/lib/api/search.ts | 122 ++++++------------ src/lib/api/settings.ts | 14 +- src/lib/api/types/columns.ts | 2 +- src/lib/api/types/search.ts | 70 ++++++++++ src/lib/search-routes.ts | 2 +- 27 files changed, 275 insertions(+), 171 deletions(-) create mode 100644 src/lib/api/types/search.ts diff --git a/package.json b/package.json index b55c529..b8acf0d 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "eslint-plugin-react": "^7.37.5", "eslint-plugin-solid": "^0.14.5", "eslint-plugin-unicorn": "^64.0.0", + "fallow": "^2.19.0", "globals": "^17.3.0", "jsdom": "^28.1.0", "tailwindcss": "^4.2.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index edc0c19..d835ba6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -96,6 +96,9 @@ importers: eslint-plugin-unicorn: specifier: ^64.0.0 version: 64.0.0(eslint@10.1.0(jiti@2.6.1)) + fallow: + specifier: ^2.19.0 + version: 2.19.0 globals: specifier: ^17.3.0 version: 17.4.0 @@ -446,6 +449,41 @@ packages: '@noble/hashes': optional: true + '@fallow-cli/darwin-arm64@2.19.0': + resolution: {integrity: sha512-f9D0jajY2SgtkrrTm92HI25gXSSLEYRz6jyywxaV40jsgLSCWDF8gpuor93Mjn4l49vSpMn1m/Hd383+LU2/IA==} + cpu: [arm64] + os: [darwin] + + '@fallow-cli/darwin-x64@2.19.0': + resolution: {integrity: sha512-zv4ZQNZUZOz/wPS3N+VkjsRZHBOCbKrZK3AH1EbEGTU6TgvE9CdoS1HP148R5Y5dd/Tbrcqc7RdzSZLeELldYQ==} + cpu: [x64] + os: [darwin] + + '@fallow-cli/linux-arm64-gnu@2.19.0': + resolution: {integrity: sha512-rQYRlFTJJiEdM3iwlgWXRZqZol4X3fiANjll4Ezpgsbo4Qe8t8BtCHwyR4iMPIT4/QgFw3iSGu/xtMvrUfwCZQ==} + cpu: [arm64] + os: [linux] + + '@fallow-cli/linux-arm64-musl@2.19.0': + resolution: {integrity: sha512-t2CFtefFYisGgpYr+LBFwXRjX9noWrIS8uvUtuIytMOlukiaBwH1FlmXuHp99pLvZLJ36ageZNnSwcikvNTXcA==} + cpu: [arm64] + os: [linux] + + '@fallow-cli/linux-x64-gnu@2.19.0': + resolution: {integrity: sha512-BMvKKC4DpvnZvF6cq4V3URKahLZbluzk2dJAqo3u4q7/pHRIjosx8NNqj2LdwOGoAcW/Z3o6LVUkL2Kea/BnNA==} + cpu: [x64] + os: [linux] + + '@fallow-cli/linux-x64-musl@2.19.0': + resolution: {integrity: sha512-qUrIMJTFGkv+O5ySJkGLz1z2pA8CY2Sl8FBZqaOJnP3yJn7v7zYhy/zerhk+bmAr+OlGW+y0nYBOsxMF/EIelw==} + cpu: [x64] + os: [linux] + + '@fallow-cli/win32-x64-msvc@2.19.0': + resolution: {integrity: sha512-W+xDGrTxePpIitaR/PsV1OaixXUzQLZwqjvAswk0xuFhykpom4K1uVe2BtlL0Kk96mWbUKhB7Fu/hwrw24U0bg==} + cpu: [x64] + os: [win32] + '@fontsource-variable/google-sans@5.2.1': resolution: {integrity: sha512-dMyMlJVRJgQvvYlJLcrg2oiVzlKAuIMrbDxIMchEiMBGmpOA6VnVblj3cPfJyA0/fNPQS6krZaiJyJjHJZmkzw==} @@ -1430,6 +1468,11 @@ packages: extendable-error@0.1.7: resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} + fallow@2.19.0: + resolution: {integrity: sha512-APjtt1JNIsW/s44se/u8nvhC6+RpJI4biAyLekNsrM3T8CFh1uBRtoSbOFCvL0mbavoPpOtvR53N25svcFpdBw==} + engines: {node: '>=16'} + hasBin: true + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -3043,6 +3086,27 @@ snapshots: '@exodus/bytes@1.15.0': {} + '@fallow-cli/darwin-arm64@2.19.0': + optional: true + + '@fallow-cli/darwin-x64@2.19.0': + optional: true + + '@fallow-cli/linux-arm64-gnu@2.19.0': + optional: true + + '@fallow-cli/linux-arm64-musl@2.19.0': + optional: true + + '@fallow-cli/linux-x64-gnu@2.19.0': + optional: true + + '@fallow-cli/linux-x64-musl@2.19.0': + optional: true + + '@fallow-cli/win32-x64-msvc@2.19.0': + optional: true + '@fontsource-variable/google-sans@5.2.1': {} '@humanfs/core@0.19.1': {} @@ -4152,6 +4216,18 @@ snapshots: extendable-error@0.1.7: {} + fallow@2.19.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + '@fallow-cli/darwin-arm64': 2.19.0 + '@fallow-cli/darwin-x64': 2.19.0 + '@fallow-cli/linux-arm64-gnu': 2.19.0 + '@fallow-cli/linux-arm64-musl': 2.19.0 + '@fallow-cli/linux-x64-gnu': 2.19.0 + '@fallow-cli/linux-x64-musl': 2.19.0 + '@fallow-cli/win32-x64-msvc': 2.19.0 + fast-deep-equal@3.1.3: {} fast-glob@3.3.3: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 5603573..e323f0f 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,4 @@ onlyBuiltDependencies: - dprint - esbuild + - fallow diff --git a/src/components/deck/AddColumnPanel.tsx b/src/components/deck/AddColumnPanel.tsx index c4632a6..58d7f0f 100644 --- a/src/components/deck/AddColumnPanel.tsx +++ b/src/components/deck/AddColumnPanel.tsx @@ -1,7 +1,7 @@ import { ActorSuggestionList, useActorSuggestions } from "$/components/actors/actor-search"; import { getFeedGenerators, getPreferences } from "$/lib/api/feeds"; -import type { SearchMode } from "$/lib/api/search"; import type { ColumnKind } from "$/lib/api/types/columns"; +import type { SearchMode } from "$/lib/api/types/search"; import { getFeedName } from "$/lib/feeds"; import type { FeedGeneratorView, LoginSuggestion, SavedFeedItem } from "$/lib/types"; import * as logger from "@tauri-apps/plugin-log"; @@ -506,7 +506,7 @@ function AddColumnPanelBody( role="dialog" aria-modal="true" aria-labelledby="add-column-panel-title" - class="relative z-10 flex h-full w-full max-w-88 flex-col bg-(--surface-container-highest) shadow-[-18px_0_48px_rgba(0,0,0,0.38)] backdrop-blur-[20px]" + class="relative z-10 flex h-full w-full max-w-88 flex-col bg-surface-container-highest shadow-[-18px_0_48px_rgba(0,0,0,0.38)] backdrop-blur-[20px]" initial={{ opacity: 0, x: 32 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: 40 }} diff --git a/src/components/notifications/notification-grouping.ts b/src/components/notifications/notification-grouping.ts index e02346e..7bbd57c 100644 --- a/src/components/notifications/notification-grouping.ts +++ b/src/components/notifications/notification-grouping.ts @@ -25,7 +25,7 @@ export type GroupedNotificationFeedItem = { sampleRecordText: string | null; }; -export const MENTION_REASONS = new Set(["mention", "reply", "quote"]); +const MENTION_REASONS = new Set(["mention", "reply", "quote"]); export function isMentionNotification(notification: NotificationView) { return MENTION_REASONS.has(notification.reason); @@ -36,7 +36,7 @@ function toTimestamp(value: string) { return Number.isNaN(timestamp) ? 0 : timestamp; } -export function compareByNewest(left: { latestIndexedAt: string }, right: { latestIndexedAt: string }) { +function compareByNewest(left: { latestIndexedAt: string }, right: { latestIndexedAt: string }) { const timestampDelta = toTimestamp(right.latestIndexedAt) - toTimestamp(left.latestIndexedAt); if (timestampDelta !== 0) { return timestampDelta; diff --git a/src/components/saved/SavedPostsPanel.test.tsx b/src/components/saved/SavedPostsPanel.test.tsx index c1ca4e1..0aac3b7 100644 --- a/src/components/saved/SavedPostsPanel.test.tsx +++ b/src/components/saved/SavedPostsPanel.test.tsx @@ -12,7 +12,13 @@ const threadOverlayMock = vi.hoisted(() => ({ openThread: vi.fn() })); vi.mock( "$/lib/api/search", - () => ({ getSyncStatus: getSyncStatusMock, listSavedPosts: listSavedPostsMock, syncPosts: syncPostsMock }), + () => ({ + SearchController: { + getSyncStatus: getSyncStatusMock, + listSavedPosts: listSavedPostsMock, + syncPosts: syncPostsMock, + }, + }), ); vi.mock( "$/components/posts/useThreadOverlayNavigation", diff --git a/src/components/saved/SavedPostsPanel.tsx b/src/components/saved/SavedPostsPanel.tsx index 2b88fd8..bc70585 100644 --- a/src/components/saved/SavedPostsPanel.tsx +++ b/src/components/saved/SavedPostsPanel.tsx @@ -5,8 +5,8 @@ import { SearchQueryInput } from "$/components/search/SearchQueryInput"; import { Icon } from "$/components/shared/Icon"; import { PostCount } from "$/components/shared/PostCount"; import { useAppSession } from "$/contexts/app-session"; -import { getSyncStatus, listSavedPosts, syncPosts } from "$/lib/api/search"; -import type { LocalPostResult, SavedPostSource, SyncStatus } from "$/lib/api/search"; +import { SearchController } from "$/lib/api/search"; +import type { LocalPostResult, SavedPostSource, SyncStatus } from "$/lib/api/types/search"; import { formatRelativeTime } from "$/lib/feeds"; import { subscribeBookmarkChanged } from "$/lib/post-events"; import { normalizeError } from "$/lib/utils/text"; @@ -191,7 +191,7 @@ export function SavedPostsPanel() { setState("syncStatusLoading", true); try { - const status = await getSyncStatus(did); + const status = await SearchController.getSyncStatus(did); if (did !== activeDid) { return; } @@ -253,7 +253,7 @@ export function SavedPostsPanel() { setState("tabs", source, "error", null); try { - const page = await listSavedPosts(source, PAGE_SIZE, offset); + const page = await SearchController.listSavedPosts(source, PAGE_SIZE, offset); if (did !== activeDid || requestId !== browseRequestIds[source]) { return; } @@ -296,7 +296,7 @@ export function SavedPostsPanel() { setState("searchTabs", source, "error", null); try { - const page = await listSavedPosts(source, PAGE_SIZE, offset, query); + const page = await SearchController.listSavedPosts(source, PAGE_SIZE, offset, query); if (did !== activeDid || requestId !== searchRequestIds[source] || trimmedQuery() !== query) { return; } @@ -362,8 +362,8 @@ export function SavedPostsPanel() { setState("refreshing", true); try { - await syncPosts(session.activeDid, "bookmark"); - await syncPosts(session.activeDid, "like"); + await SearchController.syncPosts(session.activeDid, "bookmark"); + await SearchController.syncPosts(session.activeDid, "like"); await Promise.all([ loadSyncStatus(session.activeDid), isSearching() diff --git a/src/components/search/EmbeddingsSettings.test.tsx b/src/components/search/EmbeddingsSettings.test.tsx index fb2f9b3..53f2e8b 100644 --- a/src/components/search/EmbeddingsSettings.test.tsx +++ b/src/components/search/EmbeddingsSettings.test.tsx @@ -13,10 +13,12 @@ const updateSettingMock = vi.hoisted(() => vi.fn()); vi.mock( "$/lib/api/search", () => ({ - getEmbeddingsConfig: getEmbeddingsConfigMock, - prepareEmbeddingsModel: prepareEmbeddingsModelMock, - setEmbeddingsEnabled: setEmbeddingsEnabledMock, - setEmbeddingsPreflightSeen: setEmbeddingsPreflightSeenMock, + SearchController: { + getEmbeddingsConfig: getEmbeddingsConfigMock, + prepareEmbeddingsModel: prepareEmbeddingsModelMock, + setEmbeddingsEnabled: setEmbeddingsEnabledMock, + setEmbeddingsPreflightSeen: setEmbeddingsPreflightSeenMock, + }, }), ); diff --git a/src/components/search/EmbeddingsSettings.tsx b/src/components/search/EmbeddingsSettings.tsx index 4a91d2a..cf26766 100644 --- a/src/components/search/EmbeddingsSettings.tsx +++ b/src/components/search/EmbeddingsSettings.tsx @@ -1,7 +1,7 @@ /* eslint react/jsx-max-depth: ["error", { "max": 5 }] */ import { Icon } from "$/components/shared/Icon"; import { useAppPreferences } from "$/contexts/app-preferences"; -import type { EmbeddingsConfig } from "$/lib/api/search"; +import type { EmbeddingsConfig } from "$/lib/api/types/search"; import { formatBytes, formatEtaSeconds, formatProgress } from "$/lib/utils/text"; import { createEffect, createMemo, createSignal, Match, onCleanup, onMount, Show, Switch } from "solid-js"; import { Motion, Presence } from "solid-motionone"; diff --git a/src/components/search/HashtagPanel.test.tsx b/src/components/search/HashtagPanel.test.tsx index cb5d2ea..0b71223 100644 --- a/src/components/search/HashtagPanel.test.tsx +++ b/src/components/search/HashtagPanel.test.tsx @@ -8,7 +8,7 @@ import { HashtagPanel } from "./HashtagPanel"; const searchPostsNetworkMock = vi.hoisted(() => vi.fn()); const threadOverlayMock = vi.hoisted(() => ({ openThread: vi.fn() })); -vi.mock("$/lib/api/search", () => ({ searchPostsNetwork: searchPostsNetworkMock })); +vi.mock("$/lib/api/search", () => ({ SearchController: { searchPostsNetwork: searchPostsNetworkMock } })); vi.mock( "$/components/posts/useThreadOverlayNavigation", () => ({ useThreadOverlayNavigation: () => threadOverlayMock }), diff --git a/src/components/search/HashtagPanel.tsx b/src/components/search/HashtagPanel.tsx index 5e83eb3..cda285e 100644 --- a/src/components/search/HashtagPanel.tsx +++ b/src/components/search/HashtagPanel.tsx @@ -1,17 +1,18 @@ import { PostCard } from "$/components/feeds/PostCard"; import { useThreadOverlayNavigation } from "$/components/posts/useThreadOverlayNavigation"; import { Icon } from "$/components/shared/Icon"; -import { type NetworkSearchResult, searchPostsNetwork } from "$/lib/api/search"; +import { SearchController } from "$/lib/api/search"; +import type { NetworkSearchResult } from "$/lib/api/types/search"; import { buildHashtagQuery, buildPostSearchRoute, decodeHashtagRouteTag, formatHashtagLabel, parsePostSearchFilters, - type PostSearchFilters, toLocalDayStartIso, toLocalDayUntilIso, } from "$/lib/search-routes"; +import type { PostSearchFilters } from "$/lib/search-routes"; import { normalizeError } from "$/lib/utils/text"; import { useLocation, useNavigate, useParams } from "@solidjs/router"; import * as logger from "@tauri-apps/plugin-log"; @@ -57,7 +58,7 @@ export function HashtagPanel() { async function performSearch(f: PostSearchFilters, t: string) { try { - const results = await searchPostsNetwork({ + const results = await SearchController.searchPostsNetwork({ author: f.author || null, limit: 25, mentions: f.mentions || null, diff --git a/src/components/search/LocalPostResultsList.tsx b/src/components/search/LocalPostResultsList.tsx index 19ad8c9..5a9f903 100644 --- a/src/components/search/LocalPostResultsList.tsx +++ b/src/components/search/LocalPostResultsList.tsx @@ -1,4 +1,4 @@ -import type { LocalPostResult } from "$/lib/api/search"; +import type { LocalPostResult } from "$/lib/api/types/search"; import { For } from "solid-js"; import { Motion } from "solid-motionone"; import { SearchResultCard } from "./SearchResultCard"; diff --git a/src/components/search/PostSearchFilters.tsx b/src/components/search/PostSearchFilters.tsx index 4508525..622e2a6 100644 --- a/src/components/search/PostSearchFilters.tsx +++ b/src/components/search/PostSearchFilters.tsx @@ -1,4 +1,4 @@ -import type { NetworkSearchSort } from "$/lib/api/search"; +import type { NetworkSearchSort } from "$/lib/api/types/search"; import { normalizeTagToken, type PostSearchFilters } from "$/lib/search-routes"; import { createEffect, createMemo, createSignal, For, Show } from "solid-js"; import { ArrowIcon, Icon } from "../shared/Icon"; diff --git a/src/components/search/SearchPanel.test.tsx b/src/components/search/SearchPanel.test.tsx index bdcdfb5..cec726e 100644 --- a/src/components/search/SearchPanel.test.tsx +++ b/src/components/search/SearchPanel.test.tsx @@ -16,11 +16,13 @@ const threadOverlayMock = vi.hoisted(() => ({ openThread: vi.fn() })); vi.mock( "$/lib/api/search", () => ({ - getSyncStatus: getSyncStatusMock, - searchActors: searchActorsMock, - searchPosts: searchPostsMock, - searchPostsNetwork: searchPostsNetworkMock, - syncPosts: syncPostsMock, + SearchController: { + getSyncStatus: getSyncStatusMock, + searchActors: searchActorsMock, + searchPosts: searchPostsMock, + searchPostsNetwork: searchPostsNetworkMock, + syncPosts: syncPostsMock, + }, }), ); vi.mock("$/lib/api/actors", () => ({ searchActorSuggestions: searchActorSuggestionsMock })); diff --git a/src/components/search/SearchPanel.tsx b/src/components/search/SearchPanel.tsx index 41cd712..84a2e6a 100644 --- a/src/components/search/SearchPanel.tsx +++ b/src/components/search/SearchPanel.tsx @@ -5,29 +5,20 @@ import { useThreadOverlayNavigation } from "$/components/posts/useThreadOverlayN import { Icon, SearchModeIcon } from "$/components/shared/Icon"; import { useAppPreferences } from "$/contexts/app-preferences"; import { useAppSession } from "$/contexts/app-session"; -import { - type ActorResult, - type ActorSearchResult, - getSyncStatus, - type LocalPostResult, - type NetworkSearchParams, - type NetworkSearchResult, - searchActors, - type SearchMode, - searchPosts, - searchPostsNetwork, - type SyncStatus, -} from "$/lib/api/search"; +import { SearchController } from "$/lib/api/search"; +import type { + ActorResult, + ActorSearchResult, + LocalPostResult, + NetworkSearchParams, + NetworkSearchResult, + SearchMode, + SyncStatus, +} from "$/lib/api/types/search"; import { formatRelativeTime } from "$/lib/feeds"; import { buildProfileRoute, getProfileRouteActor } from "$/lib/profile"; -import { - buildSearchRoute, - parseSearchRouteState, - type PostSearchFilters, - type SearchTab, - toLocalDayStartIso, - toLocalDayUntilIso, -} from "$/lib/search-routes"; +import { buildSearchRoute, parseSearchRouteState, toLocalDayStartIso, toLocalDayUntilIso } from "$/lib/search-routes"; +import type { PostSearchFilters, SearchTab } from "$/lib/search-routes"; import type { ProfileViewBasic } from "$/lib/types"; import { normalizeError } from "$/lib/utils/text"; import { useLocation, useNavigate } from "@solidjs/router"; @@ -151,7 +142,7 @@ export function SearchPanel(props: SearchPanelProps = {}) { setSearch({ error: null, loading: true }); try { - const response = await searchActors(searchQuery, 25); + const response = await SearchController.searchActors(searchQuery, 25); setSearch({ actorResults: response, error: null, @@ -194,7 +185,7 @@ export function SearchPanel(props: SearchPanelProps = {}) { try { if (state.mode === "network") { - const response = await searchPostsNetwork(buildNetworkSearchParams(state)); + const response = await SearchController.searchPostsNetwork(buildNetworkSearchParams(state)); setSearch({ actorResults: null, hasSearched: true, @@ -203,7 +194,7 @@ export function SearchPanel(props: SearchPanelProps = {}) { results: [], }); } else { - const response = await searchPosts(searchQuery, state.mode, 50); + const response = await SearchController.searchPosts(searchQuery, state.mode, 50); setSearch({ actorResults: null, hasSearched: true, @@ -342,7 +333,7 @@ export function SearchPanel(props: SearchPanelProps = {}) { } if (props.embedded && session.activeDid) { - void getSyncStatus(session.activeDid).then((status) => { + void SearchController.getSyncStatus(session.activeDid).then((status) => { setSearch("syncStatus", status); }).catch((error) => { logger.warn("failed to load embedded search sync status", { keyValues: { error: normalizeError(error) } }); diff --git a/src/components/search/SyncStatusPanel.test.tsx b/src/components/search/SyncStatusPanel.test.tsx index 24f617f..d15208a 100644 --- a/src/components/search/SyncStatusPanel.test.tsx +++ b/src/components/search/SyncStatusPanel.test.tsx @@ -8,7 +8,13 @@ const reindexEmbeddingsMock = vi.hoisted(() => vi.fn()); vi.mock( "$/lib/api/search", - () => ({ getSyncStatus: getSyncStatusMock, syncPosts: syncPostsMock, reindexEmbeddings: reindexEmbeddingsMock }), + () => ({ + SearchController: { + getSyncStatus: getSyncStatusMock, + syncPosts: syncPostsMock, + reindexEmbeddings: reindexEmbeddingsMock, + }, + }), ); vi.mock("@tauri-apps/plugin-log", () => ({ info: vi.fn(), error: vi.fn(), warn: vi.fn() })); diff --git a/src/components/search/SyncStatusPanel.tsx b/src/components/search/SyncStatusPanel.tsx index 27a2412..91515a9 100644 --- a/src/components/search/SyncStatusPanel.tsx +++ b/src/components/search/SyncStatusPanel.tsx @@ -1,5 +1,6 @@ import { Icon } from "$/components/shared/Icon"; -import { getSyncStatus, reindexEmbeddings, syncPosts, type SyncStatus } from "$/lib/api/search"; +import { SearchController } from "$/lib/api/search"; +import type { SyncStatus } from "$/lib/api/types/search"; import { formatRelativeTime } from "$/lib/feeds"; import * as logger from "@tauri-apps/plugin-log"; import { createMemo, createSignal, For, onCleanup, onMount, Show } from "solid-js"; @@ -123,7 +124,7 @@ export function SyncStatusPanel(props: SyncStatusPanelProps) { async function loadSyncStatus() { try { - const status = await getSyncStatus(props.did); + const status = await SearchController.getSyncStatus(props.did); setSyncStatus(status); props.onStatusChange?.(status); } catch (error) { @@ -134,8 +135,8 @@ export function SyncStatusPanel(props: SyncStatusPanelProps) { async function handleSync() { setIsSyncing(true); try { - await syncPosts(props.did, "like"); - await syncPosts(props.did, "bookmark"); + await SearchController.syncPosts(props.did, "like"); + await SearchController.syncPosts(props.did, "bookmark"); await loadSyncStatus(); } catch (error) { logger.error("sync failed", { keyValues: { error: String(error) } }); @@ -147,7 +148,7 @@ export function SyncStatusPanel(props: SyncStatusPanelProps) { async function handleReindex() { setIsReindexing(true); try { - const count = await reindexEmbeddings(); + const count = await SearchController.reindexEmbeddings(); logger.info("reindex complete", { keyValues: { count: String(count) } }); await loadSyncStatus(); } catch (error) { diff --git a/src/components/settings/SettingsInlineFeedback.tsx b/src/components/settings/SettingsInlineFeedback.tsx index e3a7b19..18a030a 100644 --- a/src/components/settings/SettingsInlineFeedback.tsx +++ b/src/components/settings/SettingsInlineFeedback.tsx @@ -1,7 +1,7 @@ import { createSignal, onCleanup, Show } from "solid-js"; import { Icon } from "../shared/Icon"; -export type SettingsFeedback = { kind: "error" | "success"; message: string }; +type SettingsFeedback = { kind: "error" | "success"; message: string }; export function useTransientFeedback(timeoutMs = 5000) { const [feedback, setFeedback] = createSignal(null); diff --git a/src/components/shared/Icon.tsx b/src/components/shared/Icon.tsx index 43bf105..73509b8 100644 --- a/src/components/shared/Icon.tsx +++ b/src/components/shared/Icon.tsx @@ -1,5 +1,5 @@ -import type { SearchMode } from "$/lib/api/search"; import type { ExplorerTargetKind } from "$/lib/api/types/explorer"; +import type { SearchMode } from "$/lib/api/types/search"; import { type JSX, Match, splitProps, Switch } from "solid-js"; type ActionIconKind = "add" | "edit" | "delete" | "save" | "cancel"; diff --git a/src/contexts/app-preferences.tsx b/src/contexts/app-preferences.tsx index 52238a1..d2708bf 100644 --- a/src/contexts/app-preferences.tsx +++ b/src/contexts/app-preferences.tsx @@ -1,11 +1,6 @@ -import { - getEmbeddingsConfig, - prepareEmbeddingsModel as prepareEmbeddingsModelRequest, - setEmbeddingsEnabled as setEmbeddingsEnabledRequest, - setEmbeddingsPreflightSeen as setEmbeddingsPreflightSeenRequest, -} from "$/lib/api/search"; -import type { EmbeddingsConfig } from "$/lib/api/search"; +import { SearchController } from "$/lib/api/search"; import { SettingsController } from "$/lib/api/settings"; +import type { EmbeddingsConfig } from "$/lib/api/types/search"; import type { AppSettings } from "$/lib/types"; import * as logger from "@tauri-apps/plugin-log"; import { createContext, onMount, type ParentProps, splitProps, untrack, useContext } from "solid-js"; @@ -76,7 +71,7 @@ function createAppPreferencesValue(): AppPreferencesContextValue { setPreferences("embeddingsLoading", true); try { - const nextConfig = await getEmbeddingsConfig(); + const nextConfig = await SearchController.getEmbeddingsConfig(); setPreferences("embeddingsConfig", nextConfig); setPreferences("settings", (current) => { if (!current) { @@ -94,7 +89,7 @@ function createAppPreferencesValue(): AppPreferencesContextValue { async function prepareEmbeddingsModel() { try { - const nextConfig = await prepareEmbeddingsModelRequest(); + const nextConfig = await SearchController.prepareEmbeddingsModel(); setPreferences("embeddingsConfig", nextConfig); setPreferences("settings", (current) => { if (!current) { @@ -110,7 +105,7 @@ function createAppPreferencesValue(): AppPreferencesContextValue { async function setEmbeddingsEnabled(enabled: boolean) { try { - await setEmbeddingsEnabledRequest(enabled); + await SearchController.setEmbeddingsEnabled(enabled); setPreferences("settings", (current) => { if (!current) { return current; @@ -128,7 +123,7 @@ function createAppPreferencesValue(): AppPreferencesContextValue { async function setEmbeddingsPreflightSeen(seen: boolean) { try { - await setEmbeddingsPreflightSeenRequest(seen); + await SearchController.setEmbeddingsPreflightSeen(seen); setPreferences("embeddingsConfig", (current) => current ? { ...current, preflightSeen: seen } : current); } catch (error) { logger.error("failed to set embeddings preflight seen", { diff --git a/src/lib/api/drafts.ts b/src/lib/api/drafts.ts index 7c2956a..53448e8 100644 --- a/src/lib/api/drafts.ts +++ b/src/lib/api/drafts.ts @@ -1,19 +1,19 @@ import type { Draft, DraftInput } from "$/lib/types"; import { invoke } from "@tauri-apps/api/core"; -export function listDrafts(accountDid: string): Promise { +function listDrafts(accountDid: string): Promise { return invoke("list_drafts", { accountDid }); } -export function getDraft(id: string): Promise { +function getDraft(id: string): Promise { return invoke("get_draft", { id }); } -export function saveDraft(input: DraftInput): Promise { +function saveDraft(input: DraftInput): Promise { return invoke("save_draft", { input }); } -export function deleteDraft(id: string): Promise { +function deleteDraft(id: string): Promise { return invoke("delete_draft", { id }); } diff --git a/src/lib/api/media.ts b/src/lib/api/media.ts index dad7a22..7b07270 100644 --- a/src/lib/api/media.ts +++ b/src/lib/api/media.ts @@ -1,6 +1,6 @@ import { invoke } from "@tauri-apps/api/core"; -export type DownloadResult = { path: string; bytes: number }; +type DownloadResult = { path: string; bytes: number }; export type DownloadProgress = { url: string; diff --git a/src/lib/api/search.ts b/src/lib/api/search.ts index 0ca5d9b..c925c85 100644 --- a/src/lib/api/search.ts +++ b/src/lib/api/search.ts @@ -1,74 +1,17 @@ -import type { PostView } from "$/lib/types"; import { invoke } from "@tauri-apps/api/core"; - -export type SearchMode = "network" | "keyword" | "semantic" | "hybrid"; -export type NetworkSearchSort = "top" | "latest"; - -export type NetworkSearchResult = { cursor?: string | null; hitsTotal?: number | null; posts: PostView[] }; -export type NetworkSearchParams = { - query: string; - sort?: NetworkSearchSort; - since?: string | null; - until?: string | null; - mentions?: string | null; - author?: string | null; - tags?: string[]; - limit?: number; - cursor?: string | null; -}; - -export type ActorResult = { - did: string; - handle: string; - displayName?: string | null; - avatar?: string | null; - description?: string | null; -}; - -export type ActorSearchResult = { cursor?: string | null; actors: ActorResult[] }; - -export type SavedPostSource = "like" | "bookmark"; - -export type LocalPostResult = { - uri: string; - cid: string; - authorDid: string; - authorHandle?: string | null; - text?: string | null; - createdAt?: string | null; - source: SavedPostSource; - score: number; - keywordMatch: boolean; - semanticMatch: boolean; -}; - -type SavedPostsPage = { posts: LocalPostResult[]; total: number; nextOffset?: number | null }; - -export type SyncStatus = { - did: string; - source: SavedPostSource; - cursor?: string | null; - lastSyncedAt?: string | null; - postCount?: number; -}; - -export type EmbeddingsConfig = { - enabled: boolean; - preflightSeen: boolean; - modelName: string; - dimensions: number; - modelSizeBytes?: number | null; - downloaded: boolean; - downloadActive: boolean; - downloadProgress?: number | null; - downloadEtaSeconds?: number | null; - downloadFile?: string | null; - downloadFileIndex?: number | null; - downloadFileTotal?: number | null; - lastError?: string | null; -}; - -export function searchPostsNetwork(params: NetworkSearchParams): Promise { +import type { + ActorSearchResult, + EmbeddingsConfig, + LocalPostResult, + NetworkSearchParams, + NetworkSearchResult, + SavedPostSource, + SavedPostsPage, + SearchMode, + SyncStatus, +} from "./types/search"; + +function searchPostsNetwork(params: NetworkSearchParams): Promise { return invoke("search_posts_network", { queryParams: { author: params.author ?? null, @@ -84,47 +27,56 @@ export function searchPostsNetwork(params: NetworkSearchParams): Promise { +function searchPosts(query: string, mode: SearchMode, limit: number): Promise { return invoke("search_posts", { query, mode, limit }); } -export function listSavedPosts( - source: SavedPostSource, - limit: number, - offset = 0, - query?: string, -): Promise { +function listSavedPosts(source: SavedPostSource, limit: number, offset = 0, query?: string): Promise { return invoke("list_saved_posts", { source, limit, offset, query: query?.trim() ? query.trim() : null }); } -export function searchActors(query: string, limit?: number, cursor?: string | null): Promise { +function searchActors(query: string, limit?: number, cursor?: string | null): Promise { return invoke("search_actors", { query, limit: limit ?? null, cursor: cursor ?? null }); } -export function syncPosts(did: string, source: SavedPostSource): Promise { +function syncPosts(did: string, source: SavedPostSource): Promise { return invoke("sync_posts", { did, source }); } -export function getSyncStatus(did: string): Promise { +function getSyncStatus(did: string): Promise { return invoke("get_sync_status", { did }); } -export function reindexEmbeddings(): Promise { +function reindexEmbeddings(): Promise { return invoke("reindex_embeddings"); } -export function setEmbeddingsEnabled(enabled: boolean): Promise { +function setEmbeddingsEnabled(enabled: boolean): Promise { return invoke("set_embeddings_enabled", { enabled }); } -export function setEmbeddingsPreflightSeen(seen: boolean): Promise { +function setEmbeddingsPreflightSeen(seen: boolean): Promise { return invoke("set_embeddings_preflight_seen", { seen }); } -export function getEmbeddingsConfig(): Promise { +function getEmbeddingsConfig(): Promise { return invoke("get_embeddings_config"); } -export function prepareEmbeddingsModel(): Promise { +function prepareEmbeddingsModel(): Promise { return invoke("prepare_embeddings_model"); } + +export const SearchController = { + searchPostsNetwork, + searchPosts, + listSavedPosts, + searchActors, + syncPosts, + getSyncStatus, + reindexEmbeddings, + setEmbeddingsEnabled, + setEmbeddingsPreflightSeen, + getEmbeddingsConfig, + prepareEmbeddingsModel, +}; diff --git a/src/lib/api/settings.ts b/src/lib/api/settings.ts index 5999fcd..497308c 100644 --- a/src/lib/api/settings.ts +++ b/src/lib/api/settings.ts @@ -1,23 +1,23 @@ import type { AppSettings, CacheClearScope, CacheSize, ExportFormat, LogEntry, LogLevelFilter } from "$/lib/types"; import { invoke } from "@tauri-apps/api/core"; -export function getSettings() { +function getSettings() { return invoke("get_settings"); } -export function updateSetting(key: string, value: string) { +function updateSetting(key: string, value: string) { return invoke("update_setting", { key, value }); } -export function getCacheSize() { +function getCacheSize() { return invoke("get_cache_size"); } -export function clearCache(scope: CacheClearScope) { +function clearCache(scope: CacheClearScope) { return invoke("clear_cache", { scope }); } -export function exportData(format: ExportFormat, path?: string) { +function exportData(format: ExportFormat, path?: string) { const now = Date.now(); return invoke("export_data", { format, path: path ?? `lazurite_${now}_export.${format}` }); } @@ -26,12 +26,12 @@ function resetApp() { return invoke("reset_app"); } -export async function resetAndRestartApp() { +async function resetAndRestartApp() { await resetApp(); restartClient("/auth"); } -export function getLogEntries(limit: number, level?: LogLevelFilter) { +function getLogEntries(limit: number, level?: LogLevelFilter) { const filterLevel = level === "all" ? null : level; return invoke("get_log_entries", { limit, level: filterLevel }); } diff --git a/src/lib/api/types/columns.ts b/src/lib/api/types/columns.ts index 1a4acfb..1b1d6ab 100644 --- a/src/lib/api/types/columns.ts +++ b/src/lib/api/types/columns.ts @@ -1,4 +1,4 @@ -import type { SearchMode } from "../search"; +import type { SearchMode } from "./search"; export type ColumnKind = "feed" | "explorer" | "diagnostics" | "messages" | "search" | "profile"; diff --git a/src/lib/api/types/search.ts b/src/lib/api/types/search.ts new file mode 100644 index 0000000..c7bfbac --- /dev/null +++ b/src/lib/api/types/search.ts @@ -0,0 +1,70 @@ +import type { PostView } from "$/lib/types"; + +export type SearchMode = "network" | "keyword" | "semantic" | "hybrid"; + +export type NetworkSearchSort = "top" | "latest"; + +export type NetworkSearchResult = { cursor?: string | null; hitsTotal?: number | null; posts: PostView[] }; + +export type NetworkSearchParams = { + query: string; + sort?: NetworkSearchSort; + since?: string | null; + until?: string | null; + mentions?: string | null; + author?: string | null; + tags?: string[]; + limit?: number; + cursor?: string | null; +}; + +export type ActorResult = { + did: string; + handle: string; + displayName?: string | null; + avatar?: string | null; + description?: string | null; +}; + +export type ActorSearchResult = { cursor?: string | null; actors: ActorResult[] }; + +export type SavedPostSource = "like" | "bookmark"; + +export type LocalPostResult = { + uri: string; + cid: string; + authorDid: string; + authorHandle?: string | null; + text?: string | null; + createdAt?: string | null; + source: SavedPostSource; + score: number; + keywordMatch: boolean; + semanticMatch: boolean; +}; + +export type SavedPostsPage = { posts: LocalPostResult[]; total: number; nextOffset?: number | null }; + +export type SyncStatus = { + did: string; + source: SavedPostSource; + cursor?: string | null; + lastSyncedAt?: string | null; + postCount?: number; +}; + +export type EmbeddingsConfig = { + enabled: boolean; + preflightSeen: boolean; + modelName: string; + dimensions: number; + modelSizeBytes?: number | null; + downloaded: boolean; + downloadActive: boolean; + downloadProgress?: number | null; + downloadEtaSeconds?: number | null; + downloadFile?: string | null; + downloadFileIndex?: number | null; + downloadFileTotal?: number | null; + lastError?: string | null; +}; diff --git a/src/lib/search-routes.ts b/src/lib/search-routes.ts index 8ba9caa..d831525 100644 --- a/src/lib/search-routes.ts +++ b/src/lib/search-routes.ts @@ -1,4 +1,4 @@ -import type { NetworkSearchSort, SearchMode } from "$/lib/api/search"; +import type { NetworkSearchSort, SearchMode } from "$/lib/api/types/search"; export type SearchTab = "posts" | "profiles"; const SEARCH_ROUTE = "/search"; -- 2.51.2