mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-23 21:06:22 +08:00
feat: bump blocksuite (#6078)
This commit is contained in:
Vendored
+2
-2
@@ -3,8 +3,8 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@blocksuite/global": "0.13.0-canary-202403050653-934469c",
|
||||
"@blocksuite/store": "0.13.0-canary-202403050653-934469c",
|
||||
"@blocksuite/global": "0.13.0-canary-202403120738-e15d583",
|
||||
"@blocksuite/store": "0.13.0-canary-202403120738-e15d583",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"vitest": "1.3.1"
|
||||
|
||||
Vendored
+4
-4
@@ -1,5 +1,5 @@
|
||||
// This file should has not side effect
|
||||
import type { Workspace } from '@blocksuite/store';
|
||||
import type { DocCollection } from '@blocksuite/store';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@@ -95,12 +95,12 @@ export const Messages = {
|
||||
};
|
||||
|
||||
export class PageNotFoundError extends TypeError {
|
||||
readonly workspace: Workspace;
|
||||
readonly docCollection: DocCollection;
|
||||
readonly pageId: string;
|
||||
|
||||
constructor(workspace: Workspace, pageId: string) {
|
||||
constructor(docCollection: DocCollection, pageId: string) {
|
||||
super();
|
||||
this.workspace = workspace;
|
||||
this.docCollection = docCollection;
|
||||
this.pageId = pageId;
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+2
-2
@@ -1,4 +1,4 @@
|
||||
import type { Workspace } from '@blocksuite/store';
|
||||
import type { DocCollection } from '@blocksuite/store';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const literalValueSchema: z.ZodType<LiteralValue, z.ZodTypeDef> =
|
||||
@@ -81,4 +81,4 @@ export const tagSchema = z.object({
|
||||
});
|
||||
export type Tag = z.input<typeof tagSchema>;
|
||||
|
||||
export type PropertiesMeta = Workspace['meta']['properties'];
|
||||
export type PropertiesMeta = DocCollection['meta']['properties'];
|
||||
|
||||
Vendored
+3
-3
@@ -1,6 +1,6 @@
|
||||
/// <reference types="@blocksuite/global" />
|
||||
import { assertEquals } from '@blocksuite/global/utils';
|
||||
import type { Workspace } from '@blocksuite/store';
|
||||
import type { DocCollection } from '@blocksuite/store';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { isDesktop, isServer } from './constant.js';
|
||||
@@ -154,9 +154,9 @@ export function setupGlobal() {
|
||||
globalThis.$AFFINE_SETUP = true;
|
||||
}
|
||||
|
||||
export function setupEditorFlags(workspace: Workspace) {
|
||||
export function setupEditorFlags(docCollection: DocCollection) {
|
||||
Object.entries(runtimeConfig.editorFlags).forEach(([key, value]) => {
|
||||
workspace.awarenessStore.setFlag(
|
||||
docCollection.awarenessStore.setFlag(
|
||||
key as keyof BlockSuiteFeatureFlags,
|
||||
value
|
||||
);
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
"@affine/debug": "workspace:*",
|
||||
"@affine/env": "workspace:*",
|
||||
"@affine/templates": "workspace:*",
|
||||
"@blocksuite/blocks": "0.13.0-canary-202403050653-934469c",
|
||||
"@blocksuite/global": "0.13.0-canary-202403050653-934469c",
|
||||
"@blocksuite/store": "0.13.0-canary-202403050653-934469c",
|
||||
"@blocksuite/blocks": "0.13.0-canary-202403120738-e15d583",
|
||||
"@blocksuite/global": "0.13.0-canary-202403120738-e15d583",
|
||||
"@blocksuite/store": "0.13.0-canary-202403120738-e15d583",
|
||||
"foxact": "^0.2.31",
|
||||
"jotai": "^2.6.5",
|
||||
"jotai-effect": "^0.6.0",
|
||||
@@ -33,8 +33,8 @@
|
||||
"devDependencies": {
|
||||
"@affine-test/fixtures": "workspace:*",
|
||||
"@affine/templates": "workspace:*",
|
||||
"@blocksuite/lit": "0.13.0-canary-202403050653-934469c",
|
||||
"@blocksuite/presets": "0.13.0-canary-202403050653-934469c",
|
||||
"@blocksuite/lit": "0.13.0-canary-202403120738-e15d583",
|
||||
"@blocksuite/presets": "0.13.0-canary-202403120738-e15d583",
|
||||
"@testing-library/react": "^14.2.1",
|
||||
"async-call-rpc": "^6.4.0",
|
||||
"react": "^18.2.0",
|
||||
|
||||
@@ -35,7 +35,7 @@ export async function migratePages(
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
schema.upgradeWorkspace(rootDoc);
|
||||
schema.upgradeCollection(rootDoc);
|
||||
|
||||
// Hard code to upgrade page version to 2.
|
||||
// Let e2e to ensure the data version is correct.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Workspace } from '@blocksuite/store';
|
||||
import type { DocCollection } from '@blocksuite/store';
|
||||
import type { Array as YArray, Doc as YDoc, Map as YMap } from 'yjs';
|
||||
|
||||
/**
|
||||
@@ -11,14 +11,14 @@ export enum MigrationPoint {
|
||||
}
|
||||
|
||||
export function checkWorkspaceCompatibility(
|
||||
workspace: Workspace,
|
||||
docCollection: DocCollection,
|
||||
isCloud: boolean
|
||||
): MigrationPoint | null {
|
||||
// check if there is any key starts with 'space:' on root doc
|
||||
const spaceMetaObj = workspace.doc.share.get('space:meta') as
|
||||
const spaceMetaObj = docCollection.doc.share.get('space:meta') as
|
||||
| YMap<any>
|
||||
| undefined;
|
||||
const docKeys = Array.from(workspace.doc.share.keys());
|
||||
const docKeys = Array.from(docCollection.doc.share.keys());
|
||||
const haveSpaceMeta = !!spaceMetaObj && spaceMetaObj.size > 0;
|
||||
const haveLegacySpace = docKeys.some(key => key.startsWith('space:'));
|
||||
|
||||
@@ -28,12 +28,12 @@ export function checkWorkspaceCompatibility(
|
||||
}
|
||||
|
||||
// exit if no pages
|
||||
if (!workspace.meta.docs?.length) {
|
||||
if (!docCollection.meta.docs?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// check guid compatibility
|
||||
const meta = workspace.doc.getMap('meta') as YMap<unknown>;
|
||||
const meta = docCollection.doc.getMap('meta') as YMap<unknown>;
|
||||
const pages = meta.get('pages') as YArray<YMap<unknown>>;
|
||||
for (const page of pages) {
|
||||
const pageId = page.get('id') as string | undefined;
|
||||
@@ -41,23 +41,23 @@ export function checkWorkspaceCompatibility(
|
||||
return MigrationPoint.GuidFix;
|
||||
}
|
||||
}
|
||||
const spaces = workspace.doc.getMap('spaces') as YMap<YDoc>;
|
||||
const spaces = docCollection.doc.getMap('spaces') as YMap<YDoc>;
|
||||
for (const [pageId, _] of spaces) {
|
||||
if (pageId.includes(':')) {
|
||||
return MigrationPoint.GuidFix;
|
||||
}
|
||||
}
|
||||
|
||||
const hasVersion = workspace.meta.hasVersion;
|
||||
const hasVersion = docCollection.meta.hasVersion;
|
||||
if (!hasVersion) {
|
||||
return MigrationPoint.BlockVersion;
|
||||
}
|
||||
|
||||
// TODO: Catch compatibility error from blocksuite to show upgrade page.
|
||||
// Temporarily follow the check logic of blocksuite.
|
||||
if ((workspace.meta.docs?.length ?? 0) <= 1) {
|
||||
if ((docCollection.meta.docs?.length ?? 0) <= 1) {
|
||||
try {
|
||||
workspace.meta.validateVersion(workspace);
|
||||
docCollection.meta.validateVersion(docCollection);
|
||||
} catch (e) {
|
||||
console.info('validateVersion error', e);
|
||||
return MigrationPoint.BlockVersion;
|
||||
@@ -65,9 +65,9 @@ export function checkWorkspaceCompatibility(
|
||||
}
|
||||
|
||||
// From v2, we depend on blocksuite to check and migrate data.
|
||||
const blockVersions = workspace.meta.blockVersions;
|
||||
const blockVersions = docCollection.meta.blockVersions;
|
||||
for (const [flavour, version] of Object.entries(blockVersions ?? {})) {
|
||||
const schema = workspace.schema.flavourSchemaMap.get(flavour);
|
||||
const schema = docCollection.schema.flavourSchemaMap.get(flavour);
|
||||
if (schema?.version !== version) {
|
||||
return MigrationPoint.BlockVersion;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import type {
|
||||
CollectionInfoSnapshot,
|
||||
Doc,
|
||||
DocSnapshot,
|
||||
JobMiddleware,
|
||||
WorkspaceInfoSnapshot,
|
||||
} from '@blocksuite/store';
|
||||
import { Job } from '@blocksuite/store';
|
||||
import { Map as YMap } from 'yjs';
|
||||
@@ -49,17 +49,17 @@ export async function buildShowcaseWorkspace(
|
||||
) {
|
||||
const meta = await workspaceManager.createWorkspace(
|
||||
flavour,
|
||||
async (blockSuiteWorkspace, blobStorage) => {
|
||||
blockSuiteWorkspace.meta.setName(workspaceName);
|
||||
async (docCollection, blobStorage) => {
|
||||
docCollection.meta.setName(workspaceName);
|
||||
const { onboarding } = await import('@affine/templates');
|
||||
|
||||
const info = onboarding['info.json'] as WorkspaceInfoSnapshot;
|
||||
const info = onboarding['info.json'] as CollectionInfoSnapshot;
|
||||
const blob = onboarding['blob.json'] as { [key: string]: string };
|
||||
|
||||
const migrationMiddleware: JobMiddleware = ({ slots, workspace }) => {
|
||||
const migrationMiddleware: JobMiddleware = ({ slots, collection }) => {
|
||||
slots.afterImport.on(payload => {
|
||||
if (payload.type === 'page') {
|
||||
workspace.schema.upgradeDoc(
|
||||
collection.schema.upgradeDoc(
|
||||
info?.pageVersion ?? 0,
|
||||
{},
|
||||
payload.page.spaceDoc
|
||||
@@ -69,11 +69,11 @@ export async function buildShowcaseWorkspace(
|
||||
};
|
||||
|
||||
const job = new Job({
|
||||
workspace: blockSuiteWorkspace,
|
||||
collection: docCollection,
|
||||
middlewares: [replaceIdMiddleware, migrationMiddleware],
|
||||
});
|
||||
|
||||
job.snapshotToWorkspaceInfo(info);
|
||||
job.snapshotToCollectionInfo(info);
|
||||
|
||||
// for now all onboarding assets are considered served via CDN
|
||||
// hack assets so that every blob exists
|
||||
@@ -92,8 +92,8 @@ export async function buildShowcaseWorkspace(
|
||||
})
|
||||
);
|
||||
|
||||
const newVersions = getLatestVersions(blockSuiteWorkspace.schema);
|
||||
blockSuiteWorkspace.doc
|
||||
const newVersions = getLatestVersions(docCollection.schema);
|
||||
docCollection.doc
|
||||
.getMap('meta')
|
||||
.set('blockVersions', new YMap(Object.entries(newVersions)));
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ export class PageManager {
|
||||
if (!pageRecord) {
|
||||
throw new Error('Page record not found');
|
||||
}
|
||||
const blockSuitePage = this.workspace.blockSuiteWorkspace.getDoc(pageId);
|
||||
const blockSuitePage = this.workspace.docCollection.getDoc(pageId);
|
||||
if (!blockSuitePage) {
|
||||
throw new Error('Page not found');
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ export class PageRecordList {
|
||||
new Observable(subscriber => {
|
||||
const emit = () => {
|
||||
subscriber.next(
|
||||
this.workspace.blockSuiteWorkspace.meta.docMetas.map(
|
||||
this.workspace.docCollection.meta.docMetas.map(
|
||||
v => new PageRecord(v.id, this.workspace, this.localState)
|
||||
)
|
||||
);
|
||||
@@ -27,7 +27,7 @@ export class PageRecordList {
|
||||
emit();
|
||||
|
||||
const dispose =
|
||||
this.workspace.blockSuiteWorkspace.meta.docMetaUpdated.on(emit).dispose;
|
||||
this.workspace.docCollection.meta.docMetaUpdated.on(emit).dispose;
|
||||
return () => {
|
||||
dispose();
|
||||
};
|
||||
|
||||
@@ -16,7 +16,7 @@ export class PageRecord {
|
||||
meta = LiveData.from<DocMeta>(
|
||||
new Observable(subscriber => {
|
||||
const emit = () => {
|
||||
const meta = this.workspace.blockSuiteWorkspace.meta.docMetas.find(
|
||||
const meta = this.workspace.docCollection.meta.docMetas.find(
|
||||
page => page.id === this.id
|
||||
);
|
||||
if (meta === undefined) {
|
||||
@@ -28,7 +28,7 @@ export class PageRecord {
|
||||
emit();
|
||||
|
||||
const dispose =
|
||||
this.workspace.blockSuiteWorkspace.meta.docMetaUpdated.on(emit).dispose;
|
||||
this.workspace.docCollection.meta.docMetaUpdated.on(emit).dispose;
|
||||
return () => {
|
||||
dispose();
|
||||
};
|
||||
@@ -42,7 +42,7 @@ export class PageRecord {
|
||||
);
|
||||
|
||||
setMeta(meta: Partial<DocMeta>): void {
|
||||
this.workspace.blockSuiteWorkspace.setDocMeta(this.id, meta);
|
||||
this.workspace.docCollection.setDocMeta(this.id, meta);
|
||||
}
|
||||
|
||||
mode: LiveData<PageMode> = LiveData.from(
|
||||
|
||||
@@ -22,7 +22,7 @@ describe('Workspace System', () => {
|
||||
|
||||
expect(workspaceListService.workspaceList.value.length).toBe(1);
|
||||
|
||||
const page = workspace.blockSuiteWorkspace.createDoc({
|
||||
const page = workspace.docCollection.createDoc({
|
||||
id: 'page0',
|
||||
});
|
||||
page.load();
|
||||
@@ -30,7 +30,7 @@ describe('Workspace System', () => {
|
||||
title: new page.Text('test-page'),
|
||||
});
|
||||
|
||||
expect(workspace.blockSuiteWorkspace.docs.size).toBe(1);
|
||||
expect(workspace.docCollection.docs.size).toBe(1);
|
||||
expect(
|
||||
(page!.getBlockByFlavour('affine:page')[0] as any).title.toString()
|
||||
).toBe('test-page');
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
* })
|
||||
*/
|
||||
|
||||
import { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
|
||||
import { DocCollection } from '@blocksuite/store';
|
||||
import { nanoid } from 'nanoid';
|
||||
import type { Awareness } from 'y-protocols/awareness.js';
|
||||
import type { Doc as YDoc } from 'yjs';
|
||||
@@ -29,7 +29,7 @@ import { globalBlockSuiteSchema } from './global-schema';
|
||||
import type { WorkspaceMetadata } from './metadata';
|
||||
import { WorkspaceScope } from './service-scope';
|
||||
|
||||
export const BlockSuiteWorkspaceContext = createIdentifier<BlockSuiteWorkspace>(
|
||||
export const BlockSuiteWorkspaceContext = createIdentifier<DocCollection>(
|
||||
'BlockSuiteWorkspaceContext'
|
||||
);
|
||||
|
||||
@@ -53,7 +53,7 @@ export function configureWorkspaceContext(
|
||||
.addImpl(WorkspaceMetadataContext, workspaceMetadata)
|
||||
.addImpl(WorkspaceIdContext, workspaceMetadata.id)
|
||||
.addImpl(BlockSuiteWorkspaceContext, provider => {
|
||||
return new BlockSuiteWorkspace({
|
||||
return new DocCollection({
|
||||
id: workspaceMetadata.id,
|
||||
blobStorages: [
|
||||
() => ({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { Workspace } from '@blocksuite/store';
|
||||
import { DocCollection } from '@blocksuite/store';
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
import { Doc } from 'yjs';
|
||||
|
||||
@@ -26,14 +26,14 @@ describe('SyncEngine', () => {
|
||||
const storage2 = new MemoryMemento();
|
||||
let prev: any;
|
||||
{
|
||||
const workspace = new Workspace({
|
||||
const docCollection = new DocCollection({
|
||||
id: 'test',
|
||||
|
||||
schema: globalBlockSuiteSchema,
|
||||
});
|
||||
|
||||
const syncEngine = new SyncEngine(
|
||||
workspace.doc,
|
||||
docCollection.doc,
|
||||
new TestingSyncStorage(testMeta, storage),
|
||||
[
|
||||
new TestingSyncStorage(testMeta, storage1),
|
||||
@@ -42,7 +42,7 @@ describe('SyncEngine', () => {
|
||||
);
|
||||
syncEngine.start();
|
||||
|
||||
const page = workspace.createDoc({
|
||||
const page = docCollection.createDoc({
|
||||
id: 'page0',
|
||||
});
|
||||
page.load();
|
||||
@@ -69,23 +69,23 @@ describe('SyncEngine', () => {
|
||||
);
|
||||
await syncEngine.waitForSynced();
|
||||
syncEngine.forceStop();
|
||||
prev = workspace.doc.toJSON();
|
||||
prev = docCollection.doc.toJSON();
|
||||
}
|
||||
|
||||
for (const current of [storage, storage1, storage2]) {
|
||||
const workspace = new Workspace({
|
||||
const docCollection = new DocCollection({
|
||||
id: 'test',
|
||||
|
||||
schema: globalBlockSuiteSchema,
|
||||
});
|
||||
const syncEngine = new SyncEngine(
|
||||
workspace.doc,
|
||||
docCollection.doc,
|
||||
new TestingSyncStorage(testMeta, current),
|
||||
[]
|
||||
);
|
||||
syncEngine.start();
|
||||
await syncEngine.waitForSynced();
|
||||
expect(workspace.doc.toJSON()).toEqual({
|
||||
expect(docCollection.doc.toJSON()).toEqual({
|
||||
...prev,
|
||||
});
|
||||
syncEngine.forceStop();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { Workspace } from '@blocksuite/store';
|
||||
import { DocCollection } from '@blocksuite/store';
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import { MemoryMemento } from '../../../../storage';
|
||||
@@ -23,19 +23,19 @@ describe('SyncPeer', () => {
|
||||
|
||||
let prev: any;
|
||||
{
|
||||
const workspace = new Workspace({
|
||||
const docCollection = new DocCollection({
|
||||
id: 'test',
|
||||
|
||||
schema: globalBlockSuiteSchema,
|
||||
});
|
||||
|
||||
const syncPeer = new SyncPeer(
|
||||
workspace.doc,
|
||||
docCollection.doc,
|
||||
new TestingSyncStorage(testMeta, storage)
|
||||
);
|
||||
await syncPeer.waitForLoaded();
|
||||
|
||||
const page = workspace.createDoc({
|
||||
const page = docCollection.createDoc({
|
||||
id: 'page0',
|
||||
});
|
||||
page.load();
|
||||
@@ -62,21 +62,21 @@ describe('SyncPeer', () => {
|
||||
);
|
||||
await syncPeer.waitForSynced();
|
||||
syncPeer.stop();
|
||||
prev = workspace.doc.toJSON();
|
||||
prev = docCollection.doc.toJSON();
|
||||
}
|
||||
|
||||
{
|
||||
const workspace = new Workspace({
|
||||
const docCollection = new DocCollection({
|
||||
id: 'test',
|
||||
|
||||
schema: globalBlockSuiteSchema,
|
||||
});
|
||||
const syncPeer = new SyncPeer(
|
||||
workspace.doc,
|
||||
docCollection.doc,
|
||||
new TestingSyncStorage(testMeta, storage)
|
||||
);
|
||||
await syncPeer.waitForSynced();
|
||||
expect(workspace.doc.toJSON()).toEqual({
|
||||
expect(docCollection.doc.toJSON()).toEqual({
|
||||
...prev,
|
||||
});
|
||||
syncPeer.stop();
|
||||
@@ -86,21 +86,21 @@ describe('SyncPeer', () => {
|
||||
test('status', async () => {
|
||||
const storage = new MemoryMemento();
|
||||
|
||||
const workspace = new Workspace({
|
||||
const docCollection = new DocCollection({
|
||||
id: 'test',
|
||||
|
||||
schema: globalBlockSuiteSchema,
|
||||
});
|
||||
|
||||
const syncPeer = new SyncPeer(
|
||||
workspace.doc,
|
||||
docCollection.doc,
|
||||
new TestingSyncStorage(testMeta, storage)
|
||||
);
|
||||
expect(syncPeer.status.step).toBe(SyncPeerStep.LoadingRootDoc);
|
||||
await syncPeer.waitForSynced();
|
||||
expect(syncPeer.status.step).toBe(SyncPeerStep.Synced);
|
||||
|
||||
const page = workspace.createDoc({
|
||||
const page = docCollection.createDoc({
|
||||
id: 'page0',
|
||||
});
|
||||
expect(syncPeer.status.step).toBe(SyncPeerStep.LoadingSubDoc);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import type { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import type { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
|
||||
import type { DocCollection } from '@blocksuite/store';
|
||||
import { differenceWith } from 'lodash-es';
|
||||
|
||||
import { createIdentifier } from '../../di';
|
||||
@@ -34,7 +34,7 @@ export interface WorkspaceListProvider {
|
||||
*/
|
||||
create(
|
||||
initial: (
|
||||
workspace: BlockSuiteWorkspace,
|
||||
docCollection: DocCollection,
|
||||
blobStorage: BlobStorage
|
||||
) => Promise<void>
|
||||
): Promise<WorkspaceMetadata>;
|
||||
@@ -124,7 +124,7 @@ export class WorkspaceListService {
|
||||
async create(
|
||||
flavour: WorkspaceFlavour,
|
||||
initial: (
|
||||
workspace: BlockSuiteWorkspace,
|
||||
docCollection: DocCollection,
|
||||
blobStorage: BlobStorage
|
||||
) => Promise<void> = () => Promise.resolve()
|
||||
) {
|
||||
|
||||
@@ -57,13 +57,13 @@ export class WorkspaceInformation {
|
||||
*/
|
||||
syncWithWorkspace(workspace: Workspace) {
|
||||
this.info = {
|
||||
avatar: workspace.blockSuiteWorkspace.meta.avatar ?? this.info.avatar,
|
||||
name: workspace.blockSuiteWorkspace.meta.name ?? this.info.name,
|
||||
avatar: workspace.docCollection.meta.avatar ?? this.info.avatar,
|
||||
name: workspace.docCollection.meta.name ?? this.info.name,
|
||||
};
|
||||
workspace.blockSuiteWorkspace.meta.commonFieldsUpdated.on(() => {
|
||||
workspace.docCollection.meta.commonFieldsUpdated.on(() => {
|
||||
this.info = {
|
||||
avatar: workspace.blockSuiteWorkspace.meta.avatar ?? this.info.avatar,
|
||||
name: workspace.blockSuiteWorkspace.meta.name ?? this.info.name,
|
||||
avatar: workspace.docCollection.meta.avatar ?? this.info.avatar,
|
||||
name: workspace.docCollection.meta.name ?? this.info.name,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { DebugLogger } from '@affine/debug';
|
||||
import { setupEditorFlags } from '@affine/env/global';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { assertEquals } from '@blocksuite/global/utils';
|
||||
import type { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
|
||||
import type { DocCollection } from '@blocksuite/store';
|
||||
import { applyUpdate, encodeStateAsUpdate } from 'yjs';
|
||||
|
||||
import { fixWorkspaceVersion } from '../blocksuite';
|
||||
@@ -105,7 +105,7 @@ export class WorkspaceManager {
|
||||
createWorkspace(
|
||||
flavour: WorkspaceFlavour,
|
||||
initial?: (
|
||||
workspace: BlockSuiteWorkspace,
|
||||
docCollection: DocCollection,
|
||||
blobStorage: BlobStorage
|
||||
) => Promise<void>
|
||||
): Promise<WorkspaceMetadata> {
|
||||
@@ -131,9 +131,9 @@ export class WorkspaceManager {
|
||||
const newId = await this.list.create(
|
||||
WorkspaceFlavour.AFFINE_CLOUD,
|
||||
async (ws, bs) => {
|
||||
applyUpdate(ws.doc, encodeStateAsUpdate(local.blockSuiteWorkspace.doc));
|
||||
applyUpdate(ws.doc, encodeStateAsUpdate(local.docCollection.doc));
|
||||
|
||||
for (const subdoc of local.blockSuiteWorkspace.doc.getSubdocs()) {
|
||||
for (const subdoc of local.docCollection.doc.getSubdocs()) {
|
||||
for (const newSubdoc of ws.doc.getSubdocs()) {
|
||||
if (newSubdoc.guid === subdoc.guid) {
|
||||
applyUpdate(newSubdoc, encodeStateAsUpdate(subdoc));
|
||||
@@ -191,9 +191,9 @@ export class WorkspaceManager {
|
||||
const workspace = provider.get(Workspace);
|
||||
|
||||
// apply compatibility fix
|
||||
fixWorkspaceVersion(workspace.blockSuiteWorkspace.doc);
|
||||
fixWorkspaceVersion(workspace.docCollection.doc);
|
||||
|
||||
setupEditorFlags(workspace.blockSuiteWorkspace);
|
||||
setupEditorFlags(workspace.docCollection);
|
||||
|
||||
return workspace;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
|
||||
import { DocCollection } from '@blocksuite/store';
|
||||
import { differenceBy } from 'lodash-es';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { applyUpdate, encodeStateAsUpdate } from 'yjs';
|
||||
@@ -43,7 +43,7 @@ export class TestingLocalWorkspaceListProvider
|
||||
}
|
||||
async create(
|
||||
initial: (
|
||||
workspace: BlockSuiteWorkspace,
|
||||
docCollection: DocCollection,
|
||||
blobStorage: BlobStorage
|
||||
) => Promise<void>
|
||||
): Promise<WorkspaceMetadata> {
|
||||
@@ -53,18 +53,18 @@ export class TestingLocalWorkspaceListProvider
|
||||
const blobStorage = new TestingBlobStorage(meta, this.state);
|
||||
const syncStorage = new TestingSyncStorage(meta, this.state);
|
||||
|
||||
const workspace = new BlockSuiteWorkspace({
|
||||
const docCollection = new DocCollection({
|
||||
id: id,
|
||||
idGenerator: () => nanoid(),
|
||||
schema: globalBlockSuiteSchema,
|
||||
});
|
||||
|
||||
// apply initial state
|
||||
await initial(workspace, blobStorage);
|
||||
await initial(docCollection, blobStorage);
|
||||
|
||||
// save workspace to storage
|
||||
await syncStorage.push(id, encodeStateAsUpdate(workspace.doc));
|
||||
for (const subdocs of workspace.doc.getSubdocs()) {
|
||||
await syncStorage.push(id, encodeStateAsUpdate(docCollection.doc));
|
||||
for (const subdocs of docCollection.doc.getSubdocs()) {
|
||||
await syncStorage.push(subdocs.guid, encodeStateAsUpdate(subdocs));
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ export class TestingLocalWorkspaceListProvider
|
||||
return;
|
||||
}
|
||||
|
||||
const bs = new BlockSuiteWorkspace({
|
||||
const bs = new DocCollection({
|
||||
id,
|
||||
schema: globalBlockSuiteSchema,
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Unreachable } from '@affine/env/constant';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { Slot } from '@blocksuite/global/utils';
|
||||
import type { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
|
||||
import type { DocCollection } from '@blocksuite/store';
|
||||
import { applyUpdate, Doc as YDoc, encodeStateAsUpdate } from 'yjs';
|
||||
|
||||
import { checkWorkspaceCompatibility, MigrationPoint } from '../blocksuite';
|
||||
@@ -38,18 +38,18 @@ export class WorkspaceUpgradeController {
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly blockSuiteWorkspace: BlockSuiteWorkspace,
|
||||
private readonly docCollection: DocCollection,
|
||||
private readonly sync: SyncEngine,
|
||||
private readonly workspaceMetadata: WorkspaceMetadata
|
||||
) {
|
||||
blockSuiteWorkspace.doc.on('update', () => {
|
||||
docCollection.doc.on('update', () => {
|
||||
this.checkIfNeedUpgrade();
|
||||
});
|
||||
}
|
||||
|
||||
checkIfNeedUpgrade() {
|
||||
const needUpgrade = !!checkWorkspaceCompatibility(
|
||||
this.blockSuiteWorkspace,
|
||||
this.docCollection,
|
||||
this.workspaceMetadata.flavour === WorkspaceFlavour.AFFINE_CLOUD
|
||||
);
|
||||
this.status = {
|
||||
@@ -72,7 +72,7 @@ export class WorkspaceUpgradeController {
|
||||
await this.sync.waitForSynced();
|
||||
|
||||
const step = checkWorkspaceCompatibility(
|
||||
this.blockSuiteWorkspace,
|
||||
this.docCollection,
|
||||
this.workspaceMetadata.flavour === WorkspaceFlavour.AFFINE_CLOUD
|
||||
);
|
||||
|
||||
@@ -82,9 +82,9 @@ export class WorkspaceUpgradeController {
|
||||
|
||||
// Clone a new doc to prevent change events.
|
||||
const clonedDoc = new YDoc({
|
||||
guid: this.blockSuiteWorkspace.doc.guid,
|
||||
guid: this.docCollection.doc.guid,
|
||||
});
|
||||
applyDoc(clonedDoc, this.blockSuiteWorkspace.doc);
|
||||
applyDoc(clonedDoc, this.docCollection.doc);
|
||||
|
||||
if (step === MigrationPoint.SubDoc) {
|
||||
const newWorkspace = await workspaceManager.createWorkspace(
|
||||
@@ -92,14 +92,11 @@ export class WorkspaceUpgradeController {
|
||||
async (workspace, blobStorage) => {
|
||||
await upgradeV1ToV2(clonedDoc, workspace.doc);
|
||||
migrateGuidCompatibility(clonedDoc);
|
||||
await forceUpgradePages(
|
||||
workspace.doc,
|
||||
this.blockSuiteWorkspace.schema
|
||||
);
|
||||
const blobList = await this.blockSuiteWorkspace.blob.list();
|
||||
await forceUpgradePages(workspace.doc, this.docCollection.schema);
|
||||
const blobList = await this.docCollection.blob.list();
|
||||
|
||||
for (const blobKey of blobList) {
|
||||
const blob = await this.blockSuiteWorkspace.blob.get(blobKey);
|
||||
const blob = await this.docCollection.blob.get(blobKey);
|
||||
if (blob) {
|
||||
await blobStorage.set(blobKey, blob);
|
||||
}
|
||||
@@ -110,13 +107,13 @@ export class WorkspaceUpgradeController {
|
||||
return newWorkspace;
|
||||
} else if (step === MigrationPoint.GuidFix) {
|
||||
migrateGuidCompatibility(clonedDoc);
|
||||
await forceUpgradePages(clonedDoc, this.blockSuiteWorkspace.schema);
|
||||
applyDoc(this.blockSuiteWorkspace.doc, clonedDoc);
|
||||
await forceUpgradePages(clonedDoc, this.docCollection.schema);
|
||||
applyDoc(this.docCollection.doc, clonedDoc);
|
||||
await this.sync.waitForSynced();
|
||||
return null;
|
||||
} else if (step === MigrationPoint.BlockVersion) {
|
||||
await forceUpgradePages(clonedDoc, this.blockSuiteWorkspace.schema);
|
||||
applyDoc(this.blockSuiteWorkspace.doc, clonedDoc);
|
||||
await forceUpgradePages(clonedDoc, this.docCollection.schema);
|
||||
applyDoc(this.docCollection.doc, clonedDoc);
|
||||
await this.sync.waitForSynced();
|
||||
return null;
|
||||
} else {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import { Slot } from '@blocksuite/global/utils';
|
||||
import type { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
|
||||
import type { DocCollection } from '@blocksuite/store';
|
||||
|
||||
import type { ServiceProvider } from '../di';
|
||||
import { CleanupService } from '../lifecycle';
|
||||
@@ -10,7 +10,7 @@ import { type WorkspaceMetadata } from './metadata';
|
||||
import type { WorkspaceUpgradeController } from './upgrade';
|
||||
import { type WorkspaceUpgradeStatus } from './upgrade';
|
||||
|
||||
export type { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
|
||||
export type { DocCollection } from '@blocksuite/store';
|
||||
|
||||
const logger = new DebugLogger('affine:workspace');
|
||||
|
||||
@@ -67,7 +67,7 @@ export class Workspace {
|
||||
constructor(
|
||||
public meta: WorkspaceMetadata,
|
||||
public engine: WorkspaceEngine,
|
||||
public blockSuiteWorkspace: BlockSuiteWorkspace,
|
||||
public docCollection: DocCollection,
|
||||
public upgrade: WorkspaceUpgradeController,
|
||||
public services: ServiceProvider
|
||||
) {
|
||||
|
||||
@@ -32,14 +32,14 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@blocksuite/global": "0.13.0-canary-202403050653-934469c",
|
||||
"@blocksuite/global": "0.13.0-canary-202403120738-e15d583",
|
||||
"idb": "^8.0.0",
|
||||
"nanoid": "^5.0.6",
|
||||
"y-provider": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@blocksuite/blocks": "0.13.0-canary-202403050653-934469c",
|
||||
"@blocksuite/store": "0.13.0-canary-202403050653-934469c",
|
||||
"@blocksuite/blocks": "0.13.0-canary-202403120738-e15d583",
|
||||
"@blocksuite/store": "0.13.0-canary-202403120738-e15d583",
|
||||
"fake-indexeddb": "^5.0.2",
|
||||
"vite": "^5.1.4",
|
||||
"vite-plugin-dts": "3.7.3",
|
||||
|
||||
@@ -8,7 +8,7 @@ import { setTimeout } from 'node:timers/promises';
|
||||
import { AffineSchemas } from '@blocksuite/blocks/schemas';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import type { Doc } from '@blocksuite/store';
|
||||
import { Schema, Workspace } from '@blocksuite/store';
|
||||
import { DocCollection, Schema } from '@blocksuite/store';
|
||||
import { openDB } from 'idb';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
@@ -69,7 +69,7 @@ async function getUpdates(id: string): Promise<Uint8Array[]> {
|
||||
}
|
||||
|
||||
let id: string;
|
||||
let workspace: Workspace;
|
||||
let docCollection: DocCollection;
|
||||
const rootDBName = DEFAULT_DB_NAME;
|
||||
|
||||
const schema = new Schema();
|
||||
@@ -78,7 +78,7 @@ schema.register(AffineSchemas);
|
||||
|
||||
beforeEach(() => {
|
||||
id = nanoid();
|
||||
workspace = new Workspace({
|
||||
docCollection = new DocCollection({
|
||||
id,
|
||||
|
||||
schema,
|
||||
@@ -93,7 +93,7 @@ afterEach(() => {
|
||||
|
||||
describe('indexeddb provider', () => {
|
||||
test('connect', async () => {
|
||||
const provider = createIndexedDBProvider(workspace.doc);
|
||||
const provider = createIndexedDBProvider(docCollection.doc);
|
||||
provider.connect();
|
||||
|
||||
// todo: has a better way to know when data is synced
|
||||
@@ -110,11 +110,11 @@ describe('indexeddb provider', () => {
|
||||
updates: [
|
||||
{
|
||||
timestamp: expect.any(Number),
|
||||
update: encodeStateAsUpdate(workspace.doc),
|
||||
update: encodeStateAsUpdate(docCollection.doc),
|
||||
},
|
||||
],
|
||||
});
|
||||
const page = workspace.createDoc({ id: 'page0' });
|
||||
const page = docCollection.createDoc({ id: 'page0' });
|
||||
page.load();
|
||||
const pageBlockId = page.addBlock(
|
||||
'affine:page' as keyof BlockSuite.BlockModels,
|
||||
@@ -139,13 +139,13 @@ describe('indexeddb provider', () => {
|
||||
const data = (await store.get(id)) as WorkspacePersist | undefined;
|
||||
assertExists(data);
|
||||
expect(data.id).toBe(id);
|
||||
const testWorkspace = new Workspace({
|
||||
const testWorkspace = new DocCollection({
|
||||
id: 'test',
|
||||
schema,
|
||||
});
|
||||
// data should only contain updates for the root doc
|
||||
data.updates.forEach(({ update }) => {
|
||||
Workspace.Y.applyUpdate(testWorkspace.doc, update);
|
||||
DocCollection.Y.applyUpdate(testWorkspace.doc, update);
|
||||
});
|
||||
const subPage = testWorkspace.doc.spaces.get('page0');
|
||||
{
|
||||
@@ -157,23 +157,23 @@ describe('indexeddb provider', () => {
|
||||
assertExists(data);
|
||||
testWorkspace.getDoc('page0')?.load();
|
||||
data.updates.forEach(({ update }) => {
|
||||
Workspace.Y.applyUpdate(subPage, update);
|
||||
DocCollection.Y.applyUpdate(subPage, update);
|
||||
});
|
||||
}
|
||||
expect(workspace.doc.toJSON()).toEqual(testWorkspace.doc.toJSON());
|
||||
expect(docCollection.doc.toJSON()).toEqual(testWorkspace.doc.toJSON());
|
||||
}
|
||||
});
|
||||
|
||||
test('connect and disconnect', async () => {
|
||||
const provider = createIndexedDBProvider(workspace.doc, rootDBName);
|
||||
const provider = createIndexedDBProvider(docCollection.doc, rootDBName);
|
||||
provider.connect();
|
||||
expect(provider.connected).toBe(true);
|
||||
await setTimeout(200);
|
||||
const snapshot = encodeStateAsUpdate(workspace.doc);
|
||||
const snapshot = encodeStateAsUpdate(docCollection.doc);
|
||||
provider.disconnect();
|
||||
expect(provider.connected).toBe(false);
|
||||
{
|
||||
const page = workspace.createDoc({ id: 'page0' });
|
||||
const page = docCollection.createDoc({ id: 'page0' });
|
||||
page.load();
|
||||
const pageBlockId = page.addBlock(
|
||||
'affine:page' as keyof BlockSuite.BlockModels
|
||||
@@ -190,7 +190,7 @@ describe('indexeddb provider', () => {
|
||||
);
|
||||
}
|
||||
{
|
||||
const updates = await getUpdates(workspace.id);
|
||||
const updates = await getUpdates(docCollection.id);
|
||||
expect(updates.length).toBe(1);
|
||||
expect(updates[0]).toEqual(snapshot);
|
||||
}
|
||||
@@ -199,7 +199,7 @@ describe('indexeddb provider', () => {
|
||||
expect(provider.connected).toBe(true);
|
||||
await setTimeout(200);
|
||||
{
|
||||
const updates = await getUpdates(workspace.id);
|
||||
const updates = await getUpdates(docCollection.id);
|
||||
expect(updates).not.toEqual([]);
|
||||
}
|
||||
expect(provider.connected).toBe(true);
|
||||
@@ -208,7 +208,7 @@ describe('indexeddb provider', () => {
|
||||
});
|
||||
|
||||
test('cleanup', async () => {
|
||||
const provider = createIndexedDBProvider(workspace.doc);
|
||||
const provider = createIndexedDBProvider(docCollection.doc);
|
||||
provider.connect();
|
||||
await setTimeout(200);
|
||||
const db = await openDB(rootDBName, dbVersion);
|
||||
@@ -218,7 +218,7 @@ describe('indexeddb provider', () => {
|
||||
.transaction('workspace', 'readonly')
|
||||
.objectStore('workspace');
|
||||
const keys = await store.getAllKeys();
|
||||
expect(keys).contain(workspace.id);
|
||||
expect(keys).contain(docCollection.id);
|
||||
}
|
||||
|
||||
await provider.cleanup();
|
||||
@@ -229,16 +229,16 @@ describe('indexeddb provider', () => {
|
||||
.transaction('workspace', 'readonly')
|
||||
.objectStore('workspace');
|
||||
const keys = await store.getAllKeys();
|
||||
expect(keys).not.contain(workspace.id);
|
||||
expect(keys).not.contain(docCollection.id);
|
||||
}
|
||||
});
|
||||
|
||||
test('merge', async () => {
|
||||
setMergeCount(5);
|
||||
const provider = createIndexedDBProvider(workspace.doc, rootDBName);
|
||||
const provider = createIndexedDBProvider(docCollection.doc, rootDBName);
|
||||
provider.connect();
|
||||
{
|
||||
const page = workspace.createDoc({ id: 'page0' });
|
||||
const page = docCollection.createDoc({ id: 'page0' });
|
||||
page.load();
|
||||
const pageBlockId = page.addBlock(
|
||||
'affine:page' as keyof BlockSuite.BlockModels
|
||||
@@ -264,7 +264,7 @@ describe('indexeddb provider', () => {
|
||||
});
|
||||
|
||||
test("data won't be lost", async () => {
|
||||
const doc = new Workspace.Y.Doc();
|
||||
const doc = new DocCollection.Y.Doc();
|
||||
const map = doc.getMap('map');
|
||||
for (let i = 0; i < 100; i++) {
|
||||
map.set(`${i}`, i);
|
||||
@@ -275,7 +275,7 @@ describe('indexeddb provider', () => {
|
||||
provider.disconnect();
|
||||
}
|
||||
{
|
||||
const newDoc = new Workspace.Y.Doc();
|
||||
const newDoc = new DocCollection.Y.Doc();
|
||||
const provider = createIndexedDBProvider(newDoc, rootDBName);
|
||||
provider.connect();
|
||||
provider.disconnect();
|
||||
@@ -412,14 +412,14 @@ describe('subDoc', () => {
|
||||
});
|
||||
|
||||
test('blocksuite', async () => {
|
||||
const page0 = workspace.createDoc({
|
||||
const page0 = docCollection.createDoc({
|
||||
id: 'page0',
|
||||
});
|
||||
page0.load();
|
||||
const { paragraphBlockId: paragraphBlockIdPage1 } = initEmptyPage(page0);
|
||||
const provider = createIndexedDBProvider(workspace.doc, rootDBName);
|
||||
const provider = createIndexedDBProvider(docCollection.doc, rootDBName);
|
||||
provider.connect();
|
||||
const page1 = workspace.createDoc({
|
||||
const page1 = docCollection.createDoc({
|
||||
id: 'page1',
|
||||
});
|
||||
page1.load();
|
||||
@@ -427,22 +427,22 @@ describe('subDoc', () => {
|
||||
await setTimeout(200);
|
||||
provider.disconnect();
|
||||
{
|
||||
const newWorkspace = new Workspace({
|
||||
const docCollection = new DocCollection({
|
||||
id,
|
||||
|
||||
schema,
|
||||
});
|
||||
const provider = createIndexedDBProvider(newWorkspace.doc, rootDBName);
|
||||
const provider = createIndexedDBProvider(docCollection.doc, rootDBName);
|
||||
provider.connect();
|
||||
await setTimeout(200);
|
||||
const page0 = newWorkspace.getDoc('page0') as Doc;
|
||||
const page0 = docCollection.getDoc('page0') as Doc;
|
||||
page0.load();
|
||||
await setTimeout(200);
|
||||
{
|
||||
const block = page0.getBlockById(paragraphBlockIdPage1);
|
||||
assertExists(block);
|
||||
}
|
||||
const page1 = newWorkspace.getDoc('page1') as Doc;
|
||||
const page1 = docCollection.getDoc('page1') as Doc;
|
||||
page1.load();
|
||||
await setTimeout(200);
|
||||
{
|
||||
@@ -455,30 +455,30 @@ describe('subDoc', () => {
|
||||
|
||||
describe('utils', () => {
|
||||
test('download binary', async () => {
|
||||
const page = workspace.createDoc({ id: 'page0' });
|
||||
const page = docCollection.createDoc({ id: 'page0' });
|
||||
page.load();
|
||||
initEmptyPage(page);
|
||||
const provider = createIndexedDBProvider(workspace.doc, rootDBName);
|
||||
const provider = createIndexedDBProvider(docCollection.doc, rootDBName);
|
||||
provider.connect();
|
||||
await setTimeout(200);
|
||||
provider.disconnect();
|
||||
const update = (await downloadBinary(
|
||||
workspace.id,
|
||||
docCollection.id,
|
||||
rootDBName
|
||||
)) as Uint8Array;
|
||||
expect(update).toBeInstanceOf(Uint8Array);
|
||||
const newWorkspace = new Workspace({
|
||||
const newDocCollection = new DocCollection({
|
||||
id,
|
||||
|
||||
schema,
|
||||
});
|
||||
applyUpdate(newWorkspace.doc, update);
|
||||
applyUpdate(newDocCollection.doc, update);
|
||||
await setTimeout();
|
||||
expect(workspace.doc.toJSON()['meta']).toEqual(
|
||||
newWorkspace.doc.toJSON()['meta']
|
||||
expect(docCollection.doc.toJSON()['meta']).toEqual(
|
||||
newDocCollection.doc.toJSON()['meta']
|
||||
);
|
||||
expect(Object.keys(workspace.doc.toJSON()['spaces'])).toEqual(
|
||||
Object.keys(newWorkspace.doc.toJSON()['spaces'])
|
||||
expect(Object.keys(docCollection.doc.toJSON()['spaces'])).toEqual(
|
||||
Object.keys(newDocCollection.doc.toJSON()['spaces'])
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"build": "vite build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@blocksuite/store": "0.13.0-canary-202403050653-934469c",
|
||||
"@blocksuite/store": "0.13.0-canary-202403120738-e15d583",
|
||||
"vite": "^5.1.4",
|
||||
"vite-plugin-dts": "3.7.3",
|
||||
"vitest": "1.3.1",
|
||||
|
||||
Reference in New Issue
Block a user