diff --git a/.flowconfig b/.flowconfig index d87afb61..ef6f9d9e 100644 --- a/.flowconfig +++ b/.flowconfig @@ -1,5 +1,7 @@ [include] .*/frontend/.* +.*/server/.* +.*/shared/.* [ignore] .*/node_modules/styled-components/.* diff --git a/frontend/components/Editor/Editor.js b/frontend/components/Editor/Editor.js index 9834d133..0573ef5c 100644 --- a/frontend/components/Editor/Editor.js +++ b/frontend/components/Editor/Editor.js @@ -29,6 +29,7 @@ type Props = { onImageUploadStart: Function, onImageUploadStop: Function, starred: boolean, + emoji: string, readOnly: boolean, heading?: ?React.Element<*>, }; @@ -213,6 +214,7 @@ type KeyData = { className={cx(styles.editor, { readOnly: this.props.readOnly })} schema={this.schema} plugins={this.plugins} + emoji={this.props.emoji} state={this.state.state} onKeyDown={this.onKeyDown} onChange={this.onChange} diff --git a/frontend/components/Editor/components/Heading.js b/frontend/components/Editor/components/Heading.js index a219135a..05e2b857 100644 --- a/frontend/components/Editor/components/Heading.js +++ b/frontend/components/Editor/components/Heading.js @@ -25,6 +25,10 @@ type Context = { starred?: boolean, }; +const Wrapper = styled.div` + margin-left: ${props => (props.hasEmoji ? '-1.2em' : 0)} +`; + const StyledStar = styled(StarIcon)` top: 3px; position: relative; @@ -61,10 +65,14 @@ function Heading(props: Props, { starred }: Context) { const showStar = readOnly && !!onStar; const showHash = readOnly && !!slugish && !showStar; const Component = component; + const emoji = editor.props.emoji || ''; + const title = node.text.trim(); + const startsWithEmojiAndSpace = + emoji && title.match(new RegExp(`^${emoji}\\s`)); return ( - {children} + {children} {showPlaceholder && {editor.props.placeholder} diff --git a/frontend/models/Document.js b/frontend/models/Document.js index 64b37a60..c13341f2 100644 --- a/frontend/models/Document.js +++ b/frontend/models/Document.js @@ -5,15 +5,11 @@ import invariant from 'invariant'; import { client } from 'utils/ApiClient'; import stores from 'stores'; import ErrorsStore from 'stores/ErrorsStore'; +import parseTitle from '../../shared/parseTitle'; import type { User } from 'types'; import Collection from './Collection'; -const parseHeader = text => { - const firstLine = text.trim().split(/\r?\n/)[0]; - return firstLine.replace(/^#/, '').trim(); -}; - const DEFAULT_TITLE = 'Untitled document'; class Document { @@ -31,6 +27,7 @@ class Document { html: string; id: string; team: string; + emoji: string; private: boolean = false; starred: boolean = false; text: string = ''; @@ -181,7 +178,11 @@ class Document { }; updateData(data: Object = {}, dirty: boolean = false) { - if (data.text) data.title = parseHeader(data.text); + if (data.text) { + const { title, emoji } = parseTitle(data.text); + data.title = title; + data.emoji = emoji; + } if (dirty) this.hasPendingChanges = true; this.data = data; extendObservable(this, data); diff --git a/frontend/scenes/Document/Document.js b/frontend/scenes/Document/Document.js index e81507db..403ca9a2 100644 --- a/frontend/scenes/Document/Document.js +++ b/frontend/scenes/Document/Document.js @@ -203,6 +203,7 @@ type Props = { { return (doc.urlId = doc.urlId || randomstring.generate(10)); }; -const extractEmoji = doc => { - const regex = emojiRegex(); - const match = regex.exec(doc.title); - - if (match.length) return match[0]; - return null; -}; - const beforeSave = async doc => { + const { emoji } = parseTitle(doc.text); + + doc.emoji = emoji; doc.html = convertToMarkdown(doc.text); doc.preview = truncateMarkdown(doc.text, 160); doc.revisionCount += 1; @@ -62,7 +58,6 @@ const beforeSave = async doc => { // We'll add the current user as revision hasn't been generated yet ids.push(doc.lastModifiedById); doc.collaboratorIds = _.uniq(ids); - doc.emoji = extractEmoji(doc); return doc; }; diff --git a/shared/parseTitle.js b/shared/parseTitle.js new file mode 100644 index 00000000..ebb3e0dc --- /dev/null +++ b/shared/parseTitle.js @@ -0,0 +1,18 @@ +// @flow +import emojiRegex from 'emoji-regex'; + +export default function parseTitle(text: string = '') { + const regex = emojiRegex(); + + // find and extract title + const firstLine = text.trim().split(/\r?\n/)[0]; + const title = firstLine.replace(/^#/, '').trim(); + + // find and extract first emoji + const matches = regex.exec(title); + const firstEmoji = matches ? matches[0] : null; + const startsWithEmoji = firstEmoji && title.startsWith(firstEmoji); + const emoji = startsWithEmoji ? firstEmoji : undefined; + + return { title, emoji }; +} diff --git a/webpack.config.js b/webpack.config.js index 91cdf941..d1a43ae7 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -26,7 +26,10 @@ module.exports = { { test: /\.js$/, loader: 'babel', - include: path.join(__dirname, 'frontend'), + include: [ + path.join(__dirname, 'frontend'), + path.join(__dirname, 'shared'), + ] }, { test: /\.json$/, loader: 'json-loader' }, // inline base64 URLs for <=8k images, direct URLs for the rest diff --git a/yarn.lock b/yarn.lock index 5422feaa..3ca1b2dc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2499,6 +2499,10 @@ emoji-regex@^6.1.0: version "6.4.2" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-6.4.2.tgz#a30b6fee353d406d96cfb9fa765bdc82897eff6e" +emoji-regex@^6.5.1: + version "6.5.1" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-6.5.1.tgz#9baea929b155565c11ea41c6626eaa65cef992c2" + emojilib@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/emojilib/-/emojilib-2.0.2.tgz#df91c45ede69f2d0ffd3d80acf8c72771b2a5ea1"