temp
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/junto.png
Normal file
After Width: | Height: | Size: 1.7 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.Router
|
||||||
//= require ./src/Metamaps.Backbone
|
//= require ./src/Metamaps.Backbone
|
||||||
//= require ./src/Metamaps.Views
|
//= require ./src/Metamaps.Views
|
||||||
|
//= require ./src/views/chatView
|
||||||
|
//= require ./src/views/videoView
|
||||||
|
//= require ./src/views/room
|
||||||
//= require ./src/JIT
|
//= require ./src/JIT
|
||||||
//= require ./src/Metamaps
|
//= require ./src/Metamaps
|
||||||
//= require ./src/Metamaps.JIT
|
//= require ./src/Metamaps.JIT
|
||||||
|
|
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
9802
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();
|
||||||
|
};
|
|
@ -1910,11 +1910,15 @@ Metamaps.Realtime = {
|
||||||
stringForLocalhost: 'http://localhost:5001',
|
stringForLocalhost: 'http://localhost:5001',
|
||||||
stringForMetamaps: 'http://metamaps.cc:5001',
|
stringForMetamaps: 'http://metamaps.cc:5001',
|
||||||
stringForHeroku: 'http://gentle-savannah-1303.herokuapp.com',
|
stringForHeroku: 'http://gentle-savannah-1303.herokuapp.com',
|
||||||
|
videoId: 'video-wrapper',
|
||||||
socket: null,
|
socket: null,
|
||||||
|
webrtc: null,
|
||||||
|
readyToCall: false,
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
changing: false,
|
changing: false,
|
||||||
mappersOnMap: {},
|
mappersOnMap: {},
|
||||||
status: true, // stores whether realtime is True/On or False/Off
|
status: true, // stores whether realtime is True/On or False/Off,
|
||||||
|
localVideo: null,
|
||||||
init: function () {
|
init: function () {
|
||||||
var self = Metamaps.Realtime;
|
var self = Metamaps.Realtime;
|
||||||
|
|
||||||
|
@ -1935,10 +1939,54 @@ Metamaps.Realtime = {
|
||||||
|
|
||||||
var railsEnv = $('body').data('env');
|
var railsEnv = $('body').data('env');
|
||||||
var whichToConnect = railsEnv === 'development' ? self.stringForLocalhost : self.stringForHeroku;
|
var whichToConnect = railsEnv === 'development' ? self.stringForLocalhost : self.stringForHeroku;
|
||||||
self.socket = io.connect(whichToConnect);
|
self.socket = new SocketIoConnection({ url: whichToConnect });
|
||||||
self.socket.on('connect', function () {
|
self.socket.on('connect', function () {
|
||||||
self.startActiveMap();
|
self.startActiveMap();
|
||||||
});
|
});
|
||||||
|
self.webrtc = new SimpleWebRTC({
|
||||||
|
connection: self.socket,
|
||||||
|
localVideoEl: self.videoId,
|
||||||
|
remoteVideosEl: '',
|
||||||
|
detectSpeakingEvents: true,
|
||||||
|
autoAdjustMic: true,
|
||||||
|
autoRequestMedia: false,
|
||||||
|
localVideo: {
|
||||||
|
autoplay: true,
|
||||||
|
mirror: true,
|
||||||
|
muted: true
|
||||||
|
},
|
||||||
|
media: {
|
||||||
|
video: true,
|
||||||
|
audio: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
self.webrtc.on('readyToCall', function () {
|
||||||
|
self.readyToCall = true;
|
||||||
|
if (self.localVideo && self.status) {
|
||||||
|
$('#wrapper').append(self.localVideo.view.$container);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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 }),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.room = new Metamaps.Views.room({
|
||||||
|
webrtc: self.webrtc,
|
||||||
|
socket: self.socket,
|
||||||
|
username: 'dude',//getUsername(opts.user, provider),
|
||||||
|
image: 'https://pbs.twimg.com/profile_images/436050101539065856/QMGlzCUn_400x400.jpeg', //getImage(opts.user, provider),
|
||||||
|
room: 'global',
|
||||||
|
$video: self.localVideo.$video,
|
||||||
|
myVideoView: self.localVideo.view,
|
||||||
|
config: { DOUBLE_CLICK_TOLERANCE: 200 }
|
||||||
|
});
|
||||||
|
|
||||||
|
self.createChat();
|
||||||
|
self.webrtc.startLocalVideo();
|
||||||
},
|
},
|
||||||
toggleBox: function (event) {
|
toggleBox: function (event) {
|
||||||
var self = Metamaps.Realtime;
|
var self = Metamaps.Realtime;
|
||||||
|
@ -2019,6 +2067,31 @@ Metamaps.Realtime = {
|
||||||
self.status = true;
|
self.status = true;
|
||||||
$(".sidebarCollaborateIcon").addClass("blue");
|
$(".sidebarCollaborateIcon").addClass("blue");
|
||||||
$(".collabCompass").show();
|
$(".collabCompass").show();
|
||||||
|
if (self.localVideo) $('#wrapper').append(self.localVideo.view.$container);
|
||||||
|
self.room.room = 'map-' + Metamaps.Active.Map.id;
|
||||||
|
self.room.join(function (err, roomDesc) {
|
||||||
|
console.log('joining');
|
||||||
|
attachMediaStream(self.webrtc.webrtc.localStream, self.localVideo.$video[0]);
|
||||||
|
|
||||||
|
function addVideo(v) {
|
||||||
|
// random position for now
|
||||||
|
var top = Math.floor((Math.random() * ($('#wrapper').height() - 100)) + 1);
|
||||||
|
var left = Math.floor((Math.random() * ($('#wrapper').width() - 100)) + 1);
|
||||||
|
//var right = Math.floor((Math.random() * (468 - 100)) + 1);
|
||||||
|
v.setParent($('#wrapper'));
|
||||||
|
$('#wrapper').append(v.$container);
|
||||||
|
v.$container.css({
|
||||||
|
top: top + 'px',
|
||||||
|
left: left + 'px'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
self.room.videoAdded(addVideo);
|
||||||
|
|
||||||
|
for (peer in self.room.videos) {
|
||||||
|
addVideo(self.room.videos[peer]);
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
turnOff: function (silent) {
|
turnOff: function (silent) {
|
||||||
var self = Metamaps.Realtime;
|
var self = Metamaps.Realtime;
|
||||||
|
@ -2031,6 +2104,7 @@ Metamaps.Realtime = {
|
||||||
self.status = false;
|
self.status = false;
|
||||||
$(".sidebarCollaborateIcon").removeClass("blue");
|
$(".sidebarCollaborateIcon").removeClass("blue");
|
||||||
$(".collabCompass").hide();
|
$(".collabCompass").hide();
|
||||||
|
$('#' + self.videoId).remove();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setupSocket: function () {
|
setupSocket: function () {
|
||||||
|
@ -2154,6 +2228,12 @@ Metamaps.Realtime = {
|
||||||
|
|
||||||
socket.on('mapChangeFromServer', self.mapChange);
|
socket.on('mapChangeFromServer', self.mapChange);
|
||||||
},
|
},
|
||||||
|
createChat: function() {
|
||||||
|
var self = Metamaps.Realtime;
|
||||||
|
|
||||||
|
$('#wrapper').append(self.room.chat.$container);
|
||||||
|
//self.room.chat.open();
|
||||||
|
},
|
||||||
sendRealtimeOn: function () {
|
sendRealtimeOn: function () {
|
||||||
var self = Metamaps.Realtime;
|
var self = Metamaps.Realtime;
|
||||||
var socket = Metamaps.Realtime.socket;
|
var socket = Metamaps.Realtime.socket;
|
||||||
|
|
272
app/assets/javascripts/src/views/chatView.js
Normal file
|
@ -0,0 +1,272 @@
|
||||||
|
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='<%= image %>' title='<%= user %>'/></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-<%= username %>'>" +
|
||||||
|
"<div class='chat-participant-image'><img src='<%= image %>' /></div>" +
|
||||||
|
"<div class='chat-participant-name'><%= username %></div>" +
|
||||||
|
"<div class='clearfloat'></div>" +
|
||||||
|
"</div>",
|
||||||
|
templates: function() {
|
||||||
|
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>');
|
||||||
|
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.$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.$container.append(this.$juntoHeader);
|
||||||
|
this.$container.append(this.$participants);
|
||||||
|
this.$container.append(this.$chatHeader);
|
||||||
|
this.$container.append(this.$messageInput);
|
||||||
|
this.$container.append(this.$button);
|
||||||
|
this.$container.append(this.$messages);
|
||||||
|
},
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
// add the event listener so that when
|
||||||
|
// the realtime module adds messages to the collection
|
||||||
|
// from other mappers, it will update the UI
|
||||||
|
this.messages.on('add', function (message) {
|
||||||
|
Private.addMessage.call(self, message);
|
||||||
|
});
|
||||||
|
|
||||||
|
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: ['/assets/sounds/sounds.mp3', '/assets/sounds/sounds.ogg'],
|
||||||
|
sprite: {
|
||||||
|
laser: [3000, 700]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
incrementUnread: function() {
|
||||||
|
this.unreadMessages++;
|
||||||
|
this.$unread.html(this.unreadMessages);
|
||||||
|
this.$unread.show();
|
||||||
|
},
|
||||||
|
addMessage: function(message) {
|
||||||
|
|
||||||
|
if (!this.isOpen) 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.timestamp);
|
||||||
|
|
||||||
|
var date = (m.timestamp.getMonth() + 1) + '/' + m.timestamp.getDate();
|
||||||
|
date += " " + addZero(m.timestamp.getHours()) + ":" + addZero(m.timestamp.getMinutes());
|
||||||
|
m.timestamp = date;
|
||||||
|
m.image = m.image || 'http://www.hotpepper.ca/wp-content/uploads/2014/11/default_profile_1_200x200.png';
|
||||||
|
m.message = linker.link(m.message);
|
||||||
|
var $html = $(this.messageTemplate(m));
|
||||||
|
this.$messages.append($html);
|
||||||
|
this.scrollMessages(200);
|
||||||
|
|
||||||
|
if (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]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleInputMessage: function() {
|
||||||
|
var message = {
|
||||||
|
message: this.$messageInput.val(),
|
||||||
|
timestamp: Date.now(),
|
||||||
|
user: this.mapper.get('name'),
|
||||||
|
image: this.mapper.get('image')
|
||||||
|
};
|
||||||
|
this.$messageInput.val('');
|
||||||
|
$(document).trigger(chatView.events.message + '-' + this.room, [message]);
|
||||||
|
},
|
||||||
|
addParticipant: function(participant) {
|
||||||
|
var p = _.clone(participant.attributes);
|
||||||
|
var html = this.participantTemplate(p);
|
||||||
|
this.$participants.append(html);
|
||||||
|
},
|
||||||
|
removeParticipant: function(participant) {
|
||||||
|
this.$container.find('.participant-' + participant.get('username')).remove();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var Handlers = {
|
||||||
|
buttonClick: function() {
|
||||||
|
if (this.isOpen) this.close();
|
||||||
|
else if (!this.isOpen) this.open();
|
||||||
|
},
|
||||||
|
videoToggleClick: function() {
|
||||||
|
this.$videoToggle.toggleClass('active');
|
||||||
|
},
|
||||||
|
cursorToggleClick: function() {
|
||||||
|
this.$cursorToggle.toggleClass('active');
|
||||||
|
},
|
||||||
|
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.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);
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
chatView.prototype.scrollMessages = function(duration) {
|
||||||
|
duration = duration || 0;
|
||||||
|
|
||||||
|
var
|
||||||
|
numMessages = this.$messages.find('.chat-message').length,
|
||||||
|
messageHeight = 52;
|
||||||
|
|
||||||
|
this.$messages.animate({
|
||||||
|
scrollTop: numMessages * messageHeight
|
||||||
|
}, duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
chatView.prototype.close = function () {
|
||||||
|
this.$container.css({
|
||||||
|
right: '-300px'
|
||||||
|
});
|
||||||
|
this.$messageInput.blur();
|
||||||
|
this.isOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
chatView.prototype.remove = function () {
|
||||||
|
this.$button.off();
|
||||||
|
this.$container.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class
|
||||||
|
* @static
|
||||||
|
*/
|
||||||
|
chatView.events = {
|
||||||
|
message: 'ChatView:message',
|
||||||
|
inputFocus: 'ChatView:inputFocus',
|
||||||
|
inputBlur: 'ChatView:inputBlur'
|
||||||
|
};
|
||||||
|
|
||||||
|
return chatView;
|
||||||
|
|
||||||
|
})();
|
||||||
|
|
111
app/assets/javascripts/src/views/room.js
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
room.prototype.leave = function() {
|
||||||
|
for (var id in this.videos) {
|
||||||
|
this.removeVideo(id);
|
||||||
|
}
|
||||||
|
this.isActiveRoom = false;
|
||||||
|
this.webrtc.leaveRoom();
|
||||||
|
this.chat.removeParticipants();
|
||||||
|
}
|
||||||
|
|
||||||
|
room.prototype.setPeopleCount = function(count) {
|
||||||
|
this.peopleCount = count;
|
||||||
|
}
|
||||||
|
|
||||||
|
room.prototype.init = function () {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
/*this.roomRef.child('messages').on('child_added', function (snap) {
|
||||||
|
self.messages.add(snap.val());
|
||||||
|
});*/
|
||||||
|
|
||||||
|
$(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) {
|
||||||
|
if (self.isActiveRoom) {
|
||||||
|
self.addVideo(peer);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.webrtc.on('peerStreamRemoved', function (peer) {
|
||||||
|
if (self.isActiveRoom) {
|
||||||
|
self.removeVideo(peer);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
v = new VideoView(video, null, id, false, { DOUBLE_CLICK_TOLERANCE: 200 });
|
||||||
|
|
||||||
|
if (this._videoAdded) this._videoAdded(v);
|
||||||
|
this.videos[peer.id] = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
room.prototype.removeVideo = function (peer) {
|
||||||
|
console.log(peer);
|
||||||
|
var id = typeof peer == 'string' ? peer : peer.id;
|
||||||
|
this.videos[id].remove();
|
||||||
|
delete this.videos[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
room.prototype.sendChatMessage = function (data) {
|
||||||
|
//this.roomRef.child('messages').push(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return room;
|
||||||
|
})();
|
182
app/assets/javascripts/src/views/videoView.js
Normal file
|
@ -0,0 +1,182 @@
|
||||||
|
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.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.$audioControl.addClass('active');
|
||||||
|
} else {
|
||||||
|
this.$audioControl.removeClass('active');
|
||||||
|
}
|
||||||
|
this.audioStatus = !this.audioStatus;
|
||||||
|
$(document).trigger(videoView.events.audioControlClick, [this]);
|
||||||
|
},
|
||||||
|
videoControlClick: function() {
|
||||||
|
if (this.videoStatus) {
|
||||||
|
this.$videoControl.addClass('active');
|
||||||
|
this.$avatar.show();
|
||||||
|
} else {
|
||||||
|
this.$videoControl.removeClass('active');
|
||||||
|
this.$avatar.hide();
|
||||||
|
}
|
||||||
|
this.videoStatus = !this.videoStatus;
|
||||||
|
$(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 = $('<img draggable="false" class="collaborator-video-avatar" src="/assets/default_profile.png" 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.remove = function () {
|
||||||
|
this.$container.off();
|
||||||
|
if (this.$parent) this.$parent.off('.video' + this.id);
|
||||||
|
this.$container.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class
|
||||||
|
* @static
|
||||||
|
*/
|
||||||
|
videoView.events = {
|
||||||
|
mousedown: "VideoView:mousedown",
|
||||||
|
mouseup: "VideoView:mouseup",
|
||||||
|
doubleClick: "VideoView:doubleClick",
|
||||||
|
dragEnd: "VideoView:dragEnd",
|
||||||
|
audioControlClick: "VideoView:audioControlClick",
|
||||||
|
videoControlClick: "VideoView:videoControlClick",
|
||||||
|
};
|
||||||
|
|
||||||
|
return videoView;
|
||||||
|
})();
|
||||||
|
|
||||||
|
|
265
app/assets/stylesheets/junto.css
Normal file
|
@ -0,0 +1,265 @@
|
||||||
|
.collaborator-video {
|
||||||
|
z-index: 2;
|
||||||
|
position: absolute;
|
||||||
|
width: 150px;
|
||||||
|
height: 150px;
|
||||||
|
cursor: default;
|
||||||
|
color: #FFF;
|
||||||
|
}
|
||||||
|
.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(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(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: 50px;
|
||||||
|
top: 50px;
|
||||||
|
}
|
||||||
|
.chat-box {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
right: -300px;
|
||||||
|
width: 300px;
|
||||||
|
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(junto.png) no-repeat 2px 9px, url(tray_tab.png) no-repeat;
|
||||||
|
}
|
||||||
|
.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(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(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%;
|
||||||
|
height: 150px;
|
||||||
|
padding: 16px 0px 0px 0px;
|
||||||
|
text-align: left;
|
||||||
|
color: #f5f5f5;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.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 {
|
||||||
|
border: 2px solid #424242;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.chat-box .participants .participant .chat-participant-name {
|
||||||
|
width: 73%;
|
||||||
|
float: left;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-top: 12px;
|
||||||
|
padding: 2px 8px 0;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.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 {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
margin-right: 32px;
|
||||||
|
margin-top: -2px;
|
||||||
|
float: right;
|
||||||
|
background: url(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 {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 80px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 8px 8px 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.chat-box .chat-messages {
|
||||||
|
width: 100%;
|
||||||
|
height: 196px;
|
||||||
|
padding: 16px 0px 0px 0px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.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;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.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;
|
||||||
|
}
|
After Width: | Height: | Size: 8.6 KiB |
BIN
public/assets/about_sprite-ec51351c3569a7f5ff1326ce9e81d4db.png
Executable file
After Width: | Height: | Size: 3.1 KiB |
BIN
public/assets/addtopic_sprite-474fa6955c61eda7edb94a99b9152f6b.png
Executable file
After Width: | Height: | Size: 361 B |
BIN
public/assets/arrow_sprite-3bb38c6207a94c9448a74511b4fbb0da.png
Executable file
After Width: | Height: | Size: 566 B |
After Width: | Height: | Size: 715 B |
BIN
public/assets/arrowdown_sprite-5a48c18bd4a43024b2107430f04ca3b1.png
Executable file
After Width: | Height: | Size: 331 B |
After Width: | Height: | Size: 543 B |
After Width: | Height: | Size: 540 B |
BIN
public/assets/arrowright_sprite-27adb8ccab46306ee1b8c577355105b9.png
Executable file
After Width: | Height: | Size: 349 B |
BIN
public/assets/browser_icons-582d09c51a2c675b9716652435de546c.png
Normal file
After Width: | Height: | Size: 9.7 KiB |
BIN
public/assets/compass_arrow-85c83dcd70a85ab0da93d7cd966459d0.png
Normal file
After Width: | Height: | Size: 145 B |
BIN
public/assets/context_sprite-ea6f317538820539fc90d69ad6e04c5f.png
Executable file
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 2 KiB |
BIN
public/assets/edit-d6d3c4e443f674ccd3934ecc7ff5ca28.png
Executable file
After Width: | Height: | Size: 324 B |
BIN
public/assets/exploremaps_sprite-51c03ee16025abf84bb098f2c1dbacf1.png
Executable file
After Width: | Height: | Size: 1.6 KiB |
BIN
public/assets/extents_sprite-0330c42177b14486b57af0bad7345e06.png
Executable file
After Width: | Height: | Size: 602 B |
After Width: | Height: | Size: 2.2 KiB |
BIN
public/assets/help_sprite-64737406a07e8575d5dad46c0a3aa707.png
Executable file
After Width: | Height: | Size: 853 B |
BIN
public/assets/home_dark-0210718ba049662df8bb312f2dd3c285.png
Executable file
After Width: | Height: | Size: 465 B |
BIN
public/assets/home_light-6a3a31a73a8611a764557126a6d634aa.png
Executable file
After Width: | Height: | Size: 452 B |
After Width: | Height: | Size: 110 KiB |
BIN
public/assets/icons/action-abd8f2e892ebe608b9112249167fb64e.png
Normal file
After Width: | Height: | Size: 9.4 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 10 KiB |
BIN
public/assets/icons/bizarre-d6ef966323ca17b300d15bf928f9a9d7.png
Normal file
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 4.1 KiB |
After Width: | Height: | Size: 5.5 KiB |
After Width: | Height: | Size: 4.9 KiB |
After Width: | Height: | Size: 4.8 KiB |
After Width: | Height: | Size: 4.9 KiB |
After Width: | Height: | Size: 5 KiB |
After Width: | Height: | Size: 5 KiB |
After Width: | Height: | Size: 5.1 KiB |
After Width: | Height: | Size: 4.9 KiB |
After Width: | Height: | Size: 5 KiB |
After Width: | Height: | Size: 4.8 KiB |
After Width: | Height: | Size: 4.7 KiB |
After Width: | Height: | Size: 5.1 KiB |
After Width: | Height: | Size: 5.2 KiB |
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 5 KiB |
After Width: | Height: | Size: 5.4 KiB |
After Width: | Height: | Size: 5.3 KiB |
After Width: | Height: | Size: 4.8 KiB |
After Width: | Height: | Size: 4.6 KiB |
After Width: | Height: | Size: 3.1 KiB |
After Width: | Height: | Size: 4.9 KiB |
After Width: | Height: | Size: 9.8 KiB |
BIN
public/assets/icons/closed-15267ab51fc3279b336662252acca9bf.png
Normal file
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 7.7 KiB |
After Width: | Height: | Size: 9.6 KiB |
BIN
public/assets/icons/example-a5e82d0be449ac0c5356bd94434b6ad3.png
Normal file
After Width: | Height: | Size: 8.5 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 8.8 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 3.6 KiB |
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 4.1 KiB |
After Width: | Height: | Size: 4.1 KiB |
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 4.4 KiB |
After Width: | Height: | Size: 3.9 KiB |
After Width: | Height: | Size: 3.7 KiB |
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 4.5 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 3.5 KiB |
After Width: | Height: | Size: 2.9 KiB |
After Width: | Height: | Size: 3.5 KiB |
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 4.4 KiB |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 4 KiB |