feat(core): support better battery save mode (#13383)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Introduced a Document Summary module, enabling live and cached
document summaries with cloud revalidation.
  * Added a feature flag for enabling battery save mode.
* Added explicit pause and resume controls for sync operations,
accessible via UI events and programmatically.

* **Improvements**
* Enhanced sync and indexing logic to support pausing, resuming, and
battery save mode, with improved job prioritization.
* Updated navigation and preview components to use the new document
summary service and improved priority handling.
* Improved logging and state reporting for sync and indexing processes.
* Refined backlink handling with reactive loading states and cloud
revalidation.
* Replaced backlink and link management to use a new dedicated document
links service.
* Enhanced workspace engine to conditionally enable battery save mode
based on feature flags and workspace flavor.

* **Bug Fixes**
* Removed unnecessary debug console logs from various components for
cleaner output.

* **Refactor**
* Replaced battery save mode methods with explicit pause/resume methods
throughout the app and services.
* Modularized and streamlined document summary and sync-related code for
better maintainability.
* Restructured backlink components to improve visibility handling and
data fetching.
* Simplified and improved document backlink data fetching with retry and
loading state management.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
EYHN
2025-08-01 16:31:31 +08:00
committed by GitHub
parent 1661ab1790
commit 1ceed6c145
34 changed files with 717 additions and 286 deletions
@@ -1,4 +1,10 @@
import { Button, Divider, useLitPortalFactory } from '@affine/component';
import {
Button,
Divider,
observeIntersection,
Skeleton,
useLitPortalFactory,
} from '@affine/component';
import { getViewManager } from '@affine/core/blocksuite/manager/view';
import {
patchReferenceRenderer,
@@ -30,11 +36,14 @@ import {
useLiveData,
useServices,
} from '@toeverything/infra';
import { debounce } from 'lodash-es';
import {
Fragment,
type ReactNode,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
@@ -177,14 +186,22 @@ const usePreviewExtensions = () => {
return [extensions, portals] as const;
};
const useBacklinkGroups: () => BacklinkGroups[] = () => {
export const BacklinkGroups = () => {
const [extensions, portals] = usePreviewExtensions();
const { workspaceService, docService } = useServices({
WorkspaceService,
DocService,
});
const { docLinksService } = useServices({
DocLinksService,
});
const backlinkGroups = useLiveData(
LiveData.computed(get => {
const links = get(docLinksService.backlinks.backlinks$);
docLinksService.backlinks.backlinks$.map(links => {
if (links === undefined) {
return undefined;
}
// group by docId
const groupedLinks = links.reduce(
@@ -203,17 +220,10 @@ const useBacklinkGroups: () => BacklinkGroups[] = () => {
})
);
return backlinkGroups;
};
useEffect(() => {
docLinksService.backlinks.revalidateFromCloud();
}, [docLinksService]);
export const BacklinkGroups = () => {
const [extensions, portals] = usePreviewExtensions();
const { workspaceService, docService } = useServices({
WorkspaceService,
DocService,
});
const backlinkGroups = useBacklinkGroups();
const textRendererOptions = useMemo(() => {
const docLinkBaseURLMiddleware: TransformerMiddleware = ({
adapterConfigs,
@@ -233,36 +243,87 @@ export const BacklinkGroups = () => {
return (
<>
{backlinkGroups.map(linkGroup => (
<CollapsibleSection
key={linkGroup.docId}
title={
<AffinePageReference
pageId={linkGroup.docId}
onClick={() => {
track.doc.biDirectionalLinksPanel.backlinkTitle.navigate();
}}
{backlinkGroups === undefined ? (
<Skeleton />
) : (
backlinkGroups.map(linkGroup => (
<CollapsibleSection
key={linkGroup.docId}
title={
<AffinePageReference
pageId={linkGroup.docId}
onClick={() => {
track.doc.biDirectionalLinksPanel.backlinkTitle.navigate();
}}
/>
}
length={linkGroup.links.length}
docId={docService.doc.id}
linkDocId={linkGroup.docId}
>
<LinkPreview
textRendererOptions={textRendererOptions}
linkGroup={linkGroup}
/>
}
length={linkGroup.links.length}
docId={docService.doc.id}
linkDocId={linkGroup.docId}
>
<LinkPreview
textRendererOptions={textRendererOptions}
linkGroup={linkGroup}
/>
</CollapsibleSection>
</CollapsibleSection>
))
)}
{portals.map(p => (
<Fragment key={p.id}>{p.portal}</Fragment>
))}
<>
{portals.map(p => (
<Fragment key={p.id}>{p.portal}</Fragment>
))}
</>
</>
);
};
const BacklinkLinks = () => {
const t = useI18n();
const [visibility, setVisibility] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const container = containerRef.current;
if (!container) return;
return observeIntersection(
container,
debounce(
entry => {
setVisibility(entry.isIntersecting);
},
500,
{
trailing: true,
}
)
);
}, []);
const { docLinksService } = useServices({
DocLinksService,
});
const backlinks = useLiveData(docLinksService.backlinks.backlinks$);
useEffect(() => {
if (visibility) {
docLinksService.backlinks.revalidateFromCloud();
}
}, [docLinksService, visibility]);
const backlinkCount = backlinks?.length;
return (
<div className={styles.linksContainer} ref={containerRef}>
<div className={styles.linksTitles}>
{t['com.affine.page-properties.backlinks']()}{' '}
{backlinkCount !== undefined ? `· ${backlinkCount}` : ''}
</div>
<BacklinkGroups />
</div>
);
};
export const LinkPreview = ({
linkGroup,
textRendererOptions,
@@ -361,18 +422,13 @@ export const BiDirectionalLinkPanel = () => {
show ? docLinksService.links.links$ : new LiveData([] as Link[])
);
const backlinkGroups = useBacklinkGroups();
const backlinkCount = useMemo(() => {
return backlinkGroups.reduce((acc, link) => acc + link.links.length, 0);
}, [backlinkGroups]);
const handleClickShow = useCallback(() => {
setShow(!show);
track.doc.biDirectionalLinksPanel.$.toggle({
type: show ? 'collapse' : 'expand',
});
}, [show, setShow]);
return (
<div className={styles.container}>
{!show && <Divider size="thinner" />}
@@ -390,12 +446,7 @@ export const BiDirectionalLinkPanel = () => {
<>
<Divider size="thinner" />
<div className={styles.linksContainer}>
<div className={styles.linksTitles}>
{t['com.affine.page-properties.backlinks']()} · {backlinkCount}
</div>
<BacklinkGroups />
</div>
<BacklinkLinks />
<div className={styles.linksContainer}>
<div className={styles.linksTitles}>
{t['com.affine.page-properties.outgoing-links']()} ·{' '}