mirror of
				https://github.com/beatriceo/polyglot.git
				synced 2025-10-30 12:02:10 +00:00 
			
		
		
		
	| @@ -17,6 +17,7 @@ let localstream; | ||||
|  | ||||
| window.onload = () => { | ||||
|   currentUser = document.getElementById("current-user").innerHTML; | ||||
|   console.log(currentUser) | ||||
|   localVideo = document.getElementById("local-video"); | ||||
|   remoteVideoContainer = document.getElementById("remote-video-container"); | ||||
| }; | ||||
| @@ -40,17 +41,49 @@ document.onreadystatechange = async () => { | ||||
|   } | ||||
| }; | ||||
|  | ||||
| // find chatroom | ||||
| // const handleJoinSession = async () => { | ||||
| //   App.session = await App.cable.subscriptions.create("VideoSessionChannel", { | ||||
| //     connected: () => { | ||||
| //       broadcastData({ | ||||
| //         type: JOIN_ROOM, | ||||
| //         from: currentUser | ||||
| //       }); | ||||
| //     }, | ||||
| //     received: data => { | ||||
| //       console.log("received", data); | ||||
| //       if (data.from === currentUser) return; | ||||
| //       switch (data.type) { | ||||
| //         case JOIN_ROOM: | ||||
| //           return joinRoom(data); | ||||
| //         case EXCHANGE: | ||||
| //           if (data.to !== currentUser) return; | ||||
| //           return exchange(data); | ||||
| //         case REMOVE_USER: | ||||
| //           return removeUser(data); | ||||
| //         default: | ||||
| //           return; | ||||
| //       } | ||||
| //     } | ||||
| //   }); | ||||
| // }; | ||||
| const chatroomId = document.getElementById('chatroom-hook').dataset["chatroomId"] | ||||
|  | ||||
| const handleJoinSession = async () => { | ||||
|   App.session = await App.cable.subscriptions.create("VideoSessionChannel", { | ||||
|   App['chatroom' + chatroomId] = await App.cable.subscriptions.create({ | ||||
|     channel: "ChatRoomsChannel", | ||||
|     room: chatroomId | ||||
|   }, { | ||||
|     connected: () => { | ||||
|       console.log(chatroomId) | ||||
|       broadcastData({ | ||||
|         type: JOIN_ROOM, | ||||
|         from: currentUser | ||||
|         from: currentUser, | ||||
|         room: chatroomId | ||||
|       }); | ||||
|     }, | ||||
|     received: data => { | ||||
|       console.log("received", data); | ||||
|       console.log(data) | ||||
|       if (data.from === currentUser) return; | ||||
|       switch (data.type) { | ||||
|         case JOIN_ROOM: | ||||
| @@ -88,7 +121,6 @@ const joinRoom = data => { | ||||
| }; | ||||
|  | ||||
| const removeUser = data => { | ||||
|   console.log("removing user", data.from); | ||||
|   let video = document.getElementById(`remoteVideoContainer+${data.from}`); | ||||
|   video && video.remove(); | ||||
|   delete pcPeers[data.from]; | ||||
| @@ -96,18 +128,21 @@ const removeUser = data => { | ||||
|  | ||||
|  | ||||
| const broadcastData = data => { | ||||
|   fetch("sessions", { | ||||
|   if (data.type === EXCHANGE) { | ||||
|     console.log("yayyy") | ||||
|   } | ||||
|   fetch("chat_room_sessions", { | ||||
|     method: "POST", | ||||
|     body: JSON.stringify(data), | ||||
|     headers: { "content-type": "application/json" } | ||||
|     headers: { "content-type": "application/json", "X-CSRF-Token": document.querySelector('meta[name=csrf-token]').content } | ||||
|   }); | ||||
| }; | ||||
|  | ||||
| const createPC = (userId, isOffer) => { | ||||
|   let pc = new RTCPeerConnection(ice); | ||||
|   let test = userId | ||||
|   pcPeers[userId] = pc; | ||||
|   pc.addStream(localstream); | ||||
|  | ||||
|   if (isOffer) { | ||||
|     pc | ||||
|       .createOffer() | ||||
| @@ -117,7 +152,8 @@ const createPC = (userId, isOffer) => { | ||||
|           type: EXCHANGE, | ||||
|           from: currentUser, | ||||
|           to: userId, | ||||
|           sdp: JSON.stringify(pc.localDescription) | ||||
|           sdp: JSON.stringify(pc.localDescription), | ||||
|           room: chatroomId | ||||
|         }); | ||||
|       }) | ||||
|       .catch(logError); | ||||
| @@ -129,7 +165,8 @@ const createPC = (userId, isOffer) => { | ||||
|         type: EXCHANGE, | ||||
|         from: currentUser, | ||||
|         to: userId, | ||||
|         candidate: JSON.stringify(event.candidate) | ||||
|         candidate: JSON.stringify(event.candidate), | ||||
|         room: chatroomId | ||||
|       }); | ||||
|     } | ||||
|   }; | ||||
| @@ -186,7 +223,8 @@ const exchange = data => { | ||||
|               type: EXCHANGE, | ||||
|               from: currentUser, | ||||
|               to: data.from, | ||||
|               sdp: JSON.stringify(pc.localDescription) | ||||
|               sdp: JSON.stringify(pc.localDescription), | ||||
|               room: chatroomId | ||||
|             }); | ||||
|           }); | ||||
|         } | ||||
|   | ||||
| @@ -50,6 +50,10 @@ a:hover { | ||||
|       color: $icon; | ||||
|       font-size: 25px; | ||||
|     } | ||||
|     button { | ||||
|       height: 0; | ||||
|       width: 0; | ||||
|     } | ||||
|   } | ||||
|  | ||||
| } | ||||
|   | ||||
							
								
								
									
										9
									
								
								app/channels/chat_rooms_channel.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								app/channels/chat_rooms_channel.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| class ChatRoomsChannel < ApplicationCable::Channel | ||||
|   def subscribed | ||||
|     stream_from "chat_room_#{params[:room]}" | ||||
|   end | ||||
|  | ||||
|   def unsubscribed | ||||
|     # Any cleanup needed when channel is unsubscribed | ||||
|   end | ||||
| end | ||||
							
								
								
									
										9
									
								
								app/channels/notifications_channel.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								app/channels/notifications_channel.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| class NotificationsChannel < ApplicationCable::Channel | ||||
|   def subscribed | ||||
|     stream_from "notifications" | ||||
|   end | ||||
|  | ||||
|   def unsubscribed | ||||
|     # Any cleanup needed when channel is unsubscribed | ||||
|   end | ||||
| end | ||||
| @@ -1,6 +1,24 @@ | ||||
| class ChatRoomsController < ApplicationController | ||||
|  | ||||
|   def create | ||||
|   def show | ||||
|     @chat_room = ChatRoom.find(params[:id]) | ||||
|   end | ||||
|  | ||||
|   def create | ||||
|     #  HTTP status code 200 with an empty body | ||||
|     head :no_content | ||||
|     puts ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>11123213213213123213" | ||||
|     puts params | ||||
|     puts ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>11123213213213123213" | ||||
|  | ||||
|     ActionCable.server.broadcast "chat_room_#{params[:room]}", session_params | ||||
|   end | ||||
|  | ||||
|   private | ||||
|  | ||||
|   def session_params | ||||
|     # SDP = Session description protocol (codec info from client) | ||||
|     # Candidate = ICE candidates (e.g. TURN and STUN server) | ||||
|     params.permit(:type, :from, :to, :sdp, :candidate, :room) | ||||
|   end | ||||
| end | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| class PagesController < ApplicationController | ||||
|   # skip_before_action :authenticate_user!, only: [:call] | ||||
|   skip_before_action :verify_authenticity_token | ||||
|  | ||||
|   def call | ||||
|   end | ||||
| @@ -9,4 +10,63 @@ class PagesController < ApplicationController | ||||
|  | ||||
|   def home | ||||
|   end | ||||
|  | ||||
|   def cable_testing | ||||
|     chatroom = 'chat_room_' + params[:chat_room_id] | ||||
|     puts params | ||||
|     ActionCable.server.broadcast(chatroom, { message: 'test' }) | ||||
|     head :ok | ||||
|   end | ||||
|  | ||||
|   def establish_call | ||||
|     head :ok | ||||
|     puts "params: #{params}" | ||||
|     chat_room = ChatRoom.create! | ||||
|     puts "Created chat room with id: #{chat_room.id}" | ||||
|     chat_room_participation = ChatRoomParticipation.create!(chat_room: chat_room, user: current_user) | ||||
|     puts "Created chat room participation with user: #{current_user.email} assigned to chat_room #{chat_room.id}" | ||||
|     puts "Subscribed user to chat room" | ||||
|  | ||||
|     contact = User.find(params[:contact_id]) | ||||
|     request = Request.create!(chat_room: chat_room, user: contact) | ||||
|     puts "Made a request to call #{contact.email}" | ||||
|     ActionCable.server.broadcast('notifications', { | ||||
|       message: { | ||||
|         user_id: contact.id, | ||||
|         chat_room_id: chat_room.id | ||||
|       } | ||||
|     }) | ||||
|  | ||||
|   end | ||||
|  | ||||
|   def accept_call | ||||
|     puts "-----------------------------------------" | ||||
|     puts params | ||||
|     puts "IT WORKED" | ||||
|     chat_room = ChatRoom.find(params[:chat_room_id]) | ||||
|     request = Request.where("user_id = ? AND chat_room_id = ?", current_user.id, chat_room.id) | ||||
|  | ||||
|     request[0].accepted = true | ||||
|     puts "create new chat room participation" | ||||
|     chat_room_participation = ChatRoomParticipation.create!(chat_room: chat_room, user: current_user) | ||||
|     puts "Created chat room participation with user: #{current_user.email} assigned to chat_room #{chat_room.id}" | ||||
|  | ||||
|     other_caller = chat_room.users.find { |u| u != current_user } # remember to update this later | ||||
|     puts ">>>>>>>>>>>>>>>>>>>>>>>>>>>>..HHHHHHHHH" | ||||
|     puts other_caller | ||||
|     puts ">>>>>>>>>>>>>>>>>>>>>>>>>>>>..HHHHHHHHH" | ||||
|     # redirect caller to chat room | ||||
|     ActionCable.server.broadcast('notifications', { | ||||
|       head: 302, # redirection code, just to make it clear what you're doing | ||||
|       path: chat_room_path(chat_room), # you'll need to use url_helpers, so include them in your file | ||||
|       body: { caller: other_caller.id } | ||||
|       } | ||||
|       # other_caller, # or however you identify your subscriber | ||||
|     ) | ||||
|     # redirect callee to chat room | ||||
|     redirect_to chat_room_path(chat_room) | ||||
|     # broadcast another message to caller | ||||
|     # head: 302 | ||||
|  | ||||
|   end | ||||
| end | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| class RequestsController < ApplicationController | ||||
|  | ||||
|   def update | ||||
|     request.accepted = true | ||||
|   def accept | ||||
|  | ||||
|     # Create new Chat Room | ||||
|   end | ||||
|   | ||||
| @@ -1 +1,2 @@ | ||||
| import "bootstrap"; | ||||
|  | ||||
|   | ||||
							
								
								
									
										35
									
								
								app/javascript/packs/chatrooms.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								app/javascript/packs/chatrooms.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| import ActionCable from 'actioncable' | ||||
|  | ||||
| // create App object with key cable == new cosumer | ||||
| (function() { | ||||
|   window.App || (window.App = {}); | ||||
|  | ||||
|   App.cable = ActionCable.createConsumer(); | ||||
|  | ||||
| }).call(this); | ||||
|  | ||||
| // find chatroom id | ||||
| const chatroomId = document.getElementById('chatroom-hook').dataset["chatroomId"] | ||||
|  | ||||
| // create subsciptions | ||||
| App['chatroom' + chatroomId] = App.cable.subscriptions.create({ | ||||
|   channel: 'ChatRoomsChannel', | ||||
|   room: chatroomId | ||||
| }, { | ||||
|   connected: () => { | ||||
|   }, | ||||
|   received: data => { | ||||
|   } | ||||
| }) | ||||
|  | ||||
| // Testing ActionCable | ||||
| const testBtn = document.getElementById('test-btn') | ||||
| testBtn.addEventListener('click', event => { | ||||
|   fetch(`/chat_rooms/${chatroomId}/cable_testing` , { | ||||
|     method: 'POST', | ||||
|     body: JSON.stringify({}) | ||||
|   }) | ||||
| }) | ||||
|  | ||||
|  | ||||
|  | ||||
							
								
								
									
										56
									
								
								app/javascript/packs/notifications.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								app/javascript/packs/notifications.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | ||||
| import ActionCable from 'actioncable' | ||||
|  | ||||
| // create App object with key cable == new cosumer | ||||
| (function() { | ||||
|   window.App || (window.App = {}); | ||||
|  | ||||
|   App.cable = ActionCable.createConsumer(); | ||||
|  | ||||
| }).call(this); | ||||
|  | ||||
|  | ||||
| const userId = parseInt(document.getElementById("my-user-id").dataset["userId"]) | ||||
| let chatRoomId = null | ||||
|  | ||||
|  | ||||
| App.cable.subscriptions.create({ | ||||
|   channel: 'NotificationsChannel' | ||||
| }, { | ||||
|   connected: () => { | ||||
|     console.log('Connected to NotificationsChannel') | ||||
|   }, | ||||
|   received: data => { | ||||
|     // console.log(data["message"]["user_id"]) | ||||
|     // console.log(userId) | ||||
|     console.log("received broadcast") | ||||
|     // console.log(data.body) | ||||
|     if (data.head === 302 && data.body["caller"] === userId && data.path ) { | ||||
|       window.location.pathname = data.path | ||||
|     } else if (data["message"]["user_id"] === userId) { | ||||
|       console.log("TRIGGER MODAL") | ||||
|       const acceptButton = document.getElementById('accept-button') | ||||
|       acceptButton.style.display = "block" | ||||
|       // const receiveCall = document.getElementById('receive-call') | ||||
|       // receiveCall.dataset.toggle = 'modal' | ||||
|       // receiveCall.dataset.target ='#calleeModal' | ||||
|       // console.log(receiveCall) | ||||
|  | ||||
|       // const calleeModal = document.getElementById('calleeModal') | ||||
|       // calleeModal.modal("show") | ||||
|       chatRoomId = data["message"]["chat_room_id"] | ||||
|       console.log(`user with id: ${userId} needs to subscribe to chatroom ${[chatRoomId]}`) | ||||
|     } else { | ||||
|       console.log(data) | ||||
|     } | ||||
|  | ||||
|  | ||||
|   } | ||||
| }) | ||||
|  | ||||
|  | ||||
| const acceptButton = document.getElementById('accept-button') | ||||
|  | ||||
| acceptButton.addEventListener('click', event => { | ||||
|   // event.preventDefault() | ||||
|   document.getElementById('chat-room-id').value = chatRoomId | ||||
| }) | ||||
| @@ -1,5 +1,5 @@ | ||||
| class ChatRoom < ApplicationRecord | ||||
|   has_many :users, through: :chat_room_participations | ||||
|   has_many :chat_room_participations | ||||
|   has_many :users, through: :chat_room_participations | ||||
|   has_many :requests | ||||
| end | ||||
|   | ||||
| @@ -5,8 +5,8 @@ class User < ApplicationRecord | ||||
|          :recoverable, :rememberable, :validatable | ||||
|  | ||||
|   has_many :connections | ||||
|   has_many :chat_rooms, through: :chat_room_participations | ||||
|   has_many :chat_room_participations | ||||
|   has_many :chat_rooms, through: :chat_room_participations | ||||
|   has_many :requests | ||||
|  | ||||
|   def contacts | ||||
|   | ||||
							
								
								
									
										21
									
								
								app/views/chat_rooms/show.html.erb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								app/views/chat_rooms/show.html.erb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| <button onclick="handleJoinSession()" data-room="<%= @chat_room.id %>">Join Room</button> | ||||
|  | ||||
| <h1 style="color: white;">This is chatroom #<%= @chat_room.id %></h1> | ||||
| <div id="chatroom-hook" data-chatroom-id='<%= @chat_room.id %>'></div> | ||||
|  | ||||
| <button id="test-btn">Test Connection</button> | ||||
| <div class="call-container"> | ||||
|   <div id="remote-video-container"> | ||||
|     <div id="video_overlays"> | ||||
|       <video id="local-video" autoplay></video> | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
|  | ||||
| <div> | ||||
|   <span class="text-color" style="display:none">Random User ID:</span> | ||||
|   <span id="current-user" class="text-color" style="display:none"><%= current_user.id %></span> | ||||
| </div> | ||||
|  | ||||
|  | ||||
| <%= javascript_pack_tag 'chatrooms' %> | ||||
| @@ -1,18 +1,30 @@ | ||||
| <% | ||||
| def hello_world | ||||
|   puts "Hello World" | ||||
| end | ||||
| %> | ||||
|  | ||||
| <div class="card"> | ||||
|   <div class="info"> | ||||
|     <img src="https://kitt.lewagon.com/placeholder/users/ssaunier" alt="" class="profile img-circle" width=65> | ||||
|     <div class="text text-color"> | ||||
|       <p>FirstName LastName</p> | ||||
|       <% if contact.nil? || contact.nil? %> <!-- REPLACE WITH FIRST NAME AND LAST NAME --> | ||||
|         <p><%= contact.email %></p> | ||||
|       <% else %> | ||||
|         <p><%= contact.email %></p> | ||||
|         <!-- <p><%#= contact.first_name %> <%#= contact.last_name %></p> --> | ||||
|       <% end %> | ||||
|       <p class="darker">Last call: <%= Time.now %></p> | ||||
|     </div> | ||||
|   </div> | ||||
|   <div class="call"> | ||||
|     <%= link_to "#", 'data-toggle':"modal", 'data-target':"#myModal" do  %> | ||||
|   <div class="call" data-user-id="<%= contact.id %>"> | ||||
|     <%= link_to establish_call_path(contact.id), 'data-toggle':"modal", 'data-target':"#myModal" do  %> | ||||
|       <i class="fas fa-phone"></i> | ||||
|     <% end %> | ||||
|   </div> | ||||
| </div> | ||||
|  | ||||
|  | ||||
| <!-- Modal --> | ||||
| <div class="modal fade" id="myModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true"> | ||||
|   <div class="vertical-alignment-helper"> | ||||
| @@ -27,7 +39,17 @@ | ||||
|               <img src="https://kitt.lewagon.com/placeholder/users/ssaunier" alt="" class="profile img-circle" width=150> | ||||
|               <div class="modal-text"> | ||||
|                 <h4 class="modal-title" id="myModalLabel">Calling</h4> | ||||
|                 <h4><strong>FirstName LastName</strong></h4> | ||||
|                 <h4> | ||||
|                   <strong> | ||||
|                     <!-- REPLACE WITH FIRST NAME AND LAST NAME --> | ||||
|                     <% if contact.nil? || contact.nil? %> | ||||
|                       <p><%= contact.email %></p> | ||||
|                     <% else %> | ||||
|                       <p><%= contact.email %></p> | ||||
|                       <!-- <p><%#= contact.first_name %> <%#= contact.last_name %></p> --> | ||||
|                     <% end %> | ||||
|                   </strong> | ||||
|                 </h4> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
|  | ||||
|  | ||||
| <!-- | ||||
| <div class="call-container"> | ||||
|   <div id="remote-video-container"> | ||||
|     <div id="video_overlays"> | ||||
| @@ -7,7 +7,7 @@ | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
|  | ||||
|  --> | ||||
|  | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -2,8 +2,8 @@ | ||||
|   <div class="contacts-container text-color"> | ||||
|     <div class="contacts half"> | ||||
|       <h2>Contacts</h2> | ||||
|       <% 5.times do %> | ||||
|         <%= render "pages/contact" %> | ||||
|       <% current_user.contacts.each do |contact| %> | ||||
|         <%= render "pages/contact", contact: contact %> | ||||
|       <% end %> | ||||
|     </div> | ||||
|     <div class="video-feed half"> | ||||
| @@ -14,6 +14,13 @@ | ||||
|   </div> | ||||
| </div> | ||||
|  | ||||
|  | ||||
| <div id="my-user-id" data-user-id="<%= current_user.id %>"></div> | ||||
|  | ||||
| <form action="/accept_call" method="post"> | ||||
|   <input type="hidden" id="chat-room-id" name="chat_room_id" value=""/> | ||||
|   <input type="submit" id="accept-button" class="btn btn-primary" id="accept-button" style="display: none;" value="ACCEPT CALL"/> | ||||
| </form> | ||||
|  | ||||
| <%= javascript_pack_tag 'notifications' %> | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| Rails.application.routes.draw do | ||||
|   get 'video_sessions/create' | ||||
|  | ||||
|   devise_for :users, path: '', path_names: { sign_out: 'logout'} | ||||
|   devise_scope :user do | ||||
|     get '/logout', to: 'devise/sessions#destroy' | ||||
| @@ -15,9 +15,22 @@ Rails.application.routes.draw do | ||||
|  | ||||
|   get '/call', to: 'pages#call' | ||||
|  | ||||
|   get '/establish_call/:contact_id', to: 'pages#establish_call', as: 'establish_call' | ||||
|  | ||||
|   post '/accept_call', to: 'pages#accept_call', as: 'accept_call/' | ||||
|  | ||||
|   patch '/accept_call/:request_id', to: 'requests#update', as: 'update_request' | ||||
|  | ||||
|   get '/contacts', to: 'pages#index' | ||||
|   post '/sessions', to: 'video_sessions#create' | ||||
|  | ||||
|   post '/chat_rooms/chat_room_sessions', to: 'chat_rooms#create' | ||||
|  | ||||
|   resources :chat_rooms, only: [ :show ] do | ||||
|     # testing action cable | ||||
|     post '/cable_testing', to: 'pages#cable_testing' | ||||
|   end | ||||
|  | ||||
|   mount ActionCable.server, at: '/cable' | ||||
|   # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html | ||||
|   get '/home', to: 'pages#home' | ||||
|   | ||||
| @@ -3,6 +3,7 @@ | ||||
|   "private": true, | ||||
|   "dependencies": { | ||||
|     "@rails/webpacker": "3.5", | ||||
|     "actioncable": "^5.2.1", | ||||
|     "bootstrap": "3", | ||||
|     "jquery": "^3.3.1" | ||||
|   }, | ||||
|   | ||||
| @@ -57,6 +57,10 @@ acorn@^5.0.0: | ||||
|   version "5.7.2" | ||||
|   resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.2.tgz#91fa871883485d06708800318404e72bfb26dcc5" | ||||
|  | ||||
| actioncable@^5.2.1: | ||||
|   version "5.2.1" | ||||
|   resolved "https://registry.yarnpkg.com/actioncable/-/actioncable-5.2.1.tgz#615428a8a302cec55117d9049f0fd7952011c962" | ||||
|  | ||||
| ajv-keywords@^3.1.0: | ||||
|   version "3.2.0" | ||||
|   resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.2.0.tgz#e86b819c602cf8821ad637413698f1dec021847a" | ||||
|   | ||||
		Reference in New Issue
	
	Block a user