Skip to content

Commit

Permalink
feat(Thumbnail.tsx, useDocument.ts, DocumentManager.ts, index.ts): ad…
Browse files Browse the repository at this point in the history
…d DocumentMetadata type and generateWhiteboardName function

feat(locales): add new fields for document name and metadata in English and French locales
The DocumentMetadata type was added to handle additional metadata for documents, such as the document name. The generateWhiteboardName function was created to generate random, creative names for new whiteboards. This enhances user experience by providing unique, auto-generated names for each new document. The locales were updated to include new fields for document name and metadata in both English and French, ensuring that these new features are properly localized.

feat(DocumentList.tsx): refactor List component to use DocumentItem component for better code organization
feat(DocumentList.tsx): use useDocument hook to fetch document data for better data handling
style(DocumentList.tsx): change navbar background color to bg-base-200/50 for better visibility

The List component has been refactored to use a new DocumentItem component. This change improves code organization and readability by separating the concerns of the list and individual document items. The useDocument hook is now used to fetch document data, which improves data handling and allows for more efficient updates. The navbar background color has been changed to bg-base-200/50 to improve visibility and user experience.

feat(ShareModal.tsx): add support for document metadata in ShareModal
refactor(ShareModal.tsx): change allowedClients to allowedClientKeys for better semantics
refactor(DocumentRegistry.ts): add support for document metadata in DocumentRegistry
The changes in ShareModal.tsx allow users to view and edit document metadata, providing more context about the document. The renaming of allowedClients to allowedClientKeys in ShareModal.tsx improves the semantics of the code, making it clearer that these are keys, not client objects. The changes in DocumentRegistry.ts allow the creation and retrieval of documents with metadata, enhancing the functionality of the DocumentRegistry.

feat(DocumentSharingClient.ts, Document.ts): add support for Metadata type in Document and DocumentSharingClient
The changes introduce support for a Metadata type in the Document and DocumentSharingClient classes. This allows for more flexibility and extensibility in handling documents, as additional metadata can now be associated with each document. This can be useful for storing additional information about the document, such as its creation date, author, or other relevant data.

refactor(DocumentHeader.ts): streamline import statements for better readability
feat(DocumentHeader.ts): add metadata field to DocumentHeaderData type for additional data storage
The import statements have been refactored into a single line for better readability and to reduce clutter. A new field, 'metadata', has been added to the DocumentHeaderData type. This allows for the storage of additional, flexible data associated with a document, enhancing the versatility of the document handling system.

refactor(DocumentHeader.ts): enhance DocumentHeader class with improved semantics and functionality
The DocumentHeader class has been refactored to improve semantics and functionality. The class now includes a helper function to validate and convert an array of client keys. The constructor has been updated to compute fields and freeze objects for better immutability. The update method has been added to create a new DocumentHeader with provided metadata and allowed clients. The verifyOwnerPrivateKey method has been added to verify the owner's private key. The create method has been updated to create a new instance of DocumentHeader with provided private key, allowed clients, and metadata. The import method has been added to construct a DocumentHeader instance from binary data. This refactoring enhances the readability, maintainability, and functionality of the DocumentHeader class.

refactor(DocumentHeader): replace import method with upgrade method for better version handling
The import method was replaced with an upgrade method to better handle version changes in DocumentHeader. This change allows for compatibility checks between old and new headers, and throws an error if they are not compatible. If the versions are the same, the new header is returned. Otherwise, a new DocumentHeader instance is created with the new data and signature. This change improves the robustness of the code when dealing with different versions of DocumentHeader.

refactor(DocumentHeader.ts): remove upgrade method from DocumentHeader class
The upgrade method was removed from the DocumentHeader class because it was not being used and was adding unnecessary complexity to the code. This change simplifies the DocumentHeader class and makes it easier to maintain.

feat(ddnet): add DocumentHeader to exports in index.ts
feat(ddnet): introduce ConcurrencyLimiter in utils.ts
refactor(Storage.ts): enhance loadBinary method to handle loading failures

The DocumentHeader is now exported from index.ts to make it accessible for other modules. A new ConcurrencyLimiter class is introduced in utils.ts to limit the execution of a certain number of tasks at the same time. This is useful to prevent overloading the system with too many concurrent tasks. The loadBinary method in Storage.ts is refactored to handle loading failures more gracefully. If loading the full document fails, it tries to discard one chunk at a time from the end until successful. If loading is successful after discarding, it saves a fresh version with cleared chunks. This approach ensures that the application can recover from loading failures and continue to function correctly.
  • Loading branch information
maxscharwath committed Jul 20, 2023
1 parent 60d0a16 commit c47cce4
Show file tree
Hide file tree
Showing 16 changed files with 492 additions and 340 deletions.
10 changes: 6 additions & 4 deletions apps/describble/src/components/Thumbnail.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, {memo, useMemo} from 'react';
import {getLayerUtil, type Layer} from '~core/layers';
import {type Asset, type SyncedDocument} from '~core/managers';
import {type Asset, type DocumentMetadata, type SyncedDocument} from '~core/managers';
import {type Document} from '@describble/ddnet';
import {QuadTree} from '~core/utils/QuadTree';
import {type Camera, type Dimension} from '~core/types';
Expand All @@ -16,7 +16,7 @@ interface ThumbnailProps {
camera: Camera;
}

export const ThumbnailRenderer = ({document, dimension, camera}: {document: Document<SyncedDocument>; dimension: Dimension; camera: Camera}) => {
export const ThumbnailRenderer = ({document, dimension, camera}: {document: Document<SyncedDocument, DocumentMetadata>; dimension: Dimension; camera: Camera}) => {
const {t} = useTranslation();
const layers = useLayers({document, dimension, camera});
const assets = document?.data.assets ?? {};
Expand Down Expand Up @@ -51,15 +51,17 @@ export const Thumbnail = memo(({documentId, dimension, camera}: ThumbnailProps)
}

if (!document) {
return <span className='loading loading-ring loading-lg'></span>;
return <div className='flex h-full w-full animate-pulse items-center justify-center rounded bg-neutral/20'>
<svg className='h-10 w-10 text-gray-200 dark:text-gray-600' aria-hidden='true' xmlns='http://www.w3.org/2000/svg' fill='currentColor' viewBox='0 0 20 18'><path d='M18 0H2a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2Zm-5.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3Zm4.376 10.481A1 1 0 0 1 16 15H4a1 1 0 0 1-.895-1.447l3.5-7A1 1 0 0 1 7.468 6a.965.965 0 0 1 .9.5l2.775 4.757 1.546-1.887a1 1 0 0 1 1.618.1l2.541 4a1 1 0 0 1 .028 1.011Z'/></svg>
</div>;
}

return <ThumbnailRenderer document={document} dimension={dimension} camera={camera}/>;
});
Thumbnail.displayName = 'Thumbnail';

interface LayerHookProps {
document: Document<SyncedDocument> | undefined | null;
document: Document<SyncedDocument, DocumentMetadata> | undefined | null;
dimension: Dimension;
camera: Camera;
}
Expand Down
19 changes: 15 additions & 4 deletions apps/describble/src/core/hooks/useDocument.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import {useEffect, useState} from 'react';
import {type Document} from '@describble/ddnet';
import {type SyncedDocument} from '~core/managers';
import {type Document, type DocumentHeader} from '@describble/ddnet';
import {type DocumentMetadata, type SyncedDocument} from '~core/managers';
import {useWhiteboard} from '~core/hooks/useWhiteboard';

export const useDocument = (documentId: string) => {
const [document, setDocument] = useState<Document<SyncedDocument> | null>(null);
const [document, setDocument] = useState<Document<SyncedDocument, DocumentMetadata> | null>(null);

const [header, setHeader] = useState<DocumentHeader<DocumentMetadata> | null>(null);
const [error, setError] = useState<Error | null>(null);
const app = useWhiteboard();
useEffect(() => {
Expand All @@ -20,5 +22,14 @@ export const useDocument = (documentId: string) => {
void fetchDocument();
}, [documentId, app]);

return {document, error};
useEffect(() => {
const unsubscribe = document?.on('header-updated', ({header}) => {
setHeader(header);
});
return () => {
unsubscribe?.();
};
}, [document]);

return {document, header, error};
};
18 changes: 13 additions & 5 deletions apps/describble/src/core/managers/DocumentManager.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {type Layer} from '~core/layers';
import {type Camera, type Patch, type PatchId} from '~core/types';
import {createUseStore, deepmerge} from '~core/utils';
import {createUseStore, deepmerge, generateWhiteboardName} from '~core/utils';
import {createStore, type StoreApi} from 'zustand/vanilla';
import {nanoid} from 'nanoid';
import {type UseBoundStore} from 'zustand';
Expand All @@ -15,6 +15,10 @@ export type SyncedDocument = {
assets: Record<string, Asset>;
};

export type DocumentMetadata = {
name: string;
};

type BaseDocument = {
id: string;
camera: Camera;
Expand Down Expand Up @@ -185,7 +189,7 @@ export class DocumentHandle {
private stack: Array<Command<SyncedDocument>> = [];
private stackIndex = -1;

constructor(documentId: string, private readonly document: Document<SyncedDocument>) {
constructor(documentId: string, private readonly document: Document<SyncedDocument, DocumentMetadata>) {
this.store = createStore(() => ({
id: documentId,
camera: {x: 0, y: 0, zoom: 1},
Expand Down Expand Up @@ -281,7 +285,11 @@ export class DocumentManager {
}

public create() {
const doc = this.repo.createDocument<SyncedDocument>();
const doc = this.repo.createDocument<SyncedDocument, DocumentMetadata>({
metadata: {
name: generateWhiteboardName(),
},
});
doc.change(state => {
state.layers = {};
state.assets = {};
Expand All @@ -291,15 +299,15 @@ export class DocumentManager {

public async open(id: string) {
await this.repo.waitForConnection();
const doc = await this.repo.requestDocument<SyncedDocument>(id);
const doc = await this.repo.requestDocument<SyncedDocument, DocumentMetadata>(id);
this.currentDocumentHandle = new DocumentHandle(id, doc);
this.currentPresence?.stop();
this.currentPresence = this.repo.getPresence(id);
return doc;
}

public async get(id: string) {
return this.repo.findDocument<SyncedDocument>(id);
return this.repo.findDocument<SyncedDocument, DocumentMetadata>(id);
}

public async delete(id: string) {
Expand Down
52 changes: 52 additions & 0 deletions apps/describble/src/core/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,55 @@ export const getCanvasBounds = (bounds: Bounds, {x, y, zoom}: Camera) => ({
width: bounds.width / zoom,
height: bounds.height / zoom,
});

export const generateWhiteboardName = () => {
const adjectives = [
'Wonderful',
'Amazing',
'Stunning',
'Remarkable',
'Marvelous',
'Fantastic',
'Incredible',
'Magnificent',
'Breathtaking',
'Astonishing',
'Epic',
'Unbelievable',
'Spectacular',
'Majestic',
'Impressive',
'Inspirational',
'Enchanting',
'Phenomenal',
'Extraordinary',
'Spellbinding',
];
const nouns = [
'Drawing',
'Sketch',
'Design',
'Illustration',
'Doodle',
'Artwork',
'Masterpiece',
'Creation',
'Composition',
'Blueprint',
'Diagram',
'Image',
'Model',
'Outline',
'Draft',
'Scheme',
'Rendering',
'Portrait',
'Picture',
'Canvas',
];

const randomAdjective = adjectives[Math.floor(Math.random() * adjectives.length)];
const randomNoun = nouns[Math.floor(Math.random() * nouns.length)];

return `My ${randomAdjective} ${randomNoun}`;
};
5 changes: 4 additions & 1 deletion apps/describble/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
"title": "Are you absolutely sure?"
},
"document": {
"title": "Document #{{index}}"
"title": "Document #{{index}}",
"unnamed": "Unnamed document"
},
"embed": {
"add": "Add this!",
Expand Down Expand Up @@ -55,6 +56,7 @@
},
"input": {
"add_public_key": "Add Public Key",
"document_name": "Document name",
"placeholder": {
"confirm_password": "Confirm Password",
"enter_word": "Fill word",
Expand Down Expand Up @@ -88,6 +90,7 @@
"share": {
"allowed_clients": "Allowed clients",
"header": "Header",
"metadata": "Metadata",
"owner": "Owner",
"share_settings": "Share Settings"
},
Expand Down
5 changes: 4 additions & 1 deletion apps/describble/src/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
"title": "Êtes-vous absolument sûr ?"
},
"document": {
"title": "Document n°{{index}}"
"title": "Document n°{{index}}",
"unnamed": "Document sans nom"
},
"embed": {
"add": "Ajoutez ceci!",
Expand Down Expand Up @@ -55,6 +56,7 @@
},
"input": {
"add_public_key": "Ajouter une clé publique",
"document_name": "Nom du document",
"placeholder": {
"confirm_password": "Confirmer le mot de passe",
"enter_word": "Remplir le mot",
Expand Down Expand Up @@ -88,6 +90,7 @@
"share": {
"allowed_clients": "Clients autorisés ",
"header": "Entête",
"metadata": "Métadonnées",
"owner": "Propriétaire",
"share_settings": "Paramètres de partage"
},
Expand Down
81 changes: 45 additions & 36 deletions apps/describble/src/pages/DocumentList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {useSession} from '~core/hooks/useSession';
import {KeyAvatar} from '~components/ui/KeyAvatar';
import {ShareModal} from '~pages/ShareModal';
import {ConfirmDialog} from '~components/ui/ConfirmDialog';
import {useDocument} from '~core/hooks/useDocument';

const useList = () => {
const app = useWhiteboard();
Expand Down Expand Up @@ -44,7 +45,7 @@ export const DocumentList = () => {

return (
<div className='mx-auto flex w-full max-w-7xl grow flex-col items-center'>
<div className='navbar rounded-box sticky top-4 z-50 mb-8 bg-base-100/50 shadow-xl backdrop-blur-xl'>
<div className='navbar rounded-box sticky top-4 z-50 mb-8 bg-base-200/50 shadow-xl backdrop-blur-xl'>
<div className='mr-8 flex-1 text-slate-800 dark:text-slate-100'>
<DescribbleLogo className='pointer-events-none absolute m-2 h-8 w-auto' textClassName='opacity-0 sm:opacity-100 transition-opacity duration-300 ease-in-out'/>
</div>
Expand All @@ -66,45 +67,53 @@ export const DocumentList = () => {
);
};

const List = ({list, onDelete}: {onDelete: (id: string) => void; list: string[]}) => {
const {t} = useTranslation();
const List = ({list, onDelete}: {onDelete: (id: string) => void; list: string[]}) => (
<div className='grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4'>
{list.map(item => (
<DocumentItem documentId={item} key={item} onDelete={onDelete}/>
))}
</div>
);

const DocumentItem = ({documentId, onDelete}: {documentId: string; onDelete: (id: string) => void}) => {
const {t} = useTranslation();
const {document, header} = useDocument(documentId);
return (
<div className='grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4'>
{list.map((item, index) => (
<div key={item} className='relative'>
<Link to={`/document/${item}`} className='peer absolute inset-0 outline-none' aria-label={t('btn.open')} />
<div className='card pointer-events-none min-w-full bg-base-100/50 shadow-lg transition-all duration-100 ease-in-out peer-focus:shadow-xl peer-focus:ring-4 peer-focus:ring-neutral peer-active:scale-95'>
<figure className='z-0 h-48 w-full bg-gradient-to-b from-black/0 to-black/10 object-cover'>
<Thumbnail
documentId={item}
camera={{x: 0, y: 0, zoom: 0.25}}
dimension={{width: 300, height: 200}}/>
</figure>
<div className='card-body overflow-hidden'>
<h2 className='card-title'>{t('document.title', {index})}</h2>
<div className='badge badge-neutral max-w-full'><span
className='overflow-hidden text-ellipsis text-xs'>{item}</span></div>
</div>
<div className='pointer-events-auto z-10 p-4 pt-0'>
<div className='card-actions justify-end'>
<ConfirmDialog
onAction={() => onDelete(item)}
title={t('confirm_dialog.title')}
description={t('confirm_dialog.description')}
actionLabel={t('confirm_dialog.action')}
cancelLabel={t('confirm_dialog.cancel')}
>
<button className='btn-error btn-sm btn-circle btn'>
<TrashIcon fontSize={20}/>
</button>
</ConfirmDialog>
<ShareModal documentId={item}/>
</div>
</div>
<div className='relative'>
<Link to={`/document/${documentId}`} className='peer absolute inset-0 outline-none' aria-label={t('btn.open')} />
<div className='card pointer-events-none h-full min-w-full bg-base-200 shadow-lg transition-all duration-100 ease-in-out peer-focus:shadow-xl peer-focus:ring-4 peer-focus:ring-neutral peer-active:scale-95'>
<figure className='z-0 h-48 w-full bg-gradient-to-b from-black/0 to-black/10 object-cover'>
<Thumbnail
documentId={documentId}
camera={{x: 0, y: 0, zoom: 0.25}}
dimension={{width: 300, height: 200}}/>
</figure>
<div className='card-body overflow-hidden'>
<h2 className='card-title'>
{header
? (header.metadata.name || t('document.unnamed'))
: <div className='mb-4 h-3 w-48 animate-pulse rounded-full bg-gray-200 dark:bg-gray-700'/>}
</h2>
<div className='badge badge-neutral max-w-full'><span
className='overflow-hidden text-ellipsis text-xs'>{documentId}</span></div>
</div>
<div className='pointer-events-auto z-10 p-4 pt-0'>
<div className='card-actions justify-end'>
<ConfirmDialog
onAction={() => onDelete(documentId)}
title={t('confirm_dialog.title')}
description={t('confirm_dialog.description')}
actionLabel={t('confirm_dialog.action')}
cancelLabel={t('confirm_dialog.cancel')}
>
<button className='btn-error btn-sm btn-circle btn'>
<TrashIcon fontSize={20}/>
</button>
</ConfirmDialog>
<ShareModal document={document}/>
</div>
</div>
))}
</div>
</div>
);
};
Loading

1 comment on commit c47cce4

@vercel
Copy link

@vercel vercel bot commented on c47cce4 Jul 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.