feat(core): remember backlink open/close state (#9073)

fix AF-1850, AF-1883
This commit is contained in:
pengx17
2024-12-10 02:28:20 +00:00
parent 4335b0dc79
commit ec140da0d9
7 changed files with 132 additions and 25 deletions

View File

@@ -1,17 +1,35 @@
export { GlobalCache, GlobalState } from './providers/global'; export {
export { GlobalCacheService, GlobalStateService } from './services/global'; GlobalCache,
GlobalSessionState,
GlobalState,
} from './providers/global';
export {
GlobalCacheService,
GlobalSessionStateService,
GlobalStateService,
} from './services/global';
import type { Framework } from '../../framework'; import type { Framework } from '../../framework';
import { MemoryMemento } from '../../storage'; import { MemoryMemento } from '../../storage';
import { GlobalCache, GlobalState } from './providers/global'; import {
import { GlobalCacheService, GlobalStateService } from './services/global'; GlobalCache,
GlobalSessionState,
GlobalState,
} from './providers/global';
import {
GlobalCacheService,
GlobalSessionStateService,
GlobalStateService,
} from './services/global';
export const configureGlobalStorageModule = (framework: Framework) => { export const configureGlobalStorageModule = (framework: Framework) => {
framework.service(GlobalStateService, [GlobalState]); framework.service(GlobalStateService, [GlobalState]);
framework.service(GlobalCacheService, [GlobalCache]); framework.service(GlobalCacheService, [GlobalCache]);
framework.service(GlobalSessionStateService, [GlobalSessionState]);
}; };
export const configureTestingGlobalStorage = (framework: Framework) => { export const configureTestingGlobalStorage = (framework: Framework) => {
framework.impl(GlobalCache, MemoryMemento); framework.impl(GlobalCache, MemoryMemento);
framework.impl(GlobalState, MemoryMemento); framework.impl(GlobalState, MemoryMemento);
framework.impl(GlobalSessionState, MemoryMemento);
}; };

View File

@@ -16,5 +16,13 @@ export const GlobalState = createIdentifier<GlobalState>('GlobalState');
* Cache may be deleted from time to time, business logic should not rely on cache. * Cache may be deleted from time to time, business logic should not rely on cache.
*/ */
export interface GlobalCache extends Memento {} export interface GlobalCache extends Memento {}
export const GlobalCache = createIdentifier<GlobalCache>('GlobalCache'); export const GlobalCache = createIdentifier<GlobalCache>('GlobalCache');
/**
* A memento object that stores session state.
*
* Session state is not persisted, it will be cleared when the application is closed. (thinking about sessionStorage)
*/
export interface GlobalSessionState extends Memento {}
export const GlobalSessionState =
createIdentifier<GlobalSessionState>('GlobalSessionState');

View File

@@ -1,5 +1,9 @@
import { Service } from '../../../framework'; import { Service } from '../../../framework';
import type { GlobalCache, GlobalState } from '../providers/global'; import type {
GlobalCache,
GlobalSessionState,
GlobalState,
} from '../providers/global';
export class GlobalStateService extends Service { export class GlobalStateService extends Service {
constructor(public readonly globalState: GlobalState) { constructor(public readonly globalState: GlobalState) {
@@ -12,3 +16,9 @@ export class GlobalCacheService extends Service {
super(); super();
} }
} }
export class GlobalSessionStateService extends Service {
constructor(public readonly globalSessionState: GlobalSessionState) {
super();
}
}

View File

@@ -16,7 +16,9 @@ import type { JobMiddleware } from '@blocksuite/affine/store';
import { ToggleExpandIcon } from '@blocksuite/icons/rc'; import { ToggleExpandIcon } from '@blocksuite/icons/rc';
import * as Collapsible from '@radix-ui/react-collapsible'; import * as Collapsible from '@radix-ui/react-collapsible';
import { import {
DocService,
getAFFiNEWorkspaceSchema, getAFFiNEWorkspaceSchema,
GlobalSessionStateService,
LiveData, LiveData,
useFramework, useFramework,
useLiveData, useLiveData,
@@ -47,16 +49,50 @@ const BlocksuiteTextRenderer = createReactComponentFromLit({
elementClass: TextRenderer, elementClass: TextRenderer,
}); });
const PREFIX = 'bi-directional-link-panel-collapse:';
const useBiDirectionalLinkPanelCollapseState = (
docId: string,
linkDocId?: string
) => {
const { globalSessionStateService } = useServices({
GlobalSessionStateService,
});
const path = linkDocId ? docId + ':' + linkDocId : docId;
const [open, setOpen] = useState(
globalSessionStateService.globalSessionState.get(PREFIX + path) ?? false
);
const wrappedSetOpen = useCallback(
(open: boolean) => {
setOpen(open);
globalSessionStateService.globalSessionState.set(PREFIX + path, open);
},
[path, globalSessionStateService]
);
return [open, wrappedSetOpen] as const;
};
const CollapsibleSection = ({ const CollapsibleSection = ({
title, title,
children, children,
length, length,
docId,
linkDocId,
}: { }: {
title: ReactNode; title: ReactNode;
children: ReactNode; children: ReactNode;
length?: number; length?: number;
docId: string;
linkDocId?: string;
}) => { }) => {
const [open, setOpen] = useState(false); const [open, setOpen] = useBiDirectionalLinkPanelCollapseState(
docId,
linkDocId
);
return ( return (
<Collapsible.Root open={open} onOpenChange={setOpen}> <Collapsible.Root open={open} onOpenChange={setOpen}>
<Collapsible.Trigger className={styles.link}> <Collapsible.Trigger className={styles.link}>
@@ -114,15 +150,19 @@ const usePreviewExtensions = () => {
}; };
export const BiDirectionalLinkPanel = () => { export const BiDirectionalLinkPanel = () => {
const [show, setShow] = useState(false); const { docLinksService, workspaceService, docService } = useServices({
const { docLinksService, workspaceService } = useServices({
DocLinksService, DocLinksService,
WorkspaceService, WorkspaceService,
DocService,
}); });
const [extensions, portals] = usePreviewExtensions(); const [extensions, portals] = usePreviewExtensions();
const t = useI18n(); const t = useI18n();
const [show, setShow] = useBiDirectionalLinkPanelCollapseState(
docService.doc.id
);
const links = useLiveData( const links = useLiveData(
show ? docLinksService.links.links$ : new LiveData([] as Link[]) show ? docLinksService.links.links$ : new LiveData([] as Link[])
); );
@@ -157,7 +197,7 @@ export const BiDirectionalLinkPanel = () => {
const handleClickShow = useCallback(() => { const handleClickShow = useCallback(() => {
setShow(!show); setShow(!show);
}, [show]); }, [show, setShow]);
const textRendererOptions = useMemo(() => { const textRendererOptions = useMemo(() => {
const docLinkBaseURLMiddleware: JobMiddleware = ({ adapterConfigs }) => { const docLinkBaseURLMiddleware: JobMiddleware = ({ adapterConfigs }) => {
@@ -205,6 +245,8 @@ export const BiDirectionalLinkPanel = () => {
key={linkGroup.docId} key={linkGroup.docId}
title={<AffinePageReference pageId={linkGroup.docId} />} title={<AffinePageReference pageId={linkGroup.docId} />}
length={linkGroup.links.length} length={linkGroup.links.length}
docId={docService.doc.id}
linkDocId={linkGroup.docId}
> >
<div className={styles.linkPreviewContainer}> <div className={styles.linkPreviewContainer}>
{linkGroup.links.map(link => { {linkGroup.links.map(link => {

View File

@@ -27,6 +27,7 @@ import { configurePermissionsModule } from './permissions';
import { configureQuickSearchModule } from './quicksearch'; import { configureQuickSearchModule } from './quicksearch';
import { configureShareDocsModule } from './share-doc'; import { configureShareDocsModule } from './share-doc';
import { configureShareSettingModule } from './share-setting'; import { configureShareSettingModule } from './share-setting';
import { configureCommonGlobalStorageImpls } from './storage';
import { configureSystemFontFamilyModule } from './system-font-family'; import { configureSystemFontFamilyModule } from './system-font-family';
import { configureTagModule } from './tag'; import { configureTagModule } from './tag';
import { configureTelemetryModule } from './telemetry'; import { configureTelemetryModule } from './telemetry';
@@ -71,4 +72,5 @@ export function configureCommonModules(framework: Framework) {
configureOpenInApp(framework); configureOpenInApp(framework);
configAtMenuConfigModule(framework); configAtMenuConfigModule(framework);
configureDndModule(framework); configureDndModule(framework);
configureCommonGlobalStorageImpls(framework);
} }

View File

@@ -1,13 +1,21 @@
import type { GlobalCache, GlobalState, Memento } from '@toeverything/infra'; import type {
GlobalCache,
GlobalSessionState,
GlobalState,
Memento,
} from '@toeverything/infra';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
export class LocalStorageMemento implements Memento { export class StorageMemento implements Memento {
constructor(private readonly prefix: string) {} constructor(
private readonly storage: Storage,
private readonly prefix: string
) {}
keys(): string[] { keys(): string[] {
const keys: string[] = []; const keys: string[] = [];
for (let i = 0; i < localStorage.length; i++) { for (let i = 0; i < this.storage.length; i++) {
const key = localStorage.key(i); const key = this.storage.key(i);
if (key && key.startsWith(this.prefix)) { if (key && key.startsWith(this.prefix)) {
keys.push(key.slice(this.prefix.length)); keys.push(key.slice(this.prefix.length));
} }
@@ -16,12 +24,12 @@ export class LocalStorageMemento implements Memento {
} }
get<T>(key: string): T | undefined { get<T>(key: string): T | undefined {
const json = localStorage.getItem(this.prefix + key); const json = this.storage.getItem(this.prefix + key);
return json ? JSON.parse(json) : undefined; return json ? JSON.parse(json) : undefined;
} }
watch<T>(key: string): Observable<T | undefined> { watch<T>(key: string): Observable<T | undefined> {
return new Observable<T | undefined>(subscriber => { return new Observable<T | undefined>(subscriber => {
const json = localStorage.getItem(this.prefix + key); const json = this.storage.getItem(this.prefix + key);
const first = json ? JSON.parse(json) : undefined; const first = json ? JSON.parse(json) : undefined;
subscriber.next(first); subscriber.next(first);
@@ -35,14 +43,14 @@ export class LocalStorageMemento implements Memento {
}); });
} }
set<T>(key: string, value: T): void { set<T>(key: string, value: T): void {
localStorage.setItem(this.prefix + key, JSON.stringify(value)); this.storage.setItem(this.prefix + key, JSON.stringify(value));
const channel = new BroadcastChannel(this.prefix + key); const channel = new BroadcastChannel(this.prefix + key);
channel.postMessage(value); channel.postMessage(value);
channel.close(); channel.close();
} }
del(key: string): void { del(key: string): void {
localStorage.removeItem(this.prefix + key); this.storage.removeItem(this.prefix + key);
} }
clear(): void { clear(): void {
@@ -53,19 +61,28 @@ export class LocalStorageMemento implements Memento {
} }
export class LocalStorageGlobalCache export class LocalStorageGlobalCache
extends LocalStorageMemento extends StorageMemento
implements GlobalCache implements GlobalCache
{ {
constructor() { constructor() {
super('global-cache:'); super(localStorage, 'global-cache:');
} }
} }
export class LocalStorageGlobalState export class LocalStorageGlobalState
extends LocalStorageMemento extends StorageMemento
implements GlobalState implements GlobalState
{ {
constructor() { constructor() {
super('global-state:'); super(localStorage, 'global-state:');
}
}
export class SessionStorageGlobalSessionState
extends StorageMemento
implements GlobalSessionState
{
constructor() {
super(sessionStorage, 'global-session-state:');
} }
} }

View File

@@ -1,11 +1,17 @@
import { type Framework, GlobalCache, GlobalState } from '@toeverything/infra'; import {
type Framework,
GlobalCache,
GlobalSessionState,
GlobalState,
} from '@toeverything/infra';
import { DesktopApiService } from '../desktop-api'; import { DesktopApiService } from '../desktop-api';
import { ElectronGlobalCache, ElectronGlobalState } from './impls/electron'; import { ElectronGlobalCache, ElectronGlobalState } from './impls/electron';
import { import {
LocalStorageGlobalCache, LocalStorageGlobalCache,
LocalStorageGlobalState, LocalStorageGlobalState,
} from './impls/local-storage'; SessionStorageGlobalSessionState,
} from './impls/storage';
export function configureLocalStorageStateStorageImpls(framework: Framework) { export function configureLocalStorageStateStorageImpls(framework: Framework) {
framework.impl(GlobalCache, LocalStorageGlobalCache); framework.impl(GlobalCache, LocalStorageGlobalCache);
@@ -16,3 +22,7 @@ export function configureElectronStateStorageImpls(framework: Framework) {
framework.impl(GlobalCache, ElectronGlobalCache, [DesktopApiService]); framework.impl(GlobalCache, ElectronGlobalCache, [DesktopApiService]);
framework.impl(GlobalState, ElectronGlobalState, [DesktopApiService]); framework.impl(GlobalState, ElectronGlobalState, [DesktopApiService]);
} }
export function configureCommonGlobalStorageImpls(framework: Framework) {
framework.impl(GlobalSessionState, SessionStorageGlobalSessionState);
}