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