mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-07 20:31:06 -05:00
Merge pull request #3192 from overleaf/msm-chat-react
Chat reactification GitOrigin-RevId: ee1268b412513a8656703257febad4975adb74e7
This commit is contained in:
parent
3ca5c4b26a
commit
f1b42a3d0d
17 changed files with 548 additions and 22 deletions
|
@ -842,7 +842,8 @@ const ProjectController = {
|
|||
gaOptimize: enableOptimize,
|
||||
customOptimizeEvent: true,
|
||||
experimentId: Settings.experimentId,
|
||||
showNewLogsUI: req.query && req.query.new_logs_ui === 'true'
|
||||
showNewLogsUI: req.query && req.query.new_logs_ui === 'true',
|
||||
showNewChatUI: req.query && req.query.new_chat_ui === 'true'
|
||||
})
|
||||
timer.done()
|
||||
}
|
||||
|
|
|
@ -102,7 +102,10 @@ block content
|
|||
|
||||
if !isRestrictedTokenMember
|
||||
.ui-layout-east
|
||||
include ./editor/chat
|
||||
if showNewChatUI
|
||||
include ./editor/chat-react
|
||||
else
|
||||
include ./editor/chat
|
||||
|
||||
include ./editor/hotkeys
|
||||
|
||||
|
|
12
services/web/app/views/project/editor/chat-react.pug
Normal file
12
services/web/app/views/project/editor/chat-react.pug
Normal file
|
@ -0,0 +1,12 @@
|
|||
aside.chat(
|
||||
ng-controller="ReactChatController"
|
||||
)
|
||||
chat(
|
||||
at-end="atEnd"
|
||||
loading="loading"
|
||||
load-more-messages="loadMoreMessages"
|
||||
messages="messages"
|
||||
reset-unread-messages="resetUnreadMessages"
|
||||
send-message="sendMessage"
|
||||
user-id="userId"
|
||||
)
|
|
@ -17,5 +17,9 @@
|
|||
"fast",
|
||||
"stop_on_validation_error",
|
||||
"ignore_validation_errors",
|
||||
"run_syntax_check_now"
|
||||
"run_syntax_check_now",
|
||||
"loading",
|
||||
"no_messages",
|
||||
"send_first_message",
|
||||
"your_message"
|
||||
]
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
import React, { useEffect } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import MessageList from './message-list'
|
||||
import MessageInput from './message-input'
|
||||
import InfiniteScroll from './infinite-scroll'
|
||||
import Icon from '../../../shared/components/icon'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
function ChatPane({
|
||||
atEnd,
|
||||
loading,
|
||||
loadMoreMessages,
|
||||
messages,
|
||||
resetUnreadMessages,
|
||||
sendMessage,
|
||||
userId
|
||||
}) {
|
||||
useEffect(() => {
|
||||
loadMoreMessages()
|
||||
}, [])
|
||||
|
||||
const shouldDisplayPlaceholder = !loading && messages.length === 0
|
||||
|
||||
const messageContentCount = messages.reduce(
|
||||
(acc, { contents }) => acc + contents.length,
|
||||
0
|
||||
)
|
||||
|
||||
return (
|
||||
<aside className="chat">
|
||||
<InfiniteScroll
|
||||
atEnd={atEnd}
|
||||
className="messages"
|
||||
fetchData={loadMoreMessages}
|
||||
isLoading={loading}
|
||||
itemCount={messageContentCount}
|
||||
>
|
||||
<div>
|
||||
{loading && <LoadingSpinner />}
|
||||
{shouldDisplayPlaceholder && <Placeholder />}
|
||||
<MessageList
|
||||
messages={messages}
|
||||
userId={userId}
|
||||
resetUnreadMessages={resetUnreadMessages}
|
||||
/>
|
||||
</div>
|
||||
</InfiniteScroll>
|
||||
<MessageInput
|
||||
resetUnreadMessages={resetUnreadMessages}
|
||||
sendMessage={sendMessage}
|
||||
/>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
|
||||
function LoadingSpinner() {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className="loading">
|
||||
<Icon type="fw" modifier="refresh" spin />
|
||||
{` ${t('loading')}...`}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Placeholder() {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<>
|
||||
<div className="no-messages text-center small">{t('no_messages')}</div>
|
||||
<div className="first-message text-center">
|
||||
{t('send_first_message')}
|
||||
<br />
|
||||
<Icon type="arrow-down" />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
ChatPane.propTypes = {
|
||||
atEnd: PropTypes.bool,
|
||||
loading: PropTypes.bool,
|
||||
loadMoreMessages: PropTypes.func.isRequired,
|
||||
messages: PropTypes.array.isRequired,
|
||||
resetUnreadMessages: PropTypes.func.isRequired,
|
||||
sendMessage: PropTypes.func.isRequired,
|
||||
userId: PropTypes.string.isRequired
|
||||
}
|
||||
|
||||
export default ChatPane
|
|
@ -0,0 +1,117 @@
|
|||
import React, { useRef, useEffect } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
const SCROLL_END_OFFSET = 30
|
||||
|
||||
function usePrevious(value) {
|
||||
const ref = useRef()
|
||||
useEffect(() => {
|
||||
ref.current = value
|
||||
})
|
||||
return ref.current
|
||||
}
|
||||
|
||||
function InfiniteScroll({
|
||||
atEnd,
|
||||
children,
|
||||
className = '',
|
||||
fetchData,
|
||||
itemCount,
|
||||
isLoading
|
||||
}) {
|
||||
const root = useRef(null)
|
||||
|
||||
const prevItemCount = usePrevious(itemCount)
|
||||
|
||||
// we keep the value in a Ref instead of state so it can be safely used in effects
|
||||
const scrollBottomRef = React.useRef(0)
|
||||
function setScrollBottom(value) {
|
||||
scrollBottomRef.current = value
|
||||
}
|
||||
|
||||
// position updates are not immediately applied. The DOM frequently can't calculate
|
||||
// element bounds after react updates, so it needs some throttling
|
||||
function scheduleScrollPositionUpdate(throttle) {
|
||||
const timeoutHandler = setTimeout(
|
||||
() =>
|
||||
(root.current.scrollTop =
|
||||
root.current.scrollHeight -
|
||||
root.current.clientHeight -
|
||||
scrollBottomRef.current),
|
||||
throttle
|
||||
)
|
||||
return () => clearTimeout(timeoutHandler)
|
||||
}
|
||||
|
||||
// Repositions the scroll after new items are loaded
|
||||
useEffect(
|
||||
() => {
|
||||
// the first render requires a longer throttling due to slower DOM updates
|
||||
const scrollThrottle = prevItemCount === 0 ? 150 : 0
|
||||
return scheduleScrollPositionUpdate(scrollThrottle)
|
||||
},
|
||||
[itemCount, prevItemCount]
|
||||
)
|
||||
|
||||
// Repositions the scroll after a window resize
|
||||
useEffect(() => {
|
||||
let clearScrollPositionUpdate
|
||||
const handleResize = () => {
|
||||
clearScrollPositionUpdate = scheduleScrollPositionUpdate(400)
|
||||
}
|
||||
window.addEventListener('resize', handleResize)
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
if (clearScrollPositionUpdate) {
|
||||
clearScrollPositionUpdate()
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
function onScrollHandler(event) {
|
||||
setScrollBottom(
|
||||
root.current.scrollHeight -
|
||||
root.current.scrollTop -
|
||||
root.current.clientHeight
|
||||
)
|
||||
if (event.target !== event.currentTarget) {
|
||||
// Ignore scroll events on nested divs
|
||||
// (this check won't be necessary in React 17: https://github.com/facebook/react/issues/15723
|
||||
return
|
||||
}
|
||||
if (shouldFetchData()) {
|
||||
fetchData()
|
||||
}
|
||||
}
|
||||
|
||||
function shouldFetchData() {
|
||||
const containerIsLargerThanContent =
|
||||
root.current.children[0].clientHeight < root.current.clientHeight
|
||||
if (atEnd || isLoading || containerIsLargerThanContent) {
|
||||
return false
|
||||
} else {
|
||||
return root.current.scrollTop < SCROLL_END_OFFSET
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={root}
|
||||
onScroll={onScrollHandler}
|
||||
className={`infinite-scroll ${className}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
InfiniteScroll.propTypes = {
|
||||
atEnd: PropTypes.bool,
|
||||
children: PropTypes.element.isRequired,
|
||||
className: PropTypes.string,
|
||||
fetchData: PropTypes.func.isRequired,
|
||||
itemCount: PropTypes.number.isRequired,
|
||||
isLoading: PropTypes.bool
|
||||
}
|
||||
|
||||
export default InfiniteScroll
|
|
@ -0,0 +1,52 @@
|
|||
import React, { useRef, useEffect } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Linkify from 'react-linkify'
|
||||
|
||||
function MessageContent({ content }) {
|
||||
const root = useRef(null)
|
||||
|
||||
useEffect(() => {
|
||||
const MJHub = window.MathJax.Hub
|
||||
const inlineMathConfig = MJHub.config && MJHub.config.tex2jax.inlineMath
|
||||
const alreadyConfigured = inlineMathConfig.some(
|
||||
c => c[0] === '$' && c[1] === '$'
|
||||
)
|
||||
if (!alreadyConfigured) {
|
||||
MJHub.Config({
|
||||
tex2jax: {
|
||||
inlineMath: inlineMathConfig.concat([['$', '$']])
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
// adds attributes to all the links generated by <Linkify/>, required due to https://github.com/tasti/react-linkify/issues/99
|
||||
root.current.getElementsByTagName('a').forEach(a => {
|
||||
a.setAttribute('target', '_blank')
|
||||
a.setAttribute('rel', 'noreferrer noopener')
|
||||
})
|
||||
|
||||
// MathJax typesetting
|
||||
const MJHub = window.MathJax.Hub
|
||||
const timeoutHandler = setTimeout(() => {
|
||||
MJHub.Queue(['Typeset', MJHub, root.current])
|
||||
}, 0)
|
||||
return () => clearTimeout(timeoutHandler)
|
||||
},
|
||||
[content]
|
||||
)
|
||||
|
||||
return (
|
||||
<p ref={root}>
|
||||
<Linkify>{content}</Linkify>
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
MessageContent.propTypes = {
|
||||
content: PropTypes.string.isRequired
|
||||
}
|
||||
|
||||
export default MessageContent
|
|
@ -0,0 +1,32 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
function MessageInput({ resetUnreadMessages, sendMessage }) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
function handleKeyDown(event) {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault()
|
||||
sendMessage(event.target.value)
|
||||
event.target.value = '' // clears the textarea content
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="new-message">
|
||||
<textarea
|
||||
placeholder={`${t('your_message')}...`}
|
||||
onKeyDown={handleKeyDown}
|
||||
onClick={resetUnreadMessages}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
MessageInput.propTypes = {
|
||||
resetUnreadMessages: PropTypes.func.isRequired,
|
||||
sendMessage: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
export default MessageInput
|
|
@ -0,0 +1,69 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import moment from 'moment'
|
||||
import Message from './message'
|
||||
|
||||
const FIVE_MINUTES = 5 * 60 * 1000
|
||||
|
||||
function formatTimestamp(date) {
|
||||
if (!date) {
|
||||
return 'N/A'
|
||||
} else {
|
||||
return `${moment(date).format('h:mm a')} ${moment(date).calendar()}`
|
||||
}
|
||||
}
|
||||
|
||||
function indexFromEnd(list, index) {
|
||||
return list.length - index - 1
|
||||
}
|
||||
|
||||
function MessageList({ messages, resetUnreadMessages, userId }) {
|
||||
function shouldRenderDate(messageIndex) {
|
||||
if (messageIndex === 0) {
|
||||
return true
|
||||
} else {
|
||||
const message = messages[messageIndex]
|
||||
const previousMessage = messages[messageIndex - 1]
|
||||
return message.timestamp - previousMessage.timestamp > FIVE_MINUTES
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
|
||||
<ul
|
||||
className="list-unstyled"
|
||||
onClick={resetUnreadMessages}
|
||||
onKeyDown={resetUnreadMessages}
|
||||
>
|
||||
{messages.map((message, index) => (
|
||||
// new messages are added to the beginning of the list, so we use a reversed index
|
||||
<li key={indexFromEnd(messages, index)} className="message">
|
||||
{shouldRenderDate(index) && (
|
||||
<div className="date">
|
||||
<time
|
||||
dateTime={
|
||||
message.timestamp
|
||||
? moment(message.timestamp).format()
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{formatTimestamp(message.timestamp)}
|
||||
</time>
|
||||
</div>
|
||||
)}
|
||||
<Message message={message} userId={userId} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
MessageList.propTypes = {
|
||||
messages: PropTypes.arrayOf(
|
||||
PropTypes.shape({ timestamp: PropTypes.instanceOf(Date) })
|
||||
).isRequired,
|
||||
resetUnreadMessages: PropTypes.func.isRequired,
|
||||
userId: PropTypes.string.isRequired
|
||||
}
|
||||
|
||||
export default MessageList
|
72
services/web/frontend/js/features/chat/components/message.js
Normal file
72
services/web/frontend/js/features/chat/components/message.js
Normal file
|
@ -0,0 +1,72 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import ColorManager from '../../../ide/colors/ColorManager'
|
||||
import MessageContent from './message-content'
|
||||
|
||||
function Message({ message, userId }) {
|
||||
const {
|
||||
chatMessageBorderSaturation,
|
||||
chatMessageBorderLightness,
|
||||
chatMessageBgSaturation,
|
||||
chatMessageBgLightness
|
||||
} = window.uiConfig
|
||||
|
||||
function hue(user) {
|
||||
return user ? ColorManager.getHueForUserId(user.id) : 0
|
||||
}
|
||||
|
||||
function getMessageStyle(user) {
|
||||
return {
|
||||
borderColor: `hsl(${hue(
|
||||
user
|
||||
)}, ${chatMessageBorderSaturation}, ${chatMessageBorderLightness})`,
|
||||
backgroundColor: `hsl(${hue(
|
||||
user
|
||||
)}, ${chatMessageBgSaturation}, ${chatMessageBgLightness})`
|
||||
}
|
||||
}
|
||||
|
||||
function getArrowStyle(user) {
|
||||
return {
|
||||
borderColor: `hsl(${hue(
|
||||
user
|
||||
)}, ${chatMessageBorderSaturation}, ${chatMessageBorderLightness})`
|
||||
}
|
||||
}
|
||||
|
||||
const isMessageFromSelf = message.user ? message.user.id === userId : false
|
||||
|
||||
return (
|
||||
<div className="message-wrapper">
|
||||
{!isMessageFromSelf && (
|
||||
<div className="name">
|
||||
<span>{message.user.first_name || message.user.email}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="message" style={getMessageStyle(message.user)}>
|
||||
{!isMessageFromSelf && (
|
||||
<div className="arrow" style={getArrowStyle(message.user)} />
|
||||
)}
|
||||
<div className="message-content">
|
||||
{message.contents.map((content, index) => (
|
||||
<MessageContent key={index} content={content} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Message.propTypes = {
|
||||
message: PropTypes.shape({
|
||||
contents: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
user: PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
email: PropTypes.string,
|
||||
first_name: PropTypes.string
|
||||
})
|
||||
}),
|
||||
userId: PropTypes.string.isRequired
|
||||
}
|
||||
|
||||
export default Message
|
|
@ -0,0 +1,42 @@
|
|||
import App from '../../../base'
|
||||
import { react2angular } from 'react2angular'
|
||||
import ChatPane from '../components/chat-pane'
|
||||
|
||||
App.controller('ReactChatController', function($scope, chatMessages, ide) {
|
||||
ide.$scope.$on('chat:more-messages-loaded', onMoreMessagesLoaded)
|
||||
function onMoreMessagesLoaded(e, chat) {
|
||||
ide.$scope.$applyAsync(() => {
|
||||
$scope.atEnd = chatMessages.state.atEnd
|
||||
$scope.loading = chat.state.loading
|
||||
$scope.messages = chat.state.messages.slice(0) // passing a new reference to trigger a prop update on react
|
||||
})
|
||||
}
|
||||
|
||||
ide.$scope.$on('chat:more-messages-loading', onMoreMessagesLoading)
|
||||
function onMoreMessagesLoading(e, chat) {
|
||||
ide.$scope.$applyAsync(() => {
|
||||
$scope.loading = true
|
||||
})
|
||||
}
|
||||
|
||||
function sendMessage(message) {
|
||||
if (message) {
|
||||
chatMessages.sendMessage(message)
|
||||
ide.$scope.$broadcast('chat:newMessage', message)
|
||||
}
|
||||
}
|
||||
|
||||
function resetUnreadMessages() {
|
||||
ide.$scope.$broadcast('chat:resetUnreadMessages')
|
||||
}
|
||||
|
||||
$scope.atEnd = chatMessages.state.atEnd
|
||||
$scope.loading = chatMessages.state.loading
|
||||
$scope.loadMoreMessages = chatMessages.loadMoreMessages
|
||||
$scope.messages = chatMessages.state.messages
|
||||
$scope.resetUnreadMessages = resetUnreadMessages
|
||||
$scope.sendMessage = sendMessage
|
||||
$scope.userId = ide.$scope.user.id
|
||||
})
|
||||
|
||||
App.component('chat', react2angular(ChatPane))
|
|
@ -1,9 +1,6 @@
|
|||
/* eslint-disable
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
import './controllers/ChatButtonController'
|
||||
import './controllers/ChatController'
|
||||
import './controllers/ChatMessageController'
|
||||
import '../../directives/mathjax'
|
||||
import '../../filters/wrapLongWords'
|
||||
import '../../features/chat/controllers/chat-controller'
|
||||
|
|
|
@ -73,6 +73,7 @@ export default App.factory('chatMessages', function($http, ide) {
|
|||
url += `&before=${chat.state.nextBeforeTimestamp}`
|
||||
}
|
||||
chat.state.loading = true
|
||||
ide.$scope.$broadcast('chat:more-messages-loading', chat)
|
||||
return $http.get(url).then(function(response) {
|
||||
const messages = response.data != null ? response.data : []
|
||||
chat.state.loading = false
|
||||
|
@ -94,15 +95,16 @@ export default App.factory('chatMessages', function($http, ide) {
|
|||
)
|
||||
)
|
||||
}
|
||||
return (chat.state.errored = true)
|
||||
chat.state.errored = true
|
||||
} else {
|
||||
messages.reverse()
|
||||
prependMessages(messages)
|
||||
return (chat.state.nextBeforeTimestamp =
|
||||
chat.state.nextBeforeTimestamp =
|
||||
chat.state.messages[0] != null
|
||||
? chat.state.messages[0].timestamp
|
||||
: undefined)
|
||||
: undefined
|
||||
}
|
||||
ide.$scope.$broadcast('chat:more-messages-loaded', chat)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -143,14 +145,15 @@ export default App.factory('chatMessages', function($http, ide) {
|
|||
message.timestamp - lastMessage.timestamp < TIMESTAMP_GROUP_SIZE
|
||||
if (shouldGroup) {
|
||||
lastMessage.timestamp = message.timestamp
|
||||
return lastMessage.contents.push(message.content)
|
||||
lastMessage.contents.push(message.content)
|
||||
} else {
|
||||
return chat.state.messages.push({
|
||||
chat.state.messages.push({
|
||||
user: message.user,
|
||||
timestamp: message.timestamp,
|
||||
contents: [message.content]
|
||||
})
|
||||
}
|
||||
ide.$scope.$broadcast('chat:more-messages-loaded', chat)
|
||||
}
|
||||
|
||||
return chat
|
||||
|
|
|
@ -54,6 +54,7 @@
|
|||
@import 'components/input-suggestions.less';
|
||||
@import 'components/nvd3.less';
|
||||
@import 'components/nvd3_override.less';
|
||||
@import 'components/infinite-scroll.less';
|
||||
|
||||
// Components w/ JavaScript
|
||||
@import 'components/modals.less';
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
.infinite-scroll {
|
||||
overflow-y: auto;
|
||||
}
|
47
services/web/package-lock.json
generated
47
services/web/package-lock.json
generated
|
@ -9791,7 +9791,7 @@
|
|||
"append-field": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
|
||||
"integrity": "sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY="
|
||||
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw=="
|
||||
},
|
||||
"aproba": {
|
||||
"version": "1.2.0",
|
||||
|
@ -12252,7 +12252,7 @@
|
|||
"buffer-shims": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz",
|
||||
"integrity": "sha1-mXjOMXOIxkmth5MCjDR37wRKi1E="
|
||||
"integrity": "sha512-Zy8ZXMyxIT6RMTeY7OP/bDndfj6bwCan7SS98CEndS6deHwWPpseeHlwarNcBim+etXnF9HBc1non5JgDaJU1g=="
|
||||
},
|
||||
"buffer-xor": {
|
||||
"version": "1.0.3",
|
||||
|
@ -12305,7 +12305,7 @@
|
|||
"busboy": {
|
||||
"version": "0.2.14",
|
||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz",
|
||||
"integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=",
|
||||
"integrity": "sha512-InWFDomvlkEj+xWLBfU3AvnbVYqeTWmQopiW0tWWEy5yehYm2YkGEc59sUmw/4ty5Zj/b0WHGs1LgecuBSBGrg==",
|
||||
"requires": {
|
||||
"dicer": "0.2.5",
|
||||
"readable-stream": "1.1.x"
|
||||
|
@ -12314,7 +12314,7 @@
|
|||
"readable-stream": {
|
||||
"version": "1.1.14",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
|
||||
"integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
|
||||
"integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==",
|
||||
"requires": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.1",
|
||||
|
@ -14969,7 +14969,7 @@
|
|||
"dicer": {
|
||||
"version": "0.2.5",
|
||||
"resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz",
|
||||
"integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=",
|
||||
"integrity": "sha512-FDvbtnq7dzlPz0wyYlOExifDEZcu8h+rErEXgfxqmLfRfC/kJidEFh4+effJRO3P0xmfqyPbSMG0LveNRfTKVg==",
|
||||
"requires": {
|
||||
"readable-stream": "1.1.x",
|
||||
"streamsearch": "0.1.2"
|
||||
|
@ -14978,7 +14978,7 @@
|
|||
"readable-stream": {
|
||||
"version": "1.1.14",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
|
||||
"integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
|
||||
"integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==",
|
||||
"requires": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.1",
|
||||
|
@ -22659,6 +22659,14 @@
|
|||
"integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=",
|
||||
"dev": true
|
||||
},
|
||||
"linkify-it": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz",
|
||||
"integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==",
|
||||
"requires": {
|
||||
"uc.micro": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"load-json-file": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz",
|
||||
|
@ -23523,7 +23531,7 @@
|
|||
"microtime-nodejs": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/microtime-nodejs/-/microtime-nodejs-1.0.0.tgz",
|
||||
"integrity": "sha1-iFlASvLipGKhXJzWvyxORo2r2+g="
|
||||
"integrity": "sha512-SthP/4JW6HUIZfgM0nadNtwKm/WMH0+z1i4RsPDnud+UasjoABzSkCk3eMhIRzipgwPhkdAYpTI69X4II4j1pA=="
|
||||
},
|
||||
"miller-rabin": {
|
||||
"version": "4.0.1",
|
||||
|
@ -23856,7 +23864,7 @@
|
|||
},
|
||||
"mkdirp": {
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
|
||||
"resolved": false,
|
||||
"integrity": "sha512-SknJC52obPfGQPnjIkXbmA6+5H15E+fR+E4iR2oQ3zzCLbd7/ONua69R/Gw7AgkTLsRG+r5fzksYwWe1AgTyWA==",
|
||||
"requires": {
|
||||
"minimist": "0.0.8"
|
||||
|
@ -29780,6 +29788,15 @@
|
|||
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
|
||||
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
|
||||
},
|
||||
"react-linkify": {
|
||||
"version": "1.0.0-alpha",
|
||||
"resolved": "https://registry.npmjs.org/react-linkify/-/react-linkify-1.0.0-alpha.tgz",
|
||||
"integrity": "sha512-7gcIUvJkAXXttt1fmBK9cwn+1jTa4hbKLGCZ9J1U6EOkyb2/+LKL1Z28d9rtDLMnpvImlNlLPdTPooorl5cpmg==",
|
||||
"requires": {
|
||||
"linkify-it": "^2.0.3",
|
||||
"tlds": "^1.199.0"
|
||||
}
|
||||
},
|
||||
"react-overlays": {
|
||||
"version": "0.9.1",
|
||||
"resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-0.9.1.tgz",
|
||||
|
@ -31415,7 +31432,7 @@
|
|||
"resolve-from": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz",
|
||||
"integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c="
|
||||
"integrity": "sha512-qpFcKaXsq8+oRoLilkwyc7zHGF5i9Q2/25NIgLQQ/+VVv9rU4qvr6nXVAw1DsnXJyQkZsR4Ytfbtg5ehfcUssQ=="
|
||||
},
|
||||
"resolve-url": {
|
||||
"version": "0.2.1",
|
||||
|
@ -33315,7 +33332,7 @@
|
|||
"streamsearch": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz",
|
||||
"integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo="
|
||||
"integrity": "sha512-jos8u++JKm0ARcSUTAZXOVC0mSox7Bhn6sBgty73P1f3JGf7yG2clTbBNHUdde/kdvP2FESam+vM6l8jBrNxHA=="
|
||||
},
|
||||
"strict-uri-encode": {
|
||||
"version": "1.1.0",
|
||||
|
@ -34924,6 +34941,11 @@
|
|||
"integrity": "sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g=",
|
||||
"dev": true
|
||||
},
|
||||
"tlds": {
|
||||
"version": "1.210.0",
|
||||
"resolved": "https://registry.npmjs.org/tlds/-/tlds-1.210.0.tgz",
|
||||
"integrity": "sha512-5bzt4JE+NlnwiKpVW9yzWxuc44m+t2opmPG+eSKDp5V5qdyGvjMngKgBb5ZK8GiheQMbRTCKpRwFJeIEO6pV7Q=="
|
||||
},
|
||||
"tmp": {
|
||||
"version": "0.0.33",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
|
||||
|
@ -35316,6 +35338,11 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"uc.micro": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
|
||||
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA=="
|
||||
},
|
||||
"uglify-js": {
|
||||
"version": "3.9.1",
|
||||
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.9.1.tgz",
|
||||
|
|
|
@ -125,6 +125,7 @@
|
|||
"react-dom": "^16.13.1",
|
||||
"react-error-boundary": "^2.3.1",
|
||||
"react-i18next": "^11.7.1",
|
||||
"react-linkify": "^1.0.0-alpha",
|
||||
"react2angular": "^4.0.6",
|
||||
"redis-sharelatex": "^1.0.13",
|
||||
"request": "^2.88.2",
|
||||
|
|
Loading…
Reference in a new issue