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
- )
- }
-}