diff --git a/app/assets/stylesheets/junto.css.erb b/app/assets/stylesheets/junto.css.erb
index 91b610fc..f738705f 100644
--- a/app/assets/stylesheets/junto.css.erb
+++ b/app/assets/stylesheets/junto.css.erb
@@ -90,13 +90,16 @@
left: 30px;
top: 72px;
}
+#chat-box-wrapper {
+ height: 100%;
+ float: right;
+}
.chat-box {
position: relative;
display: flex;
flex-direction: column;
z-index: 1;
width: 300px;
- float: right;
height: 100%;
background: #424242;
box-shadow: -8px 0px 16px 2px rgba(0, 0, 0, 0.23);
@@ -114,7 +117,6 @@
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;
@@ -176,7 +178,6 @@
overflow-y: auto;
}
.chat-box .participants .conversation-live {
- display: none;
padding: 5px 10px 5px 10px;
background: #c04f4f;
margin: 5px 10px;
@@ -187,15 +188,6 @@
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;
@@ -225,32 +217,18 @@
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 {
+.chat-box .participants .participant .chat-participant-invite-call.pending,
+.chat-box .participants .participant .chat-participant-invite-join.pending {
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 {
@@ -259,9 +237,6 @@
height: 12px;
border-radius: 6px;
}
-.chat-box .participants .participant.active .chat-participant-participating {
- display: block;
-}
.chat-box .chat-header {
width: 276px;
padding: 16px 8px 16px 16px;
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb
index 2dd2a463..26164843 100644
--- a/app/views/layouts/application.html.erb
+++ b/app/views/layouts/application.html.erb
@@ -9,6 +9,8 @@
controller-<%= controller_name %> action-<%= action_name %>">
+
+
<%= content_tag :div, class: "main" do %>
diff --git a/frontend/src/Metamaps/Realtime/index.js b/frontend/src/Metamaps/Realtime/index.js
index 318753f0..bbb28c30 100644
--- a/frontend/src/Metamaps/Realtime/index.js
+++ b/frontend/src/Metamaps/Realtime/index.js
@@ -8,6 +8,7 @@ import DataModel from '../DataModel'
import JIT from '../JIT'
import Util from '../Util'
import Views from '../Views'
+import { ChatView } from '../Views'
import Visualize from '../Visualize'
import {
@@ -173,48 +174,37 @@ let Realtime = {
self.room = new Views.Room({
webrtc: self.webrtc,
socket: self.socket,
- username: Active.Mapper ? Active.Mapper.get('name') : '',
- image: Active.Mapper ? Active.Mapper.get('image') : '',
room: 'global',
$video: self.localVideo.$video,
myVideoView: self.localVideo.view,
- config: { DOUBLE_CLICK_TOLERANCE: 200 },
- soundUrls: [
- serverData['sounds/MM_sounds.mp3'],
- serverData['sounds/MM_sounds.ogg']
- ]
+ config: { DOUBLE_CLICK_TOLERANCE: 200 }
})
self.room.videoAdded(self.handleVideoAdded)
-
- if (!Active.Map) {
- self.room.chat.$container.hide()
- }
- $('body').prepend(self.room.chat.$container)
} // if Active.Mapper
},
addJuntoListeners: function() {
var self = Realtime
- $(document).on(Views.ChatView.events.openTray, function() {
+ $(document).on(ChatView.events.openTray, function() {
$('.main').addClass('compressed')
self.chatOpen = true
self.positionPeerIcons()
})
- $(document).on(Views.ChatView.events.closeTray, function() {
+ $(document).on(ChatView.events.closeTray, function() {
$('.main').removeClass('compressed')
self.chatOpen = false
self.positionPeerIcons()
})
- $(document).on(Views.ChatView.events.videosOn, function() {
+ $(document).on(ChatView.events.videosOn, function() {
$('#wrapper').removeClass('hideVideos')
})
- $(document).on(Views.ChatView.events.videosOff, function() {
+ $(document).on(ChatView.events.videosOff, function() {
$('#wrapper').addClass('hideVideos')
})
- $(document).on(Views.ChatView.events.cursorsOn, function() {
+ $(document).on(ChatView.events.cursorsOn, function() {
$('#wrapper').removeClass('hideCursors')
})
- $(document).on(Views.ChatView.events.cursorsOff, function() {
+ $(document).on(ChatView.events.cursorsOff, function() {
$('#wrapper').addClass('hideCursors')
})
},
@@ -226,7 +216,7 @@ let Realtime = {
self.setupSocket()
self.setupLocalSendables()
}
- self.room.addMessages(new DataModel.MessageCollection(DataModel.Messages), true)
+ self.setupChat() // chat can happen on public maps too
}
},
endActiveMap: function() {
@@ -236,16 +226,14 @@ let Realtime = {
if (self.inConversation) self.leaveCall()
self.leaveMap()
$('.collabCompass').remove()
- if (self.room) {
- self.room.leave()
- self.room.chat.$container.hide()
- self.room.chat.close()
- }
+ if (self.room) self.room.leave()
+ ChatView.hide()
+ ChatView.close()
+ ChatView.reset()
},
turnOn: function(notify) {
var self = Realtime
$('.collabCompass').show()
- self.room.chat.$container.show()
self.room.room = 'map-' + Active.Map.id
self.activeMapper = {
id: Active.Mapper.id,
@@ -258,7 +246,13 @@ let Realtime = {
self.localVideo.view.$container.find('.video-cutoff').css({
border: '4px solid ' + self.activeMapper.color
})
- self.room.chat.addParticipant(self.activeMapper)
+ },
+ setupChat: function() {
+ const self = Realtime
+ ChatView.setNewMap()
+ ChatView.addParticipant(self.activeMapper)
+ ChatView.addMessages(new DataModel.MessageCollection(DataModel.Messages), true)
+ ChatView.show()
},
setupSocket: function() {
var self = Realtime
@@ -332,7 +326,7 @@ let Realtime = {
var createMessage = function(event, data) {
self.createMessage(data)
}
- $(document).on(Views.Room.events.newMessage + '.map', createMessage)
+ $(document).on(ChatView.events.newMessage + '.map', createMessage)
},
countOthersInConversation: function() {
var self = Realtime
@@ -403,7 +397,7 @@ let Realtime = {
callEnded: function() {
var self = Realtime
- self.room.conversationEnding()
+ ChatView.conversationEnded()
self.room.leaveVideoOnly()
self.inConversation = false
self.localVideo.view.$container.hide().css({
diff --git a/frontend/src/Metamaps/Realtime/receivable.js b/frontend/src/Metamaps/Realtime/receivable.js
index 32b6bf0c..b0a8ddb4 100644
--- a/frontend/src/Metamaps/Realtime/receivable.js
+++ b/frontend/src/Metamaps/Realtime/receivable.js
@@ -9,6 +9,7 @@ import { indexOf } from 'lodash'
import { JUNTO_UPDATED } from './events'
import Active from '../Active'
+import { ChatView } from '../Views'
import DataModel from '../DataModel'
import GlobalUI from '../GlobalUI'
import Control from '../Control'
@@ -152,7 +153,7 @@ export const topicCreated = self => data => {
}
export const messageCreated = self => data => {
- self.room.addMessages(new DataModel.MessageCollection(data))
+ ChatView.addMessages(new DataModel.MessageCollection(data))
}
export const mapUpdated = self => data => {
@@ -230,10 +231,10 @@ export const lostMapper = self => data => {
// data.userid
// data.username
delete self.mappersOnMap[data.userid]
- self.room.chat.sound.play('leavemap')
+ ChatView.sound.play('leavemap')
// $('#mapper' + data.userid).remove()
$('#compass' + data.userid).remove()
- self.room.chat.removeParticipant(data.username)
+ ChatView.removeParticipant(ChatView.participants.findWhere({id: data.userid}))
GlobalUI.notifyUser(data.username + ' just left the map')
@@ -262,8 +263,8 @@ export const mapperListUpdated = self => data => {
}
if (data.userid !== Active.Mapper.id) {
- self.room.chat.addParticipant(self.mappersOnMap[data.userid])
- if (data.userinconversation) self.room.chat.mapperJoinedCall(data.userid)
+ ChatView.addParticipant(self.mappersOnMap[data.userid])
+ if (data.userinconversation) ChatView.mapperJoinedCall(data.userid)
// create a div for the collaborators compass
self.createCompass(data.username, data.userid, data.avatar, self.mappersOnMap[data.userid].color)
@@ -291,8 +292,8 @@ export const newMapper = self => data => {
// create an item for them in the realtime box
if (data.userid !== Active.Mapper.id) {
- self.room.chat.sound.play('joinmap')
- self.room.chat.addParticipant(self.mappersOnMap[data.userid])
+ ChatView.sound.play('joinmap')
+ ChatView.addParticipant(self.mappersOnMap[data.userid])
// create a div for the collaborators compass
self.createCompass(data.username, data.userid, data.avatar, self.mappersOnMap[data.userid].color)
@@ -311,24 +312,24 @@ export const callAccepted = self => userid => {
// const username = self.mappersOnMap[userid].name
GlobalUI.notifyUser('Conversation starting...')
self.joinCall()
- self.room.chat.invitationAnswered(userid)
+ ChatView.invitationAnswered(userid)
}
export const callDenied = self => userid => {
var username = self.mappersOnMap[userid].name
GlobalUI.notifyUser(username + " didn't accept your invitation")
- self.room.chat.invitationAnswered(userid)
+ ChatView.invitationAnswered(userid)
}
export const inviteDenied = self => userid => {
var username = self.mappersOnMap[userid].name
GlobalUI.notifyUser(username + " didn't accept your invitation")
- self.room.chat.invitationAnswered(userid)
+ ChatView.invitationAnswered(userid)
}
export const invitedToCall = self => inviter => {
- self.room.chat.sound.stop(self.soundId)
- self.soundId = self.room.chat.sound.play('sessioninvite')
+ ChatView.sound.stop(self.soundId)
+ self.soundId = ChatView.sound.play('sessioninvite')
var username = self.mappersOnMap[inviter].name
var notifyText = '
'
@@ -341,8 +342,8 @@ export const invitedToCall = self => inviter => {
}
export const invitedToJoin = self => inviter => {
- self.room.chat.sound.stop(self.soundId)
- self.soundId = self.room.chat.sound.play('sessioninvite')
+ ChatView.sound.stop(self.soundId)
+ self.soundId = ChatView.sound.play('sessioninvite')
var username = self.mappersOnMap[inviter].name
var notifyText = username + ' is inviting you to the conversation. Join?'
@@ -355,16 +356,14 @@ export const invitedToJoin = self => inviter => {
export const mapperJoinedCall = self => id => {
var mapper = self.mappersOnMap[id]
-
if (mapper) {
if (self.inConversation) {
var username = mapper.name
var notifyText = username + ' joined the call'
GlobalUI.notifyUser(notifyText)
}
-
mapper.inConversation = true
- self.room.chat.mapperJoinedCall(id)
+ ChatView.mapperJoinedCall(id)
}
}
@@ -377,7 +376,7 @@ export const mapperLeftCall = self => id => {
GlobalUI.notifyUser(notifyText)
}
mapper.inConversation = false
- self.room.chat.mapperLeftCall(id)
+ ChatView.mapperLeftCall(id)
if ((self.inConversation && self.countOthersInConversation() === 0) ||
(!self.inConversation && self.countOthersInConversation() === 1)) {
self.callEnded()
@@ -392,8 +391,7 @@ export const callInProgress = self => () => {
GlobalUI.notifyUser(notifyText, true)
$('#toast button.yes').click(e => self.joinCall())
$('#toast button.no').click(e => GlobalUI.clearNotify())
-
- self.room.conversationInProgress()
+ ChatView.conversationInProgress()
}
export const callStarted = self => () => {
@@ -404,7 +402,6 @@ export const callStarted = self => () => {
GlobalUI.notifyUser(notifyText, true)
$('#toast button.yes').click(e => self.joinCall())
$('#toast button.no').click(e => GlobalUI.clearNotify())
-
- self.room.conversationInProgress()
+ ChatView.conversationInProgress()
}
diff --git a/frontend/src/Metamaps/Realtime/sendable.js b/frontend/src/Metamaps/Realtime/sendable.js
index ef35cb85..9a45d94b 100644
--- a/frontend/src/Metamaps/Realtime/sendable.js
+++ b/frontend/src/Metamaps/Realtime/sendable.js
@@ -1,6 +1,7 @@
/* global $ */
import Active from '../Active'
+import { ChatView } from '../Views'
import GlobalUI from '../GlobalUI'
import {
@@ -72,6 +73,7 @@ export const joinCall = self => () => {
$('#wrapper').append(self.localVideo.view.$container)
}
self.room.join()
+ ChatView.conversationInProgress(true)
})
self.inConversation = true
self.socket.emit(JOIN_CALL, {
@@ -80,7 +82,7 @@ export const joinCall = self => () => {
})
self.webrtc.startLocalVideo()
GlobalUI.clearNotify()
- self.room.chat.mapperJoinedCall(Active.Mapper.id)
+ ChatView.mapperJoinedCall(Active.Mapper.id)
}
export const leaveCall = self => () => {
@@ -89,7 +91,8 @@ export const leaveCall = self => () => {
id: Active.Mapper.id
})
- self.room.chat.mapperLeftCall(Active.Mapper.id)
+ ChatView.mapperLeftCall(Active.Mapper.id)
+ ChatView.leaveConversation() // the conversation will carry on without you
self.room.leaveVideoOnly()
self.inConversation = false
self.localVideo.view.$container.hide()
@@ -102,7 +105,7 @@ export const leaveCall = self => () => {
}
export const acceptCall = self => userid => {
- self.room.chat.sound.stop(self.soundId)
+ ChatView.sound.stop(self.soundId)
self.socket.emit(ACCEPT_CALL, {
mapid: Active.Map.id,
invited: Active.Mapper.id,
@@ -114,7 +117,7 @@ export const acceptCall = self => userid => {
}
export const denyCall = self => userid => {
- self.room.chat.sound.stop(self.soundId)
+ ChatView.sound.stop(self.soundId)
self.socket.emit(DENY_CALL, {
mapid: Active.Map.id,
invited: Active.Mapper.id,
@@ -124,7 +127,7 @@ export const denyCall = self => userid => {
}
export const denyInvite = self => userid => {
- self.room.chat.sound.stop(self.soundId)
+ ChatView.sound.stop(self.soundId)
self.socket.emit(DENY_INVITE, {
mapid: Active.Map.id,
invited: Active.Mapper.id,
@@ -139,7 +142,7 @@ export const inviteACall = self => userid => {
inviter: Active.Mapper.id,
invited: userid
})
- self.room.chat.invitationPending(userid)
+ ChatView.invitationPending(userid)
GlobalUI.clearNotify()
}
@@ -149,7 +152,7 @@ export const inviteToJoin = self => userid => {
inviter: Active.Mapper.id,
invited: userid
})
- self.room.chat.invitationPending(userid)
+ ChatView.invitationPending(userid)
}
export const sendCoords = self => coords => {
diff --git a/frontend/src/Metamaps/Views/ChatView.js b/frontend/src/Metamaps/Views/ChatView.js
index 590dd775..55a7b076 100644
--- a/frontend/src/Metamaps/Views/ChatView.js
+++ b/frontend/src/Metamaps/Views/ChatView.js
@@ -2,128 +2,27 @@
import Backbone from 'backbone'
import { Howl } from 'howler'
-import Autolinker from 'autolinker'
-import { clone, template as lodashTemplate } from 'lodash'
-import outdent from 'outdent'
+import React from 'react'
+import ReactDOM from 'react-dom'
// TODO is this line good or bad
// Backbone.$ = window.$
-const linker = new Autolinker({ newWindow: true, truncate: 50, email: false, phone: false })
+import Active from '../Active'
+import DataModel from '../DataModel'
+import Realtime from '../Realtime'
+import MapChat from '../../components/MapChat'
-var Private = {
- messageHTML: outdent`
-
-
-
{{ message }}
-
{{ timestamp }}
-
-
`,
- participantHTML: outdent`
-
-
-

-
-
- {{ username }} {{ selfName }}
-
-
-
-
-
-
-
-
`,
- templates: function() {
- const templateSettings = {
- interpolate: /\{\{(.+?)\}\}/g
- }
-
- this.messageTemplate = lodashTemplate(Private.messageHTML, templateSettings)
-
- this.participantTemplate = lodashTemplate(Private.participantHTML, templateSettings)
- },
- createElements: function() {
- this.$unread = $('')
- this.$button = $('')
- this.$messageInput = $('')
- this.$juntoHeader = $('')
- this.$videoToggle = $('')
- this.$cursorToggle = $('')
- this.$participants = $('')
- this.$conversationInProgress = $(outdent`
-
- LIVE
-
- LEAVE
-
-
- JOIN
-
-
`)
- this.$chatHeader = $('')
- this.$soundToggle = $('')
- this.$messages = $('')
- this.$container = $('')
- },
- 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(soundUrls) {
- this.sound = new Howl({
- src: soundUrls,
+const ChatView = {
+ isOpen: false,
+ messages: new Backbone.Collection(),
+ conversationLive: false,
+ isParticipating: false,
+ mapChat: null,
+ domId: 'chat-box-wrapper',
+ init: function(urls) {
+ const self = ChatView
+ self.sound = new Howl({
+ src: urls,
sprite: {
joinmap: [0, 561],
leavemap: [1000, 592],
@@ -133,226 +32,172 @@ var Private = {
}
})
},
- incrementUnread: function() {
- this.unreadMessages++
- this.$unread.html(this.unreadMessages)
- this.$unread.show()
+ setNewMap: function() {
+ const self = ChatView
+ self.conversationLive = false
+ self.isParticipating = false
+ self.alertSound = true // whether to play sounds on arrival of new messages or not
+ self.cursorsShowing = true
+ self.videosShowing = true
+ self.participants = new Backbone.Collection()
+ self.render()
},
- addMessage: function(message, isInitial, wasMe) {
- if (!this.isOpen && !isInitial) Private.incrementUnread.call(this)
-
- function addZero(i) {
- if (i < 10) {
- i = '0' + i
- }
- return i
- }
- var m = clone(message.attributes)
-
- 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
- m.message = linker.link(m.message)
- var $html = $(this.messageTemplate(m))
- this.$messages.append($html)
- if (!isInitial) this.scrollMessages(200)
-
- if (!wasMe && !isInitial && this.alertSound) this.sound.play('receivechat')
+ show: () => {
+ $('#' + ChatView.domId).show()
},
- initialMessages: function() {
- var messages = this.messages.models
- for (var i = 0; i < messages.length; i++) {
- Private.addMessage.call(this, messages[i], true)
- }
+ hide: () => {
+ $('#' + ChatView.domId).hide()
},
- handleInputMessage: function() {
- var message = {
- message: this.$messageInput.val()
- }
- this.$messageInput.val('')
- $(document).trigger(ChatView.events.message + '-' + this.room, [message])
+ render: () => {
+ if (!Active.Map) return
+ const self = ChatView
+ self.mapChat = ReactDOM.render(React.createElement(MapChat, {
+ conversationLive: self.conversationLive,
+ isParticipating: self.isParticipating,
+ onOpen: self.onOpen,
+ onClose: self.onClose,
+ leaveCall: Realtime.leaveCall,
+ joinCall: Realtime.joinCall,
+ inviteACall: Realtime.inviteACall,
+ inviteToJoin: Realtime.inviteToJoin,
+ participants: self.participants.models.map(p => p.attributes),
+ messages: self.messages.models.map(m => m.attributes),
+ videoToggleClick: self.videoToggleClick,
+ cursorToggleClick: self.cursorToggleClick,
+ soundToggleClick: self.soundToggleClick,
+ inputBlur: self.inputBlur,
+ inputFocus: self.inputFocus,
+ handleInputMessage: self.handleInputMessage
+ }), document.getElementById(ChatView.domId))
},
- 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)
+ onOpen: () => {
+ $(document).trigger(ChatView.events.openTray)
},
- 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()
+ onClose: () => {
+ $(document).trigger(ChatView.events.closeTray)
+ },
+ addParticipant: participant => {
+ ChatView.participants.add(participant)
+ ChatView.render()
+ },
+ removeParticipant: participant => {
+ ChatView.participants.remove(participant)
+ ChatView.render()
+ },
+ leaveConversation: () => {
+ ChatView.isParticipating = false
+ ChatView.render()
+ },
+ mapperJoinedCall: id => {
+ const mapper = ChatView.participants.findWhere({id})
+ mapper && mapper.set('isParticipating', true)
+ ChatView.render()
+ },
+ mapperLeftCall: id => {
+ const mapper = ChatView.participants.findWhere({id})
+ mapper && mapper.set('isParticipating', false)
+ ChatView.render()
+ },
+ invitationPending: id => {
+ const mapper = ChatView.participants.findWhere({id})
+ mapper && mapper.set('isPending', true)
+ ChatView.render()
+ },
+ invitationAnswered: id => {
+ const mapper = ChatView.participants.findWhere({id})
+ mapper && mapper.set('isPending', false)
+ ChatView.render()
+ },
+ conversationInProgress: participating => {
+ ChatView.conversationLive = true
+ ChatView.isParticipating = participating
+ ChatView.render()
+ },
+ conversationEnded: () => {
+ ChatView.conversationLive = false
+ ChatView.isParticipating = false
+ ChatView.participants.forEach(p => p.set({isParticipating: false, isPending: false}))
+ ChatView.render()
+ },
+ close: () => {
+ ChatView.mapChat.close()
+ },
+ open: () => {
+ ChatView.mapChat.open()
},
videoToggleClick: function() {
- this.$videoToggle.toggleClass('active')
- this.videosShowing = !this.videosShowing
- $(document).trigger(this.videosShowing ? ChatView.events.videosOn : ChatView.events.videosOff)
+ ChatView.videosShowing = !ChatView.videosShowing
+ $(document).trigger(ChatView.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)
+ ChatView.cursorsShowing = !ChatView.cursorsShowing
+ $(document).trigger(ChatView.cursorsShowing ? ChatView.events.cursorsOn : ChatView.events.cursorsOff)
},
soundToggleClick: function() {
- this.alertSound = !this.alertSound
- this.$soundToggle.toggleClass('active')
+ ChatView.alertSound = !ChatView.alertSound
},
- keyUp: function(event) {
- switch (event.which) {
- case 13: // enter
- Private.handleInputMessage.call(this)
- break
- }
- },
- inputFocus: function() {
+ inputFocus: () => {
$(document).trigger(ChatView.events.inputFocus)
},
- inputBlur: function() {
+ inputBlur: () => {
$(document).trigger(ChatView.events.inputBlur)
+ },
+ addMessage: (message, isInitial, wasMe) => {
+ const self = ChatView
+ if (!isInitial) self.mapChat.newMessage()
+ if (!wasMe && !isInitial && self.alertSound) self.sound.play('receivechat')
+ self.messages.add(message)
+ self.render()
+ if (!isInitial) self.mapChat.scroll()
+ },
+ sendChatMessage: message => {
+ var self = ChatView
+ if (ChatView.alertSound) ChatView.sound.play('sendchat')
+ var m = new DataModel.Message({
+ message: message.message,
+ resource_id: Active.Map.id,
+ resource_type: 'Map'
+ })
+ m.save(null, {
+ success: function(model, response) {
+ self.addMessages(new DataModel.MessageCollection(model), false, true)
+ $(document).trigger(ChatView.events.newMessage, [model])
+ },
+ error: function(model, response) {
+ console.log('error!', response)
+ }
+ })
+ },
+ handleInputMessage: text => {
+ ChatView.sendChatMessage({message: text})
+ },
+ // they should be instantiated as backbone models before they get
+ // passed to this function
+ addMessages: (messages, isInitial, wasMe) => {
+ messages.models.forEach(m => ChatView.addMessage(m, isInitial, wasMe))
+ },
+ reset: () => {
+ ChatView.mapChat.reset()
+ ChatView.participants.reset()
+ ChatView.messages.reset()
+ ChatView.render()
}
}
-const ChatView = function(messages, mapper, room, opts = {}) {
- this.room = room
- this.mapper = mapper
- this.messages = messages // backbone collection
+// ChatView.prototype.scrollMessages = function(duration) {
+// duration = duration || 0
- this.isOpen = false
- this.alertSound = true // 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, opts.soundUrls)
- 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(p => 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, wasMe) {
- this.messages.add(message)
- Private.addMessage.call(this, message, isInitial, wasMe)
-}
-
-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()
-}
+// this.$messages.animate({
+// scrollTop: this.$messages[0].scrollHeight
+// }, duration)
+// }
/**
* @class
* @static
*/
ChatView.events = {
- message: 'ChatView:message',
+ newMessage: 'ChatView:newMessage',
openTray: 'ChatView:openTray',
closeTray: 'ChatView:closeTray',
inputFocus: 'ChatView:inputFocus',
diff --git a/frontend/src/Metamaps/Views/Room.js b/frontend/src/Metamaps/Views/Room.js
index a3a79cc8..3dc43708 100644
--- a/frontend/src/Metamaps/Views/Room.js
+++ b/frontend/src/Metamaps/Views/Room.js
@@ -9,8 +9,6 @@ import attachMediaStream from 'attachmediastream'
import Active from '../Active'
import DataModel from '../DataModel'
import Realtime from '../Realtime'
-
-import ChatView from './ChatView'
import VideoView from './VideoView'
const Room = function(opts = {}) {
@@ -19,38 +17,18 @@ const Room = function(opts = {}) {
this.webrtc = opts.webrtc
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, {
- soundUrls: opts.soundUrls
- })
-
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)
}
@@ -66,14 +44,6 @@ Room.prototype.leave = function() {
this.isActiveRoom = false
this.webrtc.leaveRoom()
this.webrtc.stopLocalVideo()
- this.chat.conversationEnded()
- this.chat.removeParticipants()
- this.chat.clearMessages()
- this.messages.reset()
-}
-
-Room.prototype.setPeopleCount = function(count) {
- this.peopleCount = count
}
Room.prototype.init = function() {
@@ -129,11 +99,6 @@ Room.prototype.init = function() {
}
v.$container.show()
})
-
- var sendChatMessage = function(event, data) {
- self.sendChatMessage(data)
- }
- $(document).on(ChatView.events.message + '-' + this.room, sendChatMessage)
}
Room.prototype.videoAdded = function(callback) {
@@ -158,42 +123,4 @@ Room.prototype.removeVideo = function(peer) {
}
}
-Room.prototype.sendChatMessage = function(data) {
- var self = this
- // this.roomRef.child('messages').push(data)
- if (self.chat.alertSound) self.chat.sound.play('sendchat')
- var m = new DataModel.Message({
- message: data.message,
- resource_id: Active.Map.id,
- resource_type: 'Map'
- })
- m.save(null, {
- success: function(model, response) {
- self.addMessages(new DataModel.MessageCollection(model), false, true)
- $(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, wasMe) {
- var self = this
-
- messages.models.forEach(function(message) {
- self.chat.addMessage(message, isInitial, wasMe)
- })
-}
-
-/**
- * @class
- * @static
- */
-Room.events = {
- newMessage: 'Room:newMessage'
-}
-
export default Room
diff --git a/frontend/src/Metamaps/Views/index.js b/frontend/src/Metamaps/Views/index.js
index 89d22ad7..ab96e552 100644
--- a/frontend/src/Metamaps/Views/index.js
+++ b/frontend/src/Metamaps/Views/index.js
@@ -7,8 +7,9 @@ import Room from './Room'
import { JUNTO_UPDATED } from '../Realtime/events'
const Views = {
- init: () => {
+ init: (serverData) => {
$(document).on(JUNTO_UPDATED, () => ExploreMaps.render())
+ ChatView.init([serverData['sounds/MM_sounds.mp3'],serverData['sounds/MM_sounds.ogg']])
},
ExploreMaps,
ChatView,
diff --git a/frontend/src/components/MapChat/Message.js b/frontend/src/components/MapChat/Message.js
new file mode 100644
index 00000000..b9ddeda1
--- /dev/null
+++ b/frontend/src/components/MapChat/Message.js
@@ -0,0 +1,35 @@
+import React from 'react'
+import Autolinker from 'autolinker'
+
+const linker = new Autolinker({ newWindow: true, truncate: 50, email: false, phone: false })
+
+function addZero(i) {
+ if (i < 10) {
+ i = '0' + i
+ }
+ return i
+}
+
+function formatDate(created_at) {
+ let date = new Date(created_at)
+ let formatted = (date.getMonth() + 1) + '/' + date.getDate()
+ formatted += ' ' + addZero(date.getHours()) + ':' + addZero(date.getMinutes())
+ return formatted
+}
+
+const Message = props => {
+ const { user_image, user_name, message, created_at } = props
+ const messageHtml = {__html: linker.link(message)}
+ return (
+
+
+

+
+
+
{formatDate(created_at)}
+
+
+ )
+}
+
+export default Message
diff --git a/frontend/src/components/MapChat/Participant.js b/frontend/src/components/MapChat/Participant.js
new file mode 100644
index 00000000..340c92a4
--- /dev/null
+++ b/frontend/src/components/MapChat/Participant.js
@@ -0,0 +1,45 @@
+import React, { PropTypes, Component } from 'react'
+
+class Participant extends Component {
+ render() {
+ const { conversationLive, mapperIsLive, isParticipating, isPending, id, self, image, username, selfName, color } = this.props
+ return (
+
+
+

+
+
+ {username} {self ? '(me)' : ''}
+
+ {!self && !conversationLive &&
+ )
+ }
+}
+
+Participant.propTypes = {
+ conversationLive: PropTypes.bool,
+ mapperIsLive: PropTypes.bool,
+ isParticipating: PropTypes.bool,
+ isPending: PropTypes.bool,
+ color: PropTypes.string, // css color
+ id: PropTypes.number,
+ image: PropTypes.string, // image url
+ self: PropTypes.bool,
+ username: PropTypes.string,
+ inviteACall: PropTypes.func,
+ inviteToJoin: PropTypes.func
+}
+
+export default Participant
diff --git a/frontend/src/components/MapChat/Unread.js b/frontend/src/components/MapChat/Unread.js
new file mode 100644
index 00000000..7bd1d23c
--- /dev/null
+++ b/frontend/src/components/MapChat/Unread.js
@@ -0,0 +1,7 @@
+import React from 'react'
+
+const Unread = props => {
+ return props.count ? {props.count}
: null
+}
+
+export default Unread
diff --git a/frontend/src/components/MapChat/index.js b/frontend/src/components/MapChat/index.js
new file mode 100644
index 00000000..101b9755
--- /dev/null
+++ b/frontend/src/components/MapChat/index.js
@@ -0,0 +1,167 @@
+import React, { PropTypes, Component } from 'react'
+import Unread from './Unread'
+import Participant from './Participant'
+import Message from './Message'
+
+class MapChat extends Component {
+ constructor(props) {
+ super(props)
+
+ this.state = {
+ unreadMessages: 0,
+ open: false,
+ messageText: '',
+ alertSound: true, // whether to play sounds on arrival of new messages or not
+ cursorsShowing: true,
+ videosShowing: true
+ }
+ }
+
+ reset = () => {
+ this.setState({
+ unreadMessages: 0,
+ open: false,
+ messageText: '',
+ alertSound: true, // whether to play sounds on arrival of new messages or not
+ cursorsShowing: true,
+ videosShowing: true
+ })
+ }
+
+ close = () => {
+ this.setState({open: false})
+ this.props.onClose()
+ this.messageInput.blur()
+ }
+
+ open = () => {
+ this.scroll()
+ this.setState({open: true, unreadMessages: 0})
+ this.props.onOpen()
+ this.messageInput.focus()
+ }
+
+ newMessage = () => {
+ if (!this.state.open) this.setState({unreadMessages: this.state.unreadMessages + 1})
+ }
+
+ scroll = () => {
+ this.messagesDiv.scrollTop = this.messagesDiv.scrollHeight
+ }
+
+ toggleDrawer = () => {
+ if (this.state.open) this.close()
+ else if (!this.state.open) this.open()
+ }
+
+ toggleAlertSound = () => {
+ this.setState({alertSound: !this.state.alertSound})
+ this.props.soundToggleClick()
+ }
+
+ toggleCursorsShowing = () => {
+ this.setState({cursorsShowing: !this.state.cursorsShowing})
+ this.props.cursorToggleClick()
+ }
+
+ toggleVideosShowing = () => {
+ this.setState({videosShowing: !this.state.videosShowing})
+ this.props.videoToggleClick()
+ }
+
+ handleChange = key => e => {
+ this.setState({
+ [key]: e.target.value
+ })
+ }
+
+ handleTextareaKeyUp = e => {
+ if (e.which === 13) {
+ e.preventDefault()
+ const text = this.state.messageText
+ this.props.handleInputMessage(text)
+ this.setState({ messageText: '' })
+ }
+ }
+
+ render = () => {
+ const rightOffset = this.state.open ? '0' : '-300px'
+ const { conversationLive, isParticipating, participants, messages, inviteACall, inviteToJoin } = this.props
+ const { videosShowing, cursorsShowing, alertSound, unreadMessages } = this.state
+ return (
+
+
+
+ {conversationLive &&
+ LIVE
+ {isParticipating &&
+ LEAVE
+ }
+ {!isParticipating &&
+ JOIN
+ }
+
}
+ {participants.map(participant =>
+ )}
+
+
+
+
this.messagesDiv = div}>
+ {messages.map(message => )}
+
+
+ )
+ }
+}
+
+MapChat.propTypes = {
+ conversationLive: PropTypes.bool,
+ isParticipating: PropTypes.bool,
+ onOpen: PropTypes.func,
+ onClose: PropTypes.func,
+ leaveCall: PropTypes.func,
+ joinCall: PropTypes.func,
+ inviteACall: PropTypes.func,
+ inviteToJoin: PropTypes.func,
+ videoToggleClick: PropTypes.func,
+ cursorToggleClick: PropTypes.func,
+ soundToggleClick: PropTypes.func,
+ participants: PropTypes.arrayOf(PropTypes.shape({
+ color: PropTypes.string, // css color
+ id: PropTypes.number,
+ image: PropTypes.string, // image url
+ self: PropTypes.bool,
+ username: PropTypes.string,
+ isParticipating: PropTypes.bool,
+ isPending: PropTypes.bool
+ }))
+}
+
+export default MapChat
diff --git a/realtime/realtime-server.js b/realtime/realtime-server.js
index 0150e84d..680e6a60 100644
--- a/realtime/realtime-server.js
+++ b/realtime/realtime-server.js
@@ -16,3 +16,4 @@ junto(io, store)
map(io, store)
io.listen(parseInt(process.env.NODE_REALTIME_PORT) || 5000)
+console.log('booting up', process.env.NODE_REALTIME_PORT || 5000)