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,9 +20,12 @@
//= require ./src/Metamaps.Router
//= require ./src/Metamaps.Backbone
//= require ./src/Metamaps.Views
//= require ./src/views/chatView
//= require ./src/views/videoView
//= require ./src/views/room
//= require ./src/JIT
//= require ./src/Metamaps
//= require ./src/Metamaps.JIT
//= require_directory ./shims
//= require_directory ./require
//= require_directory ./famous
//= require_directory ./famous

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

@ -6,7 +6,7 @@ Metamaps.Backbone.Map = Backbone.Model.extend({
return _.omit(this.attributes, this.blacklist);
},
save: function (key, val, options) {
var attrs;
// Handle both `"key", value` and `{key: value}` -style arguments.
@ -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({
urlRoot: '/users',
blacklist: ['created_at', 'updated_at'],
@ -224,4 +244,4 @@ Metamaps.Backbone.Mapper = Backbone.Model.extend({
Metamaps.Backbone.MapperCollection = Backbone.Collection.extend({
model: Metamaps.Backbone.Mapper,
url: '/users'
});
});

View file

@ -1,7 +1,7 @@
var Metamaps = {}; // this variable declaration defines a Javascript object that will contain all the variables and functions used by us, broken down into 'sub-modules' that look something like this
/*
* unless you are on a page with the Javascript InfoVis Toolkit (Topic or Map) the only section in the metamaps
* unless you are on a page with the Javascript InfoVis Toolkit (Topic or Map) the only section in the metamaps
* object will be these
GlobalUI
Active
@ -32,7 +32,7 @@ Map
Mapper
Topic
Synapse
JIT
JIT
*/
Metamaps.Active = {
@ -84,9 +84,9 @@ Metamaps.GlobalUI = {
event.preventDefault();
return false;
});
$('#lightbox_screen, #lightbox_close').click(self.closeLightbox);
// initialize global backbone models and collections
if (Metamaps.Active.Mapper) Metamaps.Active.Mapper = new Metamaps.Backbone.Mapper(Metamaps.Active.Mapper);
@ -107,26 +107,26 @@ Metamaps.GlobalUI = {
},
openLightbox: function (which) {
var self = Metamaps.GlobalUI;
$('.lightboxContent').hide();
$('#' + which).show();
self.lightbox = which;
$('#lightbox_overlay').show();
var heightOfContent = '-' + ($('#lightbox_main').height() / 2) + 'px';
// animate the content in from the bottom
$('#lightbox_main').animate({
'top': '50%',
'margin-top': heightOfContent
}, 200, 'easeOutCubic');
// fade the black overlay in
$('#lightbox_screen').animate({
'opacity': '0.42'
}, 200);
if (which == "switchMetacodes") {
Metamaps.Create.isSwitchingSet = true;
}
@ -134,22 +134,22 @@ Metamaps.GlobalUI = {
closeLightbox: function (event) {
var self = Metamaps.GlobalUI;
if (event) event.preventDefault();
// animate the lightbox content offscreen
$('#lightbox_main').animate({
'top': '100%',
'margin-top': '0'
}, 200, 'easeInCubic');
// fade the black overlay out
$('#lightbox_screen').animate({
'opacity': '0.0'
}, 200, function () {
$('#lightbox_overlay').hide();
$('#lightbox_overlay').hide();
});
if (self.lightbox === 'forkmap') Metamaps.GlobalUI.CreateMap.reset('fork_map');
if (self.lightbox === 'newmap') Metamaps.GlobalUI.CreateMap.reset('new_map');
if (Metamaps.Create && Metamaps.Create.isSwitchingSet) {
@ -160,14 +160,27 @@ Metamaps.GlobalUI = {
notifyUser: function (message, leaveOpen) {
var self = Metamaps.GlobalUI;
Metamaps.Famous.toast.surf.setContent(message);
Metamaps.Famous.toast.show();
clearTimeout(self.notifyTimeOut);
if (!leaveOpen) {
self.notifyTimeOut = setTimeout(function () {
Metamaps.Famous.toast.hide();
}, 8000);
function famousReady() {
Metamaps.Famous.toast.surf.setContent(message);
Metamaps.Famous.toast.show();
clearTimeout(self.notifyTimeOut);
if (!leaveOpen) {
self.notifyTimeOut = setTimeout(function () {
Metamaps.Famous.toast.hide();
}, 8000);
}
}
// initialize the famous ui
var callFamous = function(){
if (Metamaps.Famous && Metamaps.Famous.toast) {
famousReady();
}
else {
setTimeout(callFamous, 100);
}
}
callFamous();
},
clearNotify: function() {
var self = Metamaps.GlobalUI;
@ -198,13 +211,13 @@ Metamaps.GlobalUI.CreateMap = {
},
bindFormEvents: function () {
var self = Metamaps.GlobalUI.CreateMap;
$('.new_map button.cancel').unbind().bind('click', function (event) {
event.preventDefault();
Metamaps.GlobalUI.closeLightbox();
});
$('.new_map button.submitMap').unbind().bind('click', self.submit);
// bind permission changer events on the createMap form
$('.permIcon').unbind().bind('click', self.switchPermission);
},
@ -224,17 +237,17 @@ Metamaps.GlobalUI.CreateMap = {
},
switchPermission: function () {
var self = Metamaps.GlobalUI.CreateMap;
self.newMap.set('permission', $(this).attr('data-permission'));
$(this).siblings('.permIcon').find('.mapPermIcon').removeClass('selected');
$(this).find('.mapPermIcon').addClass('selected');
var permText = $(this).find('.tip').html();
$(this).parents('.new_map').find('.permText').html(permText);
},
submit: function (event) {
if (event) event.preventDefault();
var self = Metamaps.GlobalUI.CreateMap;
if (Metamaps.GlobalUI.lightbox === 'forkmap') {
@ -257,7 +270,7 @@ Metamaps.GlobalUI.CreateMap = {
success: self.success
// TODO add error message
});
Metamaps.GlobalUI.closeLightbox();
Metamaps.GlobalUI.notifyUser('Working...');
},
@ -281,19 +294,19 @@ Metamaps.GlobalUI.CreateMap = {
//push the new map onto the collection of 'my maps'
Metamaps.Maps.Mine.add(model);
var formId = Metamaps.GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map';
var form = $(formId);
Metamaps.GlobalUI.clearNotify();
$('#wrapper').append(self.generateSuccessMessage(model.id));
},
reset: function (id) {
var self = Metamaps.GlobalUI.CreateMap;
var form = $('#' + id);
if (id === "fork_map") {
self.topicsToMap = [];
self.synapsesToMap = [];
@ -302,7 +315,7 @@ Metamaps.GlobalUI.CreateMap = {
else {
form.html(self.emptyMapForm);
}
self.bindFormEvents();
self.newMap = new Metamaps.Backbone.Map({ permission: 'commons' });
@ -318,7 +331,7 @@ Metamaps.GlobalUI.Account = {
var self = Metamaps.GlobalUI.Account;
$('.sidebarAccountIcon').click(self.toggleBox);
$('.sidebarAccountBox').click(function(event){
$('.sidebarAccountBox').click(function(event){
event.stopPropagation();
});
$('body').click(self.close);
@ -334,7 +347,6 @@ Metamaps.GlobalUI.Account = {
open: function () {
var self = Metamaps.GlobalUI.Account;
Metamaps.Realtime.close();
Metamaps.Filter.close();
$('.sidebarAccountIcon .tooltipsUnder').addClass('hide');
@ -414,7 +426,7 @@ Metamaps.GlobalUI.Search = {
self.close(0, true);
}
break;
default:
break; //console.log(e.which);
}
@ -480,7 +492,7 @@ Metamaps.GlobalUI.Search = {
var topics = {
name: 'topics',
limit: 9999,
display: function(s) { return s.label; },
templates: {
notFound: function(s) {
@ -601,7 +613,7 @@ Metamaps.GlobalUI.Search = {
// tell the autocomplete to launch a new tab with the topic, map, or mapper you clicked on
$('.sidebarSearchField').bind('typeahead:select', self.handleResultClick);
// don't do it, if they clicked on a 'addToMap' button
$('.sidebarSearch button.addToMap').click(function (event) {
event.stopPropagation();

View file

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

File diff suppressed because it is too large Load diff

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

@ -76,7 +76,7 @@ body,
html {
}
}
body {
background: #d8d9da url(<%= asset_data_uri('shattered_@2X.png') %>);
font-family: 'din-medium', helvetica, sans-serif;
@ -132,6 +132,17 @@ a.button:active,
input[type="submit"]:active {
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
*/
@ -628,8 +639,12 @@ label {
margin: 0 0 0 1.3em;
}
.main {
position: relative;
/*overflow:hidden; */
}
.main.compressed {
width: calc(100% - 300px);
}
#infovis-canvas {
-webkit-touch-callout: none;
-webkit-user-select: none;
@ -713,7 +728,7 @@ label {
text-transform: uppercase;
text-align: left;
font-size: 14px;
line-height: 14px;
line-height: 14px;
position:relative;
}
.accountInnerArrow {
@ -1022,7 +1037,7 @@ h3.filterBox {
text-align: center;
}
.sidebarFilterBox li:hover {
}
#filter_by_mapper li img {
width: 40px;
@ -1067,94 +1082,16 @@ h3.filterBox {
margin-top:8px;
}
#filter_by_metacode {
}
#filter_by_mapper {
}
#filter_by_synapse {
}
/* 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 {
padding-top: 120px;
}
@ -1189,7 +1126,7 @@ h3.realtimeBoxTitle {
padding: 0;
}
.rightclickmenu > ul {
}
.rightclickmenu li {
list-style: none;
@ -1266,7 +1203,7 @@ h3.realtimeBoxTitle {
.rc-metacode li img {
display: inline-block;
}
.rightclickmenu .rc-permission ul,
.rightclickmenu .rc-permission ul,
.rightclickmenu .rc-metacode ul,
.rightclickmenu .rc-siblings ul {
display: none;
@ -1279,7 +1216,7 @@ h3.realtimeBoxTitle {
border-top-right-radius: 2px;
box-shadow: 0px 3px 3px rgba(0,0,0,0.12), 0 3px 3px rgba(0,0,0,0.24);
}
.rightclickmenu .rc-permission:hover > ul,
.rightclickmenu .rc-permission:hover > ul,
.rightclickmenu .rc-metacode:hover > ul,
.rightclickmenu .rc-siblings:hover > ul {
display: block;
@ -1288,7 +1225,7 @@ h3.realtimeBoxTitle {
padding: 7px;
}
.rightclickmenu li.changeP {
}
.rightclickmenu li.changeP .rc-perm-icon {
position: absolute;
@ -1350,13 +1287,13 @@ h3.realtimeBoxTitle {
top: -7px;
}
.moveMenusUp .rc-metacode ul,
.moveMenusUp .rc-metacode ul,
.moveMenusUp .rc-permission ul,
.moveMenusUp .rc-siblings ul {
top: auto;
bottom: 0;
}
.moveMenusToLeft .rc-metacode ul,
.moveMenusToLeft .rc-metacode ul,
.moveMenusToLeft .rc-permission ul,
.moveMenusToLeft .rc-siblings ul {
left: auto;
@ -1379,7 +1316,7 @@ h3.realtimeBoxTitle {
#new_synapse .tt-suggestion.tt-cursor {
background: #E0E0E0;
}
#new_topic .tt-suggestion,
#new_topic .tt-suggestion,
#new_synapse .tt-dataset h3,
#new_synapse .tt-suggestion {
background: #F5F5F5;
@ -1403,7 +1340,7 @@ h3.realtimeBoxTitle {
font-size: 14px;
padding: 9px 0 9px 4px;
}
#new_synapse .synapseDesc,
#new_synapse .synapseDesc,
#new_synapse .genericSynapseDesc {
width: 190px;
line-height: 14px;
@ -2041,7 +1978,7 @@ and it won't be important on password protected instances */
}
#svi1 > p, #svi2 > p {
width: 150px;
width: 150px;
}
.lightboxContent {
font-family: 'din-regular';
@ -2120,7 +2057,7 @@ and it won't be important on password protected instances */
display: none !important;
}
.ui-tabs-vertical {
}
.ui-tabs-vertical .ui-tabs-nav {
float: left;
@ -2294,7 +2231,7 @@ and it won't be important on password protected instances */
#colophon a {
color: #c04f4f;
text-decoration: underline;
text-decoration: underline;
}
#colophonCols {
@ -2365,15 +2302,15 @@ and it won't be important on password protected instances */
background: url(<%= asset_data_uri 'browser_icons.png' %>) no-repeat -220px 0;
}
#chromeIcon:hover{
#chromeIcon:hover{
opacity: 1;
}
#fireFoxIcon:hover{
#fireFoxIcon:hover{
opacity: 1;
}
#safariIcon:hover{
#safariIcon:hover{
opacity: 1;
}
@ -2593,7 +2530,7 @@ and it won't be important on password protected instances */
margin: 5px 0;
font-size: 14px;
line-height: 18px;
font-family: 'din-regular';
font-family: 'din-regular';
}
#cheatSheet .csItem a {
@ -2793,7 +2730,7 @@ and it won't be important on password protected instances */
word-wrap: break-word;
}
.blackBox td.iconColor {
}
.blackBox .field {
margin: 15px 0 5px;
@ -2919,7 +2856,7 @@ and it won't be important on password protected instances */
display: none;
background-color: #4fb5c0;
color: #FFFFFF;
padding: 2px 8px 2px 8px;
padding: 2px 8px 2px 8px;
position: absolute;
top: 7px;
left:28px;
@ -2952,7 +2889,7 @@ and it won't be important on password protected instances */
background-position: 16px 8px;
-webkit-transform-origin: left center;
transform-origin: left center;
}
}
.blockchain-btn {
display: inline;
float: right;

View file

@ -25,7 +25,7 @@
}
#famousOverlay {
position:fixed;
position:absolute;
top: 0;
width: 100%;
height: 100%;
@ -116,7 +116,7 @@
/* upperLeftUI */
.upperLeftUI {
position: fixed;
position: absolute;
top: 10px;
left: 24px;
z-index:3;
@ -155,7 +155,7 @@
/* upperRightUI */
.upperRightUI {
position: fixed;
position: absolute;
top: 10px;
right: 24px;
z-index:4;
@ -166,9 +166,9 @@
}
.upperRightBox {
position: fixed;
top:52px;
right:24px;
position: absolute;
top:42px;
right:0;
background-color: #E0E0E0;
border-radius: 2px;
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 {
position: relative;
top: -42px; /* puts it just offscreen */
}
.mapPage .upperRightMapButtons, .topicPage .upperRightMapButtons {
top: 0;
}
.topicPage .sidebarCollaborate {
display: none;
}
.upperRightIcon {
width: 32px;
height: 32px;
@ -205,20 +200,6 @@
background-repeat: no-repeat;
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 {
background-position: -64px 0;
}
@ -384,7 +365,7 @@
}
.infoAndHelp {
position: fixed;
position: absolute;
bottom: 20px;
right: 20px;
z-index: 3;
@ -424,14 +405,14 @@
/* mapControls */
.mapControls {
position: fixed;
position: absolute;
bottom: 24px;
right:-32px; /* puts it just offscreen */
width:32px;
z-index: 3;
}
.mapPage .mapControls, .topicPage .mapControls {
right: 24px;
right: 24px;
}
.topicPage .zoomExtents {
@ -474,14 +455,13 @@
background-position: -32px 0;
}
.zoomExtents:hover .tooltips, .zoomIn:hover .tooltips, .zoomOut:hover .tooltips, .takeScreenshot:hover .tooltips, .sidebarCollaborateIcon:hover .tooltipsUnder,
.sidebarFilterIcon:hover .tooltipsUnder, .sidebarForkIcon:hover .tooltipsUnder, .addMap:hover .tooltipsUnder, .authenticated .sidebarAccountIcon:hover .tooltipsUnder,
.mapInfoIcon:hover .tooltipsAbove, .openCheatsheet:hover .tooltipsAbove {
.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,
.mapInfoIcon:hover .tooltipsAbove, .openCheatsheet:hover .tooltipsAbove, .chat-button:hover .tooltips {
display: block;
}
.hide {
display: none !important;
display: none !important;
}
.tooltips {
@ -532,10 +512,6 @@
font-style: normal;
}
.sidebarCollaborateIcon .tooltipsUnder {
margin-left: -3px;
}
.sidebarFilterIcon .tooltipsUnder {
margin-left: -4px;
}
@ -560,16 +536,20 @@
left: -11px;
}
.chat-button .tooltips {
top: 10px;
}
.openCheatsheet .tooltipsAbove {
left: -4px;
}
.sidebarAccountIcon .tooltipsUnder {
margin-left: -8px;
margin-left: -12px;
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: '';
position: absolute;
top: 57%;
@ -582,21 +562,20 @@
border-bottom: 5px solid transparent;
}
.sidebarCollaborateIcon div:after, .sidebarFilterIcon div:after, .sidebarAccountIcon .tooltipsUnder:after {
left: 38%;
}
.sidebarCollaborateIcon div:after, .sidebarFilterIcon div:after, .sidebarForkIcon div:after, .addMap div:after, .sidebarAccountIcon .tooltipsUnder:after {
.sidebarFilterIcon div:after, .sidebarForkIcon div:after, .addMap div:after, .sidebarAccountIcon .tooltipsUnder:after {
content: '';
position: absolute;
top: 129%;
margin-top: -30px;
right: 40%;
margin-top: -7px;
width: 0;
height: 0;
border-bottom: 4px solid #000000;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
}
.sidebarFilterIcon div:after {
right: 37% !important;
}
.mapInfoIcon div:after, .openCheatsheet div:after {
content: '';
@ -735,7 +714,7 @@
color: #F5F5F5;
padding: 16px;
border-radius: 2px;
z-index: 1 !important; /* important necessary for firefox */
z-index: 4 !important; /* important necessary for firefox */
font-size: 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 {
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

@ -13,11 +13,11 @@ class ApplicationController < ActionController::Base
# this is for global login
include ContentHelper
helper_method :user
helper_method :authenticated?
helper_method :admin?
def after_sign_in_path_for(resource)
sign_in_url = url_for(:action => 'new', :controller => 'sessions', :only_path => false, :protocol => 'https')
@ -42,29 +42,29 @@ private
return false
end
end
def require_user
unless authenticated?
redirect_to new_user_session_path, notice: "You must be logged in."
return false
end
end
def require_admin
unless authenticated? && admin?
redirect_to root_url, notice: "You need to be an admin for that."
return false
end
end
def user
current_user
end
def authenticated?
current_user
end
def admin?
authenticated? && current_user.admin
end

View file

@ -4,7 +4,7 @@ class MapsController < ApplicationController
after_action :verify_authorized, except: [: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]
@ -72,7 +72,7 @@ class MapsController < ApplicationController
authorize @map
respond_to do |format|
format.html {
format.html {
@allmappers = @map.contributors
@alltopics = @map.topics.to_a.delete_if {|t| t.permission == "private" && (!authenticated? || (authenticated? && current_user.id != t.user_id)) }
@allsynapses = @map.synapses.to_a.delete_if {|s| s.permission == "private" && (!authenticated? || (authenticated? && current_user.id != s.user_id)) }
@ -80,10 +80,13 @@ class MapsController < ApplicationController
object = m.mappable
!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.csv { send_data @map.to_csv }
format.xls
end
end
@ -106,6 +109,7 @@ class MapsController < ApplicationController
@json['synapses'] = @allsynapses
@json['mappings'] = @allmappings
@json['mappers'] = @allmappers
@json['messages'] = @map.messages.sort_by(&:created_at)
respond_to do |format|
format.json { render json: @json }
@ -120,7 +124,7 @@ class MapsController < ApplicationController
@map.desc = params[:desc]
@map.permission = params[:permission]
@map.user = @user
@map.arranged = false
@map.arranged = false
if params[:topicsToMap]
@all = params[:topicsToMap]

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 :topics, through: :topicmappings, source: :mappable, source_type: "Topic"
has_many :synapses, through: :synapsemappings, source: :mappable, source_type: "Synapse"
has_many :messages, as: :resource, dependent: :destroy
has_many :webhooks, as: :hookable
has_many :events, -> { includes :user }, as: :eventable, dependent: :destroy
@ -16,15 +17,16 @@ class Map < ActiveRecord::Base
#:full => ['940x630#', :png]
},
:default_url => 'https://s3.amazonaws.com/metamaps-assets/site/missing-map.png'
validates :name, presence: true
validates :arranged, inclusion: { in: [true, false] }
validates :permission, presence: true
validates :permission, inclusion: { in: Perm::ISSIONS.map(&:to_s) }
# Validate the attached image is image/jpg, image/png, etc
validates_attachment_content_type :screenshot, :content_type => /\Aimage\/.*\Z/
def mappings
def mappings
topicmappings + synapsemappings
end
@ -35,11 +37,11 @@ class Map < ActiveRecord::Base
#return an array of the contributors to the map
def contributors
contributors = []
self.mappings.each do |m|
contributors.push(m.user) if !contributors.include?(m.user)
end
return contributors
end
@ -81,10 +83,28 @@ class Map < ActiveRecord::Base
json[:updated_at_clean] = updated_at_str
json
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)
decoded_data = Base64.decode64(imgBase64)
data = StringIO.new(decoded_data)
data.class_eval do
attr_accessor :content_type, :original_filename

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

@ -1,6 +1,6 @@
class Metacode < ActiveRecord::Base
has_many :in_metacode_sets
has_many :metacode_sets, :through => :in_metacode_sets
has_many :metacode_sets, :through => :in_metacode_sets
has_many :topics
# This method associates the attribute ":aws_icon" with a file attachment
@ -36,7 +36,7 @@ class Metacode < ActiveRecord::Base
return true if user.settings.metacodes.include? self.id.to_s
return false
end
def inMetacodeSet(metacode_set)
return true if self.metacode_sets.include? metacode_set
return false

View file

@ -11,9 +11,9 @@ class User < ActiveRecord::Base
after_create :generate_code
devise :database_authenticatable, :recoverable, :rememberable, :trackable, :registerable
serialize :settings, UserPreference
validates :password, :presence => true,
:length => { :within => 8..40 },
:on => :create
@ -28,7 +28,7 @@ class User < ActiveRecord::Base
validates_uniqueness_of :email # done by devise
validates :joinedwithcode, :presence => true, :inclusion => { :in => $codes, :message => "%{value} is not valid" }, :on => :create
# This method associates the attribute ":image" with a file attachment
has_attached_file :image, :styles => {
:thirtytwo => ['32x32#', :png],
@ -37,7 +37,7 @@ class User < ActiveRecord::Base
:onetwentyeight => ['128x128#', :png]
},
:default_url => 'https://s3.amazonaws.com/metamaps-assets/site/user.png'
# Validate the attached image is image/jpg, image/png, etc
validates_attachment_content_type :image, :content_type => /\Aimage\/.*\Z/
@ -62,7 +62,7 @@ class User < ActiveRecord::Base
json['rtype'] = "mapper"
json
end
#generate a random 8 letter/digit code that they can use to invite people
def generate_code
self.code ||= rand(36**8).to_s(36)
@ -77,7 +77,7 @@ class User < ActiveRecord::Base
update(generation: User.find_by_code(joinedwithcode).generation + 1)
end
end
def settings
# make sure we always return a UserPreference instance
if read_attribute(:settings).nil?
@ -85,7 +85,7 @@ class User < ActiveRecord::Base
end
read_attribute :settings
end
def settings=(val)
write_attribute :settings, val
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="supportUs upperRightEl openLightbox" data-open="donate">SUPPORT US!</div>
<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 -->
<div class="sidebarFilter upperRightEl">
<div class="sidebarFilterIcon upperRightIcon"><div class="tooltipsUnder">Filter</div></div>
@ -58,7 +36,7 @@
<% end %>
<div class="clearfloat"></div>
</div> <!-- end mapElement -->
</div> <!-- end mapElement -->
<% if authenticated? %>
<!-- create new map -->
@ -83,4 +61,4 @@
</div><!-- end sidebarAccount -->
<% end %>
<div class="clearfloat"></div>
</div><!-- end upperRightUI -->
</div><!-- end upperRightUI -->

View file

@ -5,6 +5,17 @@
# 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>
<html>
<head>

View file

@ -13,5 +13,6 @@
Metamaps.Topics = <%= @alltopics.to_json.html_safe %>;
Metamaps.Synapses = <%= @allsynapses.to_json.html_safe %>;
Metamaps.Mappings = <%= @allmappings.to_json.html_safe %>;
Metamaps.Messages = <%= @allmessages.to_json.html_safe %>;
Metamaps.Visualize.type = "ForceDirected";
</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 'csv'
require 'rails/all'
require 'dotenv'

View file

@ -3,3 +3,5 @@
# Add new mime types for use in respond_to blocks:
# Mime::Type.register "text/richtext", :rtf
# 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
resources :messages, only: [:show, :create, :update, :destroy]
resources :mappings, except: [:index, :new, :edit]
resources :metacode_sets, :except => [:show]
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

@ -19,7 +19,7 @@ Now you are ready to clone the Metamaps git repository:
The third `bundle install` command downloads and installs the rubygem
dependencies of Metamaps.
At this point you should be in C:\git\metamaps_gen002, or whatever equivalent
directory you've chosen. The next step is to set up your database
configuration. From the metamaps_gen002 directory, run
@ -41,7 +41,7 @@ time with only one command; you don't need to repeat any of the previous steps
again. The command to run the server is:
rails s
Navigate your browser to localhost:3000 once you have the server running
Sign in with the default account

View file

@ -4,6 +4,7 @@
"version": "0.0.1",
"private": true,
"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() {
signalServer(io, stunservers);
io.on('connection', function (socket) {
// this will ping a new person with awareness of who's already on the map
@ -10,11 +17,43 @@ function start() {
userid: data.userid,
username: data.username,
userrealtime: data.userrealtime,
userinconversation: data.userinconversation,
userimage: data.userimage
};
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
socket.on('newMapperNotify', function (data) {
socket.set('mapid', data.mapid);
@ -49,7 +88,7 @@ function start() {
// this will ping everyone on a map that there's a person just left the map
socket.on('disconnect', end);
socket.on('endMapperNotify', end);
// this will ping everyone on a map that someone just turned on realtime
socket.on('notifyStartRealtime', function (data) {
var newUser = {
@ -59,7 +98,7 @@ function start() {
socket.broadcast.emit('maps-' + data.mapid + '-newrealtime', newUser);
});
// this will ping everyone on a map that someone just turned on realtime
socket.on('notifyStopRealtime', function (data) {
var newUser = {
@ -86,6 +125,13 @@ function start() {
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) {
var mapId = data.mapid;
delete data.mapid;
@ -137,4 +183,4 @@ function start() {
});
}
start();
start();

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
it { is_expected.to have_many :topics }
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