From 0d4cac9ce0e17940582382809d3f0ac0eec7748b Mon Sep 17 00:00:00 2001 From: Max Stoiber Date: Fri, 14 Dec 2018 10:21:43 +0100 Subject: [PATCH 01/45] Hacky plaintext chatinput --- src/components/chatInput/index.js | 96 +++++++------------------------ 1 file changed, 21 insertions(+), 75 deletions(-) diff --git a/src/components/chatInput/index.js b/src/components/chatInput/index.js index 3f32cbc466..830d03c5a3 100644 --- a/src/components/chatInput/index.js +++ b/src/components/chatInput/index.js @@ -106,6 +106,7 @@ const ONE_DAY = (): string => { // We persist the body and title to localStorage // so in case the app crashes users don't loose content const returnText = (type = '') => { + return; let storedContent; let storedContentDM; const currTime = new Date().getTime().toString(); @@ -151,6 +152,7 @@ const returnText = (type = '') => { }; const setText = (content, threadType = '') => { + return; if (threadType === 'directMessageThread') { localStorage && localStorage.setItem(LS_DM_KEY, JSON.stringify(toJSON(content))); @@ -187,27 +189,6 @@ class ChatInput extends React.Component { this.props.onRef(this); } - shouldComponentUpdate(next, nextState) { - const curr = this.props; - const currState = this.state; - // User changed - if (curr.currentUser !== next.currentUser) return true; - - if (curr.networkOnline !== next.networkOnline) return true; - if (curr.websocketConnection !== next.websocketConnection) return true; - - if (curr.quotedMessage !== next.quotedMessage) return true; - - // State changed - if (curr.state !== next.state) return true; - if (currState.isSendingMediaMessage !== nextState.isSendingMediaMessage) - return true; - if (currState.mediaPreview !== nextState.mediaPreview) return true; - if (currState.photoSizeError !== nextState.photoSizeError) return true; - - return false; - } - componentWillUnmount() { document.removeEventListener('keydown', this.handleKeyDown); this.props.onRef(undefined); @@ -226,8 +207,7 @@ class ChatInput extends React.Component { // the previewed image and quoted message if ( key === 27 || - ((key === 8 || key === 46) && - !this.props.state.getCurrentContent().hasText()) + ((key === 8 || key === 46) && this.props.state.length === 0) ) { this.removePreviewWrapper(); this.removeQuotedMessage(); @@ -241,22 +221,17 @@ class ChatInput extends React.Component { ); }; - onChange = (state, ...rest) => { + onChange = evt => { + const state = evt.target.value; const { onChange, threadType } = this.props; this.toggleMarkdownHint(state); persistContent(state, threadType); - onChange(state, ...rest); + onChange(state); }; toggleMarkdownHint = state => { - // eslint-disable-next-line - let hasText = false; - // NOTE(@mxstbr): This throws an error on focus, so we just ignore that - try { - hasText = state.getCurrentContent().hasText(); - } catch (err) {} this.setState({ - markdownHint: state.getCurrentContent().hasText() ? true : false, + markdownHint: state.length === 0 ? true : false, }); }; @@ -332,7 +307,7 @@ class ChatInput extends React.Component { } // If the input is empty don't do anything - if (!state.getCurrentContent().hasText()) return 'handled'; + if (state.length === 0) return 'handled'; // do one last persist before sending forcePersist(state, threadType); this.removeQuotedMessage(); @@ -359,13 +334,11 @@ class ChatInput extends React.Component { if (threadType === 'directMessageThread') { sendDirectMessage({ threadId: thread, - messageType: !isAndroid() ? 'draftjs' : 'text', + messageType: 'text', threadType, parentId: quotedMessage, content: { - body: !isAndroid() - ? JSON.stringify(toJSON(state)) - : toPlainText(state), + body: state, }, }) .then(() => { @@ -378,13 +351,11 @@ class ChatInput extends React.Component { } else { sendMessage({ threadId: thread, - messageType: !isAndroid() ? 'draftjs' : 'text', + messageType: 'text', threadType, parentId: quotedMessage, content: { - body: !isAndroid() - ? JSON.stringify(toJSON(state)) - : toPlainText(state), + body: state, }, }) .then(() => { @@ -423,6 +394,8 @@ class ChatInput extends React.Component { // SHIFT+Enter should always add a new line if (e.shiftKey) return 'not-handled'; + return; + const currentContent = this.props.state.getCurrentContent(); const selection = this.props.state.getSelection(); const key = selection.getStartKey(); @@ -663,41 +636,14 @@ class ChatInput extends React.Component { /> )}
- (this.editor = editor)} - editorKey="chat-input" - decorators={[mentionsDecorator, linksDecorator]} - networkDisabled={networkDisabled} - hasAttachment={!!mediaPreview || !!quotedMessage} - > - {mediaPreview && ( - - - - - - - )} - {quotedMessage && ( - - - - - - - )} - + ref={editor => (this.editor = editor)} + /> { - return returnText(props.threadType) || fromPlainText(''); + return returnText(props.threadType) || ''; }), withHandlers({ onChange: ({ changeState }) => state => changeState(state), - clear: ({ changeState }) => () => changeState(fromPlainText('')), + clear: ({ changeState }) => () => changeState(''), }) )(ChatInput); From 97c9fbfaed4dbab0d55440c1810669a9e6436d75 Mon Sep 17 00:00:00 2001 From: Max Stoiber Date: Fri, 14 Dec 2018 10:57:39 +0100 Subject: [PATCH 02/45] Update to latest React --- package.json | 6 +++--- yarn.lock | 58 +++++++++++++++++++++++++++++++++------------------- 2 files changed, 40 insertions(+), 24 deletions(-) diff --git a/package.json b/package.json index f1c5c2b2ca..0e20ad409f 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "prettier": "^1.14.3", "raw-loader": "^0.5.1", "react-app-rewire-hot-loader": "^1.0.3", - "react-hot-loader": "^4.3.11", + "react-hot-loader": "^4.6.0", "react-scripts": "^1.1.5", "rimraf": "^2.6.1", "sw-precache-webpack-plugin": "^0.11.4", @@ -145,12 +145,12 @@ "query-string": "5.1.1", "raf": "^3.4.0", "raven": "^2.6.4", - "react": "16.4.2", + "react": "^16.7.0-alpha.2", "react-apollo": "^2.3.2", "react-app-rewire-styled-components": "^3.0.0", "react-app-rewired": "^1.6.2", "react-clipboard.js": "^2.0.1", - "react-dom": "16.4.2", + "react-dom": "^16.7.0-alpha.2", "react-flip-move": "^3.0.2", "react-helmet-async": "^0.1.0", "react-image": "^1.5.1", diff --git a/yarn.lock b/yarn.lock index 43a2bd1066..4e9eb9eb51 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9442,7 +9442,7 @@ lodash.memoize@~3.0.3: resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-3.0.4.tgz#2dcbd2c287cbc0a55cc42328bd0c736150d53e3f" integrity sha1-LcvSwofLwKVcxCMovQxzYVDVPj8= -lodash.merge@^4.4.0: +lodash.merge@^4.4.0, lodash.merge@^4.6.1: version "4.6.1" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.1.tgz#adc25d9cb99b9391c59624f379fbba60d7111d54" integrity sha512-AOYza4+Hf5z1/0Hztxpm2/xiPZgi/cjMqdnKTUWTBSKchJlxXXuUSxCCl8rJlf4g6yww/j6mA8nC8Hw/EZWxKQ== @@ -11811,15 +11811,15 @@ react-dev-utils@^5.0.2: strip-ansi "3.0.1" text-table "0.2.0" -react-dom@16.4.2: - version "16.4.2" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.4.2.tgz#4afed569689f2c561d2b8da0b819669c38a0bda4" - integrity sha512-Usl73nQqzvmJN+89r97zmeUpQDKDlh58eX6Hbs/ERdDHzeBzWy+ENk7fsGQ+5KxArV1iOFPT46/VneklK9zoWw== +react-dom@^16.7.0-alpha.2: + version "16.7.0-alpha.2" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.7.0-alpha.2.tgz#16632880ed43676315991d8b412cce6975a30282" + integrity sha512-o0mMw8jBlwHjGZEy/vvKd/6giAX0+skREMOTs3/QHmgi+yAhUClp4My4Z9lsKy3SXV+03uPdm1l/QM7NTcGuMw== dependencies: - fbjs "^0.8.16" loose-envify "^1.1.0" object-assign "^4.1.1" - prop-types "^15.6.0" + prop-types "^15.6.2" + scheduler "^0.12.0-alpha.2" react-error-overlay@^4.0.1: version "4.0.1" @@ -11841,17 +11841,20 @@ react-helmet-async@^0.1.0: prop-types "^15.6.1" shallowequal "^1.0.2" -react-hot-loader@^4.3.11: - version "4.3.12" - resolved "https://registry.yarnpkg.com/react-hot-loader/-/react-hot-loader-4.3.12.tgz#0d56688884e7330c63a00a17217866280616b07a" - integrity sha512-GMM4TsqUVss2QPe+Y33NlgydA5/+7tAVQxR0rZqWvBpapM8JhD7p6ymMwSZzr5yxjoXXlK/6P6qNQBOqm1dqdg== +react-hot-loader@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/react-hot-loader/-/react-hot-loader-4.6.0.tgz#7a556efea51a3c79e8bffdb094fbe17883a79432" + integrity sha512-ytmtbJB0RlTUqa9HnpVsoZiZ6iRsTzu+O2WovKT++f+tDYOTNZYa7OesVAE+R90e/1w/OJO4G/tw4rNSMYCjFw== dependencies: fast-levenshtein "^2.0.6" global "^4.3.0" hoist-non-react-statics "^2.5.0" + loader-utils "^1.1.0" + lodash.merge "^4.6.1" prop-types "^15.6.1" react-lifecycles-compat "^3.0.4" shallowequal "^1.0.2" + source-map "^0.7.3" react-image@^1.5.1: version "1.5.1" @@ -12040,16 +12043,6 @@ react@*: prop-types "^15.6.2" scheduler "^0.11.2" -react@16.4.2: - version "16.4.2" - resolved "https://registry.yarnpkg.com/react/-/react-16.4.2.tgz#2cd90154e3a9d9dd8da2991149fdca3c260e129f" - integrity sha512-dMv7YrbxO4y2aqnvA7f/ik9ibeLSHQJTI6TrYAenPSaQ6OXfb+Oti+oJiy8WBxgRzlKatYqtCjphTgDSCEiWFg== - dependencies: - fbjs "^0.8.16" - loose-envify "^1.1.0" - object-assign "^4.1.1" - prop-types "^15.6.0" - "react@^0.14.0 || ^15.0.0": version "15.6.2" resolved "https://registry.yarnpkg.com/react/-/react-15.6.2.tgz#dba0434ab439cfe82f108f0f511663908179aa72" @@ -12061,6 +12054,16 @@ react@16.4.2: object-assign "^4.1.0" prop-types "^15.5.10" +react@^16.7.0-alpha.2: + version "16.7.0-alpha.2" + resolved "https://registry.yarnpkg.com/react/-/react-16.7.0-alpha.2.tgz#924f2ae843a46ea82d104a8def7a599fbf2c78ce" + integrity sha512-Xh1CC8KkqIojhC+LFXd21jxlVtzoVYdGnQAi/I2+dxbmos9ghbx5TQf9/nDxc4WxaFfUQJkya0w1k6rMeyIaxQ== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + prop-types "^15.6.2" + scheduler "^0.12.0-alpha.2" + read-only-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/read-only-stream/-/read-only-stream-2.0.0.tgz#2724fd6a8113d73764ac288d4386270c1dbf17f0" @@ -12856,6 +12859,14 @@ scheduler@^0.11.2: loose-envify "^1.1.0" object-assign "^4.1.1" +scheduler@^0.12.0-alpha.2: + version "0.12.0-alpha.3" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.12.0-alpha.3.tgz#59afcaba1cb79e3e8bee91de94eb8f42c9152c2b" + integrity sha512-KADuBlOWSrT/DCt/oA+NgsNamRCsfz7wj+leaeGjGHipNClsqhjOPogKkJgem6WLAv/QzxW8bE7zlGc9OxiYSQ== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + schema-utils@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.3.0.tgz#f5877222ce3e931edae039f17eb3716e7137f8cf" @@ -13230,6 +13241,11 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== +source-map@^0.7.3: + version "0.7.3" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" + integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== + spawn-sync@^1.0.15: version "1.0.15" resolved "https://registry.yarnpkg.com/spawn-sync/-/spawn-sync-1.0.15.tgz#b00799557eb7fb0c8376c29d44e8a1ea67e57476" From 84eb575874d62285a94bf109e73791c1a4ddc1ab Mon Sep 17 00:00:00 2001 From: Max Stoiber Date: Fri, 14 Dec 2018 10:57:48 +0100 Subject: [PATCH 03/45] Rewrite chat input with hooks --- src/components/chatInput/index.js | 660 ++++-------------------------- src/components/chatInput/input.js | 124 ------ src/components/chatInput/style.js | 18 +- 3 files changed, 83 insertions(+), 719 deletions(-) delete mode 100644 src/components/chatInput/input.js diff --git a/src/components/chatInput/index.js b/src/components/chatInput/index.js index 830d03c5a3..330d20e050 100644 --- a/src/components/chatInput/index.js +++ b/src/components/chatInput/index.js @@ -4,18 +4,8 @@ import compose from 'recompose/compose'; import withState from 'recompose/withState'; import withHandlers from 'recompose/withHandlers'; import { connect } from 'react-redux'; -import { KeyBindingUtil } from 'draft-js'; import debounce from 'debounce'; import Icon from 'src/components/icons'; -import { - toJSON, - toState, - fromPlainText, - toPlainText, - isAndroid, -} from 'shared/draft-utils'; -import mentionsDecorator from 'shared/clients/draft-js/mentions-decorator/index.web.js'; -import linksDecorator from 'shared/clients/draft-js/links-decorator/index.web.js'; import { addToastWithTimeout } from 'src/actions/toasts'; import { openModal } from 'src/actions/modals'; import { replyToMessage } from 'src/actions/message'; @@ -24,6 +14,7 @@ import { Form, ChatInputContainer, ChatInputWrapper, + Input, SendButton, PhotoSizeError, MarkdownHint, @@ -31,7 +22,6 @@ import { PreviewWrapper, RemovePreviewButton, } from './style'; -import Input from './input'; import sendMessage from 'shared/graphql/mutations/message/sendMessage'; import sendDirectMessage from 'shared/graphql/mutations/message/sendDirectMessage'; import { getMessageById } from 'shared/graphql/queries/message/getMessage'; @@ -62,21 +52,15 @@ const QuotedMessage = connect()( }) ); -type State = { - isFocused: boolean, - photoSizeError: string, - isSendingMediaMessage: boolean, - mediaPreview: string, - mediaPreviewFile: ?Blob, - markdownHint: boolean, -}; +const LS_KEY = 'last-chat-input-content'; +const LS_KEY_EXPIRE = 'last-chat-input-content-expire'; +const LS_DM_KEY = 'last-chat-input-content-dm'; +const LS_DM_KEY_EXPIRE = 'last-chat-input-content-dm-expire'; type Props = { onRef: Function, currentUser: Object, dispatch: Dispatch, - onChange: Function, - state: Object, createThread: Function, sendMessage: Function, sendDirectMessage: Function, @@ -84,8 +68,6 @@ type Props = { threadType: string, thread: string, clear: Function, - onBlur: Function, - onFocus: Function, websocketConnection: string, networkOnline: boolean, threadData?: Object, @@ -93,593 +75,89 @@ type Props = { quotedMessage: ?{ messageId: string, threadId: string }, }; -const LS_KEY = 'last-chat-input-content'; -const LS_KEY_EXPIRE = 'last-chat-input-content-expire'; -const LS_DM_KEY = 'last-chat-input-content-dm'; -const LS_DM_KEY_EXPIRE = 'last-chat-input-content-dm-expire'; - -const ONE_DAY = (): string => { - const time = new Date().getTime() + 60 * 60 * 24 * 1000; - return time.toString(); -}; - -// We persist the body and title to localStorage -// so in case the app crashes users don't loose content -const returnText = (type = '') => { - return; - let storedContent; - let storedContentDM; - const currTime = new Date().getTime().toString(); - if (localStorage) { - try { - const expireTime = localStorage.getItem(LS_KEY_EXPIRE); +const ChatInput = React.forwardRef((props: Props, ref) => { + const [text, changeText] = React.useState(''); + const [showMarkdownHint, setShowMarkdownHint] = React.useState(false); - // if current time is greater than valid till of text then please expire text back to '' - if (expireTime && currTime > expireTime) { - localStorage.removeItem(LS_KEY); - localStorage.removeItem(LS_KEY_EXPIRE); - } else { - storedContent = toState(JSON.parse(localStorage.getItem(LS_KEY) || '')); - } - } catch (err) { - localStorage.removeItem(LS_KEY); - localStorage.removeItem(LS_KEY_EXPIRE); - } - - try { - const expireTimeDM = localStorage.getItem(LS_DM_KEY_EXPIRE); - - // if current time is greater than valid till of text then please expire text back to '' - if (expireTimeDM && currTime > expireTimeDM) { - localStorage.removeItem(LS_DM_KEY); - localStorage.removeItem(LS_DM_KEY_EXPIRE); - } else { - storedContentDM = toState( - JSON.parse(localStorage.getItem(LS_DM_KEY) || '') - ); - } - } catch (err) { - localStorage.removeItem(LS_DM_KEY); - localStorage.removeItem(LS_DM_KEY_EXPIRE); - } - } - - if (type === 'directMessageThread') { - return storedContentDM; - } else { - return storedContent; - } -}; - -const setText = (content, threadType = '') => { - return; - if (threadType === 'directMessageThread') { - localStorage && - localStorage.setItem(LS_DM_KEY, JSON.stringify(toJSON(content))); - localStorage && localStorage.setItem(LS_DM_KEY_EXPIRE, ONE_DAY()); - } else { - localStorage && - localStorage.setItem(LS_KEY, JSON.stringify(toJSON(content))); - localStorage && localStorage.setItem(LS_KEY_EXPIRE, ONE_DAY()); - } -}; - -const forcePersist = (content, threadType = '') => { - setText(content, threadType); -}; -const persistContent = debounce((content, threadType = '') => { - setText(content, threadType); -}, 500); - -class ChatInput extends React.Component { - state = { - isFocused: false, - photoSizeError: '', - code: false, - isSendingMediaMessage: false, - mediaPreview: '', - mediaPreviewFile: null, - markdownHint: false, - }; - - editor: any; - - componentDidMount() { - document.addEventListener('keydown', this.handleKeyDown, true); - this.props.onRef(this); - } - - componentWillUnmount() { - document.removeEventListener('keydown', this.handleKeyDown); - this.props.onRef(undefined); - } - - componentDidUpdate(prevProps) { - const curr = this.props; - if (curr.quotedMessage !== prevProps.quotedMessage) { - this.triggerFocus(); - } - } - - handleKeyDown = (event: any) => { - const key = event.keyCode || event.charCode; - // Detect esc key or backspace key (and empty message) to remove - // the previewed image and quoted message - if ( - key === 27 || - ((key === 8 || key === 46) && this.props.state.length === 0) - ) { - this.removePreviewWrapper(); - this.removeQuotedMessage(); + const handleKeyPress = e => { + // Shift+Enter + if (e.key !== 'Enter') return; + if (!e.shiftKey) { + e.preventDefault(); + submit(); } }; - removeQuotedMessage = () => { - if (this.props.quotedMessage) - this.props.dispatch( - replyToMessage({ threadId: this.props.thread, messageId: null }) - ); + const onChange = e => { + const text = e.target.value; + const newShowMarkdownHint = text.length > 0; + if (newShowMarkdownHint !== showMarkdownHint) + setShowMarkdownHint(newShowMarkdownHint); + changeText(text); }; - onChange = evt => { - const state = evt.target.value; - const { onChange, threadType } = this.props; - this.toggleMarkdownHint(state); - persistContent(state, threadType); - onChange(state); - }; - - toggleMarkdownHint = state => { - this.setState({ - markdownHint: state.length === 0 ? true : false, - }); - }; - - triggerFocus = () => { - // NOTE(@mxstbr): This needs to be delayed for a tick, otherwise the - // decorators that are passed to the editor are removed from the editor - // state - setTimeout(() => { - this.editor && this.editor.focus && this.editor.focus(); - }, 0); - }; - - submit = e => { + const submit = e => { if (e) e.preventDefault(); - - const { - state, - thread, - threadType, - createThread, - dispatch, - sendMessage, - sendDirectMessage, - clear, - forceScrollToBottom, - networkOnline, - websocketConnection, - currentUser, - threadData, - refetchThread, - quotedMessage, - } = this.props; - - const isSendingMessageAsNonMember = - threadType === 'story' && - threadData && - !threadData.channel.channelPermissions.isMember; - - if (!networkOnline) { - return dispatch( - addToastWithTimeout( - 'error', - 'Not connected to the internet - check your internet connection or try again' - ) - ); - } - - if ( - websocketConnection !== 'connected' && - websocketConnection !== 'reconnected' - ) { - return dispatch( - addToastWithTimeout( - 'error', - 'Error connecting to the server - hang tight while we try to reconnect' - ) - ); - } - - if (!currentUser) { - // user is trying to send a message without being signed in - return dispatch(openModal('CHAT_INPUT_LOGIN_MODAL', {})); - } - - // This doesn't exist if this is a new conversation - if (forceScrollToBottom) { - // if a user sends a message, force a scroll to bottom - forceScrollToBottom(); - } - - if (this.state.mediaPreview.length) { - this.sendMediaMessage(this.state.mediaPreviewFile); - } - - // If the input is empty don't do anything - if (state.length === 0) return 'handled'; - // do one last persist before sending - forcePersist(state, threadType); - this.removeQuotedMessage(); - - // user is creating a new directMessageThread, break the chain - // and initiate a new group creation with the message being sent - // in views/directMessages/containers/newThread.js - if (thread === 'newDirectMessageThread') { - createThread({ - messageBody: !isAndroid() - ? JSON.stringify(toJSON(state)) - : toPlainText(state), - messageType: !isAndroid() ? 'draftjs' : 'text', - }); - localStorage.removeItem(LS_DM_KEY); - localStorage.removeItem(LS_DM_KEY_EXPIRE); - - clear(); - return 'handled'; - } - - // user is sending a message to an existing thread id - either a thread - // or direct message thread - if (threadType === 'directMessageThread') { - sendDirectMessage({ - threadId: thread, - messageType: 'text', - threadType, - parentId: quotedMessage, - content: { - body: state, - }, - }) - .then(() => { - localStorage.removeItem(LS_DM_KEY); - localStorage.removeItem(LS_DM_KEY_EXPIRE); - }) - .catch(err => { - dispatch(addToastWithTimeout('error', err.message)); - }); - } else { - sendMessage({ - threadId: thread, - messageType: 'text', - threadType, - parentId: quotedMessage, - content: { - body: state, - }, - }) - .then(() => { - // if the user sends a message as a non member of the community or - // channel, we need to refetch the thread to update any join buttons - // and update all clientside caching of community + channel permissions - if (isSendingMessageAsNonMember) { - if (refetchThread) { - refetchThread(); - } - } - - localStorage.removeItem(LS_KEY); - localStorage.removeItem(LS_KEY_EXPIRE); - }) - .catch(err => { - dispatch(addToastWithTimeout('error', err.message)); - }); - } - - // refocus the input - setTimeout(() => { - clear(); - this.editor && this.editor.focus && this.editor.focus(); + if (text.length === 0) return; + // Add a new line on shift+enter, don't submit + const method = + props.threadType === 'story' + ? props.sendMessage + : props.sendDirectMessage; + method({ + threadId: props.thread, + messageType: 'text', + threadType: props.threadType, + // parentId: quotedMessage, + content: { + body: text, + }, + }).then(() => { + onChange({ target: { value: '' } }); }); - - return 'handled'; }; - handleReturn = e => { - // Always submit on CMD+Enter - if (KeyBindingUtil.hasCommandModifier(e)) { - return this.submit(e); - } - - // SHIFT+Enter should always add a new line - if (e.shiftKey) return 'not-handled'; - - return; - - const currentContent = this.props.state.getCurrentContent(); - const selection = this.props.state.getSelection(); - const key = selection.getStartKey(); - const blockMap = currentContent.getBlockMap(); - const block = blockMap.get(key); - - // If we're in a code block or starting one don't submit on enter - if ( - block.get('type') === 'code-block' || - block.get('text').indexOf('```') === 0 - ) { - return 'not-handled'; - } - - return this.submit(e); - }; - - removePreviewWrapper = () => { - this.setState({ - mediaPreview: '', - mediaPreviewFile: null, - }); - }; - - sendMediaMessage = (file: ?Blob) => { - if (file == null) { - return; - } - - this.removePreviewWrapper(); - - // eslint-disable-next-line - let reader = new FileReader(); - - const { - thread, - threadType, - createThread, - dispatch, - forceScrollToBottom, - sendDirectMessage, - sendMessage, - websocketConnection, - networkOnline, - quotedMessage, - } = this.props; - - if (!networkOnline) { - return dispatch( - addToastWithTimeout( - 'error', - 'Not connected to the internet - check your internet connection or try again' - ) - ); - } - - if ( - websocketConnection !== 'connected' && - websocketConnection !== 'reconnected' - ) { - return dispatch( - addToastWithTimeout( - 'error', - 'Error connecting to the server - hang tight while we try to reconnect' - ) - ); - } - - this.setState({ - isSendingMediaMessage: true, - }); - - reader.onloadend = () => { - if (forceScrollToBottom) { - forceScrollToBottom(); - } - - if (thread === 'newDirectMessageThread') { - return createThread({ - messageType: 'media', - file, - }); - } - - if (threadType === 'directMessageThread') { - sendDirectMessage({ - threadId: thread, - messageType: 'media', - threadType, - parentId: quotedMessage, - content: { - body: reader.result, - }, - file, - }) - .then(() => { - this.setState({ - isSendingMediaMessage: false, - }); - }) - .catch(err => { - this.setState({ - isSendingMediaMessage: false, - }); - dispatch(addToastWithTimeout('error', err.message)); - }); - } else { - sendMessage({ - threadId: thread, - messageType: 'media', - threadType, - parentId: quotedMessage, - content: { - body: reader.result, - }, - file, - }) - .then(() => { - this.setState({ - isSendingMediaMessage: false, - }); - }) - .catch(err => { - this.setState({ - isSendingMediaMessage: false, - }); - dispatch(addToastWithTimeout('error', err.message)); - }); - } - }; - - if (file) { - reader.readAsDataURL(file); - } - }; - - onFocus = () => { - /* - The new direct message thread component needs to know if the chat input is focused. That component passes down an onFocus prop, which should be called if it exists - */ - const { onFocus } = this.props; - if (onFocus) { - onFocus(); - } - - this.setState({ - isFocused: true, - }); - }; - - onBlur = () => { - /* - The new direct message thread component needs to know if the chat input is focused. That component passes down an onBlur prop, which should be called if it exists - */ - const { onBlur } = this.props; - if (onBlur) { - onBlur(); - } - - this.setState({ - isFocused: false, - }); - }; - - clearError = () => { - this.setState({ photoSizeError: '' }); - }; - - setMediaMessageError = (error: string) => { - return this.setState({ - photoSizeError: error, - }); - }; - - previewMedia = blob => { - if (this.state.isSendingMediaMessage) { - return; - } - this.setState({ - isSendingMediaMessage: true, - mediaPreviewFile: blob, - }); - const reader = new FileReader(); - reader.onload = () => - this.setState({ - mediaPreview: reader.result.toString(), - isSendingMediaMessage: false, - }); - - if (blob) { - reader.readAsDataURL(blob); - } - }; - - render() { - const { - state, - currentUser, - networkOnline, - websocketConnection, - quotedMessage, - thread, - } = this.props; - const { - isFocused, - photoSizeError, - isSendingMediaMessage, - mediaPreview, - markdownHint, - } = this.state; - const networkDisabled = - !networkOnline || - (websocketConnection !== 'connected' && - websocketConnection !== 'reconnected'); - - return ( - - - {photoSizeError && ( - -

{photoSizeError}

- this.clearError()} - glyph="view-close" - size={16} - color={'warn.default'} - /> -
- )} - - {currentUser && ( - - )} - - (this.editor = editor)} - /> - - - -
- - *bold* - _italic_ - `code` - ```codeblock``` - -
- ); - } -} + return ( + + + +
+ + + +
+
+ + **bold** + *italic* + `code` + ```codeblock``` + +
+ ); +}); const map = (state, ownProps) => ({ websocketConnection: state.connectionStatus.websocketConnection, networkOnline: state.connectionStatus.networkOnline, quotedMessage: state.message.quotedMessage[ownProps.thread] || null, }); + export default compose( withCurrentUser, sendMessage, sendDirectMessage, // $FlowIssue - connect(map), - withState('state', 'changeState', props => { - return returnText(props.threadType) || ''; - }), - withHandlers({ - onChange: ({ changeState }) => state => changeState(state), - clear: ({ changeState }) => () => changeState(''), - }) + connect(map) )(ChatInput); diff --git a/src/components/chatInput/input.js b/src/components/chatInput/input.js deleted file mode 100644 index f9433088e5..0000000000 --- a/src/components/chatInput/input.js +++ /dev/null @@ -1,124 +0,0 @@ -// @flow -import React from 'react'; -import DraftEditor from '../draft-js-plugins-editor'; -import createLinkifyPlugin from 'draft-js-linkify-plugin'; -import createCodeEditorPlugin from 'draft-js-code-editor-plugin'; -import createMarkdownPlugin from 'draft-js-markdown-plugin'; -import Prism from 'prismjs'; -import 'prismjs/components/prism-java'; -import 'prismjs/components/prism-scala'; -import 'prismjs/components/prism-go'; -import 'prismjs/components/prism-sql'; -import 'prismjs/components/prism-bash'; -import 'prismjs/components/prism-c'; -import 'prismjs/components/prism-cpp'; -import 'prismjs/components/prism-kotlin'; -import 'prismjs/components/prism-perl'; -import 'prismjs/components/prism-ruby'; -import 'prismjs/components/prism-swift'; -import createPrismPlugin from 'draft-js-prism-plugin'; -import { customStyleMap } from 'src/components/rich-text-editor/style'; -import type { DraftEditorState } from 'draft-js/lib/EditorState'; - -import { InputWrapper } from './style'; - -type Props = { - editorState: DraftEditorState, - onChange: DraftEditorState => void, - placeholder: string, - className?: string, - focus?: boolean, - readOnly?: boolean, - editorRef?: any => void, - networkDisabled: boolean, - children?: React$Node, - hasAttachment?: boolean, - code?: boolean, -}; - -type State = { - plugins: Array, -}; - -/* - * NOTE(@mxstbr): DraftJS has huge troubles on Android, it's basically unusable - * We work around this by replacing the DraftJS editor with a plain text Input - * on Android, and then converting the plain text to DraftJS content State - * debounced every couple ms - */ -class Input extends React.Component { - editor: any; - - constructor(props: Props) { - super(props); - - this.state = { - plugins: [ - createPrismPlugin({ - prism: Prism, - }), - createMarkdownPlugin({ - features: { - inline: ['BOLD', 'ITALIC', 'CODE'], - block: ['CODE', 'blockquote'], - }, - renderLanguageSelect: () => null, - }), - createCodeEditorPlugin(), - createLinkifyPlugin({ - target: '_blank', - }), - ], - }; - } - - setRef = (editor: any) => { - const { editorRef } = this.props; - this.editor = editor; - if (editorRef && typeof editorRef === 'function') editorRef(editor); - }; - - render() { - const { - editorState, - onChange, - focus, - placeholder, - readOnly, - editorRef, - networkDisabled, - children, - hasAttachment, - code, - ...rest - } = this.props; - const { plugins } = this.state; - - return ( - - {children} - - - ); - } -} - -export default Input; diff --git a/src/components/chatInput/style.js b/src/components/chatInput/style.js index 9cfa5c1f28..032389cfb3 100644 --- a/src/components/chatInput/style.js +++ b/src/components/chatInput/style.js @@ -1,6 +1,7 @@ // @flow import theme from 'shared/theme'; import styled, { css } from 'styled-components'; +import Textarea from 'react-textarea-autosize'; import { IconButton } from '../buttons'; import { QuoteWrapper } from '../message/style'; import { @@ -57,7 +58,15 @@ export const Form = styled.form` position: relative; `; -export const InputWrapper = styled(EditorWrapper)` +export const Input = styled(Textarea).attrs({ + spellCheck: true, + autoCapitalize: 'sentences', + autoComplete: 'on', + autoCorrect: 'on', + async: true, + rows: 1, + maxRows: 5, +})` display: flex; flex-direction: column; align-items: stretch; @@ -116,7 +125,8 @@ export const InputWrapper = styled(EditorWrapper)` : props.theme.text.placeholder}; } - &:hover { + &:hover, + &:focus { border-color: ${props => props.networkDisabled ? props.theme.special.default @@ -281,8 +291,8 @@ export const Preformatted = styled.code` export const MarkdownHint = styled.div` display: flex; flex: 0 0 auto; - justify-content: flex-end; - margin-right: 12px; + justify-content: flex-start; + margin-left: 16px; font-size: 11px; color: ${theme.text.alt}; line-height: 1; From ed08f6b7c53735428d809eea3553a9684fa79e17 Mon Sep 17 00:00:00 2001 From: Max Stoiber Date: Fri, 14 Dec 2018 11:35:31 +0100 Subject: [PATCH 04/45] Add support for sending DM thread messages and creating new DM threads --- src/components/chatInput/index.js | 85 ++++++++++++++++++++++++++++--- 1 file changed, 77 insertions(+), 8 deletions(-) diff --git a/src/components/chatInput/index.js b/src/components/chatInput/index.js index 330d20e050..70fa83a8b7 100644 --- a/src/components/chatInput/index.js +++ b/src/components/chatInput/index.js @@ -79,12 +79,24 @@ const ChatInput = React.forwardRef((props: Props, ref) => { const [text, changeText] = React.useState(''); const [showMarkdownHint, setShowMarkdownHint] = React.useState(false); + const removeAttachments = () => { + // Remove media preview and quoted message + }; + const handleKeyPress = e => { - // Shift+Enter - if (e.key !== 'Enter') return; - if (!e.shiftKey) { - e.preventDefault(); - submit(); + switch (e.key) { + // Submit on Enter unless Shift is pressed + case 'Enter': { + if (e.shiftKey) return; + e.preventDefault(); + submit(); + return; + } + // If backspace is pressed on the empty + case 'Backspace': { + if (text.length === 0) removeAttachments(); + return; + } } }; @@ -98,7 +110,52 @@ const ChatInput = React.forwardRef((props: Props, ref) => { const submit = e => { if (e) e.preventDefault(); + + if (!props.networkOnline) { + return props.dispatch( + addToastWithTimeout( + 'error', + 'Not connected to the internet - check your internet connection or try again' + ) + ); + } + + if ( + props.websocketConnection !== 'connected' && + props.websocketConnection !== 'reconnected' + ) { + return props.dispatch( + addToastWithTimeout( + 'error', + 'Error connecting to the server - hang tight while we try to reconnect' + ) + ); + } + + if (!props.currentUser) { + // user is trying to send a message without being signed in + return props.dispatch(openModal('CHAT_INPUT_LOGIN_MODAL', {})); + } + + // If a user sends a message, force a scroll to bottom. This doesn't exist if this is a new DM thread + if (props.forceScrollToBottom) props.forceScrollToBottom(); + if (text.length === 0) return; + + // user is creating a new directMessageThread, break the chain + // and initiate a new group creation with the message being sent + // in views/directMessages/containers/newThread.js + if (props.thread === 'newDirectMessageThread') { + props.createThread({ + messageBody: text, + messageType: 'text', + }); + return; + } + + // Clear the chat input now that we're sending a message for sure + onChange({ target: { value: '' } }); + // Add a new line on shift+enter, don't submit const method = props.threadType === 'story' @@ -112,9 +169,21 @@ const ChatInput = React.forwardRef((props: Props, ref) => { content: { body: text, }, - }).then(() => { - onChange({ target: { value: '' } }); - }); + }) + .then(() => { + // If we're viewing a thread and the user sends a message as a non-member, we need to refetch the thread data + if ( + props.threadType === 'story' && + props.threadData && + !props.threadData.channel.channelPermissions.isMember && + props.refetchThread + ) { + return props.refetchThread(); + } + }) + .catch(err => { + props.dispatch(addToastWithTimeout('error', err.message)); + }); }; return ( From 26c7124b6c072a4e0c5e074ca8600fe941a5992b Mon Sep 17 00:00:00 2001 From: Max Stoiber Date: Fri, 14 Dec 2018 13:09:24 +0100 Subject: [PATCH 05/45] Quoted message support --- .../chatInput/components/mediaUploader.js | 1 - src/components/chatInput/index.js | 97 +++++++++++++++++-- src/components/chatInput/style.js | 66 +++++++------ 3 files changed, 122 insertions(+), 42 deletions(-) diff --git a/src/components/chatInput/components/mediaUploader.js b/src/components/chatInput/components/mediaUploader.js index 0f3e997a1f..570b992ce2 100644 --- a/src/components/chatInput/components/mediaUploader.js +++ b/src/components/chatInput/components/mediaUploader.js @@ -13,7 +13,6 @@ type Props = { onError: Function, currentUser: ?Object, isSendingMediaMessage: boolean, - inputFocused: boolean, }; class MediaUploader extends React.Component { diff --git a/src/components/chatInput/index.js b/src/components/chatInput/index.js index 70fa83a8b7..664e52b9c4 100644 --- a/src/components/chatInput/index.js +++ b/src/components/chatInput/index.js @@ -15,6 +15,7 @@ import { ChatInputContainer, ChatInputWrapper, Input, + InputWrapper, SendButton, PhotoSizeError, MarkdownHint, @@ -78,9 +79,11 @@ type Props = { const ChatInput = React.forwardRef((props: Props, ref) => { const [text, changeText] = React.useState(''); const [showMarkdownHint, setShowMarkdownHint] = React.useState(false); + const [photoSizeError, setPhotoSizeError] = React.useState(''); const removeAttachments = () => { - // Remove media preview and quoted message + removeQuotedMessage(); + setMediaPreview(null); }; const handleKeyPress = e => { @@ -165,7 +168,7 @@ const ChatInput = React.forwardRef((props: Props, ref) => { threadId: props.thread, messageType: 'text', threadType: props.threadType, - // parentId: quotedMessage, + parentId: props.quotedMessage, content: { body: text, }, @@ -186,18 +189,94 @@ const ChatInput = React.forwardRef((props: Props, ref) => { }); }; + const [isSendingMediaMessage, setIsSendingMediaMessage] = React.useState( + false + ); + const [mediaPreview, setMediaPreview] = React.useState(null); + + const previewMedia = blob => { + if (isSendingMediaMessage) return; + setIsSendingMediaMessage(true); + setMediaPreview(blob); + + const reader = new FileReader(); + reader.onload = () => { + setMediaPreview(reader.result.toString()); + setIsSendingMediaMessage(false); + }; + + if (blob) { + reader.readAsDataURL(blob); + } + }; + + const removeQuotedMessage = () => { + if (props.quotedMessage) + props.dispatch( + replyToMessage({ threadId: props.thread, messageId: null }) + ); + }; + return ( + {photoSizeError && ( + +

{photoSizeError}

+ setPhotoSizeError('')} + glyph="view-close" + size={16} + color={'warn.default'} + /> +
+ )} -
- setPhotoSizeError(err)} /> + )} + + + {mediaPreview && ( + + + setMediaPreview(null)}> + + + + )} + {props.quotedMessage && ( + + + + + + + )} + + (props.hasAttachment ? '16px' : '8px 16px')}; transition: padding 0.2s ease-in-out; + min-height: 40px; + max-width: calc(100% - 32px); border-radius: 24px; border: 1px solid ${props => @@ -94,10 +83,35 @@ export const Input = styled(Textarea).attrs({ ? hexa(props.theme.special.default, 0.1) : props.theme.bg.default}; + &:hover, + &:focus { + border-color: ${props => + props.networkDisabled + ? props.theme.special.default + : props.theme.text.alt}; + transition: border-color 0.2s ease-in; + } + @media (max-width: 768px) { - font-size: 16px; padding-left: 16px; - ${/* width: calc(100% - 72px); */ ''}; + } +`; + +export const Input = styled(Textarea).attrs({ + spellCheck: true, + autoCapitalize: 'sentences', + autoComplete: 'on', + autoCorrect: 'on', + async: true, + rows: 1, + maxRows: 5, +})` + font-size: 15px; + font-weight: 500; + line-height: 20px; + + @media (max-width: 768px) { + font-size: 16px; } &::placeholder { @@ -125,15 +139,6 @@ export const Input = styled(Textarea).attrs({ : props.theme.text.placeholder}; } - &:hover, - &:focus { - border-color: ${props => - props.networkDisabled - ? props.theme.special.default - : props.theme.text.alt}; - transition: border-color 0.2s ease-in; - } - pre { ${monoStack}; font-size: 15px; @@ -155,13 +160,10 @@ export const Input = styled(Textarea).attrs({ ${props => props.hasAttachment && css` - > div:not(:first-of-type) { - margin-top: 16px; - } - - > div:last-of-type { + margin-top: 16px; + ${'' /* > div:last-of-type { margin-right: 32px; - } + } */}; `}; `; @@ -292,7 +294,7 @@ export const MarkdownHint = styled.div` display: flex; flex: 0 0 auto; justify-content: flex-start; - margin-left: 16px; + margin-left: 56px; font-size: 11px; color: ${theme.text.alt}; line-height: 1; From 160e8132960ef8efe12fed8da8495ad408ea9798 Mon Sep 17 00:00:00 2001 From: Max Stoiber Date: Fri, 14 Dec 2018 13:30:31 +0100 Subject: [PATCH 06/45] Make media messages work --- src/components/chatInput/index.js | 79 +++++++++++++++++++++---------- 1 file changed, 53 insertions(+), 26 deletions(-) diff --git a/src/components/chatInput/index.js b/src/components/chatInput/index.js index 664e52b9c4..a776b05fd7 100644 --- a/src/components/chatInput/index.js +++ b/src/components/chatInput/index.js @@ -111,6 +111,40 @@ const ChatInput = React.forwardRef((props: Props, ref) => { changeText(text); }; + const sendMessage = ({ file, body }) => { + // user is creating a new directMessageThread, break the chain + // and initiate a new group creation with the message being sent + // in views/directMessages/containers/newThread.js + if (props.thread === 'newDirectMessageThread') { + if (file) { + return props.createThread({ + messageType: 'media', + file, + }); + } else { + return props.createThread({ + messageBody: body, + messageType: 'text', + }); + } + } + + const method = + props.threadType === 'story' + ? props.sendMessage + : props.sendDirectMessage; + return method({ + threadId: props.thread, + messageType: file ? 'media' : 'text', + threadType: props.threadType, + parentId: props.quotedMessage, + content: { + body, + }, + file, + }); + }; + const submit = e => { if (e) e.preventDefault(); @@ -143,36 +177,28 @@ const ChatInput = React.forwardRef((props: Props, ref) => { // If a user sends a message, force a scroll to bottom. This doesn't exist if this is a new DM thread if (props.forceScrollToBottom) props.forceScrollToBottom(); - if (text.length === 0) return; - - // user is creating a new directMessageThread, break the chain - // and initiate a new group creation with the message being sent - // in views/directMessages/containers/newThread.js - if (props.thread === 'newDirectMessageThread') { - props.createThread({ - messageBody: text, - messageType: 'text', - }); - return; + if (mediaPreviewFile) { + setIsSendingMediaMessage(true); + let reader = new FileReader(); + reader.onloadend = () => { + if (props.forceScrollToBottom) props.forceScrollToBottom(); + sendMessage({ file: mediaPreviewFile, body: reader.result }) + .then(() => setIsSendingMediaMessage(false)) + .catch(err => { + setIsSendingMediaMessage(false); + props.dispatch(addToastWithTimeout('error', err.message)); + }); + }; + reader.readAsDataURL(mediaPreviewFile); } + if (text.length === 0) return; + // Clear the chat input now that we're sending a message for sure onChange({ target: { value: '' } }); + removeQuotedMessage(); - // Add a new line on shift+enter, don't submit - const method = - props.threadType === 'story' - ? props.sendMessage - : props.sendDirectMessage; - method({ - threadId: props.thread, - messageType: 'text', - threadType: props.threadType, - parentId: props.quotedMessage, - content: { - body: text, - }, - }) + sendMessage({ body: text }) .then(() => { // If we're viewing a thread and the user sends a message as a non-member, we need to refetch the thread data if ( @@ -193,11 +219,12 @@ const ChatInput = React.forwardRef((props: Props, ref) => { false ); const [mediaPreview, setMediaPreview] = React.useState(null); + const [mediaPreviewFile, setMediaPreviewFile] = React.useState(null); const previewMedia = blob => { if (isSendingMediaMessage) return; setIsSendingMediaMessage(true); - setMediaPreview(blob); + setMediaPreviewFile(blob); const reader = new FileReader(); reader.onload = () => { From 1eff87e4d669aaf1f081ef814005b98082d5a99b Mon Sep 17 00:00:00 2001 From: Max Stoiber Date: Mon, 17 Dec 2018 08:55:49 +0100 Subject: [PATCH 07/45] "Blur" optimistic responses" --- package.json | 2 +- src/components/message/index.js | 40 +++++++++++++++++---------------- yarn.lock | 11 ++++++++- 3 files changed, 32 insertions(+), 21 deletions(-) diff --git a/package.json b/package.json index 0e20ad409f..a4b9dee1ce 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "babel-jest": "^22.4.4", "babel-plugin-import-inspector": "^2.0.0", "babel-plugin-inline-import-graphql-ast": "^2.0.4", - "babel-plugin-styled-components": "^1.8.0", + "babel-plugin-styled-components": "^1.9.4", "babel-plugin-syntax-async-generators": "^6.13.0", "babel-plugin-syntax-dynamic-import": "^6.18.0", "babel-plugin-transform-async-generator-functions": "^6.24.1", diff --git a/src/components/message/index.js b/src/components/message/index.js index a962e4b689..6230191fba 100644 --- a/src/components/message/index.js +++ b/src/components/message/index.js @@ -164,12 +164,13 @@ class Message extends React.Component { const canEditMessage = me && message.messageType !== 'media'; const selectedMessageId = btoa(new Date(message.timestamp).getTime() - 1); + const isOptimistic = typeof message.id === 'number'; const messageUrl = threadType === 'story' && thread ? `/${getThreadLink(thread)}?m=${selectedMessageId}` : threadType === 'directMessageThread' - ? `/messages/${threadId}?m=${selectedMessageId}` - : `/thread/${threadId}?m=${selectedMessageId}`; + ? `/messages/${threadId}?m=${selectedMessageId}` + : `/thread/${threadId}?m=${selectedMessageId}`; return ( @@ -225,11 +226,13 @@ class Message extends React.Component { )} {!isEditing ? ( - this.toggleOpenGallery(e, message.id)} - message={message} - /> +
+ this.toggleOpenGallery(e, message.id)} + message={message} + /> +
) : ( { /> )} - {message.modifiedAt && - !isEditing && ( - - Edited - - )} + {message.modifiedAt && !isEditing && ( + + Edited + + )} {message.reactions.count > 0 && ( Date: Mon, 17 Dec 2018 09:18:04 +0100 Subject: [PATCH 08/45] Revert ""Blur" optimistic responses"" This reverts commit 1eff87e4d669aaf1f081ef814005b98082d5a99b. --- package.json | 2 +- src/components/message/index.js | 40 ++++++++++++++++----------------- yarn.lock | 11 +-------- 3 files changed, 21 insertions(+), 32 deletions(-) diff --git a/package.json b/package.json index a4b9dee1ce..0e20ad409f 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "babel-jest": "^22.4.4", "babel-plugin-import-inspector": "^2.0.0", "babel-plugin-inline-import-graphql-ast": "^2.0.4", - "babel-plugin-styled-components": "^1.9.4", + "babel-plugin-styled-components": "^1.8.0", "babel-plugin-syntax-async-generators": "^6.13.0", "babel-plugin-syntax-dynamic-import": "^6.18.0", "babel-plugin-transform-async-generator-functions": "^6.24.1", diff --git a/src/components/message/index.js b/src/components/message/index.js index 6230191fba..a962e4b689 100644 --- a/src/components/message/index.js +++ b/src/components/message/index.js @@ -164,13 +164,12 @@ class Message extends React.Component { const canEditMessage = me && message.messageType !== 'media'; const selectedMessageId = btoa(new Date(message.timestamp).getTime() - 1); - const isOptimistic = typeof message.id === 'number'; const messageUrl = threadType === 'story' && thread ? `/${getThreadLink(thread)}?m=${selectedMessageId}` : threadType === 'directMessageThread' - ? `/messages/${threadId}?m=${selectedMessageId}` - : `/thread/${threadId}?m=${selectedMessageId}`; + ? `/messages/${threadId}?m=${selectedMessageId}` + : `/thread/${threadId}?m=${selectedMessageId}`; return ( @@ -226,13 +225,11 @@ class Message extends React.Component { )} {!isEditing ? ( -
- this.toggleOpenGallery(e, message.id)} - message={message} - /> -
+ this.toggleOpenGallery(e, message.id)} + message={message} + /> ) : ( { /> )} - {message.modifiedAt && !isEditing && ( - - Edited - - )} + {message.modifiedAt && + !isEditing && ( + + Edited + + )} {message.reactions.count > 0 && ( Date: Mon, 17 Dec 2018 09:40:16 +0100 Subject: [PATCH 09/45] Parse markdown to draftjs in optimistic responses --- shared/graphql/mutations/message/sendMessage.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/shared/graphql/mutations/message/sendMessage.js b/shared/graphql/mutations/message/sendMessage.js index f31d8ae77b..655eb139df 100644 --- a/shared/graphql/mutations/message/sendMessage.js +++ b/shared/graphql/mutations/message/sendMessage.js @@ -2,6 +2,8 @@ import gql from 'graphql-tag'; import { graphql } from 'react-apollo'; import { btoa } from 'b2a'; +import { stateFromMarkdown } from 'draft-js-import-markdown'; +import { convertToRaw } from 'draft-js'; import messageInfoFragment from '../../fragments/message/messageInfo'; import type { MessageInfoType } from '../../fragments/message/messageInfo'; import { getThreadMessageConnectionQuery } from '../../queries/thread/getThreadMessageConnection'; @@ -41,7 +43,7 @@ const sendMessageOptions = { addMessage: { id: fakeId, timestamp: JSON.parse(JSON.stringify(new Date())), - messageType: message.messageType, + messageType: message.messageType === 'media' ? 'media' : 'draftjs', modifiedAt: '', author: { user: { @@ -65,6 +67,12 @@ const sendMessageOptions = { : null, content: { ...message.content, + body: + message.messageType === 'media' + ? message.content.body + : JSON.stringify( + convertToRaw(stateFromMarkdown(message.content.body)) + ), __typename: 'MessageContent', }, reactions: { From 2e9d4614babbd92f969e0a36dff1ea711677dc80 Mon Sep 17 00:00:00 2001 From: Max Stoiber Date: Tue, 18 Dec 2018 11:11:17 +0100 Subject: [PATCH 10/45] Add localStorage persistance of latest chatInput content --- src/components/chatInput/index.js | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/components/chatInput/index.js b/src/components/chatInput/index.js index a776b05fd7..79c19acc25 100644 --- a/src/components/chatInput/index.js +++ b/src/components/chatInput/index.js @@ -77,10 +77,27 @@ type Props = { }; const ChatInput = React.forwardRef((props: Props, ref) => { + const cacheKey = `last-content-${props.thread}`; const [text, changeText] = React.useState(''); - const [showMarkdownHint, setShowMarkdownHint] = React.useState(false); const [photoSizeError, setPhotoSizeError] = React.useState(''); + // On mount, set the text state to the cached value if one exists + React.useEffect( + () => { + changeText(localStorage.getItem(cacheKey) || ''); + // NOTE(@mxstbr): We ONLY want to run this if we switch between threads, never else! + }, + [props.thread] + ); + + // Cache the latest text everytime it changes + React.useEffect( + () => { + localStorage.setItem(cacheKey, text); + }, + [text] + ); + const removeAttachments = () => { removeQuotedMessage(); setMediaPreview(null); @@ -105,13 +122,10 @@ const ChatInput = React.forwardRef((props: Props, ref) => { const onChange = e => { const text = e.target.value; - const newShowMarkdownHint = text.length > 0; - if (newShowMarkdownHint !== showMarkdownHint) - setShowMarkdownHint(newShowMarkdownHint); changeText(text); }; - const sendMessage = ({ file, body }) => { + const sendMessage = ({ file, body }: { file?: any, body?: string }) => { // user is creating a new directMessageThread, break the chain // and initiate a new group creation with the message being sent // in views/directMessages/containers/newThread.js @@ -313,7 +327,7 @@ const ChatInput = React.forwardRef((props: Props, ref) => {
- + 0} data-cy="markdownHint"> **bold** *italic* `code` From 603434fe0efa207d09343ab3b09bc56c9fb0b38b Mon Sep 17 00:00:00 2001 From: Max Stoiber Date: Tue, 18 Dec 2018 11:26:42 +0100 Subject: [PATCH 11/45] Fix dm thread creation --- src/components/chatInput/index.js | 16 +++++----------- src/views/directMessages/containers/newThread.js | 7 ++++--- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/src/components/chatInput/index.js b/src/components/chatInput/index.js index 79c19acc25..e2373ead20 100644 --- a/src/components/chatInput/index.js +++ b/src/components/chatInput/index.js @@ -130,17 +130,11 @@ const ChatInput = React.forwardRef((props: Props, ref) => { // and initiate a new group creation with the message being sent // in views/directMessages/containers/newThread.js if (props.thread === 'newDirectMessageThread') { - if (file) { - return props.createThread({ - messageType: 'media', - file, - }); - } else { - return props.createThread({ - messageBody: body, - messageType: 'text', - }); - } + return props.createThread({ + messageType: file ? 'media' : 'text', + file, + messageBody: body, + }); } const method = diff --git a/src/views/directMessages/containers/newThread.js b/src/views/directMessages/containers/newThread.js index c2db7b6fd3..2694abe4ce 100644 --- a/src/views/directMessages/containers/newThread.js +++ b/src/views/directMessages/containers/newThread.js @@ -624,12 +624,13 @@ class NewThread extends React.Component { // if no users have been selected, break out of this function and throw // an error if (selectedUsersForNewThread.length === 0) { - return this.props.dispatch( + this.props.dispatch( addToastWithTimeout( 'error', 'Choose some people to send this message to first!' ) ); + return Promise.reject(); } const input = { @@ -645,13 +646,13 @@ class NewThread extends React.Component { }; if (threadIsBeingCreated) { - return; + return Promise.resolve(); } else { this.setState({ threadIsBeingCreated: true, }); - this.props + return this.props .createDirectMessageThread(input) .then(({ data: { createDirectMessageThread } }) => { if (!createDirectMessageThread) { From 6be1f9385aa9799b4029aaf8d90c5219a55faabd Mon Sep 17 00:00:00 2001 From: Max Stoiber Date: Tue, 18 Dec 2018 11:32:13 +0100 Subject: [PATCH 12/45] Switch to same markdown draftjs conversion library on backend --- api/mutations/message/addMessage.js | 4 +-- api/mutations/thread/publishThread.js | 4 +-- api/package.json | 1 - package.json | 1 - yarn.lock | 38 --------------------------- 5 files changed, 4 insertions(+), 44 deletions(-) diff --git a/api/mutations/message/addMessage.js b/api/mutations/message/addMessage.js index 0e3db80d39..61380167d6 100644 --- a/api/mutations/message/addMessage.js +++ b/api/mutations/message/addMessage.js @@ -1,5 +1,5 @@ // @flow -import { markdownToDraft } from 'markdown-draft-js'; +import { stateFromMarkdown } from 'draft-js-import-markdown'; import type { GraphQLContext } from '../../'; import UserError from '../../utils/UserError'; import { uploadImage } from '../../utils/file-storage'; @@ -87,7 +87,7 @@ export default requireAuth(async (_: any, args: Input, ctx: GraphQLContext) => { if (message.messageType === 'text') { message.content.body = JSON.stringify( - markdownToDraft(message.content.body) + stateFromMarkdown(message.content.body) ); message.messageType = 'draftjs'; } diff --git a/api/mutations/thread/publishThread.js b/api/mutations/thread/publishThread.js index e505995edc..934b1b9407 100644 --- a/api/mutations/thread/publishThread.js +++ b/api/mutations/thread/publishThread.js @@ -1,7 +1,7 @@ // @flow const debug = require('debug')('api:mutations:thread:publish-thread'); import stringSimilarity from 'string-similarity'; -import { markdownToDraft } from 'markdown-draft-js'; +import { markdownToState } from 'draft-js-import-markdown'; import type { GraphQLContext } from '../../'; import UserError from '../../utils/UserError'; import { uploadImage } from '../../utils/file-storage'; @@ -71,7 +71,7 @@ export default requireAuth( type = 'DRAFTJS'; if (thread.content.body) { thread.content.body = JSON.stringify( - markdownToDraft(thread.content.body) + markdownToState(thread.content.body) ); } } diff --git a/api/package.json b/api/package.json index dccbf0487b..d6e5d13cac 100644 --- a/api/package.json +++ b/api/package.json @@ -73,7 +73,6 @@ "lodash": "^4.17.11", "lodash.intersection": "^4.4.0", "longjohn": "^0.2.12", - "markdown-draft-js": "^0.6.3", "moment": "^2.22.2", "node-env-file": "^0.1.8", "node-localstorage": "^1.3.1", diff --git a/package.json b/package.json index 0e20ad409f..a9ced18317 100644 --- a/package.json +++ b/package.json @@ -130,7 +130,6 @@ "linkify-it": "^2.0.3", "lodash": "^4.17.11", "lodash.intersection": "^4.4.0", - "markdown-draft-js": "^0.6.3", "moment": "^2.22.2", "node-env-file": "^0.1.8", "now-env": "^3.1.0", diff --git a/yarn.lock b/yarn.lock index 4e9eb9eb51..065d378cef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1717,14 +1717,6 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" -argparse@~0.1.15: - version "0.1.16" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-0.1.16.tgz#cfd01e0fbba3d6caed049fbd758d40f65196f57c" - integrity sha1-z9AeD7uj1srtBJ+9dY1A9lGW9Xw= - dependencies: - underscore "~1.7.0" - underscore.string "~2.4.0" - aria-query@^0.7.0: version "0.7.1" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-0.7.1.tgz#26cbb5aff64144b0a825be1846e0b16cfa00b11e" @@ -1971,11 +1963,6 @@ atob@^2.1.1: resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== -autolinker@~0.15.0: - version "0.15.3" - resolved "https://registry.yarnpkg.com/autolinker/-/autolinker-0.15.3.tgz#342417d8f2f3461b14cf09088d5edf8791dc9832" - integrity sha1-NCQX2PLzRhsUzwkIjV7fh5HcmDI= - autoprefixer@7.1.6: version "7.1.6" resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-7.1.6.tgz#fb933039f74af74a83e71225ce78d9fd58ba84d7" @@ -9655,13 +9642,6 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" -markdown-draft-js@^0.6.3: - version "0.6.3" - resolved "https://registry.yarnpkg.com/markdown-draft-js/-/markdown-draft-js-0.6.3.tgz#5847cd3d07d7b7e3d4d87c5d7e5928c3a71bddd4" - integrity sha512-8kn53iDi9M+0jOeF5dXc2vy8tCkrcD/QlltOz9GErenWrJD+VJ5ZFRjjmPVk7TnE8f5R0DiGCDL/w8M2Lj7xQw== - dependencies: - remarkable "1.7.1" - math-expression-evaluator@^1.2.14: version "1.2.17" resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz#de819fdbcd84dccd8fae59c6aeb79615b9d266ac" @@ -12456,14 +12436,6 @@ relateurl@0.2.x: resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" integrity sha1-VNvzd+UUQKypCkzSdGANP/LYiKk= -remarkable@1.7.1: - version "1.7.1" - resolved "https://registry.yarnpkg.com/remarkable/-/remarkable-1.7.1.tgz#aaca4972100b66a642a63a1021ca4bac1be3bff6" - integrity sha1-qspJchALZqZCpjoQIcpLrBvjv/Y= - dependencies: - argparse "~0.1.15" - autolinker "~0.15.0" - remove-trailing-separator@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" @@ -14239,16 +14211,6 @@ undefsafe@^2.0.2: dependencies: debug "^2.2.0" -underscore.string@~2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/underscore.string/-/underscore.string-2.4.0.tgz#8cdd8fbac4e2d2ea1e7e2e8097c42f442280f85b" - integrity sha1-jN2PusTi0uoefi6Al8QvRCKA+Fs= - -underscore@~1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.7.0.tgz#6bbaf0877500d36be34ecaa584e0db9fef035209" - integrity sha1-a7rwh3UA02vjTsqlhODbn+8DUgk= - 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" From 2d2b676532497a76b8a7ee5fea716a17bc3d7422 Mon Sep 17 00:00:00 2001 From: Max Stoiber Date: Tue, 18 Dec 2018 13:12:33 +0100 Subject: [PATCH 13/45] Add missing dep to api --- api/package.json | 1 + api/yarn.lock | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/api/package.json b/api/package.json index a22437f820..ccae577748 100644 --- a/api/package.json +++ b/api/package.json @@ -34,6 +34,7 @@ "draft-js-embed-plugin": "^1.2.0", "draft-js-focus-plugin": "2.0.0-rc2", "draft-js-image-plugin": "2.0.0-rc8", + "draft-js-import-markdown": "^1.2.1", "draft-js-linkify-plugin": "^2.0.0-beta1", "draft-js-markdown-plugin": "^1.4.4", "draft-js-plugins-editor": "^2.1.1", diff --git a/api/yarn.lock b/api/yarn.lock index cfdcf58c3c..4131576285 100644 --- a/api/yarn.lock +++ b/api/yarn.lock @@ -3464,6 +3464,22 @@ draft-js-image-plugin@2.0.0-rc8: prop-types "^15.5.8" union-class-names "^1.0.0" +draft-js-import-element@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/draft-js-import-element/-/draft-js-import-element-1.2.1.tgz#9a6a56d74690d48d35d8d089564e6d710b4926eb" + integrity sha512-T/eCDkaU8wrTCH6c+/2BE7Vx/11GABRNU/UBiHM4D903LNFar8UfjElehpiKVf+F4rxi8dfhvTgaWrpWDfX4MA== + dependencies: + draft-js-utils "^1.2.0" + synthetic-dom "^1.2.0" + +draft-js-import-markdown@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/draft-js-import-markdown/-/draft-js-import-markdown-1.2.1.tgz#ec18eb15008bab13d9878d65db50e181dd64a5ce" + integrity sha512-hn2+LJ/QdmuM2gc44uqYWjMqRv5Pt6bZFHULi8hGAR7WVWaVk4gaXHM4fE8H4iVE+QFUV6yNITrm4D/fyPkaGQ== + dependencies: + draft-js-import-element "^1.2.1" + synthetic-dom "^1.2.0" + draft-js-linkify-plugin@^2.0.0-beta1: version "2.0.1" resolved "https://registry.yarnpkg.com/draft-js-linkify-plugin/-/draft-js-linkify-plugin-2.0.1.tgz#28978b53640ce64c639cd2821a54c24de9f79c3f" @@ -3539,6 +3555,11 @@ draft-js-prism@ngs/draft-js-prism#6edb31c3805dd1de3fb897cc27fced6bac1bafbb: immutable "*" prismjs "^1.5.0" +draft-js-utils@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/draft-js-utils/-/draft-js-utils-1.2.0.tgz#f5cb23eb167325ffed3d79882fdc317721d2fd12" + integrity sha1-9csj6xZzJf/tPXmIL9wxdyHS/RI= + draft-js@0.x, draft-js@^0.10.4, draft-js@^0.10.5, draft-js@~0.10.0: version "0.10.5" resolved "https://registry.yarnpkg.com/draft-js/-/draft-js-0.10.5.tgz#bfa9beb018fe0533dbb08d6675c371a6b08fa742" @@ -8956,6 +8977,11 @@ symbol-tree@^3.2.1: resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.2.tgz#ae27db38f660a7ae2e1c3b7d1bc290819b8519e6" integrity sha1-rifbOPZgp64uHDt9G8KQgZuFGeY= +synthetic-dom@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/synthetic-dom/-/synthetic-dom-1.2.0.tgz#f3589aafe2b5e299f337bb32973a9be42dd5625e" + integrity sha1-81iar+K14pnzN7sylzqb5C3VYl4= + table@^4.0.2: version "4.0.3" resolved "https://registry.yarnpkg.com/table/-/table-4.0.3.tgz#00b5e2b602f1794b9acaf9ca908a76386a7813bc" From 9e3e8ca9686814d57ab709e7d613baf0cb953226 Mon Sep 17 00:00:00 2001 From: Brian Lovin Date: Tue, 18 Dec 2018 10:01:31 -0800 Subject: [PATCH 14/45] Silence flow errors on new react features --- src/components/chatInput/index.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/components/chatInput/index.js b/src/components/chatInput/index.js index e2373ead20..b33686a63d 100644 --- a/src/components/chatInput/index.js +++ b/src/components/chatInput/index.js @@ -76,12 +76,16 @@ type Props = { quotedMessage: ?{ messageId: string, threadId: string }, }; +// $FlowFixMe const ChatInput = React.forwardRef((props: Props, ref) => { const cacheKey = `last-content-${props.thread}`; + // $FlowFixMe const [text, changeText] = React.useState(''); + // $FlowFixMe const [photoSizeError, setPhotoSizeError] = React.useState(''); // On mount, set the text state to the cached value if one exists + // $FlowFixMe React.useEffect( () => { changeText(localStorage.getItem(cacheKey) || ''); @@ -91,6 +95,7 @@ const ChatInput = React.forwardRef((props: Props, ref) => { ); // Cache the latest text everytime it changes + // $FlowFixMe React.useEffect( () => { localStorage.setItem(cacheKey, text); @@ -223,10 +228,13 @@ const ChatInput = React.forwardRef((props: Props, ref) => { }); }; + // $FlowFixMe const [isSendingMediaMessage, setIsSendingMediaMessage] = React.useState( false ); + // $FlowFixMe const [mediaPreview, setMediaPreview] = React.useState(null); + // $FlowFixMe const [mediaPreviewFile, setMediaPreviewFile] = React.useState(null); const previewMedia = blob => { From 4711c0e7f2d6f972f69a9d47924d60ccf5652ed1 Mon Sep 17 00:00:00 2001 From: Max Stoiber Date: Wed, 19 Dec 2018 10:41:41 +0100 Subject: [PATCH 15/45] Fix chatInput focussing --- src/components/chatInput/index.js | 11 ++++++++--- src/views/directMessages/containers/existingThread.js | 8 ++++---- src/views/directMessages/containers/newThread.js | 2 +- src/views/thread/container.js | 4 ++-- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/components/chatInput/index.js b/src/components/chatInput/index.js index b33686a63d..dbc6e0f101 100644 --- a/src/components/chatInput/index.js +++ b/src/components/chatInput/index.js @@ -77,12 +77,13 @@ type Props = { }; // $FlowFixMe -const ChatInput = React.forwardRef((props: Props, ref) => { +const ChatInput = (props: Props) => { const cacheKey = `last-content-${props.thread}`; // $FlowFixMe const [text, changeText] = React.useState(''); // $FlowFixMe const [photoSizeError, setPhotoSizeError] = React.useState(''); + const [inputRef, setInputRef] = React.useState(null); // On mount, set the text state to the cached value if one exists // $FlowFixMe @@ -241,6 +242,7 @@ const ChatInput = React.forwardRef((props: Props, ref) => { if (isSendingMediaMessage) return; setIsSendingMediaMessage(true); setMediaPreviewFile(blob); + inputRef && inputRef.focus(); const reader = new FileReader(); reader.onload = () => { @@ -317,7 +319,10 @@ const ChatInput = React.forwardRef((props: Props, ref) => { value={text} onChange={onChange} onKeyDown={handleKeyPress} - ref={ref} + inputRef={node => { + if (props.onRef) props.onRef(node); + setInputRef(node); + }} /> {
); -}); +}; const map = (state, ownProps) => ({ websocketConnection: state.connectionStatus.websocketConnection, diff --git a/src/views/directMessages/containers/existingThread.js b/src/views/directMessages/containers/existingThread.js index 28935c26c4..a9ce1bfdd5 100644 --- a/src/views/directMessages/containers/existingThread.js +++ b/src/views/directMessages/containers/existingThread.js @@ -48,7 +48,7 @@ class ExistingThread extends React.Component { this.forceScrollToBottom(); // autofocus on desktop if (window && window.innerWidth > 768 && this.chatInput) { - this.chatInput.triggerFocus(); + this.chatInput.focus(); } } @@ -64,7 +64,7 @@ class ExistingThread extends React.Component { if (curr.threadSliderIsOpen) return; // if the thread slider is closed and we're viewing DMs, refocus the chat input if (prev.threadSliderIsOpen && !curr.threadSliderIsOpen && this.chatInput) { - this.chatInput.triggerFocus(); + this.chatInput.focus(); } // as soon as the direct message thread is loaded, refocus the chat input if ( @@ -72,7 +72,7 @@ class ExistingThread extends React.Component { !prev.data.directMessageThread && this.chatInput ) { - this.chatInput.triggerFocus(); + this.chatInput.focus(); } if (prev.match.params.threadId !== curr.match.params.threadId) { const threadId = curr.match.params.threadId; @@ -84,7 +84,7 @@ class ExistingThread extends React.Component { this.forceScrollToBottom(); // autofocus on desktop if (window && window.innerWidth > 768 && this.chatInput) { - this.chatInput.triggerFocus(); + this.chatInput.focus(); } } } diff --git a/src/views/directMessages/containers/newThread.js b/src/views/directMessages/containers/newThread.js index ab5512eb8f..22ef0e8010 100644 --- a/src/views/directMessages/containers/newThread.js +++ b/src/views/directMessages/containers/newThread.js @@ -600,7 +600,7 @@ class NewThread extends React.Component { return input && input.focus(); } - this.chatInput.triggerFocus(); + this.chatInput.focus(); } componentWillUnmount() { diff --git a/src/views/thread/container.js b/src/views/thread/container.js index 6c396247b3..dc44f4beac 100644 --- a/src/views/thread/container.js +++ b/src/views/thread/container.js @@ -259,13 +259,13 @@ class ThreadContainer extends React.Component { if (threadAndUser && this.chatInput) { // if the user is viewing the inbox, opens the thread slider, and then closes it again, refocus the inbox inpu if (prevProps.threadSliderIsOpen && !threadSliderIsOpen) { - return this.chatInput.triggerFocus(); + return this.chatInput.focus(); } // if the thread slider is open while in the inbox, don't focus in the inbox if (threadSliderIsOpen) return; - return this.chatInput.triggerFocus(); + return this.chatInput.focus(); } } From abda1d4a3b7b4a3590ff2331de53c403849e17fb Mon Sep 17 00:00:00 2001 From: Max Stoiber Date: Wed, 19 Dec 2018 10:53:33 +0100 Subject: [PATCH 16/45] Fix sending plaintext messages --- api/mutations/message/addMessage.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/api/mutations/message/addMessage.js b/api/mutations/message/addMessage.js index 61380167d6..63b2215dfe 100644 --- a/api/mutations/message/addMessage.js +++ b/api/mutations/message/addMessage.js @@ -1,5 +1,6 @@ // @flow import { stateFromMarkdown } from 'draft-js-import-markdown'; +import { convertToRaw } from 'draft-js'; import type { GraphQLContext } from '../../'; import UserError from '../../utils/UserError'; import { uploadImage } from '../../utils/file-storage'; @@ -87,7 +88,7 @@ export default requireAuth(async (_: any, args: Input, ctx: GraphQLContext) => { if (message.messageType === 'text') { message.content.body = JSON.stringify( - stateFromMarkdown(message.content.body) + convertToRaw(stateFromMarkdown(message.content.body)) ); message.messageType = 'draftjs'; } From 359e601046d5ad865f5d25764f79f1040cb938ae Mon Sep 17 00:00:00 2001 From: Max Stoiber Date: Wed, 19 Dec 2018 10:53:59 +0100 Subject: [PATCH 17/45] Remove unnecessary double-read of attached media --- src/components/chatInput/index.js | 36 +++++++++++++++---------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/components/chatInput/index.js b/src/components/chatInput/index.js index dbc6e0f101..4fa6d5f3df 100644 --- a/src/components/chatInput/index.js +++ b/src/components/chatInput/index.js @@ -191,27 +191,23 @@ const ChatInput = (props: Props) => { // If a user sends a message, force a scroll to bottom. This doesn't exist if this is a new DM thread if (props.forceScrollToBottom) props.forceScrollToBottom(); - if (mediaPreviewFile) { + if (mediaFile) { setIsSendingMediaMessage(true); - let reader = new FileReader(); - reader.onloadend = () => { - if (props.forceScrollToBottom) props.forceScrollToBottom(); - sendMessage({ file: mediaPreviewFile, body: reader.result }) - .then(() => setIsSendingMediaMessage(false)) - .catch(err => { - setIsSendingMediaMessage(false); - props.dispatch(addToastWithTimeout('error', err.message)); - }); - }; - reader.readAsDataURL(mediaPreviewFile); + if (props.forceScrollToBottom) props.forceScrollToBottom(); + sendMessage({ file: mediaFile, body: '{"blocks":[],"entityMap":{}}' }) + .then(() => { + setIsSendingMediaMessage(false); + setMediaPreview(null); + setAttachedMediaFile(null); + }) + .catch(err => { + setIsSendingMediaMessage(false); + props.dispatch(addToastWithTimeout('error', err.message)); + }); } if (text.length === 0) return; - // Clear the chat input now that we're sending a message for sure - onChange({ target: { value: '' } }); - removeQuotedMessage(); - sendMessage({ body: text }) .then(() => { // If we're viewing a thread and the user sends a message as a non-member, we need to refetch the thread data @@ -227,6 +223,10 @@ const ChatInput = (props: Props) => { .catch(err => { props.dispatch(addToastWithTimeout('error', err.message)); }); + + // Clear the chat input now that we're sending a message for sure + onChange({ target: { value: '' } }); + removeQuotedMessage(); }; // $FlowFixMe @@ -236,12 +236,12 @@ const ChatInput = (props: Props) => { // $FlowFixMe const [mediaPreview, setMediaPreview] = React.useState(null); // $FlowFixMe - const [mediaPreviewFile, setMediaPreviewFile] = React.useState(null); + const [mediaFile, setAttachedMediaFile] = React.useState(null); const previewMedia = blob => { if (isSendingMediaMessage) return; setIsSendingMediaMessage(true); - setMediaPreviewFile(blob); + setAttachedMediaFile(blob); inputRef && inputRef.focus(); const reader = new FileReader(); From 460fa50b9bff52746f30af5ab6ae9ba6a06e084a Mon Sep 17 00:00:00 2001 From: Max Stoiber Date: Wed, 19 Dec 2018 11:00:09 +0100 Subject: [PATCH 18/45] Resolve React warnings --- src/components/chatInput/style.js | 5 ++++- src/views/thread/components/threadDetail.js | 3 ++- src/views/thread/container.js | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/chatInput/style.js b/src/components/chatInput/style.js index a3dbc011cd..763c5f2d19 100644 --- a/src/components/chatInput/style.js +++ b/src/components/chatInput/style.js @@ -1,4 +1,5 @@ // @flow +import React from 'react'; import theme from 'shared/theme'; import styled, { css } from 'styled-components'; import Textarea from 'react-textarea-autosize'; @@ -97,7 +98,9 @@ export const InputWrapper = styled.div` } `; -export const Input = styled(Textarea).attrs({ +export const Input = styled(({ hasAttachment, networkDisabled, ...rest }) => ( +