mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-18 23:07:02 +08:00
feat(core): remember backlink open/close state (#9073)
fix AF-1850, AF-1883
This commit is contained in:
@@ -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);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 => {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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:');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user