mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-16 01:26:37 +08:00
feat: new collections (#4530)
Co-authored-by: Peng Xiao <pengxiao@outlook.com>
This commit is contained in:
@@ -1,18 +0,0 @@
|
||||
import { filterByFilterList } from '@affine/component/page-list';
|
||||
import type { Collection } from '@affine/env/filter';
|
||||
import type { PageMeta } from '@blocksuite/store';
|
||||
|
||||
export const filterPage = (collection: Collection, page: PageMeta) => {
|
||||
if (collection.excludeList?.includes(page.id)) {
|
||||
return false;
|
||||
}
|
||||
if (collection.allowList?.includes(page.id)) {
|
||||
return true;
|
||||
}
|
||||
return filterByFilterList(collection.filterList, {
|
||||
'Is Favourited': !!page.favorite,
|
||||
Created: page.createDate,
|
||||
Updated: page.updatedDate ?? page.createDate,
|
||||
Tags: page.tags,
|
||||
});
|
||||
};
|
||||
@@ -1,208 +1,43 @@
|
||||
import type { CollectionsAtom } from '@affine/component/page-list';
|
||||
import type { Collection } from '@affine/env/filter';
|
||||
import { DisposableGroup } from '@blocksuite/global/utils';
|
||||
import { currentWorkspaceAtom } from '@toeverything/infra/atom';
|
||||
import { type DBSchema, openDB } from 'idb';
|
||||
import { atom } from 'jotai';
|
||||
import { atomWithObservable } from 'jotai/utils';
|
||||
import type { Workspace } from '@blocksuite/store';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { Observable } from 'rxjs';
|
||||
import type { Map as YMap } from 'yjs';
|
||||
import { Doc as YDoc } from 'yjs';
|
||||
export class UserSetting {
|
||||
constructor(
|
||||
private workspace: Workspace,
|
||||
private userId: string
|
||||
) {}
|
||||
|
||||
import { sessionAtom } from '../atoms/cloud-user';
|
||||
|
||||
export interface PageCollectionDBV1 extends DBSchema {
|
||||
view: {
|
||||
key: Collection['id'];
|
||||
value: Collection;
|
||||
};
|
||||
}
|
||||
|
||||
export interface StorageCRUD<Value> {
|
||||
get: (key: string) => Promise<Value | null>;
|
||||
set: (key: string, value: Value) => Promise<string>;
|
||||
delete: (key: string) => Promise<void>;
|
||||
list: () => Promise<string[]>;
|
||||
}
|
||||
|
||||
type Subscribe = () => void;
|
||||
|
||||
const collectionDBAtom = atom(
|
||||
openDB<PageCollectionDBV1>('page-view', 1, {
|
||||
upgrade(database) {
|
||||
database.createObjectStore('view', {
|
||||
keyPath: 'id',
|
||||
});
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const callbackSet = new Set<Subscribe>();
|
||||
|
||||
const localCollectionCRUDAtom = atom(get => ({
|
||||
get: async (key: string) => {
|
||||
const db = await get(collectionDBAtom);
|
||||
const t = db.transaction('view').objectStore('view');
|
||||
return (await t.get(key)) ?? null;
|
||||
},
|
||||
set: async (key: string, value: Collection) => {
|
||||
const db = await get(collectionDBAtom);
|
||||
const t = db.transaction('view', 'readwrite').objectStore('view');
|
||||
await t.put(value);
|
||||
callbackSet.forEach(cb => cb());
|
||||
return key;
|
||||
},
|
||||
delete: async (key: string) => {
|
||||
const db = await get(collectionDBAtom);
|
||||
const t = db.transaction('view', 'readwrite').objectStore('view');
|
||||
callbackSet.forEach(cb => cb());
|
||||
await t.delete(key);
|
||||
},
|
||||
list: async () => {
|
||||
const db = await get(collectionDBAtom);
|
||||
const t = db.transaction('view').objectStore('view');
|
||||
return t.getAllKeys();
|
||||
},
|
||||
}));
|
||||
|
||||
const getCollections = async (
|
||||
storage: StorageCRUD<Collection>
|
||||
): Promise<Collection[]> => {
|
||||
return storage
|
||||
.list()
|
||||
.then(async keys => {
|
||||
return await Promise.all(keys.map(key => storage.get(key))).then(v =>
|
||||
v.filter((v): v is Collection => v !== null)
|
||||
get setting(): YDoc {
|
||||
const rootDoc = this.workspace.doc;
|
||||
const settingMap = rootDoc.getMap('settings') as YMap<YDoc>;
|
||||
if (!settingMap.has(this.userId)) {
|
||||
settingMap.set(
|
||||
this.userId,
|
||||
new YDoc({
|
||||
guid: nanoid(),
|
||||
})
|
||||
);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Failed to load collections', error);
|
||||
return [];
|
||||
});
|
||||
};
|
||||
|
||||
const pageCollectionBaseAtom = atomWithObservable<Collection[]>(get => {
|
||||
const currentWorkspacePromise = get(currentWorkspaceAtom);
|
||||
const session = get(sessionAtom);
|
||||
const localCRUD = get(localCollectionCRUDAtom);
|
||||
const userId = session?.data?.user.id ?? null;
|
||||
|
||||
const useLocalStorage = userId === null;
|
||||
|
||||
return new Observable<Collection[]>(subscriber => {
|
||||
// initial value
|
||||
subscriber.next([]);
|
||||
if (useLocalStorage) {
|
||||
const fn = () => {
|
||||
getCollections(localCRUD).then(async collections => {
|
||||
const workspaceId = (await currentWorkspacePromise).id;
|
||||
subscriber.next(
|
||||
collections.filter(c => c.workspaceId === workspaceId)
|
||||
);
|
||||
});
|
||||
};
|
||||
fn();
|
||||
callbackSet.add(fn);
|
||||
return () => {
|
||||
callbackSet.delete(fn);
|
||||
};
|
||||
} else {
|
||||
const group = new DisposableGroup();
|
||||
currentWorkspacePromise.then(async currentWorkspace => {
|
||||
const collectionsFromLocal = await getCollections(localCRUD);
|
||||
const rootDoc = currentWorkspace.doc;
|
||||
const settingMap = rootDoc.getMap('settings') as YMap<YDoc>;
|
||||
if (!settingMap.has(userId)) {
|
||||
settingMap.set(
|
||||
userId,
|
||||
new YDoc({
|
||||
guid: nanoid(),
|
||||
})
|
||||
);
|
||||
}
|
||||
const settingDoc = settingMap.get(userId) as YDoc;
|
||||
if (!settingDoc.isLoaded) {
|
||||
settingDoc.load();
|
||||
await settingDoc.whenLoaded;
|
||||
}
|
||||
const viewMap = settingDoc.getMap('view') as YMap<Collection>;
|
||||
// sync local storage to doc
|
||||
collectionsFromLocal.map(v => viewMap.set(v.id, v));
|
||||
// delete from indexeddb
|
||||
Promise.all(
|
||||
collectionsFromLocal.map(async v => {
|
||||
await localCRUD.delete(v.id);
|
||||
})
|
||||
).catch(error => {
|
||||
console.error('Failed to delete collections from indexeddb', error);
|
||||
});
|
||||
const collectionsFromDoc: Collection[] = Array.from(viewMap.keys())
|
||||
.map(key => viewMap.get(key))
|
||||
.filter((v): v is Collection => !!v);
|
||||
const collections = [...collectionsFromDoc];
|
||||
subscriber.next(collections);
|
||||
if (group.disposed) {
|
||||
return;
|
||||
}
|
||||
const fn = () => {
|
||||
const collectionsFromDoc: Collection[] = Array.from(viewMap.keys())
|
||||
.map(key => viewMap.get(key))
|
||||
.filter((v): v is Collection => !!v);
|
||||
const collections = [...collectionsFromLocal, ...collectionsFromDoc];
|
||||
subscriber.next(collections);
|
||||
};
|
||||
viewMap.observe(fn);
|
||||
group.add(() => {
|
||||
viewMap.unobserve(fn);
|
||||
});
|
||||
});
|
||||
return () => {
|
||||
group.dispose();
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
export const currentCollectionsAtom: CollectionsAtom = atom(
|
||||
get => get(pageCollectionBaseAtom),
|
||||
async (get, _, apply) => {
|
||||
const collections = await get(pageCollectionBaseAtom);
|
||||
let newCollections: Collection[];
|
||||
if (typeof apply === 'function') {
|
||||
newCollections = apply(collections);
|
||||
} else {
|
||||
newCollections = apply;
|
||||
}
|
||||
const session = get(sessionAtom);
|
||||
const userId = session?.data?.user.id ?? null;
|
||||
const useLocalStorage = userId === null;
|
||||
const added = newCollections.filter(v => !collections.includes(v));
|
||||
const removed = collections.filter(v => !newCollections.includes(v));
|
||||
if (useLocalStorage) {
|
||||
const localCRUD = get(localCollectionCRUDAtom);
|
||||
await Promise.all([
|
||||
...added.map(async v => {
|
||||
await localCRUD.set(v.id, v);
|
||||
}),
|
||||
...removed.map(async v => {
|
||||
await localCRUD.delete(v.id);
|
||||
}),
|
||||
]);
|
||||
} else {
|
||||
const currentWorkspace = await get(currentWorkspaceAtom);
|
||||
const rootDoc = currentWorkspace.doc;
|
||||
const settingMap = rootDoc.getMap('settings') as YMap<YDoc>;
|
||||
const settingDoc = settingMap.get(userId) as YDoc;
|
||||
const viewMap = settingDoc.getMap('view') as YMap<Collection>;
|
||||
await Promise.all([
|
||||
...added.map(async v => {
|
||||
viewMap.set(v.id, v);
|
||||
}),
|
||||
...removed.map(async v => {
|
||||
viewMap.delete(v.id);
|
||||
}),
|
||||
]);
|
||||
}
|
||||
return settingMap.get(this.userId) as YDoc;
|
||||
}
|
||||
);
|
||||
|
||||
get loaded(): Promise<void> {
|
||||
if (!this.setting.isLoaded) {
|
||||
this.setting.load();
|
||||
}
|
||||
return this.setting.whenLoaded;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
get view() {
|
||||
return this.setting.getMap('view') as YMap<Collection>;
|
||||
}
|
||||
}
|
||||
|
||||
export const getUserSetting = (workspace: Workspace, userId: string) => {
|
||||
return new UserSetting(workspace, userId);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import type {
|
||||
Collection,
|
||||
DeleteCollectionInfo,
|
||||
DeletedCollection,
|
||||
} from '@affine/env/filter';
|
||||
import type { Workspace } from '@blocksuite/store';
|
||||
import { Array as YArray } from 'yjs';
|
||||
|
||||
import { updateFirstOfYArray } from './yjs-utils';
|
||||
|
||||
const COLLECTIONS_KEY = 'collections';
|
||||
const COLLECTIONS_TRASH_KEY = 'collections_trash';
|
||||
const SETTING_KEY = 'setting';
|
||||
|
||||
export class WorkspaceSetting {
|
||||
constructor(private workspace: Workspace) {}
|
||||
|
||||
get doc() {
|
||||
return this.workspace.doc;
|
||||
}
|
||||
|
||||
get setting() {
|
||||
return this.workspace.doc.getMap(SETTING_KEY);
|
||||
}
|
||||
|
||||
get collectionsYArray() {
|
||||
if (!this.setting.has(COLLECTIONS_KEY)) {
|
||||
this.setting.set(COLLECTIONS_KEY, new YArray());
|
||||
}
|
||||
return this.setting.get(COLLECTIONS_KEY) as YArray<Collection>;
|
||||
}
|
||||
|
||||
get collectionsTrashYArray() {
|
||||
if (!this.setting.has(COLLECTIONS_TRASH_KEY)) {
|
||||
this.setting.set(COLLECTIONS_TRASH_KEY, new YArray());
|
||||
}
|
||||
return this.setting.get(COLLECTIONS_TRASH_KEY) as YArray<DeletedCollection>;
|
||||
}
|
||||
|
||||
get collections(): Collection[] {
|
||||
return this.collectionsYArray.toArray() ?? [];
|
||||
}
|
||||
|
||||
get collectionsTrash(): DeletedCollection[] {
|
||||
return this.collectionsTrashYArray.toArray() ?? [];
|
||||
}
|
||||
|
||||
updateCollection(id: string, updater: (value: Collection) => Collection) {
|
||||
updateFirstOfYArray(
|
||||
this.collectionsYArray,
|
||||
v => v.id === id,
|
||||
v => {
|
||||
return updater(v);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
addCollection(...collections: Collection[]) {
|
||||
this.doc.transact(() => {
|
||||
this.collectionsYArray.insert(0, collections);
|
||||
});
|
||||
}
|
||||
|
||||
deleteCollection(info: DeleteCollectionInfo, ...ids: string[]) {
|
||||
const set = new Set(ids);
|
||||
this.workspace.doc.transact(() => {
|
||||
const indexList: number[] = [];
|
||||
const list: Collection[] = [];
|
||||
this.collectionsYArray.forEach((collection, i) => {
|
||||
if (set.has(collection.id)) {
|
||||
set.delete(collection.id);
|
||||
indexList.unshift(i);
|
||||
list.push(JSON.parse(JSON.stringify(collection)));
|
||||
}
|
||||
});
|
||||
indexList.forEach(i => {
|
||||
this.collectionsYArray.delete(i);
|
||||
});
|
||||
this.collectionsTrashYArray.insert(
|
||||
0,
|
||||
list.map(collection => ({
|
||||
userId: info?.userId,
|
||||
userName: info ? info.userName : 'Local User',
|
||||
collection,
|
||||
}))
|
||||
);
|
||||
if (this.collectionsTrashYArray.length > 10) {
|
||||
this.collectionsTrashYArray.delete(
|
||||
10,
|
||||
this.collectionsTrashYArray.length - 10
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
deletePagesFromCollection(collection: Collection, idSet: Set<string>) {
|
||||
const newAllowList = collection.allowList.filter(id => !idSet.has(id));
|
||||
const newPages = collection.pages.filter(id => !idSet.has(id));
|
||||
if (
|
||||
newAllowList.length !== collection.allowList.length ||
|
||||
newPages.length !== collection.pages.length
|
||||
) {
|
||||
this.updateCollection(collection.id, old => {
|
||||
return {
|
||||
...old,
|
||||
allowList: newAllowList,
|
||||
pages: newPages,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
deletePages(ids: string[]) {
|
||||
const idSet = new Set(ids);
|
||||
this.workspace.doc.transact(() => {
|
||||
this.collections.forEach(collection => {
|
||||
this.deletePagesFromCollection(collection, idSet);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const getWorkspaceSetting = (workspace: Workspace) => {
|
||||
return new WorkspaceSetting(workspace);
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { Array as YArray } from 'yjs';
|
||||
|
||||
export const updateFirstOfYArray = <T>(
|
||||
array: YArray<T>,
|
||||
p: (value: T) => boolean,
|
||||
update: (value: T) => T
|
||||
) => {
|
||||
array.doc?.transact(() => {
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
const ele = array.get(i);
|
||||
if (p(ele)) {
|
||||
array.delete(i);
|
||||
array.insert(i, [update(ele)]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user