diff --git a/resources/favicon.ico b/resources/favicon.ico new file mode 100644 index 0000000000..061f413943 Binary files /dev/null and b/resources/favicon.ico differ diff --git a/src/components/NotePage/NoteDetail/NoteDetail.tsx b/src/components/NotePage/NoteDetail/NoteDetail.tsx index 2200fe80cb..8c95c25283 100644 --- a/src/components/NotePage/NoteDetail/NoteDetail.tsx +++ b/src/components/NotePage/NoteDetail/NoteDetail.tsx @@ -300,8 +300,12 @@ export default class NoteDetail extends React.Component< } appendNewTag = () => { - if (includes(this.state.newTagName, this.state.tags)) { return } - if (!isTagNameValid(this.state.newTagName)) { return } + if (includes(this.state.newTagName, this.state.tags)) { + return + } + if (!isTagNameValid(this.state.newTagName)) { + return + } this.setState( prevState => ({ newTagName: '', @@ -469,6 +473,7 @@ export default class NoteDetail extends React.Component< ) diff --git a/src/components/atoms/CustomizedMarkdownPreviewer.tsx b/src/components/atoms/CustomizedMarkdownPreviewer.tsx index b1ac17e2b9..46372fb515 100644 --- a/src/components/atoms/CustomizedMarkdownPreviewer.tsx +++ b/src/components/atoms/CustomizedMarkdownPreviewer.tsx @@ -6,11 +6,13 @@ import { usePreviewStyle } from '../../lib/preview' interface CustomizedMarkdownPreviewer { content: string storageId?: string + updateContent?: any } const CustomizedMarkdownPreviewer = ({ content, - storageId + storageId, + updateContent }: CustomizedMarkdownPreviewer) => { const { preferences } = usePreferences() const { previewStyle } = usePreviewStyle() @@ -22,6 +24,7 @@ const CustomizedMarkdownPreviewer = ({ codeBlockTheme={preferences['markdown.codeBlockTheme']} theme={preferences['general.theme']} style={previewStyle} + updateContent={updateContent} /> ) } diff --git a/src/components/atoms/MarkdownPreviewer.tsx b/src/components/atoms/MarkdownPreviewer.tsx index 1ae569c06c..4c59a518e3 100644 --- a/src/components/atoms/MarkdownPreviewer.tsx +++ b/src/components/atoms/MarkdownPreviewer.tsx @@ -159,6 +159,7 @@ interface MarkdownPreviewerProps { style?: string theme?: string storageId?: string + updateContent?: any } const MarkdownPreviewer = ({ @@ -166,14 +167,17 @@ const MarkdownPreviewer = ({ codeBlockTheme, style, theme, - storageId + storageId, + updateContent }: MarkdownPreviewerProps) => { const forceUpdate = useForceUpdate() const [rendering, setRendering] = useState(false) const previousContentRef = useRef('') const previousThemeRef = useRef('') + const checkboxIndexes = React.useRef(0) const [renderedContent, setRenderedContent] = useState([]) const { storageMap } = useDb() + // console.log(content) const markdownProcessor = useMemo(() => { const options = { codeBlockTheme, storageId } @@ -214,10 +218,56 @@ const MarkdownPreviewer = ({ {children} ) + }, + input: (props: any) => { + return ( + { + const regex = { + checked: /^(\s*>?)*\s*[+\-*] \[x]/i, + checkedAndUnchecked: /^(\s*>?)*\s*[+\-*] (\[x]|\[ ])/i + } + const lines = content.split('\n') + let checkboxIndex: any = e.target.getAttribute('id') || '' + + checkboxIndex = Number( + checkboxIndex.replace(/^checkbox|(\[|\])/gi, '') + ) + + let current = 0 + + for (let index = 0; index < lines.length; index++) { + const line = lines[index] + const matches = line.match(regex.checkedAndUnchecked) + console.log(`matches: ${!!matches}`) + if (matches) { + if (current === checkboxIndex) { + const isChecked = regex.checked.test(matches[0]) + lines[index] = line.replace( + isChecked ? '[x]' : '[ ]', + isChecked ? '[ ]' : '[x]' + ) + // Bail out early since we're done + break + } else { + current++ + } + } + } + const nextContent = lines.join('\n') + console.log(nextContent) + updateContent(nextContent) + }} + id={`checkbox[${checkboxIndexes.current++}]`} + readOnly + {...props} + disabled={props.type !== 'checkbox'} + /> + ) } } }) - }, [codeBlockTheme, storageId, storageMap]) + }, [codeBlockTheme, content, storageId, storageMap, updateContent]) const renderContent = useCallback( async (content: string) => { diff --git a/src/lib/db/NoteDb.ts b/src/lib/db/NoteDb.ts deleted file mode 100644 index c7c01c07df..0000000000 --- a/src/lib/db/NoteDb.ts +++ /dev/null @@ -1,566 +0,0 @@ -import dashify from 'dashify' -import parsePath from 'path-parse' -import { - AllDocsMap, - FolderDoc, - FolderDocEditibleProps, - TagDocEditibleProps, - TagDoc, - NoteDoc, - NoteDocEditibleProps, - ExceptRev, - ObjectMap, - Attachment, - PopulatedNoteDoc -} from './types' -import { - getFolderId, - createUnprocessableEntityError, - isFolderPathnameValid, - getParentFolderPathname, - getTagId, - isTagNameValid, - generateNoteId, - getNow, - createNotFoundError, - getFolderPathname, - isNoteDoc, - isFolderDoc, - isTagDoc, - getTagName, - values -} from './utils' -import { FOLDER_ID_PREFIX, ATTACHMENTS_ID } from './consts' -import PouchDB from './PouchDB' -import { buildCloudSyncUrl, User } from '../accounts' -import { setHeader } from '../utils/http' - -export default class NoteDb { - public initialized = false - - constructor( - public pouchDb: PouchDB.Database, - public id: string, - public name: string - ) {} - - async init() { - await this.upsertNoteListViews() - - const { noteMap, folderMap, tagMap } = await this.getAllDocsMap() - const { missingPathnameSet, missingTagNameSet, requiresUpdate } = values( - noteMap - ).reduce<{ - missingPathnameSet: Set - missingTagNameSet: Set - requiresUpdate: NoteDoc[] - }>( - (obj, noteDoc) => { - if (noteDoc.trashed) { - return obj - } - - if (noteDoc.folderPathname === '/') { - noteDoc.folderPathname = '/default' - obj.requiresUpdate.push(noteDoc) - } - - if (folderMap[noteDoc.folderPathname] == null) { - obj.missingPathnameSet.add(noteDoc.folderPathname) - } - noteDoc.tags.forEach(tagName => { - if (tagMap[tagName] == null) { - obj.missingTagNameSet.add(tagName) - } - }) - return obj - }, - { - missingPathnameSet: new Set(), - missingTagNameSet: new Set(), - requiresUpdate: [] - } - ) - - await Promise.all([ - ...[...missingPathnameSet].map(pathname => this.upsertFolder(pathname)), - ...[...missingTagNameSet].map(tagName => this.upsertTag(tagName)), - ...requiresUpdate.map(note => this.updateNote(note._id, note)) - ]) - - if (folderMap['/'] != null) { - await this.removeFolder('/') - } - } - - async getFolder(path: string): Promise { - return this.getDoc(getFolderId(path)) - } - - async upsertFolder( - pathname: string, - props?: Partial - ): Promise { - if (!isFolderPathnameValid(pathname)) { - throw createUnprocessableEntityError( - `pathname is invalid, got \`${pathname}\`` - ) - } - if (pathname !== '/') { - await this.doesParentFolderExistOrCreate(pathname) - } - const folder = await this.getFolder(pathname) - if (folder != null && props == null) { - return folder - } - const now = getNow() - const folderDocProps = { - ...(folder || { - _id: getFolderId(pathname), - createdAt: now, - data: {} - }), - ...props, - updatedAt: now - } - const { rev } = await this.pouchDb.put(folderDocProps) - - return { - _id: folderDocProps._id, - createdAt: folderDocProps.createdAt, - updatedAt: folderDocProps.updatedAt, - data: folderDocProps.data, - _rev: rev - } - } - - async doesParentFolderExistOrCreate(pathname: string) { - const parentPathname = getParentFolderPathname(pathname) - if (parentPathname !== '/') { - await this.upsertFolder(parentPathname) - } - } - - async getAllDocsMap(): Promise { - const allDocsResponse = await this.pouchDb.allDocs({ - include_docs: true - }) - - const map: AllDocsMap = { - noteMap: {}, - folderMap: {}, - tagMap: {} - } - - return allDocsResponse.rows.reduce((map, row) => { - const { doc } = row - if (isNoteDoc(doc)) { - map.noteMap[doc._id] = { - ...doc, - storageId: this.id - } as PopulatedNoteDoc - } else if (isFolderDoc(doc)) { - map.folderMap[getFolderPathname(doc._id)] = doc - } else if (isTagDoc(doc)) { - map.tagMap[getTagName(doc._id)] = doc - } - return map - }, map) - } - - async getTag(tagName: string): Promise { - return this.getDoc(getTagId(tagName)) - } - - async getDoc( - docId: string - ): Promise { - try { - return await this.pouchDb.get(docId) - } catch (error) { - switch (error.name) { - case 'not_found': - return null - default: - throw error - } - } - } - - async upsertTag(tagName: string, props?: Partial) { - if (!isTagNameValid(tagName)) { - throw createUnprocessableEntityError( - `tag name is invalid, got \`${tagName}\`` - ) - } - - const tag = await this.getTag(tagName) - if (tag != null && props == null) { - return tag - } - - const now = getNow() - const tagDocProps = { - ...(tag || { - _id: getTagId(tagName), - createdAt: now, - data: {} - }), - ...props, - updatedAt: now - } - const { rev } = await this.pouchDb.put(tagDocProps) - - return { - _id: tagDocProps._id, - createdAt: tagDocProps.createdAt, - updatedAt: tagDocProps.updatedAt, - data: tagDocProps.data, - _rev: rev - } - } - - async getNote(noteId: string): Promise { - return this.getDoc(noteId) - } - - async createNote( - noteProps: Partial = {} - ): Promise { - const now = getNow() - const noteDocProps: ExceptRev = { - _id: generateNoteId(), - title: '', - content: '', - tags: [], - folderPathname: '/default', - data: {}, - bookmarked: false, - ...noteProps, - createdAt: now, - updatedAt: now, - trashed: false - } - - await this.upsertFolder(noteDocProps.folderPathname) - await Promise.all(noteDocProps.tags.map(tagName => this.upsertTag(tagName))) - - const { rev } = await this.pouchDb.put(noteDocProps) - - return { - ...noteDocProps, - _rev: rev - } - } - - async updateNote(noteId: string, noteProps: Partial) { - const note = await this.getNote(noteId) - if (note == null) - throw createNotFoundError(`The note \`${noteId}\` does not exist`) - - if (noteProps.folderPathname) { - await this.upsertFolder(noteProps.folderPathname) - } - if (noteProps.tags) { - await Promise.all(noteProps.tags.map(tagName => this.upsertTag(tagName))) - } - - const now = getNow() - const noteDocProps = { - ...note, - ...noteProps, - updatedAt: now - } - const { rev } = await this.pouchDb.put(noteDocProps) - - return { - ...noteDocProps, - _rev: rev - } - } - - async findNotesByFolder(folderPathname: string): Promise { - const { rows } = await this.pouchDb.query('notes/by_folder', { - key: folderPathname, - include_docs: true - }) - - return rows.map(row => row.doc!) - } - - async findNotesByTag(tagName: string): Promise { - const { rows } = await this.pouchDb.query('notes/by_tag', { - key: tagName, - include_docs: true - }) - - return rows.map(row => row.doc!) - } - - async upsertNoteListViews() { - const ddoc = await this.getDoc< - { - views: { [key: string]: { map: string } } - } & PouchDB.Core.GetMeta & - PouchDB.Core.IdMeta - >('_design/notes') - const byFolderMap = `function(doc) { - if (doc._id.startsWith('note:')) { - emit(doc.folderPathname); - } - }` - const byTagMap = `function(doc) { - if (doc._id.startsWith('note:')) { - doc.tags.forEach(function(tag){ - emit(tag); - }); - } - }` - if (ddoc != null) { - if ( - ddoc.views.by_folder.map === byFolderMap && - ddoc.views.by_tag.map === byTagMap - ) { - return ddoc - } - } - - return this.pouchDb.put({ - ...(ddoc || { - _id: '_design/notes' - }), - views: { - by_folder: { - map: byFolderMap - }, - by_tag: { - map: byTagMap - } - } - }) - } - - async trashNote(noteId: string): Promise { - const note = await this.getNote(noteId) - if (note == null) - throw createNotFoundError(`The note \`${noteId}\` does not exist`) - - const noteDocProps = { - ...note, - trashed: true - } - const { rev } = await this.pouchDb.put(noteDocProps) - - return { - ...noteDocProps, - _rev: rev - } - } - - async untrashNote(noteId: string): Promise { - const note = await this.getNote(noteId) - if (note == null) - throw createNotFoundError(`The note \`${noteId}\` does not exist`) - - await this.upsertFolder(note.folderPathname) - - await Promise.all( - note.tags.map(tag => { - this.upsertTag(tag) - }) - ) - - const noteDocProps = { - ...note, - trashed: false - } - const { rev } = await this.pouchDb.put(noteDocProps) - - return { - ...noteDocProps, - _rev: rev - } - } - - async purgeNote(noteId: string): Promise { - const note = await this.getNote(noteId) - if (note == null) - throw createNotFoundError(`The note \`${noteId}\` does not exist`) - - await this.pouchDb.remove(note) - } - - async removeTag(tagName: string): Promise { - const notes = await this.findNotesByTag(tagName) - await Promise.all( - notes.map(note => { - return this.updateNote(note._id, { - tags: note.tags.filter(tag => tag !== tagName) - }) - }) - ) - - const tag = await this.getTag(tagName) - if (tag != null) { - this.pouchDb.remove(tag) - } - } - - async removeFolder(folderPathname: string): Promise { - const foldersToDelete = await this.getAllFolderUnderPathname(folderPathname) - - await Promise.all( - foldersToDelete.map(folder => - this.trashAllNotesInFolder(getFolderPathname(folder._id)) - ) - ) - - await Promise.all( - foldersToDelete.map(folder => this.pouchDb.remove(folder)) - ) - } - - async getAllFolderUnderPathname( - folderPathname: string - ): Promise { - const [folder, { rows }] = await Promise.all([ - this.getFolder(folderPathname), - this.pouchDb.allDocs({ - startkey: `${getFolderId(folderPathname)}/`, - endkey: `${getFolderId(folderPathname)}/\ufff0`, - include_docs: true - }) - ]) - const folderList = rows.map(row => row.doc!) - if (folder != null) { - folderList.unshift(folder) - } - - return folderList - } - - async trashAllNotesInFolder(folderPathname: string): Promise { - const notes = await this.findNotesByFolder(folderPathname) - - await Promise.all( - notes.filter(note => !note.trashed).map(note => this.trashNote(note._id)) - ) - } - - async getAllFolders(): Promise { - const allDocsResponse = await this.pouchDb.allDocs({ - startkey: `${FOLDER_ID_PREFIX}/`, - endkey: `${FOLDER_ID_PREFIX}/\ufff0`, - include_docs: true - }) - return allDocsResponse.rows.map(row => row.doc!) - } - - async getFoldersByPathnames(pathnames: string[]): Promise { - if (pathnames.length === 0) { - return [] - } - const allDocsResponse = await this.pouchDb.allDocs({ - keys: pathnames.map(pathname => getFolderId(pathname)), - include_docs: true - }) - return allDocsResponse.rows.map(row => row.doc!) - } - - async sync( - user: User, - cloudStorage: { id: number } - ): Promise> { - const cloudPouch = new PouchDB( - buildCloudSyncUrl(cloudStorage.id, user.id), - { - fetch: (url, opts = {}) => { - if (opts.headers == null) { - opts.headers = new Headers() - } - - opts.headers = setHeader( - 'Authorization', - `Bearer ${user.token}`, - opts.headers - ) - - return PouchDB.fetch(url, opts) - } - } - ) - - return new Promise((resolve, reject) => { - this.pouchDb - .sync(cloudPouch, { live: false }) - .on('error', reject) - .on('complete', resolve) - }) - } - - async upsertAttachments(files: File[]): Promise { - const { _rev } = await this.pouchDb.get(ATTACHMENTS_ID) - let currentRev = _rev - const attachments: Attachment[] = [] - for (const file of files) { - const { name, ext } = parsePath(file.name) - const fileName = `${dashify(name)}${ext}` - const response = await this.pouchDb.putAttachment( - ATTACHMENTS_ID, - fileName, - currentRev, - file, - file.type - ) - currentRev = response.rev - const data = await this.pouchDb.getAttachment(ATTACHMENTS_ID, fileName) - attachments.push({ - name: fileName, - type: file.type, - blob: data as Blob - }) - } - - return attachments - } - - async removeAttachment(fileName: string): Promise { - const { _rev } = await this.pouchDb.get(ATTACHMENTS_ID) - await this.pouchDb.removeAttachment(ATTACHMENTS_ID, fileName, _rev) - } - - async getAttachmentMap(): Promise> { - let attachmentDoc - try { - attachmentDoc = await this.pouchDb.get(ATTACHMENTS_ID, { - attachments: true, - binary: true - }) - } catch (error) { - if (error.name !== 'not_found') { - throw error - } - await this.pouchDb.put({ _id: ATTACHMENTS_ID }) - attachmentDoc = await this.pouchDb.get(ATTACHMENTS_ID, { - attachments: true, - binary: true - }) - } - - const { _attachments } = attachmentDoc - if (_attachments == null) { - return {} - } - return Object.entries(_attachments).reduce( - (map, [key, attachment]) => { - map[key] = { - name: key, - type: attachment.content_type, - blob: (attachment as PouchDB.Core.FullAttachment).data as Blob - } - return map - }, - {} as ObjectMap - ) - } -}