diff --git a/components/editor/dictionary.tsx b/components/editor/dictionary.tsx new file mode 100644 index 000000000..cbb319489 --- /dev/null +++ b/components/editor/dictionary.tsx @@ -0,0 +1,72 @@ +import useI18n from 'libs/web/hooks/use-i18n' +import { useMemo } from 'react' + +export const useDictionary = () => { + const { t } = useI18n() + + const dictionary = useMemo( + () => ({ + addColumnAfter: t('Insert column after'), + addColumnBefore: t('Insert column before'), + addRowAfter: t('Insert row after'), + addRowBefore: t('Insert row before'), + alignCenter: t('Align center'), + alignLeft: t('Align left'), + alignRight: t('Align right'), + bulletList: t('Bulleted list'), + checkboxList: t('Todo list'), + codeBlock: t('Code block'), + codeCopied: t('Copied to clipboard'), + codeInline: t('Code'), + createLink: t('Create link'), + createLinkError: t('Sorry, an error occurred creating the link'), + createNewDoc: t('Create a new note'), + deleteColumn: t('Delete column'), + deleteRow: t('Delete row'), + deleteTable: t('Delete table'), + deleteImage: t('Delete image'), + alignImageLeft: t('Float left half width'), + alignImageRight: t('Float right half width'), + alignImageDefault: t('Center large'), + em: t('Italic'), + embedInvalidLink: t('Sorry, that link won’t work for this embed type'), + findOrCreateDoc: t('Find or create a note…'), + h1: t('Big heading'), + h2: t('Medium heading'), + h3: t('Small heading'), + heading: t('Heading'), + hr: t('Divider'), + image: t('Image'), + imageUploadError: t('Sorry, an error occurred uploading the image'), + info: t('Info'), + infoNotice: t('Info notice'), + link: t('Link'), + linkCopied: t('Link copied to clipboard'), + mark: t('Highlight'), + newLineEmpty: t("Type '/' to insert…"), + newLineWithSlash: t('Keep typing to filter…'), + noResults: t('No results'), + openLink: t('Open link'), + orderedList: t('Ordered list'), + pageBreak: t('Page break'), + pasteLink: t('Paste a link…'), + pasteLinkWithTitle: (title: string): string => + t(`Paste a {{title}} link…`, { title }), + placeholder: t('Placeholder'), + quote: t('Quote'), + removeLink: t('Remove link'), + searchOrPasteLink: t('Search or paste a link…'), + strikethrough: t('Strikethrough'), + strong: t('Bold'), + subheading: t('Subheading'), + table: t('Table'), + tip: t('Tip'), + tipNotice: t('Tip notice'), + warning: t('Warning'), + warningNotice: t('Warning notice'), + }), + [t] + ) + + return dictionary +} diff --git a/components/editor/editor.tsx b/components/editor/editor.tsx index ed33afdfd..6f9908e73 100644 --- a/components/editor/editor.tsx +++ b/components/editor/editor.tsx @@ -1,13 +1,14 @@ -import { FC, useCallback, useEffect, useMemo, useState } from 'react' +import { FC, useEffect, useState } from 'react' import { use100vh } from 'react-div-100vh' import MarkdownEditor, { Props } from 'rich-markdown-editor' import { useEditorTheme } from './theme' import useMounted from 'libs/web/hooks/use-mounted' -import useI18n from 'libs/web/hooks/use-i18n' import Tooltip from './tooltip' import extensions from './extensions' import EditorState from 'libs/web/state/editor' import { useToast } from 'libs/web/hooks/use-toast' +import { useDictionary } from './dictionary' +import { useEmbeds } from './embeds' export type EditorProps = Pick @@ -18,88 +19,18 @@ const Editor: FC = ({ readOnly }) => { onClickLink, onUploadImage, onHoverLink, - onNoteChange, + onEditorChange, backlinks, editorEl, note, } = EditorState.useContainer() const height = use100vh() const mounted = useMounted() - const { t } = useI18n() const editorTheme = useEditorTheme() const [hasMinHeight, setHasMinHeight] = useState(true) const toast = useToast() - - const dictionary = useMemo( - () => ({ - addColumnAfter: t('Insert column after'), - addColumnBefore: t('Insert column before'), - addRowAfter: t('Insert row after'), - addRowBefore: t('Insert row before'), - alignCenter: t('Align center'), - alignLeft: t('Align left'), - alignRight: t('Align right'), - bulletList: t('Bulleted list'), - checkboxList: t('Todo list'), - codeBlock: t('Code block'), - codeCopied: t('Copied to clipboard'), - codeInline: t('Code'), - createLink: t('Create link'), - createLinkError: t('Sorry, an error occurred creating the link'), - createNewDoc: t('Create a new note'), - deleteColumn: t('Delete column'), - deleteRow: t('Delete row'), - deleteTable: t('Delete table'), - deleteImage: t('Delete image'), - alignImageLeft: t('Float left half width'), - alignImageRight: t('Float right half width'), - alignImageDefault: t('Center large'), - em: t('Italic'), - embedInvalidLink: t('Sorry, that link won’t work for this embed type'), - findOrCreateDoc: t('Find or create a note…'), - h1: t('Big heading'), - h2: t('Medium heading'), - h3: t('Small heading'), - heading: t('Heading'), - hr: t('Divider'), - image: t('Image'), - imageUploadError: t('Sorry, an error occurred uploading the image'), - info: t('Info'), - infoNotice: t('Info notice'), - link: t('Link'), - linkCopied: t('Link copied to clipboard'), - mark: t('Highlight'), - newLineEmpty: t("Type '/' to insert…"), - newLineWithSlash: t('Keep typing to filter…'), - noResults: t('No results'), - openLink: t('Open link'), - orderedList: t('Ordered list'), - pageBreak: t('Page break'), - pasteLink: t('Paste a link…'), - pasteLinkWithTitle: (title: string): string => - t(`Paste a {{title}} link…`, { title }), - placeholder: t('Placeholder'), - quote: t('Quote'), - removeLink: t('Remove link'), - searchOrPasteLink: t('Search or paste a link…'), - strikethrough: t('Strikethrough'), - strong: t('Bold'), - subheading: t('Subheading'), - table: t('Table'), - tip: t('Tip'), - tipNotice: t('Tip notice'), - warning: t('Warning'), - warningNotice: t('Warning notice'), - }), - [t] - ) - - const onEditorChange = useCallback( - (value: () => string): void => { - onNoteChange.callback({ content: value() }) - }, - [onNoteChange] - ) + const dictionary = useDictionary() + const embeds = useEmbeds() useEffect(() => { setHasMinHeight((backlinks?.length ?? 0) <= 0) @@ -124,6 +55,7 @@ const Editor: FC = ({ readOnly }) => { tooltip={Tooltip} extensions={extensions} className="px-4 md:px-0" + embeds={embeds} /> diff --git a/components/editor/embeds/bookmark.tsx b/components/editor/embeds/bookmark.tsx new file mode 100644 index 000000000..a82ee55bc --- /dev/null +++ b/components/editor/embeds/bookmark.tsx @@ -0,0 +1,57 @@ +import { Skeleton } from '@material-ui/lab' +import useFetcher from 'libs/web/api/fetcher' +import { decode } from 'qss' +import { FC, useEffect, useState } from 'react' +import { Metadata } from 'unfurl.js/dist/types' +import { EmbedProps } from '.' + +export const Bookmark: FC = ({ attrs: { href } }) => { + const { request } = useFetcher() + const [data, setData] = useState() + + useEffect(() => { + request({ + url: href, + method: 'GET', + }).then((data) => { + setData(data) + }) + }, [href, request]) + + const image = data?.open_graph?.images?.[0].url ?? data?.favicon + const title = data?.open_graph?.title ?? data?.title + const description = data?.open_graph?.description ?? data?.description + const url = + data?.open_graph?.url ?? + decode<{ url: string }>(href.replace(/.*\?/, '')).url + + if (!data) { + return + } + + return ( + +
+
+ {title} +
+
+ {description} +
+
+ {url} +
+
+ {!!image && ( +
+ {title} +
+ )} +
+ ) +} diff --git a/components/editor/embeds/embed.tsx b/components/editor/embeds/embed.tsx new file mode 100644 index 000000000..ab49d9345 --- /dev/null +++ b/components/editor/embeds/embed.tsx @@ -0,0 +1,37 @@ +import { Skeleton } from '@material-ui/lab' +import useFetcher from 'libs/web/api/fetcher' +import { FC, useEffect, useState } from 'react' +import { Metadata } from 'unfurl.js/dist/types' +import { EmbedProps } from '.' +import InnerHTML from 'dangerously-set-html-content' +import { decode } from 'qss' + +export const Embed: FC = ({ attrs: { href } }) => { + const { request } = useFetcher() + const [data, setData] = useState() + + useEffect(() => { + request({ + url: href, + method: 'GET', + }).then((data) => { + setData(data) + }) + }, [href, request]) + + if (!data) { + return + } + + const html = (data?.oEmbed as any)?.html + + if (html) { + return + } + + const url = + data?.open_graph?.url ?? + decode<{ url: string }>(href.replace(/.*\?/, '')).url + + return `, + } as any + } else if (/gist\.github\.com/.test(url)) { + result.open_graph = { + ...result.open_graph, + url: `${url}.pibb`, + } + } + + res.json(result) + }) diff --git a/pages/api/file/[...file].ts b/pages/api/file/[...file].ts index 8576420a9..eeca7b041 100644 --- a/pages/api/file/[...file].ts +++ b/pages/api/file/[...file].ts @@ -1,45 +1,22 @@ import { api } from 'libs/server/connect' -import { isLoggedIn } from 'libs/server/middlewares/auth' +import { useReferrer } from 'libs/server/middlewares/referrer' import { useStore } from 'libs/server/middlewares/store' import { getPathFileByName } from 'libs/server/note-path' import { getEnv } from 'libs/shared/env' -import { NOTE_ID_REGEXP } from 'libs/shared/note' // On aliyun `X-Amz-Expires` must be less than 604800 seconds const expires = 86400 export default api() .use(useStore) + .use(useReferrer) .get(async (req, res) => { - const referer = req.headers.referer if (!req.query.file) { return res.APIError.NEED_LOGIN.throw() } const objectPath = getPathFileByName((req.query.file as string[]).join('/')) - /** - * Check permissions - * - Logged in [pass] - * - No? - * - The note are in the meta of the file and are shared [pass] - * - fallback: From the sharing page [pass] - */ - if (!isLoggedIn(req)) { - let noteId - - if (referer) { - const pathname = new URL(referer).pathname - const m = pathname.match(new RegExp(`/(${NOTE_ID_REGEXP})$`)) - - noteId = m ? m[1] : null - } - - if (!noteId) { - return res.APIError.NOT_SUPPORTED.throw() - } - } - res.setHeader( 'Cache-Control', `public, max-age=${expires}, s-maxage=${expires}, stale-while-revalidate=${expires}` diff --git a/yarn.lock b/yarn.lock index 4609d6ce6..025691787 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3456,6 +3456,13 @@ create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: safe-buffer "^5.0.1" sha.js "^2.4.8" +cross-fetch@^3.0.4: + version "3.1.4" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.4.tgz#9723f3a3a247bf8b89039f3a380a9244e8fa2f39" + integrity sha512-1eAtFWdIubi6T4XPy6ei9iUFoKpUkIF971QLN8lIvvvwueI65+Nw5haMNKUwfJxabqlIIDODJKGrQ66gxC0PbQ== + dependencies: + node-fetch "2.6.1" + cross-spawn@^7.0.0, cross-spawn@^7.0.2: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" @@ -3596,7 +3603,7 @@ debug@2, debug@^2.6.9: dependencies: ms "2.0.0" -debug@^3.2.7: +debug@^3.1.0, debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== @@ -3725,6 +3732,14 @@ dom-helpers@^5.0.1: "@babel/runtime" "^7.8.7" csstype "^3.0.2" +dom-serializer@0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51" + integrity sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g== + dependencies: + domelementtype "^2.0.1" + entities "^2.0.0" + domain-browser@4.19.0: version "4.19.0" resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-4.19.0.tgz#1093e17c0a17dbd521182fe90d49ac1370054af1" @@ -3735,6 +3750,31 @@ domain-browser@^1.1.1: resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA== +domelementtype@1, domelementtype@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" + integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== + +domelementtype@^2.0.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57" + integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A== + +domhandler@^2.3.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803" + integrity sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA== + dependencies: + domelementtype "1" + +domutils@^1.5.1: + version "1.7.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" + integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg== + dependencies: + dom-serializer "0" + domelementtype "1" + ejs@^2.6.1: version "2.7.4" resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.7.4.tgz#48661287573dcc53e366c7a1ae52c3a120eec9ba" @@ -3799,6 +3839,16 @@ enquirer@^2.3.5, enquirer@^2.3.6: dependencies: ansi-colors "^4.1.1" +entities@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" + integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== + +entities@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" + integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== + entities@~2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.3.tgz#5c487e5742ab93c15abb5da22759b8590ec03b7f" @@ -4516,7 +4566,7 @@ hastscript@^6.0.0: property-information "^5.0.0" space-separated-tokens "^1.0.0" -he@1.2.0: +he@1.2.0, he@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== @@ -4557,6 +4607,18 @@ html-tags@^3.1.0: resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.1.0.tgz#7b5e6f7e665e9fb41f30007ed9e0d41e97fb2140" integrity sha512-1qYz89hW3lFDEazhjW0yVAV87lw8lVkrJocr72XmBkMKsoSVJCQx3W8BXsC7hO2qAt8BoVjYjtAcZ9perqGnNg== +htmlparser2@^3.9.2: + version "3.10.1" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f" + integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ== + dependencies: + domelementtype "^1.3.1" + domhandler "^2.3.0" + domutils "^1.5.1" + entities "^1.1.1" + inherits "^2.0.1" + readable-stream "^3.1.1" + http-errors@1.7.3: version "1.7.3" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" @@ -4602,7 +4664,7 @@ i18n-extract@^0.6.7: gettext-parser "^3.1.1" glob "^7.1.3" -iconv-lite@0.4.24: +iconv-lite@0.4.24, iconv-lite@^0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== @@ -6509,6 +6571,11 @@ purgecss@^4.0.3: postcss "^8.2.1" postcss-selector-parser "^6.0.2" +qss@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/qss/-/qss-2.0.3.tgz#630b38b120931b52d04704f3abfb0f861604a9ec" + integrity sha512-j48ZBT5IZbSqJiSU8EX4XrN8nXiflHvmMvv2XpFc31gh7n6EpSs75bNr6+oj3FOLWyT8m09pTmqLNl34L7/uPQ== + querystring-es3@0.2.1, querystring-es3@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" @@ -6702,7 +6769,7 @@ read-pkg@^3.0.0: normalize-package-data "^2.3.2" path-type "^3.0.0" -"readable-stream@2 || 3", readable-stream@^3.2.0, readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0: +"readable-stream@2 || 3", readable-stream@^3.1.1, readable-stream@^3.2.0, readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -7208,7 +7275,7 @@ source-map-js@^0.6.2: resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-0.6.2.tgz#0bb5de631b41cfbda6cfba8bd05a80efdfd2385e" integrity sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug== -source-map-support@^0.5.16, source-map-support@~0.5.19: +source-map-support@^0.5.16, source-map-support@^0.5.9, source-map-support@~0.5.19: version "0.5.19" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== @@ -7847,6 +7914,18 @@ unbox-primitive@^1.0.1: has-symbols "^1.0.2" which-boxed-primitive "^1.0.2" +unfurl.js@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/unfurl.js/-/unfurl.js-5.3.0.tgz#f7346ae04e664dc816484f7fe146960a1ae65235" + integrity sha512-S3vSDb+zOcrjSLhXryg6yiFc6FZoPLW+OIrCaBxXXMagM6lS1fWihn5SDCU67QkgJs1PF+Uql3xjhp2uDxfQsg== + dependencies: + cross-fetch "^3.0.4" + debug "^3.1.0" + he "^1.2.0" + htmlparser2 "^3.9.2" + iconv-lite "^0.4.24" + source-map-support "^0.5.9" + unicode-canonical-property-names-ecmascript@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818"