metamaps--metamaps/frontend/src/Metamaps/Realtime/index.js
Connor Turland 3868910dde Stream map related events from rails server via actioncable instead of nodejs (#1010)
* actioncable needs puma not webrick

* add framework

* remove the old way

* send events from server to client

* get all events working

* clean up receivable

* map is polymorphic on message

* add the moved event

* make todo comments clear

* verify before streaming from map channel

* rubocop fixes

* wasn't set up correctly for nodejs realtime
2017-01-03 16:12:58 -05:00

437 lines
12 KiB
JavaScript

/* global $ */
import SimpleWebRTC from 'simplewebrtc'
import SocketIoConnection from 'simplewebrtc/socketioconnection'
import Active from '../Active'
import Cable from '../Cable'
import DataModel from '../DataModel'
import JIT from '../JIT'
import Util from '../Util'
import Views from '../Views'
import { ChatView } from '../Views'
import Visualize from '../Visualize'
import {
JUNTO_UPDATED,
INVITED_TO_CALL,
INVITED_TO_JOIN,
CALL_ACCEPTED,
CALL_DENIED,
INVITE_DENIED,
CALL_IN_PROGRESS,
CALL_STARTED,
MAPPER_LIST_UPDATED,
MAPPER_JOINED_CALL,
MAPPER_LEFT_CALL,
NEW_MAPPER,
LOST_MAPPER,
PEER_COORDS_UPDATED,
TOPIC_DRAGGED
} from './events'
import {
juntoUpdated,
invitedToCall,
invitedToJoin,
callAccepted,
callDenied,
inviteDenied,
callInProgress,
callStarted,
mapperListUpdated,
mapperJoinedCall,
mapperLeftCall,
peerCoordsUpdated,
newMapper,
lostMapper,
topicDragged
} from './receivable'
import {
joinMap,
leaveMap,
checkForCall,
acceptCall,
denyCall,
denyInvite,
inviteToJoin,
inviteACall,
joinCall,
leaveCall,
sendCoords,
sendMapperInfo,
dragTopic
} from './sendable'
let Realtime = {
juntoState: { connectedPeople: {}, liveMaps: {} },
videoId: 'video-wrapper',
socket: null,
webrtc: null,
readyToCall: false,
mappersOnMap: {},
disconnected: false,
chatOpen: false,
soundId: null,
broadcastingStatus: false,
inConversation: false,
localVideo: null,
'junto_spinner_darkgrey.gif': '',
init: function(serverData) {
var self = Realtime
self.addJuntoListeners()
self.socket = new SocketIoConnection({
url: serverData['REALTIME_SERVER'],
socketio: {
// don't poll forever if in development
reconnectionAttempts: serverData.RAILS_ENV === 'development' ? 5 : Infinity
}
})
self['junto_spinner_darkgrey.gif'] = serverData['junto_spinner_darkgrey.gif']
self.socket.on('connect', function() {
console.log('connected')
if (Active.Map && Active.Mapper && Active.Map.authorizeToEdit(Active.Mapper)) {
self.checkForCall()
self.joinMap()
}
subscribeToEvents(self, self.socket)
self.disconnected = false
})
self.socket.on('disconnect', function() {
self.disconnected = true
})
if (Active.Mapper) {
self.webrtc = new SimpleWebRTC({
connection: self.socket,
localVideoEl: self.videoId,
remoteVideosEl: '',
debug: true,
detectSpeakingEvents: false, // true,
autoAdjustMic: false, // true,
autoRequestMedia: false,
localVideo: {
autoplay: true,
mirror: true,
muted: true
},
media: {
video: true,
audio: true
},
nick: Active.Mapper.id
})
self.webrtc.webrtc.on('iceFailed', function(peer) {
console.log('local ice failure', peer)
// local ice failure
})
self.webrtc.webrtc.on('connectivityError', function(peer) {
console.log('remote ice failure', peer)
// remote ice failure
})
var $video = $('<video></video>').attr('id', self.videoId)
self.localVideo = {
$video: $video,
view: new Views.VideoView($video[0], $('body'), 'me', true, {
DOUBLE_CLICK_TOLERANCE: 200,
avatar: Active.Mapper ? Active.Mapper.get('image') : ''
})
}
self.room = new Views.Room({
webrtc: self.webrtc,
socket: self.socket,
room: 'global',
$video: self.localVideo.$video,
myVideoView: self.localVideo.view,
config: { DOUBLE_CLICK_TOLERANCE: 200 }
})
self.room.videoAdded(self.handleVideoAdded)
self.startActiveMap()
} // if Active.Mapper
},
addJuntoListeners: function() {
var self = Realtime
$(document).on(ChatView.events.openTray, function() {
$('.main').addClass('compressed')
self.chatOpen = true
self.positionPeerIcons()
})
$(document).on(ChatView.events.closeTray, function() {
$('.main').removeClass('compressed')
self.chatOpen = false
self.positionPeerIcons()
})
$(document).on(ChatView.events.videosOn, function() {
$('#wrapper').removeClass('hideVideos')
})
$(document).on(ChatView.events.videosOff, function() {
$('#wrapper').addClass('hideVideos')
})
$(document).on(ChatView.events.cursorsOn, function() {
$('#wrapper').removeClass('hideCursors')
})
$(document).on(ChatView.events.cursorsOff, function() {
$('#wrapper').addClass('hideCursors')
})
},
startActiveMap: function() {
var self = Realtime
if (Active.Map && Active.Mapper) {
if (Active.Map.authorizeToEdit(Active.Mapper)) {
self.turnOn()
self.checkForCall()
self.joinMap()
}
self.setupChat() // chat can happen on public maps too
Cable.subscribeToMap(Active.Map.id) // people with edit rights can still see live updates
}
},
endActiveMap: function() {
var self = Realtime
$(document).off('.map')
// leave the appropriate rooms to leave
if (self.inConversation) self.leaveCall()
self.leaveMap()
$('.collabCompass').remove()
if (self.room) self.room.leave()
ChatView.hide()
ChatView.close()
ChatView.reset()
Cable.unsubscribeFromMap()
},
turnOn: function(notify) {
var self = Realtime
$('.collabCompass').show()
self.room.room = 'map-' + Active.Map.id
self.activeMapper = {
id: Active.Mapper.id,
name: Active.Mapper.get('name'),
username: Active.Mapper.get('name'),
image: Active.Mapper.get('image'),
color: Util.getPastelColor(),
self: true
}
self.localVideo.view.$container.find('.video-cutoff').css({
border: '4px solid ' + self.activeMapper.color
})
self.setupLocalEvents()
},
setupChat: function() {
const self = Realtime
ChatView.setNewMap()
ChatView.addParticipant(self.activeMapper)
ChatView.addMessages(new DataModel.MessageCollection(DataModel.Messages), true)
ChatView.show()
},
setupLocalEvents: function() {
var self = Realtime
// local event listeners that trigger events
$(document).on(JIT.events.zoom + '.map', self.positionPeerIcons)
$(document).on(JIT.events.pan + '.map', self.positionPeerIcons)
$(document).on('mousemove.map', function(event) {
var pixels = {
x: event.pageX,
y: event.pageY
}
var coords = Util.pixelsToCoords(Visualize.mGraph, pixels)
self.sendCoords(coords)
})
$(document).on(JIT.events.topicDrag + '.map', function(event, positions) {
self.dragTopic(positions)
})
},
countOthersInConversation: function() {
var self = Realtime
var count = 0
for (var key in self.mappersOnMap) {
if (self.mappersOnMap[key].inConversation) count++
}
return count
},
handleVideoAdded: function(v, id) {
var self = Realtime
self.positionVideos()
v.setParent($('#wrapper'))
v.$container.find('.video-cutoff').css({
border: '4px solid ' + self.mappersOnMap[id].color
})
$('#wrapper').append(v.$container)
},
positionVideos: function() {
var self = Realtime
var videoIds = Object.keys(self.room.videos)
// var numOfVideos = videoIds.length
// var numOfVideosToPosition = _.filter(videoIds, function(id) {
// return !self.room.videos[id].manuallyPositioned
// }).length
var screenHeight = $(document).height()
var topExtraPadding = 20
var topPadding = 30
var leftPadding = 30
var videoHeight = 150
var videoWidth = 180
var column = 0
var row = 0
var yFormula = function() {
var y = topExtraPadding + (topPadding + videoHeight) * row + topPadding
if (y + videoHeight > screenHeight) {
row = 0
column += 1
y = yFormula()
}
row++
return y
}
var xFormula = function() {
var x = (leftPadding + videoWidth) * column + leftPadding
return x
}
// do self first
var myVideo = Realtime.localVideo.view
if (!myVideo.manuallyPositioned) {
myVideo.$container.css({
top: yFormula() + 'px',
left: xFormula() + 'px'
})
}
videoIds.forEach(function(id) {
var video = self.room.videos[id]
if (!video.manuallyPositioned) {
video.$container.css({
top: yFormula() + 'px',
left: xFormula() + 'px'
})
}
})
},
callEnded: function() {
var self = Realtime
ChatView.conversationEnded()
self.room.leaveVideoOnly()
self.inConversation = false
self.localVideo.view.$container.hide().css({
top: '72px',
left: '30px'
})
self.localVideo.view.audioOn()
self.localVideo.view.videoOn()
},
createCompass: function(name, id, image, color) {
var str = '<img width="28" height="28" src="' + image + '" /><p>' + name + '</p>'
str += '<div id="compassArrow' + id + '" class="compassArrow"></div>'
$('#compass' + id).remove()
$('<div/>', {
id: 'compass' + id,
class: 'collabCompass'
}).html(str).appendTo('#wrapper')
$('#compass' + id + ' img').css({
'border': '2px solid ' + color
})
$('#compass' + id + ' p').css({
'background-color': color
})
},
positionPeerIcons: function() {
var self = Realtime
for (var key in self.mappersOnMap) {
self.positionPeerIcon(key)
}
},
positionPeerIcon: function(id) {
var self = Realtime
var mapper = self.mappersOnMap[id]
var origPixels = Util.coordsToPixels(Visualize.mGraph, mapper.coords)
var pixels = self.limitPixelsToScreen(origPixels)
$('#compass' + id).css({
left: pixels.x + 'px',
top: pixels.y + 'px'
})
/* showing the arrow if the collaborator is off of the viewport screen */
if (origPixels.x !== pixels.x || origPixels.y !== pixels.y) {
var dy = origPixels.y - pixels.y // opposite
var dx = origPixels.x - pixels.x // adjacent
var angle = Math.atan2(dy, dx)
$('#compassArrow' + id).show().css({
transform: 'rotate(' + angle + 'rad)',
'-webkit-transform': 'rotate(' + angle + 'rad)'
})
if (dx > 0) {
$('#compass' + id).addClass('labelLeft')
}
} else {
$('#compassArrow' + id).hide()
$('#compass' + id).removeClass('labelLeft')
}
},
limitPixelsToScreen: function(pixels) {
var self = Realtime
var boundary = self.chatOpen ? '#wrapper' : document
var xLimit, yLimit
var xMax = $(boundary).width()
var yMax = $(boundary).height()
var compassDiameter = 56
var compassArrowSize = 24
xLimit = Math.max(0 + compassArrowSize, pixels.x)
xLimit = Math.min(xLimit, xMax - compassDiameter)
yLimit = Math.max(0 + compassArrowSize, pixels.y)
yLimit = Math.min(yLimit, yMax - compassDiameter)
return {x: xLimit, y: yLimit}
}
}
const sendables = [
['joinMap', joinMap],
['leaveMap', leaveMap],
['checkForCall', checkForCall],
['acceptCall', acceptCall],
['denyCall', denyCall],
['denyInvite', denyInvite],
['inviteToJoin', inviteToJoin],
['inviteACall', inviteACall],
['joinCall', joinCall],
['leaveCall', leaveCall],
['sendMapperInfo', sendMapperInfo],
['sendCoords', sendCoords],
['dragTopic', dragTopic]
]
sendables.forEach(sendable => {
Realtime[sendable[0]] = sendable[1](Realtime)
})
const subscribeToEvents = (Realtime, socket) => {
socket.on(JUNTO_UPDATED, juntoUpdated(Realtime))
socket.on(INVITED_TO_CALL, invitedToCall(Realtime))
socket.on(INVITED_TO_JOIN, invitedToJoin(Realtime))
socket.on(CALL_ACCEPTED, callAccepted(Realtime))
socket.on(CALL_DENIED, callDenied(Realtime))
socket.on(INVITE_DENIED, inviteDenied(Realtime))
socket.on(CALL_IN_PROGRESS, callInProgress(Realtime))
socket.on(CALL_STARTED, callStarted(Realtime))
socket.on(MAPPER_LIST_UPDATED, mapperListUpdated(Realtime))
socket.on(MAPPER_JOINED_CALL, mapperJoinedCall(Realtime))
socket.on(MAPPER_LEFT_CALL, mapperLeftCall(Realtime))
socket.on(PEER_COORDS_UPDATED, peerCoordsUpdated(Realtime))
socket.on(NEW_MAPPER, newMapper(Realtime))
socket.on(LOST_MAPPER, lostMapper(Realtime))
socket.on(TOPIC_DRAGGED, topicDragged(Realtime))
}
export default Realtime