Remove experimental /call functionality from Connect ✂️✂️✂️ (#12979)

* Remove Twilio gem

* remove VideoChatsController + views and specs

* Remove /call related code

* Update spec

* More JS cleanup

* Restore wrongly deleted files

* Remove obsolete files

* More cleanup
This commit is contained in:
Michael Kohl 2021-03-17 16:06:15 +00:00 committed by GitHub
parent 5bd87b93d6
commit 7c7a8dcd45
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 8 additions and 724 deletions

View file

@ -108,7 +108,6 @@ gem "store_attribute", "~> 0.8.1" # ActiveRecord extension which adds typecastin
gem "storext", "~> 3.3" # Add type-casting and other features on top of ActiveRecord::Store.store_accessor
gem "stripe", "~> 5.30" # Ruby library for the Stripe API
gem "strong_migrations", "~> 0.7" # Catch unsafe migrations
gem "twilio-ruby", "~> 5.48" # The official library for communicating with the Twilio REST API
gem "twitter", "~> 7.0" # A Ruby interface to the Twitter API
gem "uglifier", "~> 4.2" # Uglifier minifies JavaScript files
gem "ulid", "~> 1.3" # Universally Unique Lexicographically Sortable Identifier implementation for Ruby

View file

@ -808,10 +808,6 @@ GEM
thread_safe (0.3.6)
tilt (2.0.10)
timecop (0.9.4)
twilio-ruby (5.48.0)
faraday (>= 0.9, < 2.0)
jwt (>= 1.5, <= 2.5)
nokogiri (>= 1.6, < 2.0)
twitter (7.0.0)
addressable (~> 2.3)
buftok (~> 0.2.0)
@ -1028,7 +1024,6 @@ DEPENDENCIES
strong_migrations (~> 0.7)
test-prof (~> 1.0)
timecop (~> 0.9)
twilio-ruby (~> 5.48)
twitter (~> 7.0)
uglifier (~> 4.2)
ulid (~> 1.3)

View file

@ -1,16 +0,0 @@
class TwilioTokensController < ApplicationController
after_action :verify_authorized
def show
video_channel = params[:id].split("private-video-channel-")[1] if params[:id].start_with?("private-video-channel-")
unless video_channel
skip_authorization
render json: { status: "failure", token: @twilio_token }, status: :not_found
return
end
@chat_channel = ChatChannel.find(video_channel.to_i)
authorize @chat_channel # show pundit method for chat_channel_policy works here, should always check though
@twilio_token = Twilio::GetJwtToken.call(current_user, params[:id])
render json: { status: "success", token: @twilio_token }, status: :ok
end
end

View file

@ -1,44 +0,0 @@
class VideoChatsController < ApplicationController
before_action :authenticate_user!
after_action :verify_authorized
def show
@chat_channel = ChatChannel.find(params[:id]) || not_found
authorize @chat_channel
account_sid = ApplicationConfig["TWILIO_ACCOUNT_SID"]
api_key = ApplicationConfig["TWILIO_VIDEO_API_KEY"]
api_secret = ApplicationConfig["TWILIO_VIDEO_API_SECRET"]
@username = display_username
@video_type = video_type
token = Twilio::JWT::AccessToken.new(
account_sid,
api_key,
api_secret,
[],
identity: @username,
)
grant = Twilio::JWT::AccessToken::VideoGrant.new
grant.room = params[:id]
token.add_grant(grant)
@token = token.to_jwt
end
private
def display_username
return "@#{params[:username]}" if params[:username] && Rails.env.development? # simpler solo testing in dev
"@#{current_user.username}"
end
def video_type
if @chat_channel.channel_type == "direct" || @chat_channel.chat_channel_memberships.size < 5
"peer-to-peer"
else
"group"
end
end
end

View file

@ -1,77 +0,0 @@
import { h } from 'preact';
import { axe } from 'jest-axe';
import { render } from '@testing-library/preact';
import { VideoContent } from '../videoContent';
describe('<VideoContent />', () => {
it('should have no a11y violations', async () => {
const { container } = render(
<VideoContent
videoPath="/some-video-path"
fullscreen="false"
onTriggerVideoContent={jest.fn()}
/>,
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it('should render in fullscreen', () => {
const { queryByLabelText } = render(
<VideoContent
videoPath="/some-video-path"
fullscreen
onTriggerVideoContent={jest.fn()}
/>,
);
expect(queryByLabelText('Leave fullscreen')).toBeNull();
expect(queryByLabelText('Fullscreen')).toBeDefined();
});
it('should not render in fullscreen', () => {
const { queryByLabelText } = render(
<VideoContent
videoPath="/some-video-path"
fullscreen={false}
onTriggerVideoContent={jest.fn()}
/>,
);
expect(queryByLabelText('Fullscreen')).toBeNull();
expect(queryByLabelText('Leave fullscreen')).toBeDefined();
});
it('should trigger video content when clicked', () => {
const onTriggerVideoContent = jest.fn();
const { getByTestId } = render(
<VideoContent
videoPath="/some-video-path"
fullscreen
onTriggerVideoContent={onTriggerVideoContent}
/>,
);
getByTestId('connect-video').click();
expect(onTriggerVideoContent).toHaveBeenCalledTimes(1);
});
it('should load the given video', () => {
const onTriggerVideoContent = jest.fn();
const { getByTitle } = render(
<VideoContent
videoPath="/some-video-path"
fullscreen
onTriggerVideoContent={onTriggerVideoContent}
/>,
);
const videoFrame = getByTitle('Video display');
expect(videoFrame.getAttribute('src')).toEqual('/some-video-path');
});
});

View file

@ -36,7 +36,6 @@ import { Compose } from './compose';
import { Message } from './message';
import { ActionMessage } from './actionMessage';
import { Content } from './content';
import { VideoContent } from './videoContent';
import { DragAndDropZone } from '@utilities/dragAndDrop';
import { dragAndUpload } from '@utilities/dragAndUpload';
import { Button } from '@crayons';
@ -83,7 +82,6 @@ export class Chat extends Component {
notificationsPermission: null,
activeContent: {},
fullscreenContent: null,
videoPath: null,
expanded: window.innerWidth > NARROW_WIDTH_LIMIT,
isMobileDevice: typeof window.orientation !== 'undefined',
subscribedPusherChannels: [],
@ -715,14 +713,6 @@ export class Chat extends Component {
// should check if user has the privilege
if (message.startsWith('/code')) {
this.setActiveContentState(activeChannelId, { type_of: 'code_editor' });
} else if (message.startsWith('/call')) {
const messageObject = {
activeChannelId,
message: '/call',
mentionedUsersId: this.getMentionedUsers(message),
};
this.setState({ videoPath: `/video_chats/${activeChannelId}` });
sendMessage(messageObject, this.handleSuccess, this.handleFailure);
} else if (message.startsWith('/play ')) {
const messageObject = {
activeChannelId,
@ -1008,17 +998,6 @@ export class Chat extends Component {
path: `/chat_channel_memberships/${activeChannel.id}/edit`,
type_of: 'article',
});
} else if (content.startsWith('sidecar-content-plus-video')) {
this.setActiveContentState(activeChannelId, {
type_of: 'loading-post',
});
this.setActiveContent({
path: target.href || target.parentElement.href,
type_of: 'article',
});
this.setState({ videoPath: `/video_chats/${activeChannelId}` });
} else if (content.startsWith('sidecar-video')) {
this.setState({ videoPath: target.href || target.parentElement.href });
} else if (
content.startsWith('sidecar') ||
content.startsWith('article')
@ -1596,11 +1575,6 @@ export class Chat extends Component {
fullscreen={state.fullscreenContent === 'sidecar'}
closeReportAbuseForm={this.closeReportAbuseForm}
/>
<VideoContent
videoPath={state.videoPath}
onTriggerVideoContent={this.onTriggerVideoContent}
fullscreen={state.fullscreenContent === 'video'}
/>
</div>
);
};
@ -1626,23 +1600,6 @@ export class Chat extends Component {
}
};
onTriggerVideoContent = (e) => {
if (e.target.dataset.content === 'exit') {
this.setState({
videoPath: null,
fullscreenContent: null,
expanded: window.innerWidth > 600,
});
} else if (this.state.fullscreenContent === 'video') {
this.setState({ fullscreenContent: null });
} else {
this.setState({
fullscreenContent: 'video',
expanded: window.innerWidth > WIDE_WIDTH_LIMIT,
});
}
};
handleMention = (e) => {
const { activeChannel } = this.state;
const mention = e.keyCode === 64;
@ -2010,12 +1967,10 @@ export class Chat extends Component {
data-testid="chat"
className={`chat chat--expanded
chat--${
state.videoPath ? 'video-visible' : 'video-not-visible'
} chat--${
state.activeContent[state.activeChannelId]
? 'content-visible'
: 'content-not-visible'
} ${fullscreenMode}`}
state.activeContent[state.activeChannelId]
? 'content-visible'
: 'content-not-visible'
} ${fullscreenMode}`}
data-no-instant
aria-expanded={state.expanded}
>

View file

@ -14,7 +14,7 @@ const smartSvgIcon = (content, d) => (
viewBox="0 0 24 24"
width="24"
height="24"
style={{ marginLeft:'-12px', marginTop:'-4px' }}
style={{ marginLeft: '-12px', marginTop: '-4px' }}
>
<path data-content={content} fill="none" d="M0 0h24v24H0z" />
<path data-content={content} d={d} />
@ -75,7 +75,7 @@ export class Content extends Component {
type="button"
className="activechatchannel__activecontentexitbutton activechatchannel__activecontentexitbutton--fullscreen crayons-btn crayons-btn--secondary"
data-content="fullscreen"
style={{ left: "-80px", marginLeft: "0px" }}
style={{ left: '-80px', marginLeft: '0px' }}
title="fullscreen"
>
{' '}

View file

@ -1,70 +0,0 @@
import { h } from 'preact';
const smartSvgIcon = (content, d) => (
<svg
data-content={content}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
>
<path data-content={content} fill="none" d="M0 0h24v24H0z" />
<path data-content={content} d={d} />
</svg>
);
/**
* Displays video in Connect.
*
* @param {string} props.videoPath The URL of a video
* @param {function} props.onTriggerVideoContent Fires when a video is clicked on
* @param {boolean} props.fullscreen Whether or not to show the video in fullscreen
*
* @returns A video component
*/
export function VideoContent(props) {
const { videoPath, onTriggerVideoContent, fullscreen } = props;
if (!videoPath) {
return null;
}
return (
<div
data-testid="connect-video"
role="presentation"
className="activechatchannel__activecontent activechatchannel__activecontent--video"
id="chat_activecontent_video"
onClick={onTriggerVideoContent}
>
<button
aria-label="Exit"
className="activechatchannel__activecontentexitbutton crayons-btn crayons-btn--secondary"
data-content="exit"
>
{smartSvgIcon(
'exit',
'M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636z',
)}
</button>
<button
aria-label={fullscreen ? 'Fullscreen' : 'Leave fullscreen'}
className="activechatchannel__activecontentexitbutton activechatchannel__activecontentexitbutton--fullscreen crayons-btn crayons-btn--secondary"
data-content="fullscreen"
style={{ left: '-80px', marginLeft:'0px' }}
>
{fullscreen
? smartSvgIcon(
'fullscreen',
'M18 7h4v2h-6V3h2v4zM8 9H2V7h4V3h2v6zm10 8v4h-2v-6h6v2h-4zM8 15v6H6v-4H2v-2h6z',
)
: smartSvgIcon(
'fullscreen',
'M20 3h2v6h-2V5h-4V3h4zM4 3h4v2H4v4H2V3h2zm16 16v-4h2v6h-6v-2h4zM4 19h4v2H2v-6h2v4z',
)}
</button>
<iframe title="Video display" src={videoPath} />
</div>
);
}

View file

@ -1,130 +0,0 @@
const { createLocalVideoTrack, connect } = require('twilio-video');
const root = document.getElementById('videochat');
const remoteMedia = document.getElementById('remote-media');
const muteButton = document.getElementById('mute-toggle');
const videoToggleButton = document.getElementById('videohide-toggle');
const localMediaContainer = document.getElementById('local-media');
const roomType = document.getElementById('room-type').dataset.type; //Group
connect(root.dataset.token, {
name: 'room-name',
audio: true,
type: roomType,
video: { width: 800 },
}).then((room) => {
room.participants.forEach(participantConnected);
room.on('participantConnected', participantConnected);
room.on('participantDisconnected', participantDisconnected);
room.once('disconnected', (_error) =>
room.participants.forEach(participantDisconnected),
);
muteButton.onclick = function (e) {
e.preventDefault();
room.localParticipant.audioTracks.forEach(function (trackPub) {
if (muteButton.dataset.muted === 'true') {
muteButton.dataset.muted = 'false';
muteButton.classList.remove('crayons-btn--danger');
muteButton.innerHTML = 'Mute';
trackPub.track.enable();
} else {
muteButton.dataset.muted = 'true';
muteButton.classList.add('crayons-btn--danger');
muteButton.innerHTML = 'Unmute';
trackPub.track.disable();
}
});
};
videoToggleButton.onclick = function (e) {
e.preventDefault();
room.localParticipant.videoTracks.forEach(function (trackPub) {
if (videoToggleButton.dataset.hidden === 'true') {
videoToggleButton.dataset.hidden = 'false';
videoToggleButton.classList.remove('crayons-btn--danger');
videoToggleButton.innerHTML = 'Hide';
localMediaContainer.classList.remove('video-hidden');
trackPub.track.enable();
} else {
videoToggleButton.dataset.hidden = 'true';
videoToggleButton.classList.add('crayons-btn--danger');
videoToggleButton.innerHTML = 'Unhide';
localMediaContainer.classList.add('video-hidden');
trackPub.track.disable();
}
});
};
});
createLocalVideoTrack().then((track) => {
localMediaContainer.appendChild(track.attach());
document.getElementById('video-controls').classList.add('showing');
});
function participantConnected(participant) {
const numExistingDivs = document.getElementsByClassName('individual-video')
.length;
const div = document.createElement('div');
div.id = participant.sid;
div.className = `individual-video${
numExistingDivs > 3 ? ' one-of-many-videos' : ''
}`;
div.innerHTML = `<div class="participant-info">\
<div class="participant-name">${participant.identity}</div>\
<div class="disabled-audio-indicator">audio off</div>\
<div class="disabled-video-indicator">video off</div>\
</div>`;
participant.on('trackSubscribed', (track) => trackSubscribed(div, track));
participant.on('trackUnsubscribed', trackUnsubscribed);
participant.on('trackDisabled', (track) => trackDisabled(div, track));
participant.on('trackEnabled', (track) => trackEnabled(div, track));
participant.tracks.forEach((publication) => {
if (publication.isSubscribed) {
trackSubscribed(div, publication.track);
}
});
remoteMedia.appendChild(div);
}
function participantDisconnected(participant) {
document.getElementById(participant.sid).remove();
}
function trackSubscribed(div, track) {
div.appendChild(track.attach());
if (!track.isEnabled) {
if (track.kind === 'video') {
div.classList.add('disabled-video');
} else {
div.classList.add('disabled-audio');
}
}
}
function trackUnsubscribed(track) {
track.detach().forEach((element) => element.remove());
}
function trackDisabled(div, track) {
if (track.kind === 'video') {
div.classList.add('disabled-video');
} else {
div.classList.add('disabled-audio');
}
}
function trackEnabled(div, track) {
if (track.kind === 'video') {
div.classList.remove('disabled-video');
} else {
div.classList.remove('disabled-audio');
}
}
if (navigator.userAgent === 'DEV-Native-ios') {
root.innerHTML =
'<h2 class="platform-unavailable-message">This feature is not yet available on iPhone</h2>';
}

View file

@ -158,14 +158,6 @@ class Message < ApplicationRecord
# rubocop:disable Rails/OutputSafety
def handle_slash_command(html)
response = case html.to_s.strip
when "<p>/call</p>"
"<a href='/video_chats/#{chat_channel_id}'
class='chatchannels__richlink chatchannels__richlink--base'
target='_blank' rel='noopener' data-content='sidecar-video'>
<h1 data-content='sidecar-video'>
Let's video chat 😄
</h1>
</a>".html_safe
when "<p>/play codenames</p>" # proof of concept
"<a href='https://www.horsepaste.com/connect-channel-#{rand(1_000_000_000)}'
class='chatchannels__richlink chatchannels__richlink--base'

View file

@ -1,19 +0,0 @@
module Twilio
class GetJwtToken
def self.call(user, room_name)
token = Twilio::JWT::AccessToken.new(
ApplicationConfig["TWILIO_ACCOUNT_SID"],
ApplicationConfig["TWILIO_VIDEO_API_KEY"],
ApplicationConfig["TWILIO_VIDEO_API_SECRET"],
[],
identity: user.id,
)
grant = Twilio::JWT::AccessToken::VideoGrant.new
grant.room = room_name
token.add_grant(grant)
token.to_jwt
end
end
end

View file

@ -1,171 +0,0 @@
<style>
body {
height: 100vh;
overflow: hidden;
}
.video-chat-wrapper {
position: relative;
height: 100vh;
overflow: scroll;
}
.individual-video video {
max-height: 100vh;
background: #232324;
width: 100%;
}
#remote-media {
display:flex;
flex-wrap: wrap;
/* flex-direction:row; */
}
.individual-video {
overflow: hidden;
position: relative;
margin-bottom: -6px;
flex: 1 0 21%;
text-align: center;
min-width:20%;
background: #232324;
}
.one-of-many-videos {
min-width:33%;
}
/* .individual-video:last-child {
margin-bottom: 100px;
} */
#local-media video {
width: 150px;
position: fixed;
bottom: 10px;
right: 10px;
z-index: 5;
border-radius:3px;
border: 1px solid #232324;
}
#local-media.video-hidden {
opacity: 0.1;
}
.participant-info {
padding: 3px 8px;
border-radius: 3px;
position: absolute;
bottom: 0;
left: 0;
right: 0;
font-size: 13px;
text-align: center;
}
.participant-name {
display: inline-block;
color: white;
background: #171717;
padding: 4px 12px;
border-radius: 100px;
margin: 8px;
}
.video-controls {
position: fixed;
bottom: 15px;
left: 5px;
width: calc(100% - 175px);
z-index: 10;
display: none;
}
.showing {
display: block;
}
.video-controls button {
font-size: 15px;
width: 100%;
margin: 3px 0px;
padding: 3px 0px;
}
.disabled-video video {
opacity: 0.1;
}
.disabled-audio-indicator,.disabled-video-indicator {
background: red;
color: white;
display: none;
position: absolute;
border-radius: 100px;
padding: 4px 10px;
right: 5px;
bottom: 12px;
}
.disabled-audio-indicator {
right: auto;
left: 5px;
}
.disabled-video .disabled-video-indicator{
display: inline-block;
}
.disabled-audio .disabled-audio-indicator{
display: inline-block;
}
.platform-unavailable-message {
padding: 20px;
color: white;
margin-top: 30px;
}
@media screen and (max-width: 400px) {
.individual-video {
flex: 1 0 100%;
width: 100%;
}
}
@media screen and (max-width: 280px) {
.video-controls {
width: 92%;
bottom: 5px;
}
#local-media video {
width: 90%;
bottom: 85px;
}
}
@media screen and (min-width: 500px) {
.video-controls {
left: auto;
right: 170px;
width: 200px;
}
}
@media screen and (max-height: 400px) {
.video-controls {
width: 90px;
}
#local-media video {
width: 90px;
bottom: 5px;
}
}
</style>
<div class="video-chat-wrapper" id="videochat" data-token="<%= @token %>" data-channel="<%= params[:id] %>" data-user="<%= @username %>">
<div id="room-type" data-type="<%= @video_type %>"></div>
<div id="remote-media"></div>
<div id="local-media"></div>
<div id="video-controls" class="video-controls">
<button id="mute-toggle" data-muted="false" class="crayons-btn crayons-btn--secondary">Mute</button>
<button id="videohide-toggle" data-hidden="false" class="crayons-btn crayons-btn--secondary">Hide</button>
</div>
<%# TODO Make paramsp[:id] map to a chat channel and ensure the users have permission %>
<%# That part is pretty straightforward %>
<%# Then close the loop on the JavaScript part of it. %>
<%# Also should be *fairly* straightforward in terms of following a basic tutorial. %>
<%# Then we drop a link in chat rather than trying to make video work with complicated state in chat. %>
<%# The complicated part has always been chat state/channels etc. Doing in sidecar means more containerization of logic. %>
<%= javascript_packs_with_chunks_tag "videoChat", defer: true %>

View file

@ -95,7 +95,8 @@ Rails.application.routes.draw do
get "/badges/badge_achievements", to: redirect("/admin/badge_achievements")
get "/badges/badge_achievements/award_badges", to: redirect("/admin/badge_achievements/award_badges")
# NOTE: @ridhwana These routes below will be deleted once we remove the admin_restructure feature flag, hence they've been regrouped them in this manner.
# NOTE: @ridhwana These routes below will be deleted once we remove the
# admin_restructure feature flag, hence they've been regrouped in this manner.
resources :articles, only: %i[index show update]
resources :badges, only: %i[index edit update new create]
resources :badge_achievements, only: %i[index destroy]

View file

@ -177,7 +177,6 @@
"pusher-js": "^7.0.3",
"rails-erb-loader": "^5.5.2",
"stimulus": "^2.0.0",
"twilio-video": "^2.13.0",
"web-share-wrapper": "^0.2.1"
},
"resolutions": {

View file

@ -68,13 +68,6 @@ RSpec.describe Message, type: :model do
expect(message.message_html).to include("sidecar-user")
end
it "creates rich call link" do
message.message_markdown = "/call"
message.validate!
expect(message.message_html).to include("sidecar-video")
end
it "creates rich embeddable link" do
message.message_markdown = "https://docs.google.com/ https://www.figma.com/file/"
message.validate!

View file

@ -1,35 +0,0 @@
require "rails_helper"
RSpec.describe "TwilioTokens", type: :request do
let(:user) { create(:user) }
let(:chat_channel) { create(:chat_channel) }
before do
sign_in user
end
describe "GET /twilio_tokens/:id" do
it "returns a token for member of a channel" do
chat_channel.add_users [user]
get "/twilio_tokens/private-video-channel-#{chat_channel.id}"
expect(response.status).to eq(200)
end
it "returns not found if unknown ID" do
chat_channel.add_users [user]
get "/twilio_tokens/sddds3423443efrwdfsd"
expect(response.status).to eq(404)
end
it "returns not found if wrong ID prefix" do
chat_channel.add_users [user]
get "/twilio_tokens/sddds3423443efrwdfsd-#{chat_channel.id}"
expect(response.status).to eq(404)
end
# it "returns unauthorized if user not member of channel" do
# expect { get "/twilio_tokens/private-video-channel-#{chat_channel.id}" }.
# to raise_error(Pundit::NotAuthorizedError)
# end
end
end

View file

@ -1,33 +0,0 @@
require "rails_helper"
RSpec.describe "VideoChats", type: :request do
let(:user) { create(:user) }
let(:second_user) { create(:user) }
let(:third_user) { create(:user) }
describe "GET /video_chats/:id" do
context "with user signed in" do
before do
sign_in user
end
it "displays basic html for working" do
channel = ChatChannels::CreateWithUsers.call(users: [user, second_user])
get "/video_chats/#{channel.id}"
expect(response.body).to include("<div class=\"video-chat-wrapper")
end
it "disallows unauthorized user" do
channel = ChatChannels::CreateWithUsers.call(users: [second_user, third_user])
expect { get "/video_chats/#{channel.id}" }.to raise_error(Pundit::NotAuthorizedError)
end
end
context "without user signed in" do
it "asks to sign in" do
get "/video_chats/1"
expect(response).to redirect_to("/enter")
end
end
end
end

View file

@ -1,9 +0,0 @@
require "rails_helper"
RSpec.describe Twilio::GetJwtToken, type: :service do
let(:user) { create(:user) }
it "returns a token" do
expect(described_class.call(user, "hello")).to be_present
end
end

Binary file not shown.

View file

@ -3068,11 +3068,6 @@
resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.1.1.tgz#3348564048e7a2d7398c935d466c0414ebb6a669"
integrity sha512-Z6DoceYb/1xSg5+e+ZlPZ9v0N16ZvZ+wYMraFue4HYrE4ttONKtsvruIRf6t9TBR0YvSOfi1hUU0fJfBLCDYow==
"@twilio/webrtc@4.3.2":
version "4.3.2"
resolved "https://registry.yarnpkg.com/@twilio/webrtc/-/webrtc-4.3.2.tgz#919520a870fdf8d87f915da47276710828e8fc5b"
integrity sha512-3gIq7XDI3vBoikRBchCnocFmlRQt45KC7v0DKkat+TV6WBXYJx+Hub22PR18PAROkwl5dAmIziByjYWWVhCHgA==
"@types/anymatch@*":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@types/anymatch/-/anymatch-1.3.1.tgz#336badc1beecb9dacc38bea2cf32adf627a8421a"
@ -5297,13 +5292,6 @@ babylon@^6.18.0:
resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3"
integrity sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==
backoff@^2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/backoff/-/backoff-2.5.0.tgz#f616eda9d3e4b66b8ca7fca79f695722c5f8e26f"
integrity sha1-9hbtqdPktmuMp/ynn2lXIsX44m8=
dependencies:
precond "0.2"
backslash@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/backslash/-/backslash-0.2.0.tgz#6c3c1fce7e7e714ccfc10fd74f0f73410677375f"
@ -15342,11 +15330,6 @@ preact@^10.5.13:
resolved "https://registry.yarnpkg.com/preact/-/preact-10.5.13.tgz#85f6c9197ecd736ce8e3bec044d08fd1330fa019"
integrity sha512-q/vlKIGNwzTLu+jCcvywgGrt+H/1P/oIRSD6mV4ln3hmlC+Aa34C7yfPI4+5bzW8pONyVXYS7SvXosy2dKKtWQ==
precond@0.2:
version "0.2.3"
resolved "https://registry.yarnpkg.com/precond/-/precond-0.2.3.tgz#aa9591bcaa24923f1e0f4849d240f47efc1075ac"
integrity sha1-qpWRvKokkj8eD0hJ0kD0fvwQdaw=
prelude-ls@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
@ -18793,16 +18776,6 @@ tweetnacl@^1.0.3:
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.3.tgz#ac0af71680458d8a6378d0d0d050ab1407d35596"
integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==
twilio-video@^2.13.0:
version "2.13.0"
resolved "https://registry.yarnpkg.com/twilio-video/-/twilio-video-2.13.0.tgz#cf3b5f2b20344724f4950d16357c60b07ba53ea9"
integrity sha512-s+lU6a1eL3zmropDDQu0LaqmBEasrZ34Pr7vftUBsy23oYApEhby54XvPb2+aWkM0vvoOez32jLRjE2YgwQ38A==
dependencies:
"@twilio/webrtc" "4.3.2"
backoff "^2.5.0"
ws "^3.3.1"
xmlhttprequest "^1.8.0"
type-check@^0.4.0, type-check@~0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
@ -18880,11 +18853,6 @@ uglify-js@^3.1.4:
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.12.0.tgz#b943f129275c41d435eb54b643bbffee71dccf57"
integrity sha512-8lBMSkFZuAK7gGF8LswsXmir8eX8d2AAMOnxSDWjKBx/fBR6MypQjs78m6ML9zQVp1/hD4TBdfeMZMC7nW1TAA==
ultron@~1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.1.tgz#9fe1536a10a664a65266a1e3ccf85fd36302bc9c"
integrity sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==
unfetch@^4.0.1, unfetch@^4.1.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/unfetch/-/unfetch-4.2.0.tgz#7e21b0ef7d363d8d9af0fb929a5555f6ef97a3be"
@ -19750,15 +19718,6 @@ write-file-atomic@^3.0.0:
signal-exit "^3.0.2"
typedarray-to-buffer "^3.1.5"
ws@^3.3.1:
version "3.3.3"
resolved "https://registry.yarnpkg.com/ws/-/ws-3.3.3.tgz#f1cf84fe2d5e901ebce94efaece785f187a228f2"
integrity sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==
dependencies:
async-limiter "~1.0.0"
safe-buffer "~5.1.0"
ultron "~1.1.0"
ws@^5.2.0:
version "5.2.2"
resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.2.tgz#dffef14866b8e8dc9133582514d1befaf96e980f"
@ -19793,11 +19752,6 @@ xmlchars@^2.2.0:
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
xmlhttprequest@^1.8.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz#67fe075c5c24fef39f9d65f5f7b7fe75171968fc"
integrity sha1-Z/4HXFwk/vOfnWX197f+dRcZaPw=
xregexp@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-2.0.0.tgz#52a63e56ca0b84a7f3a5f3d61872f126ad7a5943"