From cfcdae8aa0d30c3674de4360881fed9b5bb4828c Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Sat, 29 Jul 2017 15:06:17 -0700 Subject: [PATCH 1/2] Save title emoji against document --- package.json | 29 ++++++++++++++++++----- server/migrations/20170729215619-emoji.js | 12 ++++++++++ server/models/Document.js | 10 ++++++++ server/presenters/document.js | 1 + 4 files changed, 46 insertions(+), 6 deletions(-) create mode 100644 server/migrations/20170729215619-emoji.js diff --git a/package.json b/package.json index 52833a1b3..1148fb018 100644 --- a/package.json +++ b/package.json @@ -22,19 +22,35 @@ "precommit": "lint-staged" }, "lint-staged": { - "*.js": ["eslint --fix", "git add"] + "*.js": [ + "eslint --fix", + "git add" + ] }, "jest": { "verbose": false, - "roots": ["frontend"], + "roots": [ + "frontend" + ], "moduleNameMapper": { "^.*[.](s?css|css)$": "/__mocks__/styleMock.js", "^.*[.](gif|ttf|eot|svg)$": "/__test__/fileMock.js" }, - "moduleFileExtensions": ["js", "jsx", "json"], - "moduleDirectories": ["node_modules"], - "modulePaths": ["frontend"], - "setupFiles": ["/setupJest.js", "/__mocks__/window.js"] + "moduleFileExtensions": [ + "js", + "jsx", + "json" + ], + "moduleDirectories": [ + "node_modules" + ], + "modulePaths": [ + "frontend" + ], + "setupFiles": [ + "/setupJest.js", + "/__mocks__/window.js" + ] }, "engines": { "node": ">= 7.6" @@ -69,6 +85,7 @@ "debug": "2.2.0", "dotenv": "^4.0.0", "emoji-name-map": "1.1.2", + "emoji-regex": "^6.5.1", "eslint": "^3.19.0", "eslint-config-react-app": "^0.6.2", "eslint-import-resolver-webpack": "^0.3.1", diff --git a/server/migrations/20170729215619-emoji.js b/server/migrations/20170729215619-emoji.js new file mode 100644 index 000000000..492a4b2ee --- /dev/null +++ b/server/migrations/20170729215619-emoji.js @@ -0,0 +1,12 @@ +module.exports = { + up: (queryInterface, Sequelize) => { + queryInterface.addColumn('documents', 'emoji', { + type: Sequelize.STRING, + allowNull: true, + }); + }, + + down: (queryInterface, _Sequelize) => { + queryInterface.removeColumn('documents', 'emoji'); + }, +}; diff --git a/server/models/Document.js b/server/models/Document.js index b912bc30f..8a1252218 100644 --- a/server/models/Document.js +++ b/server/models/Document.js @@ -2,6 +2,7 @@ import slug from 'slug'; import _ from 'lodash'; import randomstring from 'randomstring'; +import emojiRegex from 'emoji-regex'; import isUUID from 'validator/lib/isUUID'; import { DataTypes, sequelize } from '../sequelize'; @@ -34,6 +35,14 @@ const createUrlId = doc => { 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 => { doc.html = convertToMarkdown(doc.text); doc.preview = truncateMarkdown(doc.text, 160); @@ -53,6 +62,7 @@ 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/server/presenters/document.js b/server/presenters/document.js index f26df3fcc..4dce75bb3 100644 --- a/server/presenters/document.js +++ b/server/presenters/document.js @@ -22,6 +22,7 @@ async function present(ctx: Object, document: Document, options: ?Options) { text: document.text, html: document.html, preview: document.preview, + emoji: document.emoji, createdAt: document.createdAt, createdBy: presentUser(ctx, document.createdBy), updatedAt: document.updatedAt, From 297bf850c21c57e8e9414e25d76217cc4e8b106b Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Sat, 29 Jul 2017 16:15:04 -0700 Subject: [PATCH 2/2] Align title correctly when first character is emoji --- .flowconfig | 2 ++ frontend/components/Editor/Editor.js | 2 ++ .../components/Editor/components/Heading.js | 10 +++++++++- frontend/models/Document.js | 13 +++++++------ frontend/scenes/Document/Document.js | 1 + server/models/Document.js | 13 ++++--------- shared/parseTitle.js | 18 ++++++++++++++++++ webpack.config.js | 5 ++++- yarn.lock | 4 ++++ 9 files changed, 51 insertions(+), 17 deletions(-) create mode 100644 shared/parseTitle.js diff --git a/.flowconfig b/.flowconfig index d87afb610..ef6f9d9ea 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 9834d133a..0573ef5c0 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 a219135a1..05e2b857b 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 64b37a606..c13341f2e 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 e81507dbb..403ca9a2d 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 000000000..ebb3e0dcf --- /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 91cdf941b..d1a43ae70 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 5422feaaa..3ca1b2dc0 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"