Merge branch 'develop' into oauth.provider

This commit is contained in:
Connor Turland 2016-03-23 18:00:59 -07:00
commit 5317711b57
52 changed files with 16235 additions and 430 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 854 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 780 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 466 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 B

BIN
app/assets/images/junto.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 717 B

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 331 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -20,6 +20,9 @@
//= require ./src/Metamaps.Router //= require ./src/Metamaps.Router
//= require ./src/Metamaps.Backbone //= require ./src/Metamaps.Backbone
//= require ./src/Metamaps.Views //= require ./src/Metamaps.Views
//= require ./src/views/chatView
//= require ./src/views/videoView
//= require ./src/views/room
//= require ./src/JIT //= require ./src/JIT
//= require ./src/Metamaps //= require ./src/Metamaps
//= require ./src/Metamaps.JIT //= require ./src/Metamaps.JIT

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,39 @@
var attachMediaStream = function (stream, el, options) {
var URL = window.URL;
var opts = {
autoplay: true,
mirror: false,
muted: false
};
var element = el || document.createElement('video');
var item;
if (options) {
for (item in options) {
opts[item] = options[item];
}
}
if (opts.autoplay) element.autoplay = 'autoplay';
if (opts.muted) element.muted = true;
if (opts.mirror) {
['', 'moz', 'webkit', 'o', 'ms'].forEach(function (prefix) {
var styleName = prefix ? prefix + 'Transform' : 'transform';
element.style[styleName] = 'scaleX(-1)';
});
}
// this first one should work most everywhere now
// but we have a few fallbacks just in case.
if (URL && URL.createObjectURL) {
element.src = URL.createObjectURL(stream);
} else if (element.srcObject) {
element.srcObject = stream;
} else if (element.mozSrcObject) {
element.mozSrcObject = stream;
} else {
return false;
}
return element;
};

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,23 @@
function SocketIoConnection(config) {
this.connection = io.connect(config.url, config.socketio);
}
SocketIoConnection.prototype.on = function (ev, fn) {
this.connection.on(ev, fn);
};
SocketIoConnection.prototype.emit = function () {
this.connection.emit.apply(this.connection, arguments);
};
SocketIoConnection.prototype.removeAllListeners = function () {
this.connection.removeAllListeners();
};
SocketIoConnection.prototype.getSessionid = function () {
return this.connection.socket.sessionid;
};
SocketIoConnection.prototype.disconnect = function () {
return this.connection.disconnect();
};

View file

@ -206,6 +206,26 @@ Metamaps.Backbone.MapsCollection = Backbone.Collection.extend({
} }
}); });
Metamaps.Backbone.Message = Backbone.Model.extend({
urlRoot: '/messages',
blacklist: ['created_at', 'updated_at'],
toJSON: function (options) {
return _.omit(this.attributes, this.blacklist);
},
prepareLiForFilter: function () {
/*var li = '';
li += '<li data-id="' + this.id.toString() + '">';
li += '<img src="' + this.get("image") + '" data-id="' + this.id.toString() + '"';
li += ' alt="' + this.get('name') + '" />';
li += '<p>' + this.get('name') + '</p></li>';
return li;*/
}
});
Metamaps.Backbone.MessageCollection = Backbone.Collection.extend({
model: Metamaps.Backbone.Message,
url: '/messages'
});
Metamaps.Backbone.Mapper = Backbone.Model.extend({ Metamaps.Backbone.Mapper = Backbone.Model.extend({
urlRoot: '/users', urlRoot: '/users',
blacklist: ['created_at', 'updated_at'], blacklist: ['created_at', 'updated_at'],

View file

@ -160,6 +160,7 @@ Metamaps.GlobalUI = {
notifyUser: function (message, leaveOpen) { notifyUser: function (message, leaveOpen) {
var self = Metamaps.GlobalUI; var self = Metamaps.GlobalUI;
function famousReady() {
Metamaps.Famous.toast.surf.setContent(message); Metamaps.Famous.toast.surf.setContent(message);
Metamaps.Famous.toast.show(); Metamaps.Famous.toast.show();
clearTimeout(self.notifyTimeOut); clearTimeout(self.notifyTimeOut);
@ -168,6 +169,18 @@ Metamaps.GlobalUI = {
Metamaps.Famous.toast.hide(); Metamaps.Famous.toast.hide();
}, 8000); }, 8000);
} }
}
// initialize the famous ui
var callFamous = function(){
if (Metamaps.Famous && Metamaps.Famous.toast) {
famousReady();
}
else {
setTimeout(callFamous, 100);
}
}
callFamous();
}, },
clearNotify: function() { clearNotify: function() {
var self = Metamaps.GlobalUI; var self = Metamaps.GlobalUI;
@ -334,7 +347,6 @@ Metamaps.GlobalUI.Account = {
open: function () { open: function () {
var self = Metamaps.GlobalUI.Account; var self = Metamaps.GlobalUI.Account;
Metamaps.Realtime.close();
Metamaps.Filter.close(); Metamaps.Filter.close();
$('.sidebarAccountIcon .tooltipsUnder').addClass('hide'); $('.sidebarAccountIcon .tooltipsUnder').addClass('hide');

View file

@ -1047,7 +1047,6 @@ Metamaps.JIT = {
Metamaps.TopicCard.hideCard(); Metamaps.TopicCard.hideCard();
Metamaps.SynapseCard.hideCard(); Metamaps.SynapseCard.hideCard();
Metamaps.Create.newTopic.hide(); Metamaps.Create.newTopic.hide();
$('.rightclickmenu').remove(); $('.rightclickmenu').remove();
// reset the draw synapse positions to false // reset the draw synapse positions to false
Metamaps.Mouse.synapseStartCoordinates = []; Metamaps.Mouse.synapseStartCoordinates = [];

View file

@ -1924,11 +1924,17 @@ Metamaps.Util = {
* *
*/ */
Metamaps.Realtime = { Metamaps.Realtime = {
videoId: 'video-wrapper',
socket: null, socket: null,
isOpen: false, webrtc: null,
changing: false, readyToCall: false,
mappersOnMap: {}, mappersOnMap: {},
status: true, // stores whether realtime is True/On or False/Off disconnected: false,
chatOpen: false,
status: true, // stores whether realtime is True/On or False/Off,
broadcastingStatus: false,
inConversation: false,
localVideo: null,
init: function () { init: function () {
var self = Metamaps.Realtime; var self = Metamaps.Realtime;
@ -1941,50 +1947,150 @@ Metamaps.Realtime = {
$(".rtOn").click(reenableRealtime); $(".rtOn").click(reenableRealtime);
$(".rtOff").click(turnOff); $(".rtOff").click(turnOff);
$('.sidebarCollaborateIcon').click(self.toggleBox); self.addJuntoListeners();
$('.sidebarCollaborateBox').click(function(event){
event.stopPropagation();
});
$('body').click(self.close);
self.socket = io.connect('<%= ENV['REALTIME_SERVER'] %>'); self.socket = new SocketIoConnection({ url: '<%= ENV['REALTIME_SERVER'] %>' });
self.socket.on('connect', function () { self.socket.on('connect', function () {
console.log('connected');
if (!self.disconnected) {
self.startActiveMap(); self.startActiveMap();
} else self.disconnected = false;
});
self.socket.on('disconnect', function () {
self.disconnected = true;
});
if (Metamaps.Active.Mapper) {
self.webrtc = new SimpleWebRTC({
connection: self.socket,
localVideoEl: self.videoId,
remoteVideosEl: '',
detectSpeakingEvents: true,
autoAdjustMic: false, //true,
autoRequestMedia: false,
localVideo: {
autoplay: true,
mirror: true,
muted: true
},
media: {
video: true,
audio: true
},
nick: Metamaps.Active.Mapper.id
});
var
$video = $('<video></video>').attr('id', self.videoId);
self.localVideo = {
$video: $video,
view: new Metamaps.Views.videoView($video[0], $('body'), 'me', true, {
DOUBLE_CLICK_TOLERANCE: 200,
avatar: Metamaps.Active.Mapper ? Metamaps.Active.Mapper.get('image') : ''
})
};
self.room = new Metamaps.Views.room({
webrtc: self.webrtc,
socket: self.socket,
username: Metamaps.Active.Mapper ? Metamaps.Active.Mapper.get('name') : '',
image: Metamaps.Active.Mapper ? Metamaps.Active.Mapper.get('image') : '',
room: 'global',
$video: self.localVideo.$video,
myVideoView: self.localVideo.view,
config: { DOUBLE_CLICK_TOLERANCE: 200 }
});
self.room.videoAdded(self.handleVideoAdded);
self.room.chat.$container.hide();
$('body').prepend(self.room.chat.$container);
} // if Metamaps.Active.Mapper
},
addJuntoListeners: function () {
var self = Metamaps.Realtime;
$(document).on(Metamaps.Views.chatView.events.openTray, function () {
$('.main').addClass('compressed');
self.chatOpen = true;
self.positionPeerIcons();
});
$(document).on(Metamaps.Views.chatView.events.closeTray, function () {
$('.main').removeClass('compressed');
self.chatOpen = false;
self.positionPeerIcons();
});
$(document).on(Metamaps.Views.chatView.events.videosOn, function () {
$('#wrapper').removeClass('hideVideos');
});
$(document).on(Metamaps.Views.chatView.events.videosOff, function () {
$('#wrapper').addClass('hideVideos');
});
$(document).on(Metamaps.Views.chatView.events.cursorsOn, function () {
$('#wrapper').removeClass('hideCursors');
});
$(document).on(Metamaps.Views.chatView.events.cursorsOff, function () {
$('#wrapper').addClass('hideCursors');
}); });
}, },
toggleBox: function (event) { handleVideoAdded: function (v, id) {
var self = Metamaps.Realtime; var self = Metamaps.Realtime;
self.positionVideos();
if (self.isOpen) self.close(); v.setParent($('#wrapper'));
else self.open(); v.$container.find('.video-cutoff').css({
border: '4px solid ' + self.mappersOnMap[id].color
event.stopPropagation(); });
$('#wrapper').append(v.$container);
}, },
open: function () { positionVideos: function () {
var self = Metamaps.Realtime; var self = Metamaps.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;
Metamaps.GlobalUI.Account.close(); var screenHeight = $(document).height();
Metamaps.Filter.close(); var screenWidth = $(document).width();
$('.sidebarCollaborateIcon div').addClass('hide'); 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;
};
if (!self.isOpen && !self.changing) { // do self first
self.changing = true; var myVideo = Metamaps.Realtime.localVideo.view;
$('.sidebarCollaborateBox').fadeIn(200, function () { if (!myVideo.manuallyPositioned) {
self.changing = false; myVideo.$container.css({
self.isOpen = true; top: yFormula() + 'px',
left: xFormula() + 'px'
}); });
} }
}, videoIds.forEach(function (id) {
close: function () { var video = self.room.videos[id];
var self = Metamaps.Realtime; if (!video.manuallyPositioned) {
$(".sidebarCollaborateIcon div").removeClass('hide'); video.$container.css({
if (!self.changing) { top: yFormula() + 'px',
self.changing = true; left: xFormula() + 'px'
$('.sidebarCollaborateBox').fadeOut(200, function () {
self.changing = false;
self.isOpen = false;
}); });
} }
});
}, },
startActiveMap: function () { startActiveMap: function () {
var self = Metamaps.Realtime; var self = Metamaps.Realtime;
@ -2000,6 +2106,7 @@ Metamaps.Realtime = {
else if (publicMap) { else if (publicMap) {
self.attachMapListener(); self.attachMapListener();
} }
self.room.addMessages(new Metamaps.Backbone.MessageCollection(Metamaps.Messages), true);
} }
}, },
endActiveMap: function () { endActiveMap: function () {
@ -2007,9 +2114,13 @@ Metamaps.Realtime = {
$(document).off('mousemove'); $(document).off('mousemove');
self.socket.removeAllListeners(); self.socket.removeAllListeners();
if (self.inConversation) self.leaveCall();
self.socket.emit('endMapperNotify'); self.socket.emit('endMapperNotify');
$(".collabCompass").remove(); $(".collabCompass").remove();
self.status = false; self.status = false;
self.room.leave();
self.room.chat.$container.hide();
self.room.chat.close();
}, },
reenableRealtime: function() { reenableRealtime: function() {
var confirmString = "The layout of your map has fallen out of sync with the saved copy. "; var confirmString = "The layout of your map has fallen out of sync with the saved copy. ";
@ -2025,24 +2136,257 @@ Metamaps.Realtime = {
var self = Metamaps.Realtime; var self = Metamaps.Realtime;
if (notify) self.sendRealtimeOn(); if (notify) self.sendRealtimeOn();
$(".rtMapperSelf").removeClass('littleRtOff').addClass('littleRtOn'); //$(".rtMapperSelf").removeClass('littleRtOff').addClass('littleRtOn');
$('.rtOn').addClass('active'); //$('.rtOn').addClass('active');
$('.rtOff').removeClass('active'); //$('.rtOff').removeClass('active');
self.status = true; self.status = true;
$(".sidebarCollaborateIcon").addClass("blue"); //$(".sidebarCollaborateIcon").addClass("blue");
$(".collabCompass").show(); $(".collabCompass").show();
self.room.chat.$container.show();
self.room.room = 'map-' + Metamaps.Active.Map.id;
self.checkForACallToJoin();
self.activeMapper = {
id: Metamaps.Active.Mapper.id,
name: Metamaps.Active.Mapper.get('name'),
username: Metamaps.Active.Mapper.get('name'),
image: Metamaps.Active.Mapper.get('image'),
color: Metamaps.Util.getPastelColor(),
self: true
};
self.localVideo.view.$container.find('.video-cutoff').css({
border: '4px solid ' + self.activeMapper.color
});
self.room.chat.addParticipant(self.activeMapper);
},
checkForACallToJoin: function () {
var self = Metamaps.Realtime;
self.socket.emit('checkForCall', { room: self.room.room, mapid: Metamaps.Active.Map.id });
},
promptToJoin: function () {
var self = Metamaps.Realtime;
var notifyText = 'There\'s a conversation happening, want to join?';
notifyText += ' <button type="button" class="toast-button button" onclick="Metamaps.Realtime.joinCall()">Yes</button>';
notifyText += ' <button type="button" class="toast-button button btn-no" onclick="Metamaps.GlobalUI.clearNotify()">No</button>';
Metamaps.GlobalUI.notifyUser(notifyText, true);
self.room.conversationInProgress();
},
conversationHasBegun: function () {
var self = Metamaps.Realtime;
if (self.inConversation) return;
var notifyText = 'There\'s a conversation starting, want to join?';
notifyText += ' <button type="button" class="toast-button button" onclick="Metamaps.Realtime.joinCall()">Yes</button>';
notifyText += ' <button type="button" class="toast-button button btn-no" onclick="Metamaps.GlobalUI.clearNotify()">No</button>';
Metamaps.GlobalUI.notifyUser(notifyText, true);
self.room.conversationInProgress();
},
countOthersInConversation: function () {
var self = Metamaps.Realtime;
var count = 0;
for (var key in self.mappersOnMap) {
if (self.mappersOnMap[key].inConversation) count++;
}
return count;
},
mapperJoinedCall: function (id) {
var self = Metamaps.Realtime;
var mapper = self.mappersOnMap[id];
if (mapper) {
if (self.inConversation) {
var username = mapper.name;
var notifyText = username + ' joined the call';
Metamaps.GlobalUI.notifyUser(notifyText);
}
mapper.inConversation = true;
self.room.chat.mapperJoinedCall(id);
}
},
mapperLeftCall: function (id) {
var self = Metamaps.Realtime;
var mapper = self.mappersOnMap[id];
if (mapper) {
if (self.inConversation) {
var username = mapper.name;
var notifyText = username + ' left the call';
Metamaps.GlobalUI.notifyUser(notifyText);
}
mapper.inConversation = false;
self.room.chat.mapperLeftCall(id);
if ((self.inConversation && self.countOthersInConversation() === 0) ||
(!self.inConversation && self.countOthersInConversation() === 1)) {
self.callEnded();
}
}
},
callEnded: function () {
var self = Metamaps.Realtime;
self.room.conversationEnding();
self.room.leaveVideoOnly();
self.inConversation = false;
self.localVideo.view.$container.hide().css({
top: '72px',
left: '30px'
});
self.localVideo.view.audioOn();
self.localVideo.view.videoOn();
self.webrtc.webrtc.localStreams.forEach(function (stream) {
stream.getTracks().forEach(function (track) {
track.stop();
});
});
self.webrtc.webrtc.localStreams = [];
},
invitedToCall: function (inviter) {
var self = Metamaps.Realtime;
var username = self.mappersOnMap[inviter].name;
var notifyText = username + ' is suggesting a video call. What do you think?';
notifyText += ' <button type="button" class="toast-button button" onclick="Metamaps.Realtime.acceptCall(' + inviter + ')">Yes</button>';
notifyText += ' <button type="button" class="toast-button button btn-no" onclick="Metamaps.Realtime.denyCall(' + inviter + ')">No</button>';
Metamaps.GlobalUI.notifyUser(notifyText, true);
},
invitedToJoin: function (inviter) {
var self = Metamaps.Realtime;
var username = self.mappersOnMap[inviter].name;
var notifyText = username + ' is inviting you to the conversation. Join?';
notifyText += ' <button type="button" class="toast-button button" onclick="Metamaps.Realtime.joinCall()">Yes</button>';
notifyText += ' <button type="button" class="toast-button button btn-no" onclick="Metamaps.Realtime.denyInvite(' + inviter + ')">No</button>';
Metamaps.GlobalUI.notifyUser(notifyText, true);
},
acceptCall: function (userid) {
var self = Metamaps.Realtime;
self.socket.emit('callAccepted', {
mapid: Metamaps.Active.Map.id,
invited: Metamaps.Active.Mapper.id,
inviter: userid
});
self.joinCall();
Metamaps.GlobalUI.clearNotify();
},
denyCall: function (userid) {
var self = Metamaps.Realtime;
self.socket.emit('callDenied', {
mapid: Metamaps.Active.Map.id,
invited: Metamaps.Active.Mapper.id,
inviter: userid
});
Metamaps.GlobalUI.clearNotify();
},
denyInvite: function (userid) {
var self = Metamaps.Realtime;
self.socket.emit('inviteDenied', {
mapid: Metamaps.Active.Map.id,
invited: Metamaps.Active.Mapper.id,
inviter: userid
});
Metamaps.GlobalUI.clearNotify();
},
inviteACall: function (userid) {
var self = Metamaps.Realtime;
self.socket.emit('inviteACall', {
mapid: Metamaps.Active.Map.id,
inviter: Metamaps.Active.Mapper.id,
invited: userid
});
self.room.chat.invitationPending(userid);
Metamaps.GlobalUI.clearNotify();
},
inviteToJoin: function (userid) {
var self = Metamaps.Realtime;
self.socket.emit('inviteToJoin', {
mapid: Metamaps.Active.Map.id,
inviter: Metamaps.Active.Mapper.id,
invited: userid
});
self.room.chat.invitationPending(userid);
},
callAccepted: function (userid) {
var self = Metamaps.Realtime;
var username = self.mappersOnMap[userid].name;
Metamaps.GlobalUI.notifyUser('Conversation starting...');
self.joinCall();
self.room.chat.invitationAnswered(userid);
},
callDenied: function (userid) {
var self = Metamaps.Realtime;
var username = self.mappersOnMap[userid].name;
Metamaps.GlobalUI.notifyUser(username + ' didn\'t accept your invite.');
self.room.chat.invitationAnswered(userid);
},
inviteDenied: function (userid) {
var self = Metamaps.Realtime;
var username = self.mappersOnMap[userid].name;
Metamaps.GlobalUI.notifyUser(username + ' didn\'t accept your invite.');
self.room.chat.invitationAnswered(userid);
},
joinCall: function () {
var self = Metamaps.Realtime;
self.webrtc.off('readyToCall');
self.webrtc.once('readyToCall', function () {
self.videoInitialized = true;
self.readyToCall = true;
self.localVideo.view.manuallyPositioned = false;
self.positionVideos();
self.localVideo.view.$container.show();
if (self.localVideo && self.status) {
$('#wrapper').append(self.localVideo.view.$container);
}
self.room.join();
});
self.inConversation = true;
self.socket.emit('mapperJoinedCall', {
mapid: Metamaps.Active.Map.id,
id: Metamaps.Active.Mapper.id
});
self.webrtc.startLocalVideo();
Metamaps.GlobalUI.clearNotify();
self.room.chat.mapperJoinedCall(Metamaps.Active.Mapper.id);
},
leaveCall: function () {
var self = Metamaps.Realtime;
self.socket.emit('mapperLeftCall', {
mapid: Metamaps.Active.Map.id,
id: Metamaps.Active.Mapper.id
});
self.room.chat.mapperLeftCall(Metamaps.Active.Mapper.id);
self.room.leaveVideoOnly();
self.inConversation = false;
self.localVideo.view.$container.hide();
// if there's only two people in the room, and we're leaving
// we should shut down the call locally
if (self.countOthersInConversation() === 1) {
self.callEnded();
}
}, },
turnOff: function (silent) { turnOff: function (silent) {
var self = Metamaps.Realtime; var self = Metamaps.Realtime;
if (self.status) { if (self.status) {
if (!silent) self.sendRealtimeOff(); if (!silent) self.sendRealtimeOff();
$(".rtMapperSelf").removeClass('littleRtOn').addClass('littleRtOff'); //$(".rtMapperSelf").removeClass('littleRtOn').addClass('littleRtOff');
$('.rtOn').removeClass('active'); //$('.rtOn').removeClass('active');
$('.rtOff').addClass('active'); //$('.rtOff').addClass('active');
self.status = false; self.status = false;
$(".sidebarCollaborateIcon").removeClass("blue"); //$(".sidebarCollaborateIcon").removeClass("blue");
$(".collabCompass").hide(); $(".collabCompass").hide();
$('#' + self.videoId).remove();
} }
}, },
setupSocket: function () { setupSocket: function () {
@ -2057,6 +2401,19 @@ Metamaps.Realtime = {
mapid: Metamaps.Active.Map.id mapid: Metamaps.Active.Map.id
}); });
socket.on(myId + '-' + Metamaps.Active.Map.id + '-invitedToCall', self.invitedToCall); // new call
socket.on(myId + '-' + Metamaps.Active.Map.id + '-invitedToJoin', self.invitedToJoin); // call already in progress
socket.on(myId + '-' + Metamaps.Active.Map.id + '-callAccepted', self.callAccepted);
socket.on(myId + '-' + Metamaps.Active.Map.id + '-callDenied', self.callDenied);
socket.on(myId + '-' + Metamaps.Active.Map.id + '-inviteDenied', self.inviteDenied);
// receive word that there's a conversation in progress
socket.on('maps-' + Metamaps.Active.Map.id + '-callInProgress', self.promptToJoin);
socket.on('maps-' + Metamaps.Active.Map.id + '-callStarting', self.conversationHasBegun);
socket.on('maps-' + Metamaps.Active.Map.id + '-mapperJoinedCall', self.mapperJoinedCall);
socket.on('maps-' + Metamaps.Active.Map.id + '-mapperLeftCall', self.mapperLeftCall);
// if you're the 'new guy' update your list with who's already online // if you're the 'new guy' update your list with who's already online
socket.on(myId + '-' + Metamaps.Active.Map.id + '-UpdateMapperList', self.updateMapperList); socket.on(myId + '-' + Metamaps.Active.Map.id + '-UpdateMapperList', self.updateMapperList);
@ -2078,6 +2435,9 @@ Metamaps.Realtime = {
// //
socket.on('maps-' + Metamaps.Active.Map.id + '-newTopic', self.newTopic); socket.on('maps-' + Metamaps.Active.Map.id + '-newTopic', self.newTopic);
//
socket.on('maps-' + Metamaps.Active.Map.id + '-newMessage', self.newMessage);
// //
socket.on('maps-' + Metamaps.Active.Map.id + '-removeTopic', self.removeTopic); socket.on('maps-' + Metamaps.Active.Map.id + '-removeTopic', self.removeTopic);
@ -2159,6 +2519,11 @@ Metamaps.Realtime = {
}; };
$(document).on(Metamaps.JIT.events.removeSynapse, sendRemoveSynapse); $(document).on(Metamaps.JIT.events.removeSynapse, sendRemoveSynapse);
var sendNewMessage = function (event, data) {
self.sendNewMessage(data);
};
$(document).on(Metamaps.Views.room.events.newMessage, sendNewMessage);
}, },
attachMapListener: function(){ attachMapListener: function(){
var self = Metamaps.Realtime; var self = Metamaps.Realtime;
@ -2200,31 +2565,22 @@ Metamaps.Realtime = {
// data.userrealtime // data.userrealtime
self.mappersOnMap[data.userid] = { self.mappersOnMap[data.userid] = {
id: data.userid,
name: data.username, name: data.username,
username: data.username,
image: data.userimage, image: data.userimage,
color: Metamaps.Util.getPastelColor(), color: Metamaps.Util.getPastelColor(),
realtime: data.userrealtime, realtime: data.userrealtime,
inConversation: data.userinconversation,
coords: { coords: {
x: 0, x: 0,
y: 0 y: 0
}, }
}; };
var onOff = data.userrealtime ? "On" : "Off";
var mapperListItem = '<li id="mapper';
mapperListItem += data.userid;
mapperListItem += '" class="rtMapper littleRt';
mapperListItem += onOff;
mapperListItem += '">';
mapperListItem += '<img style="border: 2px solid ' + self.mappersOnMap[data.userid].color + ';"';
mapperListItem += ' src="' + data.userimage + '" width="24" height="24" class="rtUserImage" />';
mapperListItem += data.username;
mapperListItem += '<div class="littleJuntoIcon"></div>';
mapperListItem += '</li>';
if (data.userid !== Metamaps.Active.Mapper.id) { if (data.userid !== Metamaps.Active.Mapper.id) {
$('#mapper' + data.userid).remove(); self.room.chat.addParticipant(self.mappersOnMap[data.userid]);
$('.realtimeMapperList ul').append(mapperListItem); if (data.userinconversation) self.room.chat.mapperJoinedCall(data.userid);
// create a div for the collaborators compass // create a div for the collaborators compass
self.createCompass(data.username, data.userid, data.userimage, self.mappersOnMap[data.userid].color, !self.status); self.createCompass(data.username, data.userid, data.userimage, self.mappersOnMap[data.userid].color, !self.status);
@ -2238,9 +2594,12 @@ Metamaps.Realtime = {
// data.username // data.username
// data.userimage // data.userimage
// data.coords // data.coords
var firstOtherPerson = Object.keys(self.mappersOnMap).length === 0;
self.mappersOnMap[data.userid] = { self.mappersOnMap[data.userid] = {
id: data.userid,
name: data.username, name: data.username,
username: data.username,
image: data.userimage, image: data.userimage,
color: Metamaps.Util.getPastelColor(), color: Metamaps.Util.getPastelColor(),
realtime: true, realtime: true,
@ -2252,19 +2611,16 @@ Metamaps.Realtime = {
// create an item for them in the realtime box // create an item for them in the realtime box
if (data.userid !== Metamaps.Active.Mapper.id && self.status) { if (data.userid !== Metamaps.Active.Mapper.id && self.status) {
var mapperListItem = '<li id="mapper' + data.userid + '" class="rtMapper littleRtOn">'; self.room.chat.addParticipant(self.mappersOnMap[data.userid]);
mapperListItem += '<img style="border: 2px solid ' + self.mappersOnMap[data.userid].color + ';"';
mapperListItem += ' src="' + data.userimage + '" width="24" height="24" class="rtUserImage" />';
mapperListItem += data.username;
mapperListItem += '<div class="littleJuntoIcon"></div>';
mapperListItem += '</li>';
$('#mapper' + data.userid).remove();
$('.realtimeMapperList ul').append(mapperListItem);
// create a div for the collaborators compass // create a div for the collaborators compass
self.createCompass(data.username, data.userid, data.userimage, self.mappersOnMap[data.userid].color, !self.status); self.createCompass(data.username, data.userid, data.userimage, self.mappersOnMap[data.userid].color, !self.status);
Metamaps.GlobalUI.notifyUser(data.username + ' just joined the map'); var notifyMessage = data.username + ' just joined the map';
if (firstOtherPerson) {
notifyMessage += ' <button type="button" class="toast-button button" onclick="Metamaps.Realtime.inviteACall(' + data.userid + ')">Suggest A Video Call</button>';
}
Metamaps.GlobalUI.notifyUser(notifyMessage);
// send this new mapper back your details, and the awareness that you've loaded the map // send this new mapper back your details, and the awareness that you've loaded the map
var update = { var update = {
@ -2273,6 +2629,7 @@ Metamaps.Realtime = {
userimage: Metamaps.Active.Mapper.get("image"), userimage: Metamaps.Active.Mapper.get("image"),
userid: Metamaps.Active.Mapper.id, userid: Metamaps.Active.Mapper.id,
userrealtime: self.status, userrealtime: self.status,
userinconversation: self.inConversation,
mapid: Metamaps.Active.Map.id mapid: Metamaps.Active.Map.id
}; };
socket.emit('updateNewMapperList', update); socket.emit('updateNewMapperList', update);
@ -2305,10 +2662,16 @@ Metamaps.Realtime = {
delete self.mappersOnMap[data.userid]; delete self.mappersOnMap[data.userid];
$('#mapper' + data.userid).remove(); //$('#mapper' + data.userid).remove();
$('#compass' + data.userid).remove(); $('#compass' + data.userid).remove();
self.room.chat.removeParticipant(data.username);
Metamaps.GlobalUI.notifyUser(data.username + ' just left the map'); Metamaps.GlobalUI.notifyUser(data.username + ' just left the map');
if ((self.inConversation && self.countOthersInConversation() === 0) ||
(!self.inConversation && self.countOthersInConversation() === 1)) {
self.callEnded();
}
}, },
newCollaborator: function (data) { newCollaborator: function (data) {
var self = Metamaps.Realtime; var self = Metamaps.Realtime;
@ -2319,7 +2682,7 @@ Metamaps.Realtime = {
self.mappersOnMap[data.userid].realtime = true; self.mappersOnMap[data.userid].realtime = true;
$('#mapper' + data.userid).removeClass('littleRtOff').addClass('littleRtOn'); //$('#mapper' + data.userid).removeClass('littleRtOff').addClass('littleRtOn');
$('#compass' + data.userid).show(); $('#compass' + data.userid).show();
Metamaps.GlobalUI.notifyUser(data.username + ' just turned on realtime'); Metamaps.GlobalUI.notifyUser(data.username + ' just turned on realtime');
@ -2333,7 +2696,7 @@ Metamaps.Realtime = {
self.mappersOnMap[data.userid].realtime = false; self.mappersOnMap[data.userid].realtime = false;
$('#mapper' + data.userid).removeClass('littleRtOn').addClass('littleRtOff'); //$('#mapper' + data.userid).removeClass('littleRtOn').addClass('littleRtOff');
$('#compass' + data.userid).hide(); $('#compass' + data.userid).hide();
Metamaps.GlobalUI.notifyUser(data.username + ' just turned off realtime'); Metamaps.GlobalUI.notifyUser(data.username + ' just turned off realtime');
@ -2362,9 +2725,10 @@ Metamaps.Realtime = {
var self = Metamaps.Realtime; var self = Metamaps.Realtime;
var socket = Metamaps.Realtime.socket; var socket = Metamaps.Realtime.socket;
var boundary = self.chatOpen ? '#wrapper' : document;
var mapper = self.mappersOnMap[id]; var mapper = self.mappersOnMap[id];
var xMax=$(document).width(); var xMax=$(boundary).width();
var yMax=$(document).height(); var yMax=$(boundary).height();
var compassDiameter=56; var compassDiameter=56;
var compassArrowSize=24; var compassArrowSize=24;
@ -2399,9 +2763,10 @@ Metamaps.Realtime = {
var self = Metamaps.Realtime; var self = Metamaps.Realtime;
var socket = Metamaps.Realtime.socket; var socket = Metamaps.Realtime.socket;
var boundary = self.chatOpen ? '#wrapper' : document;
var xLimit, yLimit; var xLimit, yLimit;
var xMax=$(document).width(); var xMax=$(boundary).width();
var yMax=$(document).height(); var yMax=$(boundary).height();
var compassDiameter=56; var compassDiameter=56;
var compassArrowSize=24; var compassArrowSize=24;
@ -2536,6 +2901,21 @@ Metamaps.Realtime = {
}); });
} }
}, },
// newMessage
sendNewMessage: function (data) {
var self = Metamaps.Realtime;
var socket = self.socket;
var message = data.attributes;
message.mapid = Metamaps.Active.Map.id;
socket.emit('newMessage', message);
},
newMessage: function (data) {
var self = Metamaps.Realtime;
var socket = self.socket;
self.room.addMessages(new Metamaps.Backbone.MessageCollection(data));
},
// newTopic // newTopic
sendNewTopic: function (data) { sendNewTopic: function (data) {
var self = Metamaps.Realtime; var self = Metamaps.Realtime;
@ -3000,7 +3380,6 @@ Metamaps.Control = {
if (edge.getData("synapses").length - 1 === 0) { if (edge.getData("synapses").length - 1 === 0) {
Metamaps.Control.hideEdge(edge); Metamaps.Control.hideEdge(edge);
} }
var mappableid = synapse.id; var mappableid = synapse.id;
synapse.destroy(); synapse.destroy();
@ -3220,7 +3599,6 @@ Metamaps.Filter = {
var self = Metamaps.Filter; var self = Metamaps.Filter;
Metamaps.GlobalUI.Account.close(); Metamaps.GlobalUI.Account.close();
Metamaps.Realtime.close();
$('.sidebarFilterIcon div').addClass('hide'); $('.sidebarFilterIcon div').addClass('hide');
@ -3712,6 +4090,7 @@ Metamaps.Listeners = {
$(window).resize(function () { $(window).resize(function () {
if (Metamaps.Visualize && Metamaps.Visualize.mGraph) Metamaps.Visualize.mGraph.canvas.resize($(window).width(), $(window).height()); if (Metamaps.Visualize && Metamaps.Visualize.mGraph) Metamaps.Visualize.mGraph.canvas.resize($(window).width(), $(window).height());
if ((Metamaps.Active.Map || Metamaps.Active.Topic) && Metamaps.Famous && Metamaps.Famous.maps.surf) Metamaps.Famous.maps.reposition(); if ((Metamaps.Active.Map || Metamaps.Active.Topic) && Metamaps.Famous && Metamaps.Famous.maps.surf) Metamaps.Famous.maps.reposition();
if (Metamaps.Active.Map && Metamaps.Realtime.inConversation) Metamaps.Realtime.positionVideos();
}); });
} }
}; // end Metamaps.Listeners }; // end Metamaps.Listeners
@ -4391,6 +4770,7 @@ Metamaps.Map = {
Metamaps.Topics = new bb.TopicCollection(data.topics); Metamaps.Topics = new bb.TopicCollection(data.topics);
Metamaps.Synapses = new bb.SynapseCollection(data.synapses); Metamaps.Synapses = new bb.SynapseCollection(data.synapses);
Metamaps.Mappings = new bb.MappingCollection(data.mappings); Metamaps.Mappings = new bb.MappingCollection(data.mappings);
Metamaps.Messages = data.messages;
Metamaps.Backbone.attachCollectionEvents(); Metamaps.Backbone.attachCollectionEvents();
var map = Metamaps.Active.Map; var map = Metamaps.Active.Map;
@ -5187,4 +5567,3 @@ Metamaps.Admin = {
} }
} }
}; };

View file

@ -0,0 +1,339 @@
Metamaps.Views = Metamaps.Views || {};
Metamaps.Views.chatView = (function () {
var
chatView,
linker = new Autolinker({ newWindow: true, truncate: 50, email: false, phone: false, twitter: false });
var Private = {
messageHTML: "<div class='chat-message'>" +
"<div class='chat-message-user'><img src='{{ user_image }}' title='{{user_name }}'/></div>" +
"<div class='chat-message-text'>{{ message }}</div>" +
"<div class='chat-message-time'>{{ timestamp }}</div>" +
"<div class='clearfloat'></div>" +
"</div>",
participantHTML: "<div class='participant participant-{{ id }} {{ selfClass }}'>" +
"<div class='chat-participant-image'><img src='{{ image }}' style='border: 2px solid {{ color }};' /></div>" +
"<div class='chat-participant-name'>{{ username }} {{ selfName }}</div>" +
"<button type='button' class='button chat-participant-invite-call' onclick='Metamaps.Realtime.inviteACall({{ id}});'></button>" +
"<button type='button' class='button chat-participant-invite-join' onclick='Metamaps.Realtime.inviteToJoin({{ id}});'></button>" +
"<span class='chat-participant-participating'><div class='green-dot'></div></span>" +
"<div class='clearfloat'></div>" +
"</div>",
templates: function() {
_.templateSettings = {
interpolate: /\{\{(.+?)\}\}/g
};
this.messageTemplate = _.template(Private.messageHTML);
this.participantTemplate = _.template(Private.participantHTML);
},
createElements: function() {
this.$unread = $('<div class="chat-unread"></div>');
this.$button = $('<div class="chat-button"><div class="tooltips">Chat</div></div>');
this.$messageInput = $('<textarea placeholder="Send a message..." class="chat-input"></textarea>');
this.$juntoHeader = $('<div class="junto-header">PARTICIPANTS</div>');
this.$videoToggle = $('<div class="video-toggle"></div>');
this.$cursorToggle = $('<div class="cursor-toggle"></div>');
this.$participants = $('<div class="participants"></div>');
this.$conversationInProgress = $('<div class="conversation-live">LIVE <span class="call-action leave" onclick="Metamaps.Realtime.leaveCall();">LEAVE</span><span class="call-action join" onclick="Metamaps.Realtime.joinCall();">JOIN</span></div>');
this.$chatHeader = $('<div class="chat-header">CHAT</div>');
this.$soundToggle = $('<div class="sound-toggle active"></div>');
this.$messages = $('<div class="chat-messages"></div>');
this.$container = $('<div class="chat-box"></div>');
},
attachElements: function() {
this.$button.append(this.$unread);
this.$juntoHeader.append(this.$videoToggle);
this.$juntoHeader.append(this.$cursorToggle);
this.$chatHeader.append(this.$soundToggle);
this.$participants.append(this.$conversationInProgress);
this.$container.append(this.$juntoHeader);
this.$container.append(this.$participants);
this.$container.append(this.$chatHeader);
this.$container.append(this.$button);
this.$container.append(this.$messages);
this.$container.append(this.$messageInput);
},
addEventListeners: function() {
var self = this;
this.participants.on('add', function (participant) {
Private.addParticipant.call(self, participant);
});
this.participants.on('remove', function (participant) {
Private.removeParticipant.call(self, participant);
});
this.$button.on('click', function () {
Handlers.buttonClick.call(self);
});
this.$videoToggle.on('click', function () {
Handlers.videoToggleClick.call(self);
});
this.$cursorToggle.on('click', function () {
Handlers.cursorToggleClick.call(self);
});
this.$soundToggle.on('click', function () {
Handlers.soundToggleClick.call(self);
});
this.$messageInput.on('keyup', function (event) {
Handlers.keyUp.call(self, event);
});
this.$messageInput.on('focus', function () {
Handlers.inputFocus.call(self);
});
this.$messageInput.on('blur', function () {
Handlers.inputBlur.call(self);
});
},
initializeSounds: function() {
this.sound = new Howl({
urls: ["<%= asset_path 'sounds/sounds.mp3' %>", "<%= asset_path 'sounds/sounds.ogg' %>"],
sprite: {
laser: [3000, 700]
}
});
},
incrementUnread: function() {
this.unreadMessages++;
this.$unread.html(this.unreadMessages);
this.$unread.show();
},
addMessage: function(message, isInitial) {
if (!this.isOpen && !isInitial) Private.incrementUnread.call(this);
function addZero(i) {
if (i < 10) {
i = "0" + i;
}
return i;
}
var m = _.clone(message.attributes);
var today = new Date();
m.timestamp = new Date(m.created_at);
var date = (m.timestamp.getMonth() + 1) + '/' + m.timestamp.getDate();
date += " " + addZero(m.timestamp.getHours()) + ":" + addZero(m.timestamp.getMinutes());
m.timestamp = date;
m.image = m.user_image || 'http://www.hotpepper.ca/wp-content/uploads/2014/11/default_profile_1_200x200.png'; // TODO: remove
m.message = linker.link(m.message);
var $html = $(this.messageTemplate(m));
this.$messages.append($html);
if (!isInitial) this.scrollMessages(200);
if (!isInitial && this.alertSound) this.sound.play('laser');
},
initialMessages: function() {
var messages = this.messages.models;
for (var i = 0; i < messages.length; i++) {
Private.addMessage.call(this, messages[i], true);
}
},
handleInputMessage: function() {
var message = {
message: this.$messageInput.val(),
};
this.$messageInput.val('');
$(document).trigger(chatView.events.message + '-' + this.room, [message]);
},
addParticipant: function(participant) {
var p = _.clone(participant.attributes);
if (p.self) {
p.selfClass = 'is-self';
p.selfName = '(me)';
} else {
p.selfClass = '';
p.selfName = '';
}
var html = this.participantTemplate(p);
this.$participants.append(html);
},
removeParticipant: function(participant) {
this.$container.find('.participant-' + participant.get('id')).remove();
}
};
var Handlers = {
buttonClick: function() {
if (this.isOpen) this.close();
else if (!this.isOpen) this.open();
},
videoToggleClick: function() {
this.$videoToggle.toggleClass('active');
this.videosShowing = !this.videosShowing;
$(document).trigger(this.videosShowing ? chatView.events.videosOn : chatView.events.videosOff);
},
cursorToggleClick: function() {
this.$cursorToggle.toggleClass('active');
this.cursorsShowing = !this.cursorsShowing;
$(document).trigger(this.cursorsShowing ? chatView.events.cursorsOn : chatView.events.cursorsOff);
},
soundToggleClick: function() {
this.alertSound = !this.alertSound;
this.$soundToggle.toggleClass('active');
},
keyUp: function(event) {
switch(event.which) {
case 13: // enter
Private.handleInputMessage.call(this);
break;
}
},
inputFocus: function() {
$(document).trigger(chatView.events.inputFocus);
},
inputBlur: function() {
$(document).trigger(chatView.events.inputBlur);
}
};
chatView = function(messages, mapper, room) {
var self = this;
this.room = room;
this.mapper = mapper;
this.messages = messages; // backbone collection
this.isOpen = false;
this.alertSound = false; // whether to play sounds on arrival of new messages or not
this.cursorsShowing = true;
this.videosShowing = true;
this.unreadMessages = 0;
this.participants = new Backbone.Collection();
Private.templates.call(this);
Private.createElements.call(this);
Private.attachElements.call(this);
Private.addEventListeners.call(this);
Private.initialMessages.call(this);
Private.initializeSounds.call(this);
this.$container.css({
right: '-300px'
});
};
chatView.prototype.conversationInProgress = function (participating) {
this.$conversationInProgress.show();
this.$participants.addClass('is-live');
if (participating) this.$participants.addClass('is-participating');
this.$button.addClass('active');
// hide invite to call buttons
}
chatView.prototype.conversationEnded = function () {
this.$conversationInProgress.hide();
this.$participants.removeClass('is-live');
this.$participants.removeClass('is-participating');
this.$button.removeClass('active');
this.$participants.find('.participant').removeClass('active');
this.$participants.find('.participant').removeClass('pending');
}
chatView.prototype.leaveConversation = function () {
this.$participants.removeClass('is-participating');
}
chatView.prototype.mapperJoinedCall = function (id) {
this.$participants.find('.participant-' + id).addClass('active');
}
chatView.prototype.mapperLeftCall = function (id) {
this.$participants.find('.participant-' + id).removeClass('active');
}
chatView.prototype.invitationPending = function (id) {
this.$participants.find('.participant-' + id).addClass('pending');
}
chatView.prototype.invitationAnswered = function (id) {
this.$participants.find('.participant-' + id).removeClass('pending');
}
chatView.prototype.addParticipant = function (participant) {
this.participants.add(participant);
}
chatView.prototype.removeParticipant = function (username) {
var p = this.participants.find(function (p) { return p.get('username') === username; });
if (p) {
this.participants.remove(p);
}
}
chatView.prototype.removeParticipants = function () {
this.participants.remove(this.participants.models);
}
chatView.prototype.open = function () {
this.$container.css({
right: '0'
});
this.$messageInput.focus();
this.isOpen = true;
this.unreadMessages = 0;
this.$unread.hide();
this.scrollMessages(0);
$(document).trigger(chatView.events.openTray);
}
chatView.prototype.addMessage = function(message, isInitial) {
this.messages.add(message);
Private.addMessage.call(this, message, isInitial);
}
chatView.prototype.scrollMessages = function(duration) {
duration = duration || 0;
this.$messages.animate({
scrollTop: this.$messages[0].scrollHeight
}, duration);
}
chatView.prototype.clearMessages = function () {
this.unreadMessages = 0;
this.$unread.hide();
this.$messages.empty();
}
chatView.prototype.close = function () {
this.$container.css({
right: '-300px'
});
this.$messageInput.blur();
this.isOpen = false;
$(document).trigger(chatView.events.closeTray);
}
chatView.prototype.remove = function () {
this.$button.off();
this.$container.remove();
}
/**
* @class
* @static
*/
chatView.events = {
message: 'ChatView:message',
openTray: 'ChatView:openTray',
closeTray: 'ChatView:closeTray',
inputFocus: 'ChatView:inputFocus',
inputBlur: 'ChatView:inputBlur',
cursorsOff: 'ChatView:cursorsOff',
cursorsOn: 'ChatView:cursorsOn',
videosOff: 'ChatView:videosOff',
videosOn: 'ChatView:videosOn'
};
return chatView;
})();

View file

@ -0,0 +1,194 @@
Metamaps.Views = Metamaps.Views || {};
Metamaps.Views.room = (function () {
var ChatView = Metamaps.Views.chatView;
var VideoView = Metamaps.Views.videoView;
var room = function(opts) {
var self = this;
this.isActiveRoom = false;
this.socket = opts.socket;
this.webrtc = opts.webrtc;
//this.roomRef = opts.firebase;
this.room = opts.room;
this.config = opts.config;
this.peopleCount = 0;
this.$myVideo = opts.$video;
this.myVideo = opts.myVideoView;
this.messages = new Backbone.Collection();
this.currentMapper = new Backbone.Model({ name: opts.username, image: opts.image });
this.chat = new ChatView(this.messages, this.currentMapper, this.room);
this.videos = {};
this.init();
};
room.prototype.join = function(cb) {
this.isActiveRoom = true;
this.webrtc.joinRoom(this.room, cb);
this.chat.conversationInProgress(true); // true indicates participation
}
room.prototype.conversationInProgress = function() {
this.chat.conversationInProgress(false); // false indicates not participating
}
room.prototype.conversationEnding = function() {
this.chat.conversationEnded();
}
room.prototype.leaveVideoOnly = function() {
this.chat.leaveConversation(); // the conversation will carry on without you
for (var id in this.videos) {
this.removeVideo(id);
}
this.isActiveRoom = false;
this.webrtc.leaveRoom();
}
room.prototype.leave = function() {
for (var id in this.videos) {
this.removeVideo(id);
}
this.isActiveRoom = false;
this.webrtc.leaveRoom();
this.chat.conversationEnded();
this.chat.removeParticipants();
this.chat.clearMessages();
this.messages.reset();
}
room.prototype.setPeopleCount = function(count) {
this.peopleCount = count;
}
room.prototype.init = function () {
var self = this;
$(document).on(VideoView.events.audioControlClick, function (event, videoView) {
if (!videoView.audioStatus) self.webrtc.mute();
else if (videoView.audioStatus) self.webrtc.unmute();
});
$(document).on(VideoView.events.videoControlClick, function (event, videoView) {
if (!videoView.videoStatus) self.webrtc.pauseVideo();
else if (videoView.videoStatus) self.webrtc.resumeVideo();
});
this.webrtc.webrtc.off('peerStreamAdded');
this.webrtc.webrtc.off('peerStreamRemoved');
this.webrtc.on('peerStreamAdded', function (peer) {
var mapper = Metamaps.Realtime.mappersOnMap[peer.nick];
peer.avatar = mapper.image;
peer.username = mapper.name;
if (self.isActiveRoom) {
self.addVideo(peer);
}
});
this.webrtc.on('peerStreamRemoved', function (peer) {
if (self.isActiveRoom) {
self.removeVideo(peer);
}
});
this.webrtc.on('mute', function (data) {
var v = self.videos[data.id];
if (!v) return;
if (data.name === 'audio') {
v.audioStatus = false;
}
else if (data.name === 'video') {
v.videoStatus = false;
v.$avatar.show();
}
if (!v.audioStatus && !v.videoStatus) v.$container.hide();
});
this.webrtc.on('unmute', function (data) {
var v = self.videos[data.id];
if (!v) return;
if (data.name === 'audio') {
v.audioStatus = true;
}
else if (data.name === 'video') {
v.videoStatus = true;
v.$avatar.hide();
}
v.$container.show();
});
var sendChatMessage = function (event, data) {
self.sendChatMessage(data);
};
$(document).on(ChatView.events.message + '-' + this.room, sendChatMessage);
}
room.prototype.videoAdded = function (callback) {
this._videoAdded = callback;
}
room.prototype.addVideo = function (peer) {
var
id = this.webrtc.getDomId(peer),
video = attachMediaStream(peer.stream);
var
v = new VideoView(video, null, id, false, { DOUBLE_CLICK_TOLERANCE: 200, avatar: peer.avatar, username: peer.username });
this.videos[peer.id] = v;
if (this._videoAdded) this._videoAdded(v, peer.nick);
}
room.prototype.removeVideo = function (peer) {
var id = typeof peer == 'string' ? peer : peer.id;
if (this.videos[id]) {
this.videos[id].remove();
delete this.videos[id];
}
}
room.prototype.sendChatMessage = function (data) {
var self = this;
//this.roomRef.child('messages').push(data);
var m = new Metamaps.Backbone.Message({
message: data.message,
resource_id: Metamaps.Active.Map.id,
resource_type: "Map"
});
m.save(null, {
success: function (model, response) {
self.addMessages(new Metamaps.Backbone.MessageCollection(model));
$(document).trigger(room.events.newMessage, [model]);
},
error: function (model, response) {
console.log('error!', response);
}
});
}
// they should be instantiated as backbone models before they get
// passed to this function
room.prototype.addMessages = function (messages, isInitial) {
var self = this;
messages.models.forEach(function (message) {
self.chat.addMessage(message, isInitial);
});
}
/**
* @class
* @static
*/
room.events = {
newMessage: "Room:newMessage"
};
return room;
})();

View file

@ -0,0 +1,207 @@
Metamaps.Views = Metamaps.Views || {};
Metamaps.Views.videoView = (function () {
var videoView;
var Private = {
addControls: function() {
var self = this;
this.$audioControl = $('<div class="video-audio"></div>');
this.$videoControl = $('<div class="video-video"></div>');
this.$audioControl.on('click', function () {
Handlers.audioControlClick.call(self);
});
this.$videoControl.on('click', function () {
Handlers.videoControlClick.call(self);
});
this.$container.append(this.$audioControl);
this.$container.append(this.$videoControl);
},
cancelClick: function() {
this.mouseIsDown = false;
if (this.hasMoved) {
}
$(document).trigger(videoView.events.dragEnd);
}
};
var Handlers = {
mousedown: function(event) {
this.mouseIsDown = true;
this.hasMoved = false;
this.mouseMoveStart = {
x: event.pageX,
y: event.pageY
};
this.posStart = {
x: parseInt(this.$container.css('left'), '10'),
y: parseInt(this.$container.css('top'), '10')
}
$(document).trigger(videoView.events.mousedown);
},
mouseup: function(event) {
$(document).trigger(videoView.events.mouseup, [this]);
var storedTime = this.lastClick;
var now = Date.now();
this.lastClick = now;
if (now - storedTime < this.config.DOUBLE_CLICK_TOLERANCE) {
$(document).trigger(videoView.events.doubleClick, [this]);
}
},
mousemove: function(event) {
var
diffX,
diffY,
newX,
newY;
if (this.$parent && this.mouseIsDown) {
this.manuallyPositioned = true;
this.hasMoved = true;
diffX = event.pageX - this.mouseMoveStart.x;
diffY = this.mouseMoveStart.y - event.pageY;
newX = this.posStart.x + diffX;
newY = this.posStart.y - diffY;
this.$container.css({
top: newY,
left: newX
});
}
},
audioControlClick: function() {
if (this.audioStatus) {
this.audioOff();
} else {
this.audioOn();
}
$(document).trigger(videoView.events.audioControlClick, [this]);
},
videoControlClick: function() {
if (this.videoStatus) {
this.videoOff();
} else {
this.videoOn();
}
$(document).trigger(videoView.events.videoControlClick, [this]);
},
};
var videoView = function(video, $parent, id, isMyself, config) {
var self = this;
this.$parent = $parent; // mapView
this.video = video;
this.id = id;
this.config = config;
this.mouseIsDown = false;
this.mouseDownOffset = { x: 0, y: 0 };
this.lastClick = null;
this.hasMoved = false;
this.audioStatus = true;
this.videoStatus = true;
this.$container = $('<div></div>');
this.$container.addClass('collaborator-video' + (isMyself ? ' my-video' : ''));
this.$container.attr('id', 'container_' + id);
var $vidContainer = $('<div></div>');
$vidContainer.addClass('video-cutoff');
$vidContainer.append(this.video);
this.avatar = config.avatar;
this.$avatar = $('<img draggable="false" class="collaborator-video-avatar" src="' + config.avatar + '" width="150" height="150" />');
$vidContainer.append(this.$avatar);
this.$container.append($vidContainer);
this.$container.on('mousedown', function (event) {
Handlers.mousedown.call(self, event);
});
if (isMyself) {
Private.addControls.call(this);
}
// suppress contextmenu
this.video.oncontextmenu = function () { return false; };
if (this.$parent) this.setParent(this.$parent);
};
videoView.prototype.setParent = function($parent) {
var self = this;
this.$parent = $parent;
this.$parent.off('.video' + this.id);
this.$parent.on('mouseup.video' + this.id, function (event) {
Handlers.mouseup.call(self, event);
Private.cancelClick.call(self);
});
this.$parent.on('mousemove.video' + this.id, function (event) {
Handlers.mousemove.call(self, event);
});
}
videoView.prototype.setAvatar = function (src) {
this.$avatar.attr('src', src);
this.avatar = src;
}
videoView.prototype.remove = function () {
this.$container.off();
if (this.$parent) this.$parent.off('.video' + this.id);
this.$container.remove();
}
videoView.prototype.videoOff = function () {
this.$videoControl.addClass('active');
this.$avatar.show();
this.videoStatus = false;
}
videoView.prototype.videoOn = function () {
this.$videoControl.removeClass('active');
this.$avatar.hide();
this.videoStatus = true;
}
videoView.prototype.audioOff = function () {
this.$audioControl.addClass('active');
this.audioStatus = false;
}
videoView.prototype.audioOn = function () {
this.$audioControl.removeClass('active');
this.audioStatus = true;
}
/**
* @class
* @static
*/
videoView.events = {
mousedown: "VideoView:mousedown",
mouseup: "VideoView:mouseup",
doubleClick: "VideoView:doubleClick",
dragEnd: "VideoView:dragEnd",
audioControlClick: "VideoView:audioControlClick",
videoControlClick: "VideoView:videoControlClick",
};
return videoView;
})();

View file

@ -132,6 +132,17 @@ a.button:active,
input[type="submit"]:active { input[type="submit"]:active {
background: #429B46; background: #429B46;
} }
button.button.btn-no {
background-color: #c04f4f;
}
button.button.btn-no:hover {
background-color: #A54242;
}
.toast .toast-button {
margin-top: -10px;
margin-left: 10px;
}
/* /*
* Utility * Utility
*/ */
@ -628,8 +639,12 @@ label {
margin: 0 0 0 1.3em; margin: 0 0 0 1.3em;
} }
.main { .main {
position: relative;
/*overflow:hidden; */ /*overflow:hidden; */
} }
.main.compressed {
width: calc(100% - 300px);
}
#infovis-canvas { #infovis-canvas {
-webkit-touch-callout: none; -webkit-touch-callout: none;
-webkit-user-select: none; -webkit-user-select: none;
@ -1077,84 +1092,6 @@ h3.filterBox {
} }
/* end filter by metacode */ /* end filter by metacode */
/* collaborate */
.sidebarCollaborate {
width: 32px;
height: 32px;
}
.sidebarCollaborateBox {
display: none;
height: auto;
padding: 16px;
width: 238px;
}
h3.realtimeBoxTitle {
margin-bottom: 10px;
text-align: left;
float: left;
font-size:18px;
line-height:18px;
}
.sidebarCollaborateBox .realtimeOnOff {
float: right;
padding: 4px;
border-radius: 2px;
margin-left: 12px;
cursor: pointer;
text-align: center;
font-size:12px;
}
.sidebarCollaborateBox .realtimeOnOff:hover, .sidebarCollaborateBox .realtimeOnOff.active {
color: #00bcd4;
}
.sidebarCollaborateBox .rtOff {
}
.sidebarCollaborateBox .rtOn {
}
.realtimeMapperList .rtMapper {
list-style-type: none;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 10px 34px;
display: block;
height: 14px;
font-family: 'din-regular', helvetica, sans-serif;
font-size: 14px;
line-height: 14px;
position: relative;
}
.rtMapperSelf img {
border: 2px solid #424242;
}
.rtUserImage {
position: absolute;
top: 4px;
left: 0;
border-radius: 14px;
}
.littleJuntoIcon {
width: 24px;
height:24px;
position: absolute;
top: 4px;
right: 0;
background-image: url(<%= asset_data_uri('junto24_sprite.png') %>);
}
.realtimeMapperList .littleRtOff .littleJuntoIcon {
background-position: 0 0;
}
.realtimeMapperList .littleRtOn .littleJuntoIcon {
background-position: -24px 0;
}
/* end collaborate */
.nodemargin { .nodemargin {
padding-top: 120px; padding-top: 120px;
} }

View file

@ -25,7 +25,7 @@
} }
#famousOverlay { #famousOverlay {
position:fixed; position:absolute;
top: 0; top: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -116,7 +116,7 @@
/* upperLeftUI */ /* upperLeftUI */
.upperLeftUI { .upperLeftUI {
position: fixed; position: absolute;
top: 10px; top: 10px;
left: 24px; left: 24px;
z-index:3; z-index:3;
@ -155,7 +155,7 @@
/* upperRightUI */ /* upperRightUI */
.upperRightUI { .upperRightUI {
position: fixed; position: absolute;
top: 10px; top: 10px;
right: 24px; right: 24px;
z-index:4; z-index:4;
@ -166,9 +166,9 @@
} }
.upperRightBox { .upperRightBox {
position: fixed; position: absolute;
top:52px; top:42px;
right:24px; right:0;
background-color: #E0E0E0; background-color: #E0E0E0;
border-radius: 2px; border-radius: 2px;
box-shadow: 0 3px 3px rgba(0,0,0,0.23), 0 3px 3px rgba(0,0,0,0.16); box-shadow: 0 3px 3px rgba(0,0,0,0.23), 0 3px 3px rgba(0,0,0,0.16);
@ -187,17 +187,12 @@
} }
.upperRightMapButtons { .upperRightMapButtons {
position: relative;
top: -42px; /* puts it just offscreen */ top: -42px; /* puts it just offscreen */
} }
.mapPage .upperRightMapButtons, .topicPage .upperRightMapButtons { .mapPage .upperRightMapButtons, .topicPage .upperRightMapButtons {
top: 0; top: 0;
} }
.topicPage .sidebarCollaborate {
display: none;
}
.upperRightIcon { .upperRightIcon {
width: 32px; width: 32px;
height: 32px; height: 32px;
@ -205,20 +200,6 @@
background-repeat: no-repeat; background-repeat: no-repeat;
cursor: pointer; cursor: pointer;
} }
.sidebarCollaborateIcon {
background-position: 0 0;
display: none;
}
.sidebarCollaborateIcon.blue {
background-position: -32px 0;
}
.sidebarCollaborateIcon.blue:hover {
background-position: -32px -32px;
}
/* only show the collaborate icon on commons */
.commonsMap .sidebarCollaborateIcon {
display: block;
}
.sidebarFilterIcon { .sidebarFilterIcon {
background-position: -64px 0; background-position: -64px 0;
} }
@ -384,7 +365,7 @@
} }
.infoAndHelp { .infoAndHelp {
position: fixed; position: absolute;
bottom: 20px; bottom: 20px;
right: 20px; right: 20px;
z-index: 3; z-index: 3;
@ -424,7 +405,7 @@
/* mapControls */ /* mapControls */
.mapControls { .mapControls {
position: fixed; position: absolute;
bottom: 24px; bottom: 24px;
right:-32px; /* puts it just offscreen */ right:-32px; /* puts it just offscreen */
width:32px; width:32px;
@ -474,9 +455,8 @@
background-position: -32px 0; background-position: -32px 0;
} }
.zoomExtents:hover .tooltips, .zoomIn:hover .tooltips, .zoomOut:hover .tooltips, .takeScreenshot:hover .tooltips, .sidebarCollaborateIcon:hover .tooltipsUnder, .zoomExtents:hover .tooltips, .zoomIn:hover .tooltips, .zoomOut:hover .tooltips, .takeScreenshot:hover .tooltips, .sidebarFilterIcon:hover .tooltipsUnder, .sidebarForkIcon:hover .tooltipsUnder, .addMap:hover .tooltipsUnder, .authenticated .sidebarAccountIcon:hover .tooltipsUnder,
.sidebarFilterIcon:hover .tooltipsUnder, .sidebarForkIcon:hover .tooltipsUnder, .addMap:hover .tooltipsUnder, .authenticated .sidebarAccountIcon:hover .tooltipsUnder, .mapInfoIcon:hover .tooltipsAbove, .openCheatsheet:hover .tooltipsAbove, .chat-button:hover .tooltips {
.mapInfoIcon:hover .tooltipsAbove, .openCheatsheet:hover .tooltipsAbove {
display: block; display: block;
} }
@ -532,10 +512,6 @@
font-style: normal; font-style: normal;
} }
.sidebarCollaborateIcon .tooltipsUnder {
margin-left: -3px;
}
.sidebarFilterIcon .tooltipsUnder { .sidebarFilterIcon .tooltipsUnder {
margin-left: -4px; margin-left: -4px;
} }
@ -560,16 +536,20 @@
left: -11px; left: -11px;
} }
.chat-button .tooltips {
top: 10px;
}
.openCheatsheet .tooltipsAbove { .openCheatsheet .tooltipsAbove {
left: -4px; left: -4px;
} }
.sidebarAccountIcon .tooltipsUnder { .sidebarAccountIcon .tooltipsUnder {
margin-left: -8px; margin-left: -12px;
margin-top: 40px; margin-top: 40px;
} }
.zoomExtents div::after, .zoomIn div::after, .zoomOut div::after, .takeScreenshot div:after { .zoomExtents div::after, .zoomIn div::after, .zoomOut div::after, .takeScreenshot div:after, .chat-button div.tooltips::after {
content: ''; content: '';
position: absolute; position: absolute;
top: 57%; top: 57%;
@ -582,21 +562,20 @@
border-bottom: 5px solid transparent; border-bottom: 5px solid transparent;
} }
.sidebarCollaborateIcon div:after, .sidebarFilterIcon div:after, .sidebarAccountIcon .tooltipsUnder:after { .sidebarFilterIcon div:after, .sidebarForkIcon div:after, .addMap div:after, .sidebarAccountIcon .tooltipsUnder:after {
left: 38%;
}
.sidebarCollaborateIcon div:after, .sidebarFilterIcon div:after, .sidebarForkIcon div:after, .addMap div:after, .sidebarAccountIcon .tooltipsUnder:after {
content: ''; content: '';
position: absolute; position: absolute;
top: 129%; right: 40%;
margin-top: -30px; margin-top: -7px;
width: 0; width: 0;
height: 0; height: 0;
border-bottom: 4px solid #000000; border-bottom: 4px solid #000000;
border-left: 5px solid transparent; border-left: 5px solid transparent;
border-right: 5px solid transparent; border-right: 5px solid transparent;
} }
.sidebarFilterIcon div:after {
right: 37% !important;
}
.mapInfoIcon div:after, .openCheatsheet div:after { .mapInfoIcon div:after, .openCheatsheet div:after {
content: ''; content: '';
@ -735,7 +714,7 @@
color: #F5F5F5; color: #F5F5F5;
padding: 16px; padding: 16px;
border-radius: 2px; border-radius: 2px;
z-index: 1 !important; /* important necessary for firefox */ z-index: 4 !important; /* important necessary for firefox */
font-size: 14px; font-size: 14px;
line-height:14px; line-height:14px;
} }
@ -764,3 +743,11 @@ box-shadow: 0px 1px 1.5px rgba(0,0,0,0.12), 0 1px 1px rgba(0,0,0,0.24);
body a#barometer_tab:hover { body a#barometer_tab:hover {
background-position: 0 -110px; background-position: 0 -110px;
} }
.hideVideos .collaborator-video {
display: none !important;
}
.hideCursors .collabCompass {
display: none !important;
}

View file

@ -0,0 +1,348 @@
.collaborator-video {
z-index: 1;
position: absolute;
width: 150px;
height: 150px;
cursor: default;
color: #FFF;
}
.collaborator-video .video-receive {
position: absolute;
width: 160px;
padding: 20px 20px 20px 170px;
background: #424242;
height: 110px;
border-top-left-radius: 75px;
border-bottom-left-radius: 75px;
border-top-right-radius: 2px;
border-bottom-right-radius: 2px;
}
.collaborator-video .video-receive .video-statement {
margin-bottom: 10px;
}
.collaborator-video .video-receive .btn-group .btn-yes {
margin-right: 10px;
}
.collaborator-video .video-receive .btn-group .btn-no {
background-color: #c04f4f;
}
.collaborator-video .video-receive .btn-group .btn-no:hover {
background-color: #A54242;
}
.collaborator-video .video-cutoff {
width: 150px;
height: 150px;
overflow: hidden;
border-radius: 75px;
z-index: 0;
position: relative;
-webkit-box-shadow: 0px 6px 3px rgba(0, 0, 0, 0.23), 10px 10px 10px rgba(0, 0, 0, 0.19);
-moz-box-shadow: 0px 6px 3px rgba(0, 0, 0, 0.23), 10px 10px 10px rgba(0, 0, 0, 0.19);
box-shadow: 0px 6px 3px rgba(0, 0, 0, 0.23), 10px 10px 10px rgba(0, 0, 0, 0.19);
}
.collaborator-video .video-cutoff video {
height: 150px;
margin-left: -25px;
}
.collaborator-video .video-cutoff .collaborator-video-avatar {
position: absolute;
top: 0;
left: 0;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-o-user-select: none;
user-select: none;
-webkit-user-drag: none;
display: none;
}
.collaborator-video .video-audio {
position: absolute;
width: 24px;
height: 24px;
top: 85%;
right: 0px;
cursor: pointer;
background: url(<%= asset_path 'audio_sprite.png' %>) no-repeat;
}
.collaborator-video .video-audio:hover {
background-position-x: -24px;
}
.collaborator-video .video-audio.active {
background-position-y: -24px;
}
.collaborator-video .video-video {
position: absolute;
width: 24px;
height: 24px;
top: 85%;
left: 0px;
cursor: pointer;
background: url(<%= asset_path 'camera_sprite.png' %>) no-repeat;
}
.collaborator-video .video-video:hover {
background-position-x: -24px;
}
.collaborator-video .video-video.active {
background-position-y: -24px;
}
.collaborator-video.my-video {
left: 30px;
top: 72px;
}
.chat-box {
position: relative;
display: flex;
flex-direction: column;
z-index: 1;
width: 300px;
float: right;
height: 100%;
background: #424242;
box-shadow: 0px 0px 16px 8px rgba(0, 0, 0, 0.23), -2px 10px 10px rgba(0, 0, 0, 0.19);
}
.chat-box .chat-button {
position: absolute;
top: 50%;
left: -36px;
width: 36px;
height: 49px;
background: url(<%= asset_path 'junto.png' %>) no-repeat 2px 9px, url(<%= asset_path 'tray_tab.png' %>) no-repeat;
cursor: pointer;
}
.chat-box .chat-button.active {
background: url(<%= asset_path 'junto_spinner_dark.gif' %>) no-repeat 2px 8px, url(<%= asset_path 'tray_tab.png' %>) no-repeat !important;
}
.chat-box .chat-button .chat-unread {
display: none;
background: #DAB539;
position: absolute;
top: -3px;
left: -11px;
width: 20px;
height: 20px;
border-radius: 11px;
border: 2px solid #424242;
color: #424242;
text-align: center;
font-size: 12px;
font-weight: bold;
line-height: 20px;
}
.chat-box .junto-header {
width: 100%;
padding: 16px 8px 16px 16px;
font-size: 16px;
text-align: left;
font-weight: bold;
background-color: #000000;
color: #f5f5f5;
box-shadow: 0px 6px 3px rgba(0, 0, 0, 0.23), 10px 10px 10px rgba(0, 0, 0, 0.19);
}
.chat-box .junto-header .cursor-toggle {
width: 32px;
height: 32px;
margin-right: 8px;
margin-top: -8px;
float: right;
background: url(<%= asset_path 'cursor_sprite.png' %>) no-repeat;
}
.chat-box .junto-header .cursor-toggle:hover {
background-position-x: -32px;
}
.chat-box .junto-header .cursor-toggle.active {
background-position-y: -32px;
}
.chat-box .junto-header .video-toggle {
width: 32px;
height: 32px;
margin-right: 32px;
margin-top: -8px;
float: right;
background: url(<%= asset_path 'video_sprite.png' %>) no-repeat;
}
.chat-box .junto-header .video-toggle:hover {
background-position-x: -32px;
}
.chat-box .junto-header .video-toggle.active {
background-position-y: -32px;
}
.chat-box .participants {
width: 100%;
min-height: 150px;
padding: 16px 0px 16px 0px;
text-align: left;
color: #f5f5f5;
overflow-y: auto;
}
.chat-box .participants .conversation-live {
display: none;
padding: 5px 10px 5px 10px;
background: #c04f4f;
margin: 5px 10px;
border-radius: 2px;
}
.chat-box .participants .conversation-live .call-action {
float: right;
cursor: pointer;
color: #EBFF00;
}
.chat-box .participants .conversation-live .leave {
display: none;
}
.chat-box .participants.is-participating .conversation-live .leave {
display: block;
}
.chat-box .participants.is-participating .conversation-live .join {
display: none;
}
.chat-box .participants .participant {
width: 89%;
padding: 8px 8px 2px 8px;
color: #f5f5f5;
font-family: arial, sans-serif;
font-size: 13px;
line-height: 14px;
}
.chat-box .participants .participant .chat-participant-image {
width: 15%;
float: left;
overflow: hidden;
color: #BBB;
padding-top: 2px;
}
.chat-box .participants .participant .chat-participant-image img {
width: 32px;
height: 32px;
border-radius: 18px;
}
.chat-box .participants .participant .chat-participant-name {
width: 53%;
float: left;
font-size: 13px;
font-weight: bold;
margin-top: 12px;
padding: 2px 8px 0;
text-align: left;
}
.chat-box .participants .participant.is-self .chat-participant-invite-call,
.chat-box .participants .participant.is-self .chat-participant-invite-join {
display: none !important;
}
.chat-box .participants.is-live .participant .chat-participant-invite-call {
display: none;
}
.chat-box .participants .participant .chat-participant-invite-join {
display: none;
}
.chat-box .participants.is-live.is-participating .participant:not(.active) .chat-participant-invite-join {
display: block;
}
.chat-box .participants .participant .chat-participant-invite-call,
.chat-box .participants .participant .chat-participant-invite-join
{
float: right;
background: #4FC059 url(<%= asset_path 'invitepeer16.png' %>) no-repeat center center;
}
.chat-box .participants .participant.pending .chat-participant-invite-call,
.chat-box .participants .participant.pending .chat-participant-invite-join {
background: #dab539 url(<%= asset_path 'ellipsis.gif' %>) no-repeat center center;
}
.chat-box .participants .participant .chat-participant-participating {
float: right;
display: none;
margin-top: 14px;
}
.chat-box .participants .participant .chat-participant-participating .green-dot {
background: #4fc059;
width: 12px;
height: 12px;
border-radius: 6px;
}
.chat-box .participants .participant.active .chat-participant-participating {
display: block;
}
.chat-box .chat-header {
width: 100%;
padding: 16px 8px 16px 16px;
font-size: 16px;
text-align: left;
font-weight: bold;
background-color: #000000;
color: #f5f5f5;
box-shadow: 0px 6px 3px rgba(0, 0, 0, 0.23), 10px 10px 10px rgba(0, 0, 0, 0.19);
}
.chat-box .chat-header .sound-toggle {
display: none;
width: 24px;
height: 24px;
margin-right: 32px;
margin-top: -2px;
float: right;
background: url(<%= asset_path 'sound_sprite.png' %>) no-repeat;
}
.chat-box .chat-header .sound-toggle:hover {
background-position-x: -24px;
}
.chat-box .chat-header .sound-toggle.active {
background-position-y: -24px;
}
.chat-box .chat-input {
min-height: 80px;
width: 94%;
padding: 8px 3% 8px 3%;
font-size: 13px;
outline: none;
resize: none;
}
.chat-box .chat-messages {
width: 100%;
padding: 16px 0px 0px 0px;
overflow-y: auto;
flex-grow: 1;
}
.chat-box .chat-messages .chat-message {
width: 89%;
padding: 8px 8px 2px 8px;
color: #f5f5f5;
font-family: arial, sans-serif;
font-size: 13px;
line-height: 14px;
}
.chat-box .chat-messages .chat-message a:link {
color: #4fb5c0;
text-decoration: underline;
}
.chat-box .chat-messages .chat-message a:visited {
color: #aea9fd;
text-decoration: underline;
}
.chat-box .chat-messages .chat-message a:hover {
color: #dab539;
text-decoration: underline;
}
.chat-box .chat-messages .chat-message .chat-message-user {
width: 15%;
float: left;
overflow: hidden;
color: #BBB;
padding-top: 2px;
}
.chat-box .chat-messages .chat-message .chat-message-user img {
border: 2px solid #424242;
width: 32px;
height: 32px;
border-radius: 18px;
}
.chat-box .chat-messages .chat-message .chat-message-text {
width: 73%;
float: left;
margin-top: 12px;
padding: 2px 8px 0;
text-align: left;
}
.chat-box .chat-messages .chat-message .chat-message-time {
float: right;
font-size: 10px;
color: #757575;
}

View file

@ -4,7 +4,7 @@ class MapsController < ApplicationController
after_action :verify_authorized, except: [:activemaps, :featuredmaps, :mymaps, :usermaps] after_action :verify_authorized, except: [:activemaps, :featuredmaps, :mymaps, :usermaps]
after_action :verify_policy_scoped, only: [:activemaps, :featuredmaps, :mymaps, :usermaps] after_action :verify_policy_scoped, only: [:activemaps, :featuredmaps, :mymaps, :usermaps]
respond_to :html, :json respond_to :html, :json, :csv
autocomplete :map, :name, :full => true, :extra_data => [:user_id] autocomplete :map, :name, :full => true, :extra_data => [:user_id]
@ -80,10 +80,13 @@ class MapsController < ApplicationController
object = m.mappable object = m.mappable
!object || (object.permission == "private" && (!authenticated? || (authenticated? && current_user.id != object.user_id))) !object || (object.permission == "private" && (!authenticated? || (authenticated? && current_user.id != object.user_id)))
} }
@allmessages = @map.messages.sort_by(&:created_at)
respond_with(@allmappers, @allmappings, @allsynapses, @alltopics, @map) respond_with(@allmappers, @allmappings, @allsynapses, @alltopics, @allmessages, @map)
} }
format.json { render json: @map } format.json { render json: @map }
format.csv { send_data @map.to_csv }
format.xls
end end
end end
@ -106,6 +109,7 @@ class MapsController < ApplicationController
@json['synapses'] = @allsynapses @json['synapses'] = @allsynapses
@json['mappings'] = @allmappings @json['mappings'] = @allmappings
@json['mappers'] = @allmappers @json['mappers'] = @allmappers
@json['messages'] = @map.messages.sort_by(&:created_at)
respond_to do |format| respond_to do |format|
format.json { render json: @json } format.json { render json: @json }

View file

@ -0,0 +1,67 @@
class MessagesController < ApplicationController
before_action :require_user, except: [:show]
after_action :verify_authorized
# GET /messages/1.json
def show
@message = Message.find(params[:id])
authorize @message
respond_to do |format|
format.json { render json: @message }
end
end
# POST /messages
# POST /messages.json
def create
@message = Message.new(message_params)
@message.user = current_user
authorize @message
respond_to do |format|
if @message.save
format.json { render json: @message, status: :created, location: messages_url }
else
format.json { render json: @message.errors, status: :unprocessable_entity }
end
end
end
# PUT /messages/1
# PUT /messages/1.json
def update
@message = Message.find(params[:id])
authorize @message
respond_to do |format|
if @message.update_attributes(message_params)
format.json { head :no_content }
else
format.json { render json: @message.errors, status: :unprocessable_entity }
end
end
end
# DELETE /messages/1
# DELETE /messages/1.json
def destroy
@message = Message.find(params[:id])
authorize @message
@message.destroy
respond_to do |format|
format.json { head :no_content }
end
end
private
# Never trust parameters from the scary internet, only allow the white list through.
def message_params
#params.require(:message).permit(:id, :resource_id, :message)
params.permit(:id, :resource_id, :resource_type, :message)
end
end

View file

@ -6,6 +6,7 @@ class Map < ActiveRecord::Base
has_many :synapsemappings, -> { Mapping.synapsemapping }, class_name: :Mapping, dependent: :destroy has_many :synapsemappings, -> { Mapping.synapsemapping }, class_name: :Mapping, dependent: :destroy
has_many :topics, through: :topicmappings, source: :mappable, source_type: "Topic" has_many :topics, through: :topicmappings, source: :mappable, source_type: "Topic"
has_many :synapses, through: :synapsemappings, source: :mappable, source_type: "Synapse" has_many :synapses, through: :synapsemappings, source: :mappable, source_type: "Synapse"
has_many :messages, as: :resource, dependent: :destroy
has_many :webhooks, as: :hookable has_many :webhooks, as: :hookable
has_many :events, -> { includes :user }, as: :eventable, dependent: :destroy has_many :events, -> { includes :user }, as: :eventable, dependent: :destroy
@ -16,6 +17,7 @@ class Map < ActiveRecord::Base
#:full => ['940x630#', :png] #:full => ['940x630#', :png]
}, },
:default_url => 'https://s3.amazonaws.com/metamaps-assets/site/missing-map.png' :default_url => 'https://s3.amazonaws.com/metamaps-assets/site/missing-map.png'
validates :name, presence: true validates :name, presence: true
validates :arranged, inclusion: { in: [true, false] } validates :arranged, inclusion: { in: [true, false] }
validates :permission, presence: true validates :permission, presence: true
@ -82,6 +84,24 @@ class Map < ActiveRecord::Base
json json
end end
def to_csv(options = {})
CSV.generate(options) do |csv|
csv << ["id", "name", "metacode", "desc", "link", "user.name", "permission", "synapses"]
self.topics.each do |topic|
csv << [
topic.id,
topic.name,
topic.metacode.name,
topic.desc,
topic.link,
topic.user.name,
topic.permission,
topic.synapses_csv("text")
]
end
end
end
def decode_base64(imgBase64) def decode_base64(imgBase64)
decoded_data = Base64.decode64(imgBase64) decoded_data = Base64.decode64(imgBase64)

19
app/models/message.rb Normal file
View file

@ -0,0 +1,19 @@
class Message < ActiveRecord::Base
belongs_to :user
belongs_to :resource, polymorphic: true
def user_name
self.user.name
end
def user_image
self.user.image.url
end
def as_json(options={})
json = super(:methods =>[:user_name, :user_image])
json
end
end

View file

@ -0,0 +1,36 @@
class MessagePolicy < ApplicationPolicy
class Scope < Scope
def resolve
visible = ['public', 'commons']
permission = 'maps.permission IN (?)'
if user
scope.joins(:maps).where(permission + ' OR maps.user_id = ?', visible, user.id)
else
scope.where(permission, visible)
end
end
end
def show?
resource_policy.show?
end
def create?
record.resource.present? && resource_policy.update?
end
def update?
record.user == user
end
def destroy?
record.user == user || admin_override
end
# Helpers
def resource_policy
@resource_policy ||= Pundit.policy(user, record.resource)
end
end

View file

@ -20,28 +20,6 @@
<div class="upperRightUI"> <div class="upperRightUI">
<div class="supportUs upperRightEl openLightbox" data-open="donate">SUPPORT US!</div> <div class="supportUs upperRightEl openLightbox" data-open="donate">SUPPORT US!</div>
<div class="mapElement upperRightEl upperRightMapButtons"> <div class="mapElement upperRightEl upperRightMapButtons">
<% if authenticated? %>
<!-- Realtime -->
<div class="sidebarCollaborate upperRightEl">
<div class="sidebarCollaborateIcon upperRightIcon blue"><div class="tooltipsUnder">Junto</div></div>
<div class="sidebarCollaborateBox upperRightBox">
<h3 class="realtimeBoxTitle">REALTIME</h3>
<span class="realtimeOnOff rtOff">OFF</span>
<span class="realtimeOnOff rtOn">ON</span>
<div class="clearfloat"></div>
<div class="realtimeMapperList">
<ul>
<li class="rtMapper littleRtOn rtMapperSelf">
<%= image_tag user.image.url(:thirtytwo), :size => "24x24", :class => "rtUserImage" %>
<%= user.name %> (me)
<div class="littleJuntoIcon"></div>
</li>
</ul>
</div>
</div>
</div> <!-- end sidebarCollaborate a.k.a realtime -->
<% end %>
<!-- filtering --> <!-- filtering -->
<div class="sidebarFilter upperRightEl"> <div class="sidebarFilter upperRightEl">
<div class="sidebarFilterIcon upperRightIcon"><div class="tooltipsUnder">Filter</div></div> <div class="sidebarFilterIcon upperRightIcon"><div class="tooltipsUnder">Filter</div></div>

View file

@ -5,6 +5,17 @@
# displayed within, based on URL # displayed within, based on URL
#%> #%>
<!--
Do you want to learn more about web development using Ruby or Javascript?
Metamaps.cc is an open source project, and we are always looking for new
developers to help contribute to our codebase! To get involved, send an
email to team@metamaps.cc or find us on Github at
https://github.com/metamaps/metamaps_gen002.
-->
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>

View file

@ -13,5 +13,6 @@
Metamaps.Topics = <%= @alltopics.to_json.html_safe %>; Metamaps.Topics = <%= @alltopics.to_json.html_safe %>;
Metamaps.Synapses = <%= @allsynapses.to_json.html_safe %>; Metamaps.Synapses = <%= @allsynapses.to_json.html_safe %>;
Metamaps.Mappings = <%= @allmappings.to_json.html_safe %>; Metamaps.Mappings = <%= @allmappings.to_json.html_safe %>;
Metamaps.Messages = <%= @allmessages.to_json.html_safe %>;
Metamaps.Visualize.type = "ForceDirected"; Metamaps.Visualize.type = "ForceDirected";
</script> </script>

View file

@ -0,0 +1,26 @@
<table>
<tr>
<th>ID</th>
<th>Name</th>
<th>Metacode</th>
<th>Description</th>
<th>Link</th>
<th>Username</th>
<th>Permission</th>
<th>Synapses</th>
</tr>
<% @map.topics.each do |topic| %>
<tr>
<td><%= topic.id %></td>
<td><%= topic.name %></td>
<td><%= topic.metacode.name %></td>
<td><%= topic.desc %></td>
<td><%= topic.link %></td>
<td><%= topic.user.name %></td>
<td><%= topic.permission %></td>
<% topic.synapses_csv.each do |s_text| %>
<td><%= s_text %></td>
<% end %>
</tr>
<% end %>
</table>

View file

@ -1,5 +1,6 @@
require File.expand_path('../boot', __FILE__) require File.expand_path('../boot', __FILE__)
require 'csv'
require 'rails/all' require 'rails/all'
require 'dotenv' require 'dotenv'

View file

@ -3,3 +3,5 @@
# Add new mime types for use in respond_to blocks: # Add new mime types for use in respond_to blocks:
# Mime::Type.register "text/richtext", :rtf # Mime::Type.register "text/richtext", :rtf
# Mime::Type.register_alias "text/html", :iphone # Mime::Type.register_alias "text/html", :iphone
Mime::Type.register "application/xls", :xls

View file

@ -20,6 +20,7 @@ Metamaps::Application.routes.draw do
end end
end end
resources :messages, only: [:show, :create, :update, :destroy]
resources :mappings, except: [:index, :new, :edit] resources :mappings, except: [:index, :new, :edit]
resources :metacode_sets, :except => [:show] resources :metacode_sets, :except => [:show]
resources :metacodes, :except => [:show, :destroy] resources :metacodes, :except => [:show, :destroy]

View file

@ -0,0 +1,15 @@
class Messages < ActiveRecord::Migration
def change
create_table :messages do |t|
t.text :message
t.references :user
t.integer :resource_id
t.string :resource_type
t.timestamps
end
add_index :messages, :user_id
add_index :messages, :resource_id
add_index :messages, :resource_type
end
end

38
doc/RailsIntroduction.md Normal file
View file

@ -0,0 +1,38 @@
# How does Ruby on Rails work?
Ruby on Rails is a pretty intimidating framework to get started with, since there are so many files. Here's a quick rundown on getting started:
1. Where should I look for code?
2. How do I know what code generates what pages of metamaps.cc?
## Where should I look for code?
Here are the top level folders you should know about:
- app: holds the ruby code + assets that make up the app. This is the only directory you really need to see how the app works.
- spec: tests describing how the code *should* work
- db: code for handling interaction with the underlying Postgresql database
- config: low-level, in-depth configuration variables. The most interesting file is `config/routes.rb`.
- Gemfile: listing of app dependencies from https://rubygems.org/
- realtime: code for our Node.JS realtime server. This is a separate server written in Javascript that isn't served by ruby on rails.
Within the app/ folder, you can find these important folders:
- models: files describing the logic surrounding maps, topics, synapses and more in the framework
- views: HTML template files that allow you to generate HTML using ruby code
- helpers: globally accessible helper functions available to views; they help us take logic out of the view files
- controllers: functions that map a route (e.g. `GET https://metamaps.cc/maps/2`) to a controller action (e.g. maps_controller.rb's `show` function).
- services: files that encapsulate a certain feature or logic into one file that can be referenced. Usually services help us take logic out of models and controllers.
- assets/stylesheets: CSS stylesheets for look and feel
- assets/javascripts: This is a huge folder, containing all of our Javascript code. This folder itself is at least as important as the rest of the repository.
## How do I know what code generates what pages of metamaps.cc?
The lifecycle works something like this.
1. run `rake routes` inside the metamaps_gen002 directory on your computer, and it will generate a list with entries looking something like `GET /maps/:id maps#show`. This tells you which URL will end up at which *controller*. In this example, if you accessed `https://metamaps.cc/maps/2`, you are looking for the maps_controller's `show` function, and there will be a variable params["id"] that is equal to 2.
2. Now in `app/controllers/maps_controller.rb`, you can find the function. It should do some calculations, create an instance variable @map, and then do one of two things:
- If it doesn't call anything, ruby on rails will automatically load app/views/map/show.html.erb. (NB: If you loaded `/maps/2.json`, it would look for app/views/map/show.json.erb). Any instance variables assigned (e.g. @map) will be available to the view file (show.html.erb).
- You can also call the render function directly. See the codebase or http://guides.rubyonrails.org/layouts_and_rendering.html#using-render for details.
3. The map's show template (show.html.erb) will contain actual HTML, which gets us a lot closer to an HTML page. Ruby on rails will fill in a "layout" from app/views/layouts to wrap the content of the page. It will also let you include code with `<% %>` (for logical operations) or `<%= %>` (to print a ruby string directly to the HTML page). The view may refer to attributes on the @map object passed from the controller. For more details on how the @map object works, you can check its definition in app/models/map.rb.
4. The shortest possible rails model file would look like this: `class Map < ActiveRecord::Base; end`. In this case, rails would look for a database table called "maps" and allow access to the columns. For instance, a postgresql INTEGER column called "id" would be accessible as @map.id. However, you can also specify validations, shorthand queries called scopes, and helper functions that specify the logic of the model. It is generally preferable to put logic in the model rather than in a controller or view, so these files are excellent sources of information about how the app works.

View file

@ -4,6 +4,7 @@
"version": "0.0.1", "version": "0.0.1",
"private": true, "private": true,
"dependencies": { "dependencies": {
"socket.io": "0.9.12" "socket.io": "0.9.12",
"node-uuid": "1.2.0"
} }
} }

View file

@ -1,7 +1,14 @@
var io = require('socket.io').listen(5001); var
io = require('socket.io').listen(5001),
signalServer = require('./signal'),
stunservers = [{"url": "stun:stun.l.google.com:19302"}];
io.set('log', false);
function start() { function start() {
signalServer(io, stunservers);
io.on('connection', function (socket) { io.on('connection', function (socket) {
// this will ping a new person with awareness of who's already on the map // this will ping a new person with awareness of who's already on the map
@ -10,11 +17,43 @@ function start() {
userid: data.userid, userid: data.userid,
username: data.username, username: data.username,
userrealtime: data.userrealtime, userrealtime: data.userrealtime,
userinconversation: data.userinconversation,
userimage: data.userimage userimage: data.userimage
}; };
socket.broadcast.emit(data.userToNotify + '-' + data.mapid + '-UpdateMapperList', existingUser); socket.broadcast.emit(data.userToNotify + '-' + data.mapid + '-UpdateMapperList', existingUser);
}); });
// as a new mapper check whether there's a call in progress to join
socket.on('checkForCall', function (data) {
var socketsInRoom = io.sockets.clients(data.room);
if (socketsInRoom.length) socket.emit('maps-' + data.mapid + '-callInProgress');
});
// send the invitation to start a call
socket.on('inviteACall', function (data) {
socket.broadcast.emit(data.invited + '-' + data.mapid + '-invitedToCall', data.inviter);
});
// send an invitation to join a call in progress
socket.on('inviteToJoin', function (data) {
socket.broadcast.emit(data.invited + '-' + data.mapid + '-invitedToJoin', data.inviter);
});
// send response back to the inviter
socket.on('callAccepted', function (data) {
socket.broadcast.emit(data.inviter + '-' + data.mapid + '-callAccepted', data.invited);
socket.broadcast.emit('maps-' + data.mapid + '-callStarting');
});
socket.on('callDenied', function (data) {
socket.broadcast.emit(data.inviter + '-' + data.mapid + '-callDenied', data.invited);
});
socket.on('inviteDenied', function (data) {
socket.broadcast.emit(data.inviter + '-' + data.mapid + '-inviteDenied', data.invited);
});
socket.on('mapperJoinedCall', function (data) {
socket.broadcast.emit('maps-' + data.mapid + '-mapperJoinedCall', data.id);
});
socket.on('mapperLeftCall', function (data) {
socket.broadcast.emit('maps-' + data.mapid + '-mapperLeftCall', data.id);
});
// this will ping everyone on a map that there's a person just joined the map // this will ping everyone on a map that there's a person just joined the map
socket.on('newMapperNotify', function (data) { socket.on('newMapperNotify', function (data) {
socket.set('mapid', data.mapid); socket.set('mapid', data.mapid);
@ -86,6 +125,13 @@ function start() {
socket.broadcast.emit('maps-' + mapId + '-topicDrag', data); socket.broadcast.emit('maps-' + mapId + '-topicDrag', data);
}); });
socket.on('newMessage', function (data) {
var mapId = data.mapid;
delete data.mapid;
socket.broadcast.emit('maps-' + mapId + '-newMessage', data);
});
socket.on('newTopic', function (data) { socket.on('newTopic', function (data) {
var mapId = data.mapid; var mapId = data.mapid;
delete data.mapid; delete data.mapid;

111
realtime/signal.js Normal file
View file

@ -0,0 +1,111 @@
var uuid = require('node-uuid');
module.exports = function(io, stunservers) {
var
activePeople = 0;
function describeRoom(name) {
var clients = io.sockets.clients(name);
var result = {
clients: {}
};
clients.forEach(function (client) {
result.clients[client.id] = client.resources;
});
return result;
}
function safeCb(cb) {
if (typeof cb === 'function') {
return cb;
} else {
return function () {};
}
}
io.sockets.on('connection', function (client) {
activePeople += 1;
client.resources = {
screen: false,
video: true,
audio: false
};
// pass a message to another id
client.on('message', function (details) {
if (!details) return;
var otherClient = io.sockets.sockets[details.to];
if (!otherClient) return;
details.from = client.id;
otherClient.emit('message', details);
});
client.on('shareScreen', function () {
client.resources.screen = true;
});
client.on('unshareScreen', function (type) {
client.resources.screen = false;
removeFeed('screen');
});
client.on('join', join);
function removeFeed(type) {
if (client.room) {
io.sockets.in(client.room).emit('remove', {
id: client.id,
type: type
});
if (!type) {
client.leave(client.room);
client.room = undefined;
}
}
}
function join(name, cb) {
// sanity check
if (typeof name !== 'string') return;
// leave any existing rooms
removeFeed();
safeCb(cb)(null, describeRoom(name));
client.join(name);
client.room = name;
}
// we don't want to pass "leave" directly because the
// event type string of "socket end" gets passed too.
client.on('disconnect', function () {
removeFeed();
activePeople -= 1;
});
client.on('leave', function () {
removeFeed();
});
client.on('create', function (name, cb) {
if (arguments.length == 2) {
cb = (typeof cb == 'function') ? cb : function () {};
name = name || uuid();
} else {
cb = name;
name = uuid();
}
// check if exists
if (io.sockets.clients(name).length) {
safeCb(cb)('taken');
} else {
join(name);
safeCb(cb)(null, name);
}
});
// tell client about stun and turn servers and generate nonces
client.emit('stunservers', stunservers || []);
});
};

View file

@ -3,4 +3,28 @@ require 'rails_helper'
RSpec.describe Metacode, type: :model do RSpec.describe Metacode, type: :model do
it { is_expected.to have_many :topics } it { is_expected.to have_many :topics }
it { is_expected.to have_many :metacode_sets } it { is_expected.to have_many :metacode_sets }
context 'BOTH aws_icon and manual_icon' do
let(:icon) { File.open(Rails.root.join('app', 'assets', 'images',
'user.png')) }
let(:metacode) { build(:metacode, aws_icon: icon,
manual_icon: 'https://metamaps.cc/assets/user.png') }
it 'raises a validation error' do
expect { metacode.save! }.to raise_error ActiveRecord::RecordInvalid
end
end
context 'NEITHER aws_icon or manual_icon' do
let(:metacode) { build(:metacode, aws_icon: nil, manual_icon: nil) }
it 'raises a validation error' do
expect { metacode.save! }.to raise_error ActiveRecord::RecordInvalid
end
end
context 'non-https manual icon' do
let(:metacode) { build(:metacode, manual_icon: 'http://metamaps.cc/assets/user.png') }
it 'raises a validation error' do
expect { metacode.save! }.to raise_error ActiveRecord::RecordInvalid
end
end
end end