Add content sidecar for chat (#413)

* Rearchitect chat channel subscriptions

* Add browser push notifications (attempt)

* Add content sidecar for chat

* Add live code basics

* Add content features to chat

* Fix chat_channels test
This commit is contained in:
Ben Halpern 2018-06-11 11:00:51 -04:00 committed by GitHub
parent 7bc3e60283
commit fb20b8adf7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 480 additions and 55 deletions

View file

@ -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"

View file

@ -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)

View file

@ -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%;
}
}

View file

@ -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

View file

@ -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 ||

View file

@ -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

View file

@ -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

View file

@ -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);
}

View file

@ -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 => (
<Message
user={message.username}
userID={message.user_id}
profileImageUrl={message.profile_image_url}
message={message.message}
messageColor={message.messageColor}
timestamp={showTimestamp ? message.timestamp : null}
color={message.color}
type={message.type}
onContentTrigger={this.triggerActiveContent}
/>
));
};
@ -276,19 +316,27 @@ export default class Chat extends Component {
renderActiveChatChannel = () => (
<div className="activechatchannel">
<div className="activechatchannel__messages" id="messagelist">
{this.renderMessage()}
<div className="messagelist__sentinel" id="messagelist__sentinel" />
<div className="activechatchannel__conversation">
<div className="activechatchannel__messages" id="messagelist">
{this.renderMessages()}
<div className="messagelist__sentinel" id="messagelist__sentinel" />
</div>
<div className="activechatchannel__alerts">
<Alert showAlert={this.state.showAlert} />
</div>
<div className="activechatchannel__form">
<Compose
handleSubmitOnClick={this.handleSubmitOnClick}
handleKeyDown={this.handleKeyDown}
/>
</div>
</div>
<div className="activechatchannel__alerts">
<Alert showAlert={this.state.showAlert} />
</div>
<div className="activechatchannel__form">
<Compose
handleKeyDown={this.handleKeyDown}
handleSubmitOnClick={this.handleSubmitOnClick}
<Content
resource={this.state.activeContent}
onExit={this.triggerActiveContent}
activeChannelId={this.state.activeChannelId}
pusherKey={this.props.pusherKey}
/>
</div>
</div>
);

View file

@ -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 <div id="codeeditor" className="chatcodeeditor"></div>
}
}

View file

@ -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 (
<div className="activechatchannel__activecontent">
<button
class="activechatchannel__activecontentexitbutton"
onClick={this.props.onExit}
data-content="exit"
>×</button>
{display(this.props)}
</div>
);
}
}
}
function display(props) {
if (props.resource.type_of === "loading") {
return <div style={{height: "210px",
width: "210px",
margin:" 15px auto",
display: "block",
borderRadius: "500px",
backgroundColor: "#f5f6f7"}}></div>
} else if (props.resource.type_of === "user") {
return <div><img
src={props.resource.profile_image}
style={{height: "210px",
width: "210px",
margin:" 15px auto",
display: "block",
borderRadius: "500px"}} />
<h1 style={{textAlign: "center"}}>{props.resource.name}</h1>
<div style={{fontStyle: "italic"}}>
{props.resource.summary}
</div>
</div>
} else if (props.resource.type_of === "article") {
console.log(props.resource)
return (
<div class="container">
<div class="title">
<h1>{props.resource.title}</h1>
<h3>
<a href={'/'+props.resource.user.username} class="author">
<img class="profile-pic" src={props.resource.user.profile_image_90} alt={props.resource.user.username}/>
<span itemprop="name">{props.resource.user.name}</span>
</a>
</h3>
</div>
<div class="body">
<div dangerouslySetInnerHTML={{__html: props.resource.body_html}} ></div>
</div>
</div>)
} else if (props.resource.type_of === "code_editor") {
return <CodeEditor
activeChannelId={props.activeChannelId}
pusherKey={props.pusherKey}
/>
}
}

View file

@ -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}}
></span>
);
return (
<div className="chatmessage">
<div className="chatmessage__body">
<a href={`/${user}`} target="_blank">
<div className="chatmessage__body" onClick={onContentTrigger}>
<a
href={`/${user}`}
target="_blank"
data-content={`users/${userID}`}
onClick={onContentTrigger}
>
<img
className="chatmessagebody__profileimage"
src={profileImageUrl}
alt={`${user} profile`}
data-content={`users/${userID}`}
onClick={onContentTrigger}
/>
</a>
<span className="chatmessagebody__username" style={spanStyle}>
@ -41,6 +51,8 @@ const Message = ({
className="chatmessagebody__username--link"
href={`/${user}`}
target="_blank"
data-content={`users/${userID}`}
onClick={onContentTrigger}
>
{user}
</a>
@ -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 = {

View file

@ -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 <a
href={"/gether/"+channel.adjusted_slug}
href={"/connect/"+channel.adjusted_slug}
style={{
background: "#66e2d5",
color: "black",
@ -79,7 +79,7 @@ class UnopenedChannelNotice extends Component {
}
export default function getUnopenedChannels(user, successCb) {
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
}
fetch('/chat_channels?state=unopened', {

View file

@ -11,8 +11,11 @@ export default function setupPusher(key, callbackObjects) {
cluster: 'us2',
encrypted: true,
});
pusher.unsubscribe(callbackObjects.channelId.toString());
const channel = pusher.subscribe(callbackObjects.channelId.toString());
channel.bind('message-created', callbackObjects.messageCreated);
channel.bind('channel-cleared', callbackObjects.channelCleared);
channel.bind('user-banned', callbackObjects.redactUserMessages);
}
channel.bind('client-livecode', callbackObjects.liveCoding);
return channel;
}

View file

@ -47,6 +47,8 @@ class CacheBuster
end
bust_home_pages(article)
bust_tag_pages(article)
bust("/api/articles/#{article.id}")
bust("/api/articles/by_path?url=#{article.path}")
if article.collection
article.collection.articles.each do |a|

31
app/labor/twilio_token.rb Normal file
View file

@ -0,0 +1,31 @@
class TwilioToken
attr_accessor :user
def initialize(user)
@user = user
end
def get
require 'twilio-ruby'
# Replace with ENV vars
account_sid = 'AC1403f2c95cd4912bcb13e1fdc893aef1'
api_key = 'SK02d79fd84d84df170adce9a95f826c0d'
api_secret = 'MQC9HvLDMbwyQTJNs3x1IHNodidq2Zqf'
token = Twilio::JWT::AccessToken.new(
account_sid,
api_key,
api_secret,
[],
identity: user.id
)
grant = Twilio::JWT::AccessToken::VideoGrant.new
grant.room = 'DailyStandup'
token.add_grant(grant)
token.to_jwt
end
end

View file

@ -0,0 +1,11 @@
json.type_of "user"
json.username @user.username
json.name @user.name
json.summary @user.summary
json.twitter_username @user.twitter_username
json.github_username @user.github_username
json.website_url @user.name
json.name @user.name
json.name @user.name
json.name @user.name
json.profile_image ProfileImage.new(@user).get(320)

View file

@ -3,6 +3,10 @@
<% title "DEV Chat" %>
<link rel="canonical" href="https://dev.to/chat"/>
<meta name="description" content="DEV Chat">
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.38.0/codemirror.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.38.0/codemirror.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.38.0/theme/material.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.38.0/mode/javascript/javascript.min.js"></script>
<% 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 %>"
>
</div>
<% else %>
<%= render "devise/registrations/registration_form" %>

View file

@ -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

View file

@ -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"

View file

@ -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"
}
}

View file

@ -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

View file

@ -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"