From b5af8eb2156440d35d5177e2737ee31962057487 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Wed, 18 Nov 2020 15:27:13 +1100 Subject: [PATCH] Add Mentions with react-mentions --- js/models/conversations.js | 1 - js/modules/loki_app_dot_net_api.js | 6 +- js/modules/loki_public_chat_api.d.ts | 7 +- js/modules/signal.js | 2 - package.json | 3 +- stylesheets/_mentions.scss | 49 ++-- ts/components/conversation/AddMentions.tsx | 104 ++++----- ts/components/conversation/MemberList.tsx | 16 +- ts/components/conversation/MessageBody.tsx | 2 +- .../conversation/SessionCompositionBox.tsx | 221 ++++++++++++++++-- .../conversation/SessionConversation.tsx | 47 ++++ .../session/network/SessionOffline.tsx | 2 +- ts/receiver/attachments.ts | 4 +- ts/session/utils/Toast.tsx | 9 +- ts/window.d.ts | 1 - yarn.lock | 81 ++++--- 16 files changed, 383 insertions(+), 172 deletions(-) diff --git a/js/models/conversations.js b/js/models/conversations.js index aa9351014..e38445dfe 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -13,7 +13,6 @@ clipboard, BlockedNumberController, lokiPublicChatAPI, - JobQueue, */ /* eslint-disable more/no-then */ diff --git a/js/modules/loki_app_dot_net_api.js b/js/modules/loki_app_dot_net_api.js index 027930430..f90504ca7 100644 --- a/js/modules/loki_app_dot_net_api.js +++ b/js/modules/loki_app_dot_net_api.js @@ -123,9 +123,11 @@ const sendViaOnion = async (srvPubKey, url, fetchOptions, options = {}) => { options.requestNumber ); if (typeof result === 'number') { - window.log.error('sendOnionRequestLsrpcDest() returned a number indicating an error: ', result); + window.log.error( + 'sendOnionRequestLsrpcDest() returned a number indicating an error: ', + result + ); } - } catch (e) { log.error( 'loki_app_dot_net:::sendViaOnion - lokiRpcUtils error', diff --git a/js/modules/loki_public_chat_api.d.ts b/js/modules/loki_public_chat_api.d.ts index b4795a523..caba232e1 100644 --- a/js/modules/loki_public_chat_api.d.ts +++ b/js/modules/loki_public_chat_api.d.ts @@ -11,7 +11,12 @@ export interface LokiPublicChatFactoryInterface { channelId: number, conversationId: string ): Promise; - getListOfMembers(): Promise>; + getListOfMembers(): Promise< + Array<{ authorPhoneNumber: string; authorProfileName?: string }> + >; + setListOfMembers( + members: Array<{ authorPhoneNumber: string; authorProfileName?: string }> + ); } declare class LokiPublicChatFactoryAPI diff --git a/js/modules/signal.js b/js/modules/signal.js index 404efed5f..d9cc7c126 100644 --- a/js/modules/signal.js +++ b/js/modules/signal.js @@ -92,7 +92,6 @@ const { MediaGallery, } = require('../../ts/components/conversation/media-gallery/MediaGallery'); const { Message } = require('../../ts/components/conversation/Message'); -const { MessageBody } = require('../../ts/components/conversation/MessageBody'); const { MessageDetail, } = require('../../ts/components/conversation/MessageDetail'); @@ -266,7 +265,6 @@ exports.setup = (options = {}) => { SessionPasswordPrompt, MediaGallery, Message, - MessageBody, MessageDetail, Quote, Types: { diff --git a/package.json b/package.json index 494b3d30b..7592f992f 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "@types/emoji-mart": "^2.11.3", "@types/moment": "^2.13.0", "@types/rc-slider": "^8.6.5", + "@types/react-mentions": "^3.3.1", "@types/react-mic": "^12.4.1", "@types/styled-components": "^5.1.4", "abort-controller": "3.0.0", @@ -105,12 +106,12 @@ "protobufjs": "^6.9.0", "rc-slider": "^8.7.1", "react": "^16.13.1", - "react-autosize-textarea": "^7.0.0", "react-contexify": "^4.1.1", "react-dom": "16.8.3", "react-emoji": "^0.5.0", "react-emoji-render": "^1.2.4", "react-h5-audio-player": "^3.2.0", + "react-mentions": "^4.0.2", "react-portal": "^4.2.0", "react-qr-svg": "^2.2.1", "react-redux": "7.2.1", diff --git a/stylesheets/_mentions.scss b/stylesheets/_mentions.scss index 34337564c..a019bd7d9 100644 --- a/stylesheets/_mentions.scss +++ b/stylesheets/_mentions.scss @@ -12,9 +12,6 @@ min-width: 20px; } - .invisible { - visibility: hidden; - } .existing-member { color: green; @@ -45,31 +42,35 @@ } } -.member-list-container, -.create-group-dialog, -.add-moderators-dialog, -.remove-moderators-dialog, -.invite-friends-dialog { - .member-item { - padding: 4px; - user-select: none; - - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; - - .name-part { - font-weight: 300; - margin-inline-start: 12px; - } - .pubkey-part { - margin-inline-start: 10px; - opacity: 0.6; - } +.invisible { + visibility: hidden; +} + +.member-item { + @include themify($themes) { + background-color: themed('cellBackground'); + color: themed('textColor'); + } + padding: 4px; + + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + + .name-part { + font-weight: 300; + margin-inline-start: 12px; + } + + .pubkey-part { + margin-inline-start: 10px; + opacity: 0.6; } } + + .mention-profile-name { color: rgb(194, 244, 255); background-color: rgb(66, 121, 150); diff --git a/ts/components/conversation/AddMentions.tsx b/ts/components/conversation/AddMentions.tsx index 285098ce9..a3c643a65 100644 --- a/ts/components/conversation/AddMentions.tsx +++ b/ts/components/conversation/AddMentions.tsx @@ -4,81 +4,56 @@ import { RenderTextCallbackType } from '../../types/Util'; import classNames from 'classnames'; import { MultiDeviceProtocol } from '../../session/protocols'; import { FindMember } from '../../util'; +import { useInterval } from '../../hooks/useInterval'; interface MentionProps { - key: number; + key: string; text: string; convoId: string; } -interface MentionState { - found: any; - us: boolean; -} - -class Mention extends React.Component { - private intervalHandle: any = null; - constructor(props: any) { - super(props); - - this.tryRenameMention = this.tryRenameMention.bind(this); - } - - public componentWillMount() { - this.setState({ found: false }); - - // TODO: give up after some period of time? - this.intervalHandle = setInterval(this.tryRenameMention, 30000); - - this.tryRenameMention().ignore(); - } - - public componentWillUnmount() { - this.clearOurInterval(); - } - - public render() { - if (this.state.found) { - // TODO: We don't have to search the database of message just to know that the message is for us! - const us = this.state.us; - const className = classNames( - 'mention-profile-name', - us && 'mention-profile-name-us' - ); - - const profileName = this.state.found.authorProfileName; - const displayedName = - profileName && profileName.length > 0 ? profileName : 'Anonymous'; +const Mention = (props: MentionProps) => { + const [found, setFound] = React.useState(undefined); + const [us, setUs] = React.useState(false); - return {displayedName}; - } else { - return ( - - {window.shortenPubkey(this.props.text)} - + const tryRenameMention = async () => { + if (!found) { + const foundMember = await FindMember.findMember( + props.text.slice(1), + props.convoId ); + if (foundMember) { + const itsUs = await MultiDeviceProtocol.isOurDevice( + foundMember.authorPhoneNumber + ); + setUs(itsUs); + setFound(foundMember); + // FIXME stop this interval once we found it. + } } - } + }; - private clearOurInterval() { - clearInterval(this.intervalHandle); - } + useInterval(() => void tryRenameMention(), 10000); - private async tryRenameMention() { - const bound = this.clearOurInterval.bind(this); - const found = await FindMember.findMember( - this.props.text.slice(1), - this.props.convoId, - bound + if (found) { + // TODO: We don't have to search the database of message just to know that the message is for us! + const className = classNames( + 'mention-profile-name', + us && 'mention-profile-name-us' ); - if (found) { - const us = await MultiDeviceProtocol.isOurDevice(found.authorPhoneNumber); - this.setState({ found, us }); - this.clearOurInterval(); - } + const profileName = found.authorProfileName; + const displayedName = + profileName && profileName.length > 0 ? profileName : 'Anonymous'; + return {displayedName}; + } else { + return ( + + {window.shortenPubkey(props.text)} + + ); } -} +}; interface Props { text: string; @@ -109,15 +84,16 @@ export class AddMentions extends React.Component { if (!match) { return renderOther({ text, key: 0 }); } - while (match) { + count++; + const key = count; if (last < match.index) { const otherText = text.slice(last, match.index); - results.push(renderOther({ text: otherText, key: count++ })); + results.push(renderOther({ text: otherText, key })); } const pubkey = text.slice(match.index, FIND_MENTIONS.lastIndex); - results.push(); + results.push(); // @ts-ignore last = FIND_MENTIONS.lastIndex; diff --git a/ts/components/conversation/MemberList.tsx b/ts/components/conversation/MemberList.tsx index 0ea697af2..c0ab95ecb 100644 --- a/ts/components/conversation/MemberList.tsx +++ b/ts/components/conversation/MemberList.tsx @@ -21,22 +21,19 @@ interface MemberItemProps { checkmarked: boolean; } -class MemberItem extends React.Component { +export class MemberItem extends React.Component { constructor(props: any) { super(props); this.handleClick = this.handleClick.bind(this); } public render() { - const name = this.props.member.authorProfileName; - const pubkey = this.props.member.authorPhoneNumber; - const selected = this.props.selected; - const existingMember = this.props.existingMember; + const {authorProfileName: name, authorPhoneNumber: pubkey, selected, existingMember, checkmarked} = this.props.member; const shortPubkey = window.shortenPubkey(pubkey); let markType: 'none' | 'kicked' | 'added' | 'existing' = 'none'; - if (this.props.checkmarked) { + if (checkmarked) { if (existingMember) { markType = 'kicked'; } else { @@ -65,7 +62,6 @@ class MemberItem extends React.Component { default: // do nothing } - const mark = markType === 'kicked' ? '✘' : '✔'; return ( @@ -124,16 +120,16 @@ export class MemberList extends React.Component { } public render() { - const { members } = this.props; + const { members, selected } = this.props; const itemList = members.map(item => { - const selected = item === this.props.selected; + const isSelected = item === selected; return ( ( - + ); const renderDefault: RenderTextCallbackType = ({ text }) => text; diff --git a/ts/components/session/conversation/SessionCompositionBox.tsx b/ts/components/session/conversation/SessionCompositionBox.tsx index f92f019d2..c1c835057 100644 --- a/ts/components/session/conversation/SessionCompositionBox.tsx +++ b/ts/components/session/conversation/SessionCompositionBox.tsx @@ -4,8 +4,6 @@ import _, { debounce } from 'lodash'; import { Attachment, AttachmentType } from '../../../types/Attachment'; import * as MIME from '../../../types/MIME'; -import TextareaAutosize from 'react-autosize-textarea'; - import { SessionIconButton, SessionIconSize, SessionIconType } from '../icon'; import { SessionEmojiPanel } from './SessionEmojiPanel'; import { SessionRecording } from './SessionRecording'; @@ -24,8 +22,10 @@ import { LINK_PREVIEW_TIMEOUT, SessionStagedLinkPreview, } from './SessionStagedLinkPreview'; -import { AbortController, AbortSignal } from 'abort-controller'; +import { AbortController } from 'abort-controller'; import { SessionQuotedMessageComposition } from './SessionQuotedMessageComposition'; +import { Mention, MentionsInput } from 'react-mentions'; +import { MemberItem } from '../../conversation/MemberList'; export interface ReplyingToMessageProps { convoId: string; @@ -61,6 +61,8 @@ interface Props { isPrivate: boolean; isKickedFromGroup: boolean; leftGroup: boolean; + conversationKey: string; + isPublic: boolean; quotedMessageProps?: ReplyingToMessageProps; removeQuotedMessage: () => void; @@ -83,24 +85,54 @@ interface State { stagedLinkPreview?: StagedLinkPreviewData; } +const sendMessageStyle = { + control: { + wordBreak: 'break-all', + }, + input: { + overflow: 'auto', + maxHeight: 70, + wordBreak: 'break-all', + padding: '0px', + margin: '0px', + }, + highlighter: { + boxSizing: 'border-box', + overflow: 'hidden', + maxHeight: 70, + }, + flexGrow: 1, + minHeight: '24px', + width: '100%', +}; + +const getDefaultState = () => { + return { + message: '', + voiceRecording: undefined, + showRecordingView: false, + mediaSetting: null, + showEmojiPanel: false, + ignoredLink: undefined, + stagedLinkPreview: undefined, + }; +}; + export class SessionCompositionBox extends React.Component { - private readonly textarea: React.RefObject; + private readonly textarea: React.RefObject; private readonly fileInput: React.RefObject; private emojiPanel: any; private linkPreviewAbortController?: AbortController; + private container: any; + private mentionsData: Array<{ display: string; id: string }>; constructor(props: any) { super(props); - this.state = { - message: '', - voiceRecording: undefined, - showRecordingView: false, - mediaSetting: null, - showEmojiPanel: false, - }; + this.state = getDefaultState(); this.textarea = props.textarea; this.fileInput = React.createRef(); + this.mentionsData = []; // Emojis this.emojiPanel = null; @@ -132,6 +164,8 @@ export class SessionCompositionBox extends React.Component { this.onKeyDown = this.onKeyDown.bind(this); this.onChange = this.onChange.bind(this); this.focusCompositionBox = this.focusCompositionBox.bind(this); + + this.fetchUsersForGroup = this.fetchUsersForGroup.bind(this); } public async componentWillMount() { @@ -147,6 +181,13 @@ export class SessionCompositionBox extends React.Component { this.linkPreviewAbortController?.abort(); this.linkPreviewAbortController = undefined; } + public componentDidUpdate(prevProps: Props, prevState: State) { + // reset the state on new conversation key + if (prevProps.conversationKey !== this.props.conversationKey) { + this.setState(getDefaultState()); + this.mentionsData = []; + } + } public render() { const { showRecordingView } = this.state; @@ -253,19 +294,59 @@ export class SessionCompositionBox extends React.Component { className="send-message-input" role="main" onClick={this.focusCompositionBox} + ref={el => { + this.container = el; + }} > - + maxLength={Constants.CONVERSATION.MAX_MESSAGE_BODY_LENGTH} + rows={1} + // maxRows={3} + style={sendMessageStyle} + suggestionsPortalHost={this.container} + allowSuggestionsAboveCursor={true} + > + `@${id}`} + data={this.fetchUsersForGroup} + renderSuggestion={( + suggestion, + _search, + _highlightedDisplay, + _index, + focused + ) => ( + {}} + existingMember={false} + member={{ + id: `${suggestion.id}`, + authorPhoneNumber: `${suggestion.id}`, + selected: false, + authorProfileName: `${suggestion.display}`, + authorName: `${suggestion.display}`, + existingMember: false, + checkmarked: false, + authorAvatarPath: '', + }} + checkmarked={false} + /> + )} + /> + {typingEnabled && ( @@ -301,6 +382,84 @@ export class SessionCompositionBox extends React.Component { ); } + private fetchUsersForGroup(query: any, callback: any) { + if (!query) { + return; + } + if (this.props.isPublic) { + this.fetchUsersForOpenGroup(query, callback); + return; + } + if (!this.props.isPrivate) { + this.fetchUsersForClosedGroup(query, callback); + return; + } + } + + private fetchUsersForOpenGroup(query: any, callback: any) { + if (!query) { + return; + } + void window.lokiPublicChatAPI + .getListOfMembers() + .then(members => + members + .filter(d => !!d) + .filter(d => d.authorProfileName !== 'Anonymous') + .filter(d => + d.authorProfileName?.toLowerCase()?.includes(query.toLowerCase()) + ) + ) + // Transform the users to what react-mentions expects + .then(members => { + const toRet = members.map(user => ({ + display: user.authorProfileName, + id: user.authorPhoneNumber, + })); + return toRet; + }) + .then(callback); + } + + private fetchUsersForClosedGroup(query: any, callback: any) { + if (!query) { + return; + } + const conversationModel = window.ConversationController.get( + this.props.conversationKey + ); + if (!conversationModel) { + return; + } + const allPubKeys = conversationModel.get('members'); + + const allMembers = allPubKeys.map(pubKey => { + const conv = window.ConversationController.get(pubKey); + let profileName = 'Anonymous'; + if (conv) { + profileName = conv.getProfileName(); + } + return { + id: pubKey, + authorPhoneNumber: pubKey, + authorProfileName: profileName, + }; + }); + const members = allMembers + .filter(d => !!d) + .filter(d => d.authorProfileName !== 'Anonymous') + .filter(d => + d.authorProfileName?.toLowerCase()?.includes(query.toLowerCase()) + ); + // Transform the users to what react-mentions expects + + const mentionsData = members.map(user => ({ + display: user.authorProfileName, + id: user.authorPhoneNumber, + })); + this.mentionsData = mentionsData; + callback(mentionsData); + } private renderStagedLinkPreview(): JSX.Element { // Don't generate link previews if user has turned them off @@ -495,7 +654,25 @@ export class SessionCompositionBox extends React.Component { // tslint:disable-next-line: cyclomatic-complexity private async onSendMessage() { - const messagePlaintext = this.parseEmojis(this.state.message); + // replace all @(xxx) by @xxx + const cleanMentions = (text: string): string => { + const mentionRegex = /@\(05[0-9a-f]{64}\)/g; + const matches = text.match(mentionRegex); + let replacedMentions = text; + (matches || []).forEach(match => { + const replacedMention = match.substring(2, match.length - 1); + replacedMentions = replacedMentions.replace( + match, + `@${replacedMention}` + ); + }); + + return replacedMentions; + }; + + const messagePlaintext = cleanMentions( + this.parseEmojis(this.state.message) + ); const { isBlocked, isPrivate, leftGroup, isKickedFromGroup } = this.props; diff --git a/ts/components/session/conversation/SessionConversation.tsx b/ts/components/session/conversation/SessionConversation.tsx index d7e427e29..307879108 100644 --- a/ts/components/session/conversation/SessionConversation.tsx +++ b/ts/components/session/conversation/SessionConversation.tsx @@ -77,6 +77,7 @@ export class SessionConversation extends React.Component { private readonly compositionBoxRef: React.RefObject; private readonly messageContainerRef: React.RefObject; private dragCounter: number; + private publicMembersRefreshTimeout?: NodeJS.Timeout; constructor(props: any) { super(props); @@ -145,6 +146,8 @@ export class SessionConversation extends React.Component { this.handleDragOut = this.handleDragOut.bind(this); this.handleDrag = this.handleDrag.bind(this); this.handleDrop = this.handleDrop.bind(this); + + this.updateMemberList = this.updateMemberList.bind(this); } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -172,6 +175,22 @@ export class SessionConversation extends React.Component { div?.addEventListener('dragover', this.handleDrag); div?.addEventListener('drop', this.handleDrop); }, 100); + + // if the conversation changed, we have to stop our refresh of member list + if (this.publicMembersRefreshTimeout) { + global.clearInterval(this.publicMembersRefreshTimeout); + this.publicMembersRefreshTimeout = undefined; + } + + // if the newConversation changed, and is public, start our refresh members list + if (newConversation.isPublic) { + // TODO use abort controller to stop those requests too + void this.updateMemberList(); + this.publicMembersRefreshTimeout = global.setInterval( + this.updateMemberList, + 10000 + ); + } } // if we do not have a model, unregister for events if (!newConversation) { @@ -203,6 +222,11 @@ export class SessionConversation extends React.Component { div?.removeEventListener('dragleave', this.handleDragOut); div?.removeEventListener('dragover', this.handleDrag); div?.removeEventListener('drop', this.handleDrop); + + if (this.publicMembersRefreshTimeout) { + global.clearInterval(this.publicMembersRefreshTimeout); + this.publicMembersRefreshTimeout = undefined; + } } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -291,6 +315,8 @@ export class SessionConversation extends React.Component { leftGroup={conversation.leftGroup} isKickedFromGroup={conversation.isKickedFromGroup} isPrivate={conversation.type === 'direct'} + isPublic={conversation.isPublic || false} + conversationKey={conversationKey} sendMessage={sendMessageFn} stagedAttachments={stagedAttachments} onMessageSending={this.onMessageSending} @@ -1138,4 +1164,25 @@ export class SessionConversation extends React.Component { this.setState({ isDraggingFile: false }); } } + + private async updateMemberList() { + const allPubKeys = await window.Signal.Data.getPubkeysInPublicConversation( + this.props.conversationKey + ); + + const allMembers = allPubKeys.map((pubKey: string) => { + const conv = window.ConversationController.get(pubKey); + let profileName = 'Anonymous'; + if (conv) { + profileName = conv.getProfileName(); + } + return { + id: pubKey, + authorPhoneNumber: pubKey, + authorProfileName: profileName, + }; + }); + + window.lokiPublicChatAPI.setListOfMembers(allMembers); + } } diff --git a/ts/components/session/network/SessionOffline.tsx b/ts/components/session/network/SessionOffline.tsx index be3a7f8d1..8230520c3 100644 --- a/ts/components/session/network/SessionOffline.tsx +++ b/ts/components/session/network/SessionOffline.tsx @@ -1,6 +1,6 @@ import React from 'react'; import styled from 'styled-components'; -import { useNetwork } from './useNetwork'; +import { useNetwork } from '../../../hooks/useNetwork'; type ContainerProps = { show: boolean; diff --git a/ts/receiver/attachments.ts b/ts/receiver/attachments.ts index 849d165d2..e8ec4250a 100644 --- a/ts/receiver/attachments.ts +++ b/ts/receiver/attachments.ts @@ -33,7 +33,9 @@ export async function downloadAttachment(attachment: any) { // FIXME "178" test to remove once this is fixed server side. if (!res.response || !res.response.data || res.response.data.length === 178) { if (res.response.data.length === 178) { - window.log.error('Data of 178 length corresponds of a 404 returned as 200 by file.getsession.org.'); + window.log.error( + 'Data of 178 length corresponds of a 404 returned as 200 by file.getsession.org.' + ); } throw new Error( `downloadAttachment: invalid response for ${attachment.url}` diff --git a/ts/session/utils/Toast.tsx b/ts/session/utils/Toast.tsx index e7d58810b..6e37ccd59 100644 --- a/ts/session/utils/Toast.tsx +++ b/ts/session/utils/Toast.tsx @@ -17,8 +17,7 @@ export function pushToastError( title={title} description={description} type={SessionToastType.Error} - />, - { toastId: id } + /> ); } @@ -32,8 +31,7 @@ export function pushToastWarning( title={title} description={description} type={SessionToastType.Warning} - />, - { toastId: id } + /> ); } @@ -43,8 +41,7 @@ export function pushToastInfo(id: string, title: string, description?: string) { title={title} description={description} type={SessionToastType.Info} - />, - { toastId: id } + /> ); } diff --git a/ts/window.d.ts b/ts/window.d.ts index 775bb9e1b..759408580 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -71,7 +71,6 @@ declare global { lokiMessageAPI: LokiMessageInterface; lokiPublicChatAPI: LokiPublicChatFactoryInterface; lokiSnodeAPI: LokiSnodeAPI; - lokiPublicChatAPI: LokiPublicChatFactoryAPI; mnemonic: RecoveryPhraseUtil; onLogin: any; passwordUtil: any; diff --git a/yarn.lock b/yarn.lock index 1a64929c5..5d1614d00 100644 --- a/yarn.lock +++ b/yarn.lock @@ -105,6 +105,13 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.11.5.tgz#c7ff6303df71080ec7a4f5b8c003c58f1cf51037" integrity sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q== +"@babel/runtime@7.4.5": + version "7.4.5" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.4.5.tgz#582bb531f5f9dc67d2fcb682979894f75e253f12" + integrity sha512-TuI4qpWZP6lGOGIuGWtp9sPluqYICmbk8T/1vpSysqJxRPkudh/ofFWyqdcMsDf2s7KvDL4/YHgKyvcS3g9CJQ== + dependencies: + regenerator-runtime "^0.13.2" + "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2": version "7.8.7" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.8.7.tgz#8fefce9802db54881ba59f90bb28719b4996324d" @@ -119,6 +126,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.3.4", "@babel/runtime@^7.8.7": + version "7.12.5" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e" + integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/runtime@^7.5.5": version "7.12.0" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.0.tgz#98bd7666186969c04be893d747cf4a6c6c8fa6b0" @@ -126,13 +140,6 @@ dependencies: regenerator-runtime "^0.13.4" -"@babel/runtime@^7.8.7": - version "7.12.5" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e" - integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg== - dependencies: - regenerator-runtime "^0.13.4" - "@babel/template@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278" @@ -599,6 +606,13 @@ dependencies: "@types/react" "*" +"@types/react-mentions@^3.3.1": + version "3.3.1" + resolved "https://registry.yarnpkg.com/@types/react-mentions/-/react-mentions-3.3.1.tgz#f0fa1138f7fbf3012546260b634cbf23f735c0fe" + integrity sha512-o27Fb89lX4pGcA6lMkE+n7d/jqE9Zty3DYIUTVL0SBx8YV4uiqjI6d/34zVL2bUxq1mXSHb34hm2i358C/rRoA== + dependencies: + "@types/react" "*" + "@types/react-mic@^12.4.1": version "12.4.1" resolved "https://registry.yarnpkg.com/@types/react-mic/-/react-mic-12.4.1.tgz#7c261988c3be918b108df642a14101873b42846b" @@ -1276,11 +1290,6 @@ autoprefixer@^6.3.1: postcss "^5.2.16" postcss-value-parser "^3.2.3" -autosize@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/autosize/-/autosize-4.0.2.tgz#073cfd07c8bf45da4b9fd153437f5bafbba1e4c9" - integrity sha512-jnSyH2d+qdfPGpWlcuhGiHmqBJ6g3X+8T+iRwFrHPLVcdoGJE/x6Qicm6aDHfTsbgZKxyV8UU/YB2p4cjKDRRA== - aws-sign2@~0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" @@ -2450,11 +2459,6 @@ compression@^1.7.3: safe-buffer "5.1.2" vary "~1.1.2" -computed-style@~0.1.3: - version "0.1.4" - resolved "https://registry.yarnpkg.com/computed-style/-/computed-style-0.1.4.tgz#7f344fd8584b2e425bedca4a1afc0e300bb05d74" - integrity sha1-fzRP2FhLLkJb7cpKGvwOMAuwXXQ= - concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -5365,7 +5369,7 @@ internal-ip@1.2.0: dependencies: meow "^3.3.0" -invariant@^2.2.1, invariant@^2.2.2: +invariant@^2.2.1, invariant@^2.2.2, invariant@^2.2.4: version "2.2.4" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== @@ -6237,13 +6241,6 @@ lie@*: dependencies: immediate "~3.0.5" -line-height@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/line-height/-/line-height-0.3.1.tgz#4b1205edde182872a5efa3c8f620b3187a9c54c9" - integrity sha1-SxIF7d4YKHKl76PI9iCzGHqcVMk= - dependencies: - computed-style "~0.1.3" - linkify-it@2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-2.0.3.tgz#d94a4648f9b1c179d64fa97291268bdb6ce9434f" @@ -8326,7 +8323,7 @@ promise-inflight@^1.0.1: resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM= -prop-types@15.x, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.6, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: +prop-types@15.x, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -8649,15 +8646,6 @@ rc@^1.2.1, rc@^1.2.7, rc@^1.2.8: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-autosize-textarea@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/react-autosize-textarea/-/react-autosize-textarea-7.0.0.tgz#4f633e4238de7ba73c1da8fdc307353c50f1c5ab" - integrity sha512-rGQLpGUaELvzy3NKzp0kkcppaUtZTptsyR0PGuLotaJDjwRbT0DpD000yCzETpXseJQ/eMsyVGDDHXjXP93u8w== - dependencies: - autosize "^4.0.2" - line-height "^0.3.1" - prop-types "^15.5.6" - react-codemirror2@^4.2.1: version "4.3.0" resolved "https://registry.yarnpkg.com/react-codemirror2/-/react-codemirror2-4.3.0.tgz#e79aedca4da60d22402d2cd74f2885a3e5c009fd" @@ -8805,6 +8793,16 @@ react-lifecycles-compat@^3.0.4: resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== +react-mentions@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/react-mentions/-/react-mentions-4.0.2.tgz#a3c06ee9b0da7c792227b0fb4620b04c9c891189" + integrity sha512-NlWzoAHWeeciewwUqH5jF0PT1wqVKCeHUwy5BvcMspDR3M22jPtqQLsTBJTcLuu+c7dzy/f3snaa0S+UJnyJiA== + dependencies: + "@babel/runtime" "7.4.5" + invariant "^2.2.4" + prop-types "^15.5.8" + substyle "^9.1.0" + react-portal@^4.2.0: version "4.2.1" resolved "https://registry.yarnpkg.com/react-portal/-/react-portal-4.2.1.tgz#12c1599238c06fb08a9800f3070bea2a3f78b1a6" @@ -9145,6 +9143,11 @@ regenerator-runtime@^0.11.0: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== +regenerator-runtime@^0.13.2: + version "0.13.7" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55" + integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew== + regenerator-runtime@^0.13.4: version "0.13.5" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz#d878a1d094b4306d10b9096484b33ebd55e26697" @@ -10354,6 +10357,14 @@ styled-components@5.1.1: shallowequal "^1.1.0" supports-color "^5.5.0" +substyle@^9.1.0: + version "9.3.0" + resolved "https://registry.yarnpkg.com/substyle/-/substyle-9.3.0.tgz#569af81723f74cd895b08b6b1e6bc06727f2a2bd" + integrity sha512-OK6A6EpqOfRvlwOnrgwFKIi8UDJwCQ2UB5cIJGMEFvl3zUUA83XDbRUJizECj66CdeZ9pGjkmwRxyc/9wBGQMA== + dependencies: + "@babel/runtime" "^7.3.4" + invariant "^2.2.4" + sumchecker@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/sumchecker/-/sumchecker-2.0.2.tgz#0f42c10e5d05da5d42eea3e56c3399a37d6c5b3e"