Merge branch 'develop' into oauth.provider
BIN
app/assets/images/audio_sprite.png
Normal file
After Width: | Height: | Size: 854 B |
BIN
app/assets/images/camera_sprite.png
Normal file
After Width: | Height: | Size: 780 B |
BIN
app/assets/images/chat32.png
Normal file
After Width: | Height: | Size: 466 B |
BIN
app/assets/images/cursor_sprite.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
app/assets/images/default_profile.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
app/assets/images/ellipsis.gif
Normal file
After Width: | Height: | Size: 220 B |
BIN
app/assets/images/invitepeer16.png
Normal file
After Width: | Height: | Size: 223 B |
BIN
app/assets/images/junto.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
app/assets/images/junto_spinner_dark.gif
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
app/assets/images/sound_sprite.png
Normal file
After Width: | Height: | Size: 717 B |
BIN
app/assets/images/sounds/sounds.mp3
Normal file
BIN
app/assets/images/sounds/sounds.ogg
Normal file
BIN
app/assets/images/tray_tab.png
Normal file
After Width: | Height: | Size: 331 B |
BIN
app/assets/images/video_sprite.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
|
@ -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
|
||||
|
|
2756
app/assets/javascripts/lib/Autolinker.js
Normal file
39
app/assets/javascripts/lib/attachMediaStream.js
Normal 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;
|
||||
};
|
1353
app/assets/javascripts/lib/howler.js
Normal file
9808
app/assets/javascripts/lib/simplewebrtc.bundle.js
Normal file
23
app/assets/javascripts/lib/socketIoConnection.js
Normal 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();
|
||||
};
|
|
@ -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'
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 = [];
|
||||
|
|
339
app/assets/javascripts/src/views/chatView.js.erb
Normal 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;
|
||||
|
||||
})();
|
194
app/assets/javascripts/src/views/room.js
Normal 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;
|
||||
})();
|
207
app/assets/javascripts/src/views/videoView.js
Normal 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;
|
||||
})();
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
348
app/assets/stylesheets/junto.css.erb
Normal 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;
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
|
67
app/controllers/messages_controller.rb
Normal 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
|
|
@ -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
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
36
app/policies/message_policy.rb
Normal 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
|
|
@ -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 -->
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
26
app/views/maps/show.xls.erb
Normal 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>
|
|
@ -1,5 +1,6 @@
|
|||
require File.expand_path('../boot', __FILE__)
|
||||
|
||||
require 'csv'
|
||||
require 'rails/all'
|
||||
require 'dotenv'
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
|
15
db/migrate/20151205205831_messages.rb
Normal 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
|
@ -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.
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
@ -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 || []);
|
||||
});
|
||||
};
|
|
@ -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
|
||||
|
|