diff --git a/.changeset/25921-collaboration-zero-consumer-exports.md b/.changeset/25921-collaboration-zero-consumer-exports.md new file mode 100644 index 00000000000..2e16a6d109a --- /dev/null +++ b/.changeset/25921-collaboration-zero-consumer-exports.md @@ -0,0 +1,19 @@ +--- +'@object-ui/collaboration': patch +--- + +refactor(collaboration)!: retire the zero-consumer `useCommentSearch` and `createPresenceUpdater` exports + +Clause-②: no (narrowing) +Metadata-protocol: none + + + +**BREAKING** for a host that imports any of these four names from `@object-ui/collaboration`. None had an importer anywhere in the workspace, so the Console and every `@object-ui/*` package are unaffected. `useCommentSearch` is deleted with its module; `createPresenceUpdater` leaves the package entry only. Everything else the entry exports is unchanged, including `usePresence`, `PresenceProvider`, `CommentThread` and their types. + +What leaves, and what to write instead: + +- FROM `useCommentSearch` TO a `useMemo` over your own comment list that keeps the entries whose `text` or `author` contains the trimmed, lower-cased query. +- FROM `CommentSearchConfig` TO `{ comments: CommentEntry[] }`, with `CommentEntry` from `@object-ui/types`. +- FROM `CommentSearchReturn` TO the shape your own search state returns; the result rows can keep using `CommentSearchResult` from `@object-ui/types`. +- FROM `createPresenceUpdater` TO a `Map` keyed by `userId` that you update from your realtime channel, or `PresenceProvider` with `useTenantPresence` / `useRecordPresence`, which the package keeps exporting. diff --git a/.changeset/25921-mobile-zero-consumer-exports.md b/.changeset/25921-mobile-zero-consumer-exports.md new file mode 100644 index 00000000000..7d66bd25aa5 --- /dev/null +++ b/.changeset/25921-mobile-zero-consumer-exports.md @@ -0,0 +1,19 @@ +--- +'@object-ui/mobile': patch +--- + +refactor(mobile)!: retire the zero-consumer `useOfflineSync`, `requestBackgroundSync` and `resolveResponsiveValue` exports + +Clause-②: no (narrowing) +Metadata-protocol: none + + + +**BREAKING** for a host that imports any of these four names from `@object-ui/mobile`. None had an importer anywhere in the workspace, so the Console and every `@object-ui/*` package are unaffected. `useOfflineSync` is deleted with its module; `requestBackgroundSync` and `resolveResponsiveValue` leave the package entry only (`useResponsive` still resolves through the latter internally). The offline data source (`createOfflineDataSource`, `createOfflineQueue` and its queue classes), `getServiceWorkerSource`, `useResponsive` and `BREAKPOINTS` stay exported unchanged. + +What leaves, and what to write instead: + +- FROM `useOfflineSync(source)` TO your own effect on the `OfflineDataSource` that `createOfflineDataSource` returns: read `source.pending()` into state, call `source.replay()` on the window `online` event, and use `source.drop(opId)` / `source.clear()` for the queue controls. +- FROM `OfflineSyncState` TO the state shape that effect keeps (`isOnline`, `pending: OfflineOperation[]`, `isReplaying`). +- FROM `requestBackgroundSync(tag)` TO `(await navigator.serviceWorker.ready).sync?.register(tag)`, guarded for browsers without Background Sync. +- FROM `resolveResponsiveValue(value, breakpoint)` TO `useResponsive(value)` inside a component, which reads the current breakpoint itself. diff --git a/ui/packages/collaboration/src/index.ts b/ui/packages/collaboration/src/index.ts index 2a4ac2f713b..1703d015649 100644 --- a/ui/packages/collaboration/src/index.ts +++ b/ui/packages/collaboration/src/index.ts @@ -29,7 +29,6 @@ export { export { usePresence, - createPresenceUpdater, type PresenceUser, type PresenceConfig, type PresenceResult, @@ -69,12 +68,6 @@ export { type MentionNotificationsResult, } from './useMentionNotifications.js'; -export { - useCommentSearch, - type CommentSearchConfig, - type CommentSearchReturn, -} from './useCommentSearch.js'; - export { PresenceAvatars, type PresenceAvatarsProps, diff --git a/ui/packages/collaboration/src/useCommentSearch.ts b/ui/packages/collaboration/src/useCommentSearch.ts deleted file mode 100644 index 41b6880cefa..00000000000 --- a/ui/packages/collaboration/src/useCommentSearch.ts +++ /dev/null @@ -1,86 +0,0 @@ -/** - * ObjectUI - * Copyright (c) 2024-present ObjectStack Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -import { useState, useCallback, useMemo } from 'react'; -import type { CommentEntry, CommentSearchResult } from '@object-ui/types'; - -export interface CommentSearchConfig { - /** All comments across records, each must have objectName and recordId set */ - comments: CommentEntry[]; -} - -export interface CommentSearchReturn { - /** Current search query */ - query: string; - /** Update the search query */ - setQuery: (query: string) => void; - /** Filtered/matched results */ - results: CommentSearchResult[]; - /** Whether a search is active */ - isSearching: boolean; - /** Clear the search */ - clearSearch: () => void; -} - -/** - * Build a highlighted snippet around the first match of `query` in `text`. - * Returns the match wrapped in tags for display. - */ -function buildHighlight(text: string, query: string): string { - if (!query) return text; - const idx = text.toLowerCase().indexOf(query.toLowerCase()); - if (idx === -1) return text; - const start = Math.max(0, idx - 30); - const end = Math.min(text.length, idx + query.length + 30); - const before = start > 0 ? '…' : ''; - const after = end < text.length ? '…' : ''; - return `${before}${text.slice(start, end)}${after}`; -} - -/** - * Hook for searching comments across all records. - * - * Accepts a flat list of CommentEntry items (each should have objectName - * and recordId set) and provides a search interface that returns matching - * results with highlighted snippets. - */ -export function useCommentSearch({ comments }: CommentSearchConfig): CommentSearchReturn { - const [query, setQuery] = useState(''); - - const isSearching = query.trim().length > 0; - - const results = useMemo(() => { - const trimmed = query.trim().toLowerCase(); - if (!trimmed) return []; - - return comments - .filter(c => { - const textMatch = c.text.toLowerCase().includes(trimmed); - const authorMatch = c.author.toLowerCase().includes(trimmed); - return textMatch || authorMatch; - }) - .map(c => ({ - comment: c, - objectName: c.objectName ?? '', - recordId: c.recordId ?? '', - highlight: buildHighlight(c.text, trimmed), - })); - }, [query, comments]); - - const clearSearch = useCallback(() => { - setQuery(''); - }, []); - - return { - query, - setQuery, - results, - isSearching, - clearSearch, - }; -} diff --git a/ui/packages/mobile/src/index.ts b/ui/packages/mobile/src/index.ts index 0f75b68efc1..49ce561aa99 100644 --- a/ui/packages/mobile/src/index.ts +++ b/ui/packages/mobile/src/index.ts @@ -50,13 +50,11 @@ export { type OfflineDataSourceOptions, type QueueableDataSource, } from './createOfflineDataSource.js'; -export { useOfflineSync, type OfflineSyncState } from './useOfflineSync.js'; export { getServiceWorkerSource, - requestBackgroundSync, type ServiceWorkerSourceOptions, } from './serviceWorkerSource.js'; -export { BREAKPOINTS, resolveResponsiveValue } from './breakpoints.js'; +export { BREAKPOINTS } from './breakpoints.js'; // Re-export types for convenience export type { diff --git a/ui/packages/mobile/src/useOfflineSync.ts b/ui/packages/mobile/src/useOfflineSync.ts deleted file mode 100644 index 44a5ac4f2ce..00000000000 --- a/ui/packages/mobile/src/useOfflineSync.ts +++ /dev/null @@ -1,79 +0,0 @@ -/** - * ObjectUI - * Copyright (c) 2024-present ObjectStack Inc. - * - * React hook that subscribes to an `OfflineDataSource`, exposing the current - * online/offline status and the pending queue for status-bar / banner UIs. - */ - -import { useEffect, useState, useCallback } from 'react'; -import type { OfflineDataSource } from './createOfflineDataSource.js'; -import type { OfflineOperation } from './offlineQueue.js'; - -export interface OfflineSyncState { - /** True if the browser reports `navigator.onLine`. */ - isOnline: boolean; - /** Pending ops, oldest first. */ - pending: OfflineOperation[]; - /** True while a replay is in-flight. */ - isReplaying: boolean; - /** Manually trigger a replay. */ - replay: () => Promise; - /** Drop a queued op without replaying it. */ - drop: (opId: string) => Promise; - /** Drop everything. */ - clear: () => Promise; -} - -/** - * Subscribe to a wrapped offline DataSource. Auto-replays the queue - * whenever the browser fires `online`. - */ -export function useOfflineSync(source: OfflineDataSource): OfflineSyncState { - const [pending, setPending] = useState([]); - const [isOnline, setOnline] = useState(() => - typeof navigator === 'undefined' ? true : navigator.onLine !== false, - ); - const [isReplaying, setReplaying] = useState(false); - - const refresh = useCallback(async () => { - try { setPending(await source.pending()); } catch { setPending([]); } - }, [source]); - - const replay = useCallback(async () => { - setReplaying(true); - try { await source.replay(); } finally { - setReplaying(false); - await refresh(); - } - }, [source, refresh]); - - const drop = useCallback(async (opId: string) => { - await source.drop(opId); - await refresh(); - }, [source, refresh]); - - const clear = useCallback(async () => { - await source.clear(); - await refresh(); - }, [source, refresh]); - - // Initial load + listen for online/offline transitions. - useEffect(() => { - refresh(); - if (typeof window === 'undefined') return; - const onOnline = () => { - setOnline(true); - void replay(); - }; - const onOffline = () => setOnline(false); - window.addEventListener('online', onOnline); - window.addEventListener('offline', onOffline); - return () => { - window.removeEventListener('online', onOnline); - window.removeEventListener('offline', onOffline); - }; - }, [refresh, replay]); - - return { isOnline, pending, isReplaying, replay, drop, clear }; -}