diff --git a/Gemfile b/Gemfile index 01e455c69..dd74f1c2f 100644 --- a/Gemfile +++ b/Gemfile @@ -85,6 +85,7 @@ gem "stream_rails", "~> 2.5" gem "stripe", "~> 3.9" gem "therubyracer", "~> 0.12", platforms: :ruby gem "timber", "~> 2.6" +gem 'twilio-ruby', '~> 5.10.3' gem "twitter", "~> 6.2" gem "uglifier", "~> 3.2" gem "validate_url", "~> 1.0" diff --git a/Gemfile.lock b/Gemfile.lock index c2451ab28..cf4134cf2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -841,6 +841,10 @@ GEM msgpack (~> 1.0) timecop (0.9.1) trollop (2.1.2) + twilio-ruby (5.10.3) + faraday (~> 0.9) + jwt (>= 1.5, <= 2.5) + nokogiri (>= 1.6, < 2.0) twitter (6.2.0) addressable (~> 2.3) buftok (~> 0.2.0) @@ -1020,6 +1024,7 @@ DEPENDENCIES therubyracer (~> 0.12) timber (~> 2.6) timecop (~> 0.9) + twilio-ruby (~> 5.10.3) twitter (~> 6.2) uglifier (~> 3.2) validate_url (~> 1.0) diff --git a/app/assets/stylesheets/chat.scss b/app/assets/stylesheets/chat.scss index 8647313a0..db86f7b77 100644 --- a/app/assets/stylesheets/chat.scss +++ b/app/assets/stylesheets/chat.scss @@ -79,13 +79,20 @@ height: inherit; max-height: inherit; display: flex; - flex-direction: column; + flex-direction: row; justify-content: space-between; border: 1px solid $outline-color; box-shadow: $bold-shadow; background: white; } +.activechatchannel__conversation{ + display: flex; + flex-direction: column; + width: 100%; + flex-grow: 3; +} + .activechatchannel__messages { font-size: 15px; padding: 10px 0px; @@ -137,6 +144,36 @@ } +.activechatchannel__activecontent{ + border-left: 2px solid $outline-color; + padding: 13px; + min-width: 290px; + margin-left: -180px; + background: white; + flex-basis: 80%; + z-index: 200; + position: relative; + box-sizing: border-box; + overflow-y: scroll; + @media screen and ( min-width: 1000px ){ + margin-left: initial; + min-width: 0px; + padding: 18px; + max-width:480px; + } +} + +.activechatchannel__activecontentexitbutton{ + border: 0px; + font-size: 36px; + color: $medium-gray; + padding:0px; + background: transparent; + position: absolute; + left: 6px; + top:-4px; +} + .chatchannels { display: flex; flex-direction: column; @@ -294,3 +331,46 @@ .messagelist__sentinel { height: 5px; } + +.chatcodeeditor{ + position: absolute; + top: 40px; + left: 0px; + right: 24px; + bottom: 0px; +} + +.CodeMirror{ + padding: 12px; + font-size:0.9em; +} + +.cursorelement{ + border-left: 3px solid $red; + animation: blinker 1s linear infinite; + padding: 0px; + z-index: 0; +} + +@keyframes blinker { + 50% { + opacity: 0; + } +} + +.chat .container { + box-shadow: 0px 0px 0px #ffffff !important; + border: 0px !important; + margin-top: 8px !important; + .title{ + width:95%; + font-size:18px; + h1{ + font-size:28px !important; + } + } + .body { + font-size:20px; + width:96%; + } +} \ No newline at end of file diff --git a/app/controllers/api/v0/articles_controller.rb b/app/controllers/api/v0/articles_controller.rb index 8639ef4ef..56fc6aa2a 100644 --- a/app/controllers/api/v0/articles_controller.rb +++ b/app/controllers/api/v0/articles_controller.rb @@ -16,8 +16,12 @@ module Api end def show - @article = Article.includes(:user).find(params[:id]).decorate - not_found unless @article.published + if params[:id] == "by_path" + @article = Article.includes(:user).find_by_path(params[:url])&.decorate + else + @article = Article.includes(:user).find(params[:id])&.decorate + end + not_found unless @article&.published end def onboarding diff --git a/app/controllers/api/v0/users_controller.rb b/app/controllers/api/v0/users_controller.rb index f420d7608..1d286ff39 100644 --- a/app/controllers/api/v0/users_controller.rb +++ b/app/controllers/api/v0/users_controller.rb @@ -15,6 +15,10 @@ module Api end end + def show + @user = User.find(params[:id]) + end + def less_than_one_day_old?(user) range = (Time.now.beginning_of_day - 1.day)..(Time.now) user_identity_age = user.github_created_at || diff --git a/app/controllers/chat_channels_controller.rb b/app/controllers/chat_channels_controller.rb index 7be942754..5aee01d2f 100644 --- a/app/controllers/chat_channels_controller.rb +++ b/app/controllers/chat_channels_controller.rb @@ -4,13 +4,15 @@ class ChatChannelsController < ApplicationController def index if params[:state] == "unopened" render_unopened_json_response + elsif params[:state] == "additional" + render_additional_json_response else render_channels_html end end def show - @chat_channel = ChatChannel.includes(:messages).find_by(id: params[:id]) + @chat_channel = current_user.chat_channels.includes(:messages).find_by(id: params[:id]) if @chat_channel @chat_channel else @@ -71,7 +73,7 @@ class ChatChannelsController < ApplicationController def render_unopened_json_response if current_user.has_role?(:super_admin) || Rails.env.development? @chat_channels_memberships = current_user. - chat_channel_memberships. + chat_channel_memberships.includes(:chat_channel). where(has_unopened_messages: true).order("updated_at DESC") else @chat_channels_memberships = [] @@ -79,19 +81,26 @@ class ChatChannelsController < ApplicationController render "index.json" end + def render_additional_json_response + @chat_channels_memberships = current_user. + chat_channel_memberships.includes(:chat_channel).limit(50).order("updated_at DESC") + render "index.json" + end + def render_channels_html - return unless current_user - @chat_channels = current_user.chat_channels. - order("last_message_at DESC"). - includes(:chat_channel_memberships) - @chat_channels.each do |channel| - channel.current_user = current_user - end - slug = if params[:slug] && params[:slug].start_with?("@") - [current_user.username, params[:slug].gsub("@", "")].sort.join("/") - else - params[:slug] - end - @active_channel = ChatChannel.find_by_slug(slug) || @chat_channels.first + return unless current_user + @chat_channels = current_user.chat_channels. + order("last_message_at DESC"). + limit(25) + @chat_channels.each do |channel| + channel.current_user = current_user + end + slug = if params[:slug] && params[:slug].start_with?("@") + [current_user.username, params[:slug].gsub("@", "")].sort.join("/") + else + params[:slug] + end + @active_channel = ChatChannel.find_by_slug(slug) || @chat_channels.first + # @twilio_token = TwilioToken.new(current_user).get end end diff --git a/app/controllers/pusher_controller.rb b/app/controllers/pusher_controller.rb index 99f57cff0..acd5481dc 100644 --- a/app/controllers/pusher_controller.rb +++ b/app/controllers/pusher_controller.rb @@ -20,7 +20,7 @@ class PusherController < ApplicationController end def valid_presence_channel - id = params[:channel_name].split("presence-channel-")[1] + id = params[:channel_name].split("presence-channel-")[1].split("-")[0] channel = ChatChannel.find(id) channel.has_member?(current_user) end diff --git a/app/javascript/chat/actions.js b/app/javascript/chat/actions.js index 992aba23f..de8743738 100644 --- a/app/javascript/chat/actions.js +++ b/app/javascript/chat/actions.js @@ -2,6 +2,7 @@ export function getAllMessages(channelId, successCb, failureCb) { fetch(`/chat_channels/${channelId}`, { Accept: 'application/json', 'Content-Type': 'application/json', + credentials: 'same-origin', }) .then(response => response.json()) .then(successCb) @@ -71,6 +72,16 @@ export function conductModeration( .catch(failureCb); } +export function getAdditionalChannels(successCb, failureCb) { + fetch(`/chat_channels?state=additional`, { + Accept: 'application/json', + 'Content-Type': 'application/json', + credentials: 'same-origin', + }) + .then(response => response.json()) + .then(successCb) + .catch(failureCb); +} export function sendKeys(subscription, successCb, failureCb) { fetch(`/push_notification_subscriptions`, { @@ -88,4 +99,15 @@ export function sendKeys(subscription, successCb, failureCb) { .then(response => response.json()) .then(successCb) .catch(failureCb); +} + +export function getContent(url, successCb, failureCb) { + fetch(`${url}`, { + Accept: 'application/json', + 'Content-Type': 'application/json', + credentials: 'same-origin', + }) + .then(response => response.json()) + .then(successCb) + .catch(failureCb); } \ No newline at end of file diff --git a/app/javascript/chat/chat.jsx b/app/javascript/chat/chat.jsx index 4bfe02f63..af809a49a 100644 --- a/app/javascript/chat/chat.jsx +++ b/app/javascript/chat/chat.jsx @@ -1,11 +1,12 @@ import { h, Component } from 'preact'; import PropTypes from 'prop-types'; -import { conductModeration, getAllMessages, sendMessage, sendOpen } from './actions'; +import { conductModeration, getAllMessages, sendMessage, sendOpen, getAdditionalChannels, getContent } from './actions'; import { hideMessages, scrollToBottom, setupObserver, setupNotifications, getNotificationState } from './util'; import Alert from './alert'; import Channels from './channels'; import Compose from './compose'; import Message from './message'; +import Content from './content'; import setupPusher from '../src/utils/pusher'; export default class Chat extends Component { @@ -30,7 +31,8 @@ export default class Chat extends Component { activeChannelId: chatOptions.activeChannelId, showChannelsList: chatOptions.showChannelsList, showTimestamp: chatOptions.showTimestamp, - notificationsPermission: null + notificationsPermission: null, + activeContent: null }; } @@ -39,14 +41,13 @@ export default class Chat extends Component { if ( index < 6 ) { this.setupChannel(channel.id); } - if (channel.channel_type === "invite_only") { - setupPusher(this.props.pusherKey, { - channelId: `presence-channel-${channel.id}`, - messageCreated: this.receiveNewMessage, - channelCleared: this.clearChannel, - redactUserMessages: this.redactUserMessages, - }); - } + setupPusher(this.props.pusherKey, { + channelId: `presence-channel-${channel.id}`, + messageCreated: this.receiveNewMessage, + channelCleared: this.clearChannel, + redactUserMessages: this.redactUserMessages, + liveCoding: this.liveCoding + }); }); setupObserver(this.observerCallback); setupPusher(this.props.pusherKey, { @@ -61,6 +62,8 @@ export default class Chat extends Component { null, ); this.setState({notificationsPermission: getNotificationState()}); + getAdditionalChannels(this.loadAdditionalChannels); + document.getElementById("messageform").focus(); } componentDidUpdate() { @@ -69,11 +72,23 @@ export default class Chat extends Component { } } + liveCoding = e => { + if (this.state.activeContent === {type_of: "code_editor"}) { + return + } + this.setState({activeContent: {type_of: "code_editor"}}) + } + + loadAdditionalChannels = channels => { + this.setState({chatChannels: channels}); + } + setupChannel = channelId => { if (this.state.messages[channelId].length === 0 || this.state.messages[channelId][0].reception_method === 'pushed'){ getAllMessages(channelId, this.receiveAllMessages); } }; + observerCallback = entries => { entries.forEach(entry => { @@ -156,7 +171,9 @@ export default class Chat extends Component { handleMessageSubmit = message => { // should check if user has the priviledge - if (message[0] === '/') { + if (message.startsWith('/code')) { + this.setState({activeContent: {type_of: "code_editor"}}) + } else if (message[0] === '/') { conductModeration( this.state.activeChannelId, message, @@ -180,8 +197,9 @@ export default class Chat extends Component { activeChannelId: parseInt(e.target.dataset.channelId), scrolled: false, showAlert: false, + activeContent: null }); - window.history.replaceState(null, null, "/gether/"+e.target.dataset.channelSlug); + window.history.replaceState(null, null, "/connect/"+e.target.dataset.channelSlug); document.getElementById("messageform").focus(); if (window.ga && ga.create) { ga('send', 'pageview', location.pathname + location.search); @@ -218,6 +236,26 @@ export default class Chat extends Component { }); } + triggerActiveContent = e => { + const target = e.target + if (e.target.dataset.content && e.target.dataset.content != "exit") { + e.preventDefault(); + this.setState({activeContent: {type_of: "loading"}}) + getContent('/api/'+target.dataset.content, this.setActiveContent, null) + } + else if (target.tagName.toLowerCase() === 'a' && target.href.startsWith('https://dev.to/')) { + e.preventDefault(); + getContent(`/api/articles/by_path?url=${target.href.split('https://dev.to')[1]}`, this.setActiveContent, null) + } else if (target.dataset.content === "exit") { + e.preventDefault(); + this.setState({activeContent: null}) + } + } + + setActiveContent = response => { + this.setState({activeContent: response}); + } + handleChannelOpenSuccess = response => { const newChannelsObj = this.state.chatChannels.map(channel => { if (parseInt(response.channel) === channel["id"]){ @@ -232,17 +270,19 @@ export default class Chat extends Component { console.error(err); }; - renderMessage = () => { + renderMessages = () => { const { activeChannelId, messages, showTimestamp } = this.state; return messages[activeChannelId].map(message => ( )); }; @@ -276,19 +316,27 @@ export default class Chat extends Component { renderActiveChatChannel = () => (
-
- {this.renderMessage()} -
+
+
+ {this.renderMessages()} +
+
+
+ +
+
+ +
-
- -
-
- -
); diff --git a/app/javascript/chat/codeEditor.jsx b/app/javascript/chat/codeEditor.jsx new file mode 100644 index 000000000..6025c5fc6 --- /dev/null +++ b/app/javascript/chat/codeEditor.jsx @@ -0,0 +1,64 @@ +import { h, Component } from 'preact'; +import PropTypes from 'prop-types'; +import setupPusher from '../src/utils/pusher'; + +export default class CodeEditor extends Component { + + componentDidMount() { + const editor = document.getElementById("codeeditor"); + const channel = setupPusher(this.props.pusherKey, { + channelId: `presence-channel-${this.props.activeChannelId}`, + liveCoding: this.liveCoding, + }); + const myCodeMirror = CodeMirror(editor, { + mode: "javascript", + theme: "material", + autofocus: true, + }); + myCodeMirror.setSize("100%", "100%"); + //Initial trigger: + channel.trigger('client-livecode', { + value: myCodeMirror.getValue(), + cursorPos: myCodeMirror.getCursor(), + }); + //Coding trigger: + myCodeMirror.on('keyup', cm => { + channel.trigger('client-livecode', { + keyPressed: true, + value: cm.getValue(), + cursorPos: cm.getCursor(), + }); + }); + } + + shouldComponentUpdate() { + return false; + } + + liveCoding = e => { + console.log("live coding") + if (e.keyPressed === true || e.value.length > 0) { + let cm = document.querySelector(".CodeMirror").CodeMirror + const cursorCoords = e.cursorPos + const cursorElement = document.createElement('span'); + cursorElement.classList.add("cursorelement") + cursorElement.style.height = `${(cursorCoords.bottom - cursorCoords.top)}px`; + cm.setValue(e.value); + cm.setBookmark(e.cursorPos, { widget: cursorElement }); + } else { + let cm = document.querySelector(".CodeMirror").CodeMirror + console.log("NEW LIVE CODING") + channel.trigger('client-livecode', { + value: cm.getValue(), + cursorPos: cm.getCursor(), + }); + } + } + + render() { + + return
+ + } + +} \ No newline at end of file diff --git a/app/javascript/chat/content.jsx b/app/javascript/chat/content.jsx new file mode 100644 index 000000000..1ac40e4b2 --- /dev/null +++ b/app/javascript/chat/content.jsx @@ -0,0 +1,74 @@ +import { h, Component } from 'preact'; +import PropTypes from 'prop-types'; +import CodeEditor from './codeEditor'; + +export default class Content extends Component { + static propTypes = { + resource: PropTypes.object, + onExit: PropTypes.func, + activeChannelId: PropTypes.number, + pusherKey: PropTypes.string, + }; + render() { + if (!this.props.resource) { + return "" + } else { + return ( +
+ + {display(this.props)} +
+ ); + } + } +} + +function display(props) { + if (props.resource.type_of === "loading") { + return
+ } else if (props.resource.type_of === "user") { + return
+

{props.resource.name}

+
+ {props.resource.summary} +
+
+ } else if (props.resource.type_of === "article") { + console.log(props.resource) + return ( +
+
+

{props.resource.title}

+

+ + {props.resource.user.username}/ + {props.resource.user.name} + +

+
+
+
+
+
) + } else if (props.resource.type_of === "code_editor") { + return + } +} \ No newline at end of file diff --git a/app/javascript/chat/message.jsx b/app/javascript/chat/message.jsx index a65ca1ca0..206928204 100644 --- a/app/javascript/chat/message.jsx +++ b/app/javascript/chat/message.jsx @@ -5,12 +5,14 @@ import ErrorMessage from './messages/errorMessage'; const Message = ({ user, + userID, message, color, type, messageColor, timestamp, profileImageUrl, + onContentTrigger, }) => { const spanStyle = { color }; const messageStyle = { color: messageColor }; @@ -26,14 +28,22 @@ const Message = ({ dangerouslySetInnerHTML={{__html: message}} > ); + return (
-
- +
+ {`${user} @@ -41,6 +51,8 @@ const Message = ({ className="chatmessagebody__username--link" href={`/${user}`} target="_blank" + data-content={`users/${userID}`} + onClick={onContentTrigger} > {user} @@ -61,12 +73,14 @@ const Message = ({ Message.propTypes = { user: PropTypes.string.isRequired, + userID: PropTypes.number.isRequired, color: PropTypes.string.isRequired, message: PropTypes.string.isRequired, messageColor: PropTypes.string, type: PropTypes.string, timestamp: PropTypes.string, profileImageUrl: PropTypes.string, + onContentTrigger: PropTypes.func, }; Message.defaultProps = { diff --git a/app/javascript/src/utils/getUnopenedChannels.jsx b/app/javascript/src/utils/getUnopenedChannels.jsx index feb50a43e..33c101c83 100644 --- a/app/javascript/src/utils/getUnopenedChannels.jsx +++ b/app/javascript/src/utils/getUnopenedChannels.jsx @@ -21,7 +21,7 @@ class UnopenedChannelNotice extends Component { } receiveNewMessage = e => { - if (location.pathname.startsWith("/gether") || location.pathname.startsWith("/%F0%9F%92%8C")) { + if (location.pathname.startsWith("/connect") || location.pathname.startsWith("/%F0%9F%92%8C")) { return } let channels = this.state.unopenedChannels; @@ -40,7 +40,7 @@ class UnopenedChannelNotice extends Component { if (this.state.visible) { const channels = this.state.unopenedChannels.map(channel => { return + + + + <% if user_signed_in? %> <%= javascript_pack_tag 'chat', defer: true %> @@ -25,7 +29,9 @@ class="live-chat" data-pusher-key="<%= ENV["PUSHER_KEY"] %>" data-chat-channels="<%= @chat_channels.to_json(methods: :last_opened_at) %>" - data-chat-options="<%= {showChannelsList:true, showTimestamp: true, activeChannelId: @active_channel.id }.to_json %>"> + data-chat-options="<%= {showChannelsList:true, showTimestamp: true, activeChannelId: @active_channel.id }.to_json %>" + data-twilio-token="<%= @twilio_token %>" + >
<% else %> <%= render "devise/registrations/registration_form" %> diff --git a/app/views/chat_channels/index.json.jbuilder b/app/views/chat_channels/index.json.jbuilder index fd747df2d..14b3141c9 100644 --- a/app/views/chat_channels/index.json.jbuilder +++ b/app/views/chat_channels/index.json.jbuilder @@ -1,3 +1,10 @@ -json.array! @chat_channels_memberships do |membership| +json.array! (@chat_channels_memberships.sort_by { |m| m.chat_channel.last_message_at}.reverse!) do |membership| + json.id membership.chat_channel.id + membership.chat_channel.current_user = current_user + json.slug membership.chat_channel.slug + json.channel_name membership.chat_channel.channel_name + json.channel_type membership.chat_channel.channel_type + json.last_opened_at membership.chat_channel.last_opened_at + json.last_message_at membership.chat_channel.last_message_at json.adjusted_slug membership.chat_channel.adjusted_slug(current_user) end diff --git a/config/routes.rb b/config/routes.rb index d0c7d6a86..7a6f9c6a8 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -50,7 +50,7 @@ Rails.application.routes.draw do post "/onboarding", to: "reactions#onboarding" end end - resources :users, only: [:index] do + resources :users, only: [:index, :show] do collection do get "/sidebar_suggestions", to: "users#sidebar_suggestions" end @@ -101,8 +101,8 @@ Rails.application.routes.draw do get "email_subscriptions/unsubscribe" post "/chat_channels/:id/moderate" => "chat_channels#moderate" post "/chat_channels/:id/open" => "chat_channels#open" - get "/gether" => "chat_channels#index" - get "/gether/:slug" => "chat_channels#index" + get "/connect" => "chat_channels#index" + get "/connect/:slug" => "chat_channels#index" post "/pusher/auth" => "pusher#auth" diff --git a/package.json b/package.json index 73f4705fd..f76c87408 100644 --- a/package.json +++ b/package.json @@ -84,6 +84,7 @@ "preact": "^8.2.5", "prop-types": "^15.6.0", "pusher-js": "^4.2.2", - "sendbird": "^3.0.52" + "sendbird": "^3.0.52", + "twilio-video": "^1.10.0" } } diff --git a/spec/requests/chat_channels_spec.rb b/spec/requests/chat_channels_spec.rb index ac211c338..1b38e070f 100644 --- a/spec/requests/chat_channels_spec.rb +++ b/spec/requests/chat_channels_spec.rb @@ -5,6 +5,12 @@ RSpec.describe "ChatChannels", type: :request do let(:test_subject) { create(:user) } let(:chat_channel) { create(:chat_channel) } + before do + sign_in user + chat_channel.add_users([user]) + end + + describe "GET /chat_channels/:id" do context "when request is valid" do before do diff --git a/yarn.lock b/yarn.lock index 7d607920e..fa9f8484a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -315,6 +315,14 @@ react-split-pane "^0.1.77" react-treebeard "^2.1.0" +"@twilio/sip.js@^0.7.7": + version "0.7.7" + resolved "https://registry.yarnpkg.com/@twilio/sip.js/-/sip.js-0.7.7.tgz#cff50f66f9b295d46f219737b72780c7d0e6df52" + +"@twilio/webrtc@github:twilio/twilio-webrtc.js#2.0.1-rc1": + version "2.0.1-rc1" + resolved "https://codeload.github.com/twilio/twilio-webrtc.js/tar.gz/3532058a5083e69b6ce1ae2984dd59ad5c864f29" + "@types/node@*": version "9.3.0" resolved "https://registry.yarnpkg.com/@types/node/-/node-9.3.0.tgz#3a129cda7c4e5df2409702626892cb4b96546dd5" @@ -718,6 +726,10 @@ async-foreach@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542" +async-limiter@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8" + async@^1.4.0, async@^1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" @@ -9424,6 +9436,15 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" +twilio-video@^1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/twilio-video/-/twilio-video-1.10.0.tgz#5345f7b664a815a0fc35b99820cdfe19984ae56f" + dependencies: + "@twilio/sip.js" "^0.7.7" + "@twilio/webrtc" twilio/twilio-webrtc.js#2.0.1-rc1 + ws "^3.3.1" + xmlhttprequest "^1.8.0" + type-check@~0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" @@ -9510,6 +9531,10 @@ uid-number@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" +ultron@~1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.1.tgz#9fe1536a10a664a65266a1e3ccf85fd36302bc9c" + union-value@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.0.tgz#5c71c34cb5bad5dcebe3ea0cd08207ba5aa1aea4" @@ -9944,6 +9969,14 @@ write@^0.2.1: dependencies: mkdirp "^0.5.1" +ws@^3.3.1: + version "3.3.3" + resolved "https://registry.yarnpkg.com/ws/-/ws-3.3.3.tgz#f1cf84fe2d5e901ebce94efaece785f187a228f2" + dependencies: + async-limiter "~1.0.0" + safe-buffer "~5.1.0" + ultron "~1.1.0" + xhr2@0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/xhr2/-/xhr2-0.1.4.tgz#7f87658847716db5026323812f818cadab387a5f"