From fb427a11f03e6aec393180e7ccb2ec41fe0e3ed8 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Fri, 21 Oct 2016 09:29:04 -0400 Subject: [PATCH] [WIP] keep client fresh with junto info globally (#791) * push state to client * junto status is live on map cards XD * little fixes * eslint stuff * remove object rest spread * i think this makes realtime work without needing babel-node --- app/assets/images/junto.gif | Bin 0 -> 30862 bytes app/assets/stylesheets/mapcard.scss.erb | 287 ++++++++++--------- frontend/src/Metamaps/Realtime/events.js | 103 ++++--- frontend/src/Metamaps/Realtime/index.js | 25 +- frontend/src/Metamaps/Realtime/receivable.js | 29 +- frontend/src/Metamaps/Realtime/sendable.js | 9 +- frontend/src/Metamaps/Views/ChatView.js | 2 +- frontend/src/Metamaps/Views/ExploreMaps.js | 4 + frontend/src/Metamaps/Views/index.js | 13 +- frontend/src/Metamaps/index.js | 2 +- frontend/src/components/Maps/MapCard.js | 16 +- frontend/src/components/Maps/index.js | 5 +- package.json | 1 + realtime/global.js | 54 ++-- realtime/junto.js | 13 +- realtime/map.js | 15 +- realtime/realtime-server.js | 19 +- realtime/reducer.js | 75 +++++ realtime/rooms.js | 1 - realtime/signal.js | 3 +- 20 files changed, 387 insertions(+), 289 deletions(-) create mode 100644 app/assets/images/junto.gif create mode 100644 realtime/reducer.js diff --git a/app/assets/images/junto.gif b/app/assets/images/junto.gif new file mode 100644 index 0000000000000000000000000000000000000000..e4a72d4bb7261c84ea7e0a8b5c9341c97c8ac1a9 GIT binary patch literal 30862 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"); - background-repeat: no-repeat; - background-position: 0 0; - position: absolute; - left: 50%; - margin-left: -36px; - top: 50%; - margin-top: -36px; -} -.map:hover .newMapImage { - background-position: 0 -72px; -} -.newMap span { - font-family: 'din-regular', sans-serif; - font-size: 18px; - line-height: 22px; - text-align: center; - display: block; - padding-top: 220px; -} + display:inline-block; + width:220px; + height:340px; + font-size: 12px; + text-align: left; + overflow: visible; + background: #e8e8e8; + border-radius:2px; + margin:16px; + box-shadow: 0px 3px 3px rgba(0,0,0,0.23), 0 3px 3px rgba(0,0,0,0.16); -.mapCard { - position:relative; - width:100%; - height:308px; - padding: 0 0 16px 0; - color: #424242; + &.newMap { + float: left; + position: relative; -.mapScreenshot { - width: 100%; - height: 220px; -} + &:hover { + background: #dcdcdc; -.mapScreenshot img { - width: 100%; -} + .newMapImage { + background-position: 0 -72px; + } + } -.title { - word-wrap: break-word; - font-size:18px; - line-height:22px; - height: 71px; - display:table; - padding: 0 16px; - font-family: 'din-regular', sans-serif; - margin: 0 auto; + a { + height: 340px; + display: block; + position: relative; + } + + .newMapImage { + display: block; + width: 72px; + height: 72px; + background-image: url("<%= asset_data_uri('newmap_sprite.png') %>"); + background-repeat: no-repeat; + background-position: 0 0; + position: absolute; + left: 50%; + margin-left: -36px; + top: 50%; + margin-top: -36px; + } + + span { + font-family: 'din-regular', sans-serif; + font-size: 18px; + line-height: 22px; + text-align: center; + display: block; + padding-top: 220px; + } + } + + .mapCard { + position:relative; + width:100%; + height:308px; + padding: 0 0 16px 0; + color: #424242; + + .mapHasMapper, .mapHasConversation { + position: absolute; + top: 5px; + left: 5px; + width: 32px; + height: 32px; + } + .mapHasMapper { + background: url('<%= asset_path('junto.png') %>'); + } + .mapHasConversation { + background: url('<%= asset_path('junto.gif') %>'); + } + + .mapScreenshot { + width: 100%; + height: 220px; + } + + .mapScreenshot img { + width: 100%; + } + + .title { + word-wrap: break-word; + font-size:18px; + line-height:22px; + height: 71px; + display:table; + padding: 0 16px; + font-family: 'din-regular', sans-serif; + margin: 0 auto; + + .innerTitle { + display: table-cell; + vertical-align: middle; + text-align: center; + } + } + + .creatorAndPerm { + padding: 8px; + } + + .creatorImage { + display: inline-block; + border-radius: 16px; + vertical-align: middle; + width: 32px; + height: 32px; + } + + span.creatorName { + margin-left: 8px; + } + + + .scroll { + display:block; + font-family: helvetica, sans-serif; + font-size: 12px; + word-wrap: break-word; + text-align: center; + margin-top: 16px; + } + + &:hover .mainContent { + filter: blur(2px); + } + + &:hover .mapMetadata { + display: block; + } + + .mapMetadata { + display: none; + position: absolute; + top: 0; + left: 0; + padding: 40px 20px 0; + height: 300px; + font-family: 'din-regular', sans-serif; + font-size: 12px; + color: #FFF; + background: -moz-linear-gradient(top, rgba(0,0,0,0.65) 0%, rgba(0,0,0,0.43) 81%, rgba(0,0,0,0) 100%); + background: -webkit-linear-gradient(top, rgba(0,0,0,0.65) 0%,rgba(0,0,0,0.43) 81%,rgba(0,0,0,0) 100%); + background: linear-gradient(to bottom, rgba(0,0,0,0.65) 0%,rgba(0,0,0,0.43) 81%,rgba(0,0,0,0) 100%); + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#a6000000', endColorstr='#00000000',GradientType=0 ); + } + + .metadataSection { + padding: 16px 0; + width: 90px; + float: left; + font-family: 'din-medium', sans-serif; + text-align: center; + } - .innerTitle { - display: table-cell; - vertical-align: middle; - text-align: center; } } - -.creatorAndPerm { - padding: 8px; -} - -.creatorImage { - display: inline-block; - border-radius: 16px; - vertical-align: middle; - width: 32px; - height: 32px; -} - -span.creatorName { - margin-left: 8px; -} - - -.scroll { - display:block; - font-family: helvetica, sans-serif; - font-size: 12px; - word-wrap: break-word; - text-align: center; - margin-top: 16px; -} - -&:hover .mainContent { - filter: blur(2px); -} - -&:hover .mapMetadata { - display: block; -} - -.mapMetadata { - display: none; - position: absolute; - top: 0; - left: 0; - padding: 40px 20px 0; - height: 300px; - font-family: 'din-regular', sans-serif; - font-size: 12px; - color: #FFF; - background: -moz-linear-gradient(top, rgba(0,0,0,0.65) 0%, rgba(0,0,0,0.43) 81%, rgba(0,0,0,0) 100%); - background: -webkit-linear-gradient(top, rgba(0,0,0,0.65) 0%,rgba(0,0,0,0.43) 81%,rgba(0,0,0,0) 100%); - background: linear-gradient(to bottom, rgba(0,0,0,0.65) 0%,rgba(0,0,0,0.43) 81%,rgba(0,0,0,0) 100%); - filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#a6000000', endColorstr='#00000000',GradientType=0 ); -} - -.metadataSection { - padding: 16px 0; - width: 90px; - float: left; - font-family: 'din-medium', sans-serif; - text-align: center; -} - -} - diff --git a/frontend/src/Metamaps/Realtime/events.js b/frontend/src/Metamaps/Realtime/events.js index 20265154..a1fabf04 100644 --- a/frontend/src/Metamaps/Realtime/events.js +++ b/frontend/src/Metamaps/Realtime/events.js @@ -1,54 +1,53 @@ /* EVENTS SENDABLE */ -export const REQUEST_LIVE_MAPS = 'REQUEST_LIVE_MAPS' -export const JOIN_MAP = 'JOIN_MAP' -export const LEAVE_MAP = 'LEAVE_MAP' -export const CHECK_FOR_CALL = 'CHECK_FOR_CALL' -export const ACCEPT_CALL = 'ACCEPT_CALL' -export const DENY_CALL = 'DENY_CALL' -export const DENY_INVITE = 'DENY_INVITE' -export const INVITE_TO_JOIN = 'INVITE_TO_JOIN' -export const INVITE_A_CALL = 'INVITE_A_CALL' -export const JOIN_CALL = 'JOIN_CALL' -export const LEAVE_CALL = 'LEAVE_CALL' -export const SEND_MAPPER_INFO = 'SEND_MAPPER_INFO' -export const SEND_COORDS = 'SEND_COORDS' -export const CREATE_MESSAGE = 'CREATE_MESSAGE' -export const DRAG_TOPIC = 'DRAG_TOPIC' -export const CREATE_TOPIC = 'CREATE_TOPIC' -export const UPDATE_TOPIC = 'UPDATE_TOPIC' -export const REMOVE_TOPIC = 'REMOVE_TOPIC' -export const DELETE_TOPIC = 'DELETE_TOPIC' -export const CREATE_SYNAPSE = 'CREATE_SYNAPSE' -export const UPDATE_SYNAPSE = 'UPDATE_SYNAPSE' -export const REMOVE_SYNAPSE = 'REMOVE_SYNAPSE' -export const DELETE_SYNAPSE = 'DELETE_SYNAPSE' -export const UPDATE_MAP = 'UPDATE_MAP' +module.exports = { + JOIN_MAP: 'JOIN_MAP', + CHECK_FOR_CALL: 'CHECK_FOR_CALL', + LEAVE_MAP: 'LEAVE_MAP', + ACCEPT_CALL: 'ACCEPT_CALL', + DENY_CALL: 'DENY_CALL', + DENY_INVITE: 'DENY_INVITE', + INVITE_TO_JOIN: 'INVITE_TO_JOIN', + INVITE_A_CALL: 'INVITE_A_CALL', + JOIN_CALL: 'JOIN_CALL', + LEAVE_CALL: 'LEAVE_CALL', + SEND_MAPPER_INFO: 'SEND_MAPPER_INFO', + SEND_COORDS: 'SEND_COORDS', + CREATE_MESSAGE: 'CREATE_MESSAGE', + DRAG_TOPIC: 'DRAG_TOPIC', + CREATE_TOPIC: 'CREATE_TOPIC', + UPDATE_TOPIC: 'UPDATE_TOPIC', + REMOVE_TOPIC: 'REMOVE_TOPIC', + DELETE_TOPIC: 'DELETE_TOPIC', + CREATE_SYNAPSE: 'CREATE_SYNAPSE', + UPDATE_SYNAPSE: 'UPDATE_SYNAPSE', + REMOVE_SYNAPSE: 'REMOVE_SYNAPSE', + DELETE_SYNAPSE: 'DELETE_SYNAPSE', + UPDATE_MAP: 'UPDATE_MAP', -/* EVENTS RECEIVABLE */ -export const INVITED_TO_CALL = 'INVITED_TO_CALL' -export const INVITED_TO_JOIN = 'INVITED_TO_JOIN' -export const CALL_ACCEPTED = 'CALL_ACCEPTED' -export const CALL_DENIED = 'CALL_DENIED' -export const INVITE_DENIED = 'INVITE_DENIED' -export const CALL_IN_PROGRESS = 'CALL_IN_PROGRESS' -export const CALL_STARTED = 'CALL_STARTED' -export const MAPPER_JOINED_CALL = 'MAPPER_JOINED_CALL' -export const MAPPER_LEFT_CALL = 'MAPPER_LEFT_CALL' -export const MAPPER_LIST_UPDATED = 'MAPPER_LIST_UPDATED' -export const NEW_MAPPER = 'NEW_MAPPER' -export const LOST_MAPPER = 'LOST_MAPPER' -export const MESSAGE_CREATED = 'MESSAGE_CREATED' -export const TOPIC_DRAGGED = 'TOPIC_DRAGGED' -export const TOPIC_CREATED = 'TOPIC_CREATED' -export const TOPIC_UPDATED = 'TOPIC_UPDATED' -export const TOPIC_REMOVED = 'TOPIC_REMOVED' -export const TOPIC_DELETED = 'TOPIC_DELETED' -export const SYNAPSE_CREATED = 'SYNAPSE_CREATED' -export const SYNAPSE_UPDATED = 'SYNAPSE_UPDATED' -export const SYNAPSE_REMOVED = 'SYNAPSE_REMOVED' -export const SYNAPSE_DELETED = 'SYNAPSE_DELETED' -export const PEER_COORDS_UPDATED = 'PEER_COORDS_UPDATED' -export const MAP_UPDATED = 'MAP_UPDATED' -export const LIVE_MAPS_RECEIVED = 'LIVE_MAPS_RECEIVED' -export const MAP_WENT_LIVE = 'MAP_WENT_LIVE' -export const MAP_CEASED_LIVE = 'MAP_CEASED_LIVE' + /* EVENTS RECEIVABLE */ + JUNTO_UPDATED: 'JUNTO_UPDATED', + INVITED_TO_CALL: 'INVITED_TO_CALL', + INVITED_TO_JOIN: 'INVITED_TO_JOIN', + CALL_ACCEPTED: 'CALL_ACCEPTED', + CALL_DENIED: 'CALL_DENIED', + INVITE_DENIED: 'INVITE_DENIED', + CALL_IN_PROGRESS: 'CALL_IN_PROGRESS', + CALL_STARTED: 'CALL_STARTED', + MAPPER_JOINED_CALL: 'MAPPER_JOINED_CALL', + MAPPER_LEFT_CALL: 'MAPPER_LEFT_CALL', + MAPPER_LIST_UPDATED: 'MAPPER_LIST_UPDATED', + NEW_MAPPER: 'NEW_MAPPER', + LOST_MAPPER: 'LOST_MAPPER', + MESSAGE_CREATED: 'MESSAGE_CREATED', + TOPIC_DRAGGED: 'TOPIC_DRAGGED', + TOPIC_CREATED: 'TOPIC_CREATED', + TOPIC_UPDATED: 'TOPIC_UPDATED', + TOPIC_REMOVED: 'TOPIC_REMOVED', + TOPIC_DELETED: 'TOPIC_DELETED', + SYNAPSE_CREATED: 'SYNAPSE_CREATED', + SYNAPSE_UPDATED: 'SYNAPSE_UPDATED', + SYNAPSE_REMOVED: 'SYNAPSE_REMOVED', + SYNAPSE_DELETED: 'SYNAPSE_DELETED', + PEER_COORDS_UPDATED: 'PEER_COORDS_UPDATED', + MAP_UPDATED: 'MAP_UPDATED' +} diff --git a/frontend/src/Metamaps/Realtime/index.js b/frontend/src/Metamaps/Realtime/index.js index e891263c..7982618a 100644 --- a/frontend/src/Metamaps/Realtime/index.js +++ b/frontend/src/Metamaps/Realtime/index.js @@ -27,6 +27,7 @@ import Views from '../Views' import Visualize from '../Visualize' import { + JUNTO_UPDATED, INVITED_TO_CALL, INVITED_TO_JOIN, CALL_ACCEPTED, @@ -34,9 +35,9 @@ import { INVITE_DENIED, CALL_IN_PROGRESS, CALL_STARTED, + MAPPER_LIST_UPDATED, MAPPER_JOINED_CALL, MAPPER_LEFT_CALL, - MAPPER_LIST_UPDATED, NEW_MAPPER, LOST_MAPPER, MESSAGE_CREATED, @@ -50,13 +51,11 @@ import { SYNAPSE_REMOVED, SYNAPSE_DELETED, PEER_COORDS_UPDATED, - LIVE_MAPS_RECEIVED, - MAP_WENT_LIVE, - MAP_CEASED_LIVE, MAP_UPDATED } from './events' import { + juntoUpdated, invitedToCall, invitedToJoin, callAccepted, @@ -64,9 +63,9 @@ import { inviteDenied, callInProgress, callStarted, + mapperListUpdated, mapperJoinedCall, mapperLeftCall, - mapperListUpdated, peerCoordsUpdated, newMapper, lostMapper, @@ -81,13 +80,9 @@ import { synapseRemoved, synapseDeleted, mapUpdated, - liveMapsReceived, - mapWentLive, - mapCeasedLive } from './receivable' import { - requestLiveMaps, joinMap, leaveMap, checkForCall, @@ -98,8 +93,8 @@ import { inviteACall, joinCall, leaveCall, - sendMapperInfo, sendCoords, + sendMapperInfo, createMessage, dragTopic, createTopic, @@ -114,6 +109,7 @@ import { } from './sendable' let Realtime = { + juntoState: { connectedPeople: {}, liveMaps: {} }, videoId: 'video-wrapper', socket: null, webrtc: null, @@ -499,12 +495,11 @@ let Realtime = { } const sendables = [ - ['requestLiveMaps',requestLiveMaps], ['joinMap',joinMap], ['leaveMap',leaveMap], ['checkForCall',checkForCall], ['acceptCall',acceptCall], - ['denyAll',denyCall], + ['denyCall',denyCall], ['denyInvite',denyInvite], ['inviteToJoin',inviteToJoin], ['inviteACall',inviteACall], @@ -529,6 +524,7 @@ sendables.forEach(sendable => { }) const subscribeToEvents = (Realtime, socket) => { + socket.on(JUNTO_UPDATED, juntoUpdated(Realtime)) socket.on(INVITED_TO_CALL, invitedToCall(Realtime)) socket.on(INVITED_TO_JOIN, invitedToJoin(Realtime)) socket.on(CALL_ACCEPTED, callAccepted(Realtime)) @@ -536,9 +532,9 @@ const subscribeToEvents = (Realtime, socket) => { socket.on(INVITE_DENIED, inviteDenied(Realtime)) socket.on(CALL_IN_PROGRESS, callInProgress(Realtime)) socket.on(CALL_STARTED, callStarted(Realtime)) + socket.on(MAPPER_LIST_UPDATED, mapperListUpdated(Realtime)) socket.on(MAPPER_JOINED_CALL, mapperJoinedCall(Realtime)) socket.on(MAPPER_LEFT_CALL, mapperLeftCall(Realtime)) - socket.on(MAPPER_LIST_UPDATED, mapperListUpdated(Realtime)) socket.on(PEER_COORDS_UPDATED, peerCoordsUpdated(Realtime)) socket.on(NEW_MAPPER, newMapper(Realtime)) socket.on(LOST_MAPPER, lostMapper(Realtime)) @@ -553,9 +549,6 @@ const subscribeToEvents = (Realtime, socket) => { socket.on(SYNAPSE_REMOVED, synapseRemoved(Realtime)) socket.on(SYNAPSE_DELETED, synapseDeleted(Realtime)) socket.on(MAP_UPDATED, mapUpdated(Realtime)) - socket.on(LIVE_MAPS_RECEIVED, liveMapsReceived(Realtime)) - socket.on(MAP_WENT_LIVE, mapWentLive(Realtime)) - socket.on(MAP_CEASED_LIVE, mapCeasedLive(Realtime)) } export default Realtime diff --git a/frontend/src/Metamaps/Realtime/receivable.js b/frontend/src/Metamaps/Realtime/receivable.js index bf974bbe..477736c5 100644 --- a/frontend/src/Metamaps/Realtime/receivable.js +++ b/frontend/src/Metamaps/Realtime/receivable.js @@ -1,7 +1,11 @@ +/* global $ */ + /* everthing in this file happens as a result of websocket events */ +import { JUNTO_UPDATED } from './events' + import Active from '../Active' import GlobalUI from '../GlobalUI' import Control from '../Control' @@ -12,6 +16,11 @@ import Synapse from '../Synapse' import Util from '../Util' import Visualize from '../Visualize' +export const juntoUpdated = self => state => { + self.juntoState = state + $(document).trigger(JUNTO_UPDATED) +} + export const synapseRemoved = self => data => { var synapse = Metamaps.Synapses.get(data.mappableid) if (synapse) { @@ -239,13 +248,13 @@ export const lostMapper = self => data => { export const mapperListUpdated = self => data => { // data.userid // data.username - // data.userimage + // data.avatar self.mappersOnMap[data.userid] = { id: data.userid, name: data.username, username: data.username, - image: data.userimage, + image: data.avatar, color: Util.getPastelColor(), inConversation: data.userinconversation, coords: { @@ -259,14 +268,14 @@ export const mapperListUpdated = self => data => { 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.createCompass(data.username, data.userid, data.avatar, self.mappersOnMap[data.userid].color) } } export const newMapper = self => data => { // data.userid // data.username - // data.userimage + // data.avatar // data.coords var firstOtherPerson = Object.keys(self.mappersOnMap).length === 0 @@ -274,13 +283,12 @@ export const newMapper = self => data => { id: data.userid, name: data.username, username: data.username, - image: data.userimage, + image: data.avatar, color: Util.getPastelColor(), - realtime: true, coords: { x: 0, y: 0 - }, + } } // create an item for them in the realtime box @@ -289,7 +297,7 @@ export const newMapper = self => data => { 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.createCompass(data.username, data.userid, data.avatar, self.mappersOnMap[data.userid].color) var notifyMessage = data.username + ' just joined the map' if (firstOtherPerson) { @@ -324,7 +332,7 @@ export const invitedToCall = self => inviter => { self.soundId = self.room.chat.sound.play('sessioninvite') var username = self.mappersOnMap[inviter].name - var notifyText = '' + var notifyText = '' notifyText += username + ' is inviting you to a conversation. Join live?' notifyText += ' ' notifyText += ' ' @@ -391,6 +399,3 @@ export const callStarted = self => () => { self.room.conversationInProgress() } -export const liveMapsReceived = self => () => {} -export const mapWentLive = self => () => {} -export const mapCeasedLive = self => () => {} diff --git a/frontend/src/Metamaps/Realtime/sendable.js b/frontend/src/Metamaps/Realtime/sendable.js index a1ce2ea7..71abc35c 100644 --- a/frontend/src/Metamaps/Realtime/sendable.js +++ b/frontend/src/Metamaps/Realtime/sendable.js @@ -2,7 +2,6 @@ import Active from '../Active' import GlobalUI from '../GlobalUI' import { - REQUEST_LIVE_MAPS, JOIN_MAP, LEAVE_MAP, CHECK_FOR_CALL, @@ -28,15 +27,11 @@ import { UPDATE_MAP } from './events' -export const requestLiveMaps = self => () => { - self.socket.emit(REQUEST_LIVE_MAPS) -} - export const joinMap = self => () => { self.socket.emit(JOIN_MAP, { userid: Active.Mapper.id, username: Active.Mapper.get('name'), - userimage: Active.Mapper.get('image'), + avatar: Active.Mapper.get('image'), mapid: Active.Map.id, map: Active.Map.attributes }) @@ -55,7 +50,7 @@ export const sendMapperInfo = self => userid => { var update = { userToNotify: userid, username: Active.Mapper.get('name'), - userimage: Active.Mapper.get('image'), + avatar: Active.Mapper.get('image'), userid: Active.Mapper.id, userinconversation: self.inConversation, mapid: Active.Map.id diff --git a/frontend/src/Metamaps/Views/ChatView.js b/frontend/src/Metamaps/Views/ChatView.js index 8c586a0c..8febe9e1 100644 --- a/frontend/src/Metamaps/Views/ChatView.js +++ b/frontend/src/Metamaps/Views/ChatView.js @@ -159,7 +159,7 @@ var Private = { 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.image = m.user_image m.message = linker.link(m.message) var $html = $(this.messageTemplate(m)) this.$messages.append($html) diff --git a/frontend/src/Metamaps/Views/ExploreMaps.js b/frontend/src/Metamaps/Views/ExploreMaps.js index e843e7fe..421b3c22 100644 --- a/frontend/src/Metamaps/Views/ExploreMaps.js +++ b/frontend/src/Metamaps/Views/ExploreMaps.js @@ -4,6 +4,7 @@ import React from 'react' import ReactDOM from 'react-dom' // TODO ensure this isn't a double import import Active from '../Active' +import Realtime from '../Realtime' import Maps from '../../components/Maps' /* @@ -27,6 +28,8 @@ const ExploreMaps = { render: function (mapperObj, cb) { var self = ExploreMaps + if (!self.collection) return + if (typeof mapperObj === 'function') { cb = mapperObj mapperObj = null @@ -36,6 +39,7 @@ const ExploreMaps = { currentUser: Active.Mapper, section: self.collection.id, maps: self.collection, + juntoState: Realtime.juntoState, moreToLoad: self.collection.page != 'loadedAll', user: mapperObj, loadMore: self.loadMore diff --git a/frontend/src/Metamaps/Views/index.js b/frontend/src/Metamaps/Views/index.js index d13482d0..39104b18 100644 --- a/frontend/src/Metamaps/Views/index.js +++ b/frontend/src/Metamaps/Views/index.js @@ -1,7 +1,18 @@ +/* global $ */ + import ExploreMaps from './ExploreMaps' import ChatView from './ChatView' import VideoView from './VideoView' import Room from './Room' +import { JUNTO_UPDATED } from '../Realtime/events' -const Views = { ExploreMaps, ChatView, VideoView, Room } +const Views = { + init: () => { + $(document).on(JUNTO_UPDATED, () => ExploreMaps.render()) + }, + ExploreMaps, + ChatView, + VideoView, + Room +} export default Views diff --git a/frontend/src/Metamaps/index.js b/frontend/src/Metamaps/index.js index 44bbfdb6..bf2e2d60 100644 --- a/frontend/src/Metamaps/index.js +++ b/frontend/src/Metamaps/index.js @@ -88,7 +88,7 @@ document.addEventListener('DOMContentLoaded', function () { if (Metamaps.currentSection === 'explore') { const capitalize = Metamaps.currentPage.charAt(0).toUpperCase() + Metamaps.currentPage.slice(1) - Metamaps.Views.ExploreMaps.setCollection(Metamaps.Maps[capitalize]) + Views.ExploreMaps.setCollection(Metamaps.Maps[capitalize]) if (Metamaps.currentPage === 'mapper') { Views.ExploreMaps.fetchUserThenRender() } else { diff --git a/frontend/src/components/Maps/MapCard.js b/frontend/src/components/Maps/MapCard.js index 3a1557ee..1cb99530 100644 --- a/frontend/src/components/Maps/MapCard.js +++ b/frontend/src/components/Maps/MapCard.js @@ -1,8 +1,19 @@ import React, { Component, PropTypes } from 'react' +import { find, values } from 'lodash' + +const IN_CONVERSATION = 1 // shared with /realtime/reducer.js + +const MapperList = (props) => { + +} class MapCard extends Component { render = () => { - const { map, currentUser } = this.props + const { map, juntoState, currentUser } = this.props + + const hasMap = juntoState.liveMaps[map.id] + const hasConversation = hasMap && find(values(hasMap), v => v === IN_CONVERSATION) + const hasMapper = hasMap && !hasConversation function capitalize (string) { return string.charAt(0).toUpperCase() + string.slice(1) @@ -59,6 +70,8 @@ class MapCard extends Component { + { hasMapper &&
} + { hasConversation &&
} @@ -69,6 +82,7 @@ class MapCard extends Component { MapCard.propTypes = { map: PropTypes.object.isRequired, + juntoState: PropTypes.object, currentUser: PropTypes.object } diff --git a/frontend/src/components/Maps/index.js b/frontend/src/components/Maps/index.js index 670f5aaf..a714e4ca 100644 --- a/frontend/src/components/Maps/index.js +++ b/frontend/src/components/Maps/index.js @@ -41,7 +41,7 @@ class Maps extends Component { } render = () => { - const { maps, currentUser, section, user, moreToLoad, loadMore } = this.props + const { maps, currentUser, juntoState, section, user, moreToLoad, loadMore } = this.props const style = { width: this.state.mapsWidth + 'px' } return ( @@ -50,7 +50,7 @@ class Maps extends Component {
{ user ? : null } { currentUser && !user ? : null } - { maps.models.map(map => ) } + { maps.models.map(map => ) }
{!moreToLoad ? null : [ , @@ -70,6 +70,7 @@ class Maps extends Component { Maps.propTypes = { section: PropTypes.string.isRequired, maps: PropTypes.object.isRequired, + juntoState: PropTypes.object.isRequired, moreToLoad: PropTypes.bool.isRequired, user: PropTypes.object, currentUser: PropTypes.object, diff --git a/package.json b/package.json index c43b7599..8dca7615 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "react": "15.3.2", "react-dom": "15.3.2", "react-dropzone": "3.6.0", + "redux": "^3.6.0", "simplewebrtc": "2.2.0", "socket.io": "1.3.7", "underscore": "1.4.4", diff --git a/realtime/global.js b/realtime/global.js index 8e4d0c97..1df9d8ea 100644 --- a/realtime/global.js +++ b/realtime/global.js @@ -1,17 +1,15 @@ - -import { +const { // server sendable, client receivable TOPIC_UPDATED, TOPIC_DELETED, SYNAPSE_UPDATED, SYNAPSE_DELETED, - LIVE_MAPS_RECEIVED, - MAP_WENT_LIVE, - MAP_CEASED_LIVE, MAP_UPDATED, + JUNTO_UPDATED, // server receivable, client sendable - REQUEST_LIVE_MAPS, + JOIN_CALL, + LEAVE_CALL, JOIN_MAP, LEAVE_MAP, UPDATE_TOPIC, @@ -19,43 +17,23 @@ import { UPDATE_SYNAPSE, DELETE_SYNAPSE, UPDATE_MAP -} from '../frontend/src/Metamaps/Realtime/events' +} = require('../frontend/src/Metamaps/Realtime/events') -const adjustAndBroadcast = (io, socket, state, event, data) => { - if (event === JOIN_MAP) { - if (!state.liveMaps[data.mapid]) { - state.liveMaps[data.mapid] = data.map // { name: '', desc: '', numTopics: '' } - state.liveMaps[data.mapid].mapper_count = 1 - io.sockets.emit(MAP_WENT_LIVE, state.liveMaps[data.mapid]) - } - else { - state.liveMaps[data.mapid].mapper_count++ - } - } - else if (event === LEAVE_MAP) { - const mapid = socket.mapid - if (state.liveMaps[mapid] && state.liveMaps[mapid].mapper_count == 1) { - delete state.liveMaps[mapid] - io.sockets.emit(MAP_CEASED_LIVE, { id: mapid }) - } - else if (state.liveMaps[mapid]) { - state.liveMaps[mapid].mapper_count-- - } - } -} +module.exports = function (io, store) { + store.subscribe(() => { + console.log(store.getState()) + io.sockets.emit(JUNTO_UPDATED, store.getState()) + }) -module.exports = function (io, state) { io.on('connection', function (socket) { - socket.on(REQUEST_LIVE_MAPS, function (activeUser) { - //constrain response to maps visible to user - var maps = Object.keys(state.liveMaps).map(function(key) { return state.liveMaps[key] }) - socket.emit(LIVE_MAPS_RECEIVED, maps) - }) + io.sockets.emit(JUNTO_UPDATED, store.getState()) - socket.on(JOIN_MAP, data => adjustAndBroadcast(io, socket, state, JOIN_MAP, data)) - socket.on(LEAVE_MAP, () => adjustAndBroadcast(io, socket, state, LEAVE_MAP)) - socket.on('disconnect', () => adjustAndBroadcast(io, socket, state, LEAVE_MAP)) + socket.on(JOIN_MAP, data => store.dispatch({ type: JOIN_MAP, payload: data })) + socket.on(LEAVE_MAP, () => store.dispatch({ type: LEAVE_MAP, payload: socket })) + socket.on(JOIN_CALL, data => store.dispatch({ type: JOIN_CALL, payload: data })) + socket.on(LEAVE_CALL, () => store.dispatch({ type: LEAVE_CALL, payload: socket })) + socket.on('disconnect', () => store.dispatch({ type: 'DISCONNECT', payload: socket })) socket.on(UPDATE_TOPIC, function (data) { socket.broadcast.emit(TOPIC_UPDATED, data) diff --git a/realtime/junto.js b/realtime/junto.js index aa7f6152..f97efdbc 100644 --- a/realtime/junto.js +++ b/realtime/junto.js @@ -1,4 +1,4 @@ -import { +const { INVITED_TO_CALL, INVITED_TO_JOIN, CALL_ACCEPTED, @@ -17,11 +17,11 @@ import { INVITE_A_CALL, JOIN_CALL, LEAVE_CALL -} from '../frontend/src/Metamaps/Realtime/events' +} = require('../frontend/src/Metamaps/Realtime/events') const { mapRoom, userMapRoom } = require('./rooms') -module.exports = function (io, state) { +module.exports = function (io, store) { io.on('connection', function (socket) { socket.on(CHECK_FOR_CALL, function (data) { @@ -39,6 +39,8 @@ module.exports = function (io, state) { socket.on(ACCEPT_CALL, function (data) { socket.broadcast.in(userMapRoom(data.inviter, data.mapid)).emit(CALL_ACCEPTED, data.invited) + // convert this so that it broadcasts to all sockets and includes the map id + // and who's participating socket.broadcast.in(mapRoom(data.mapid)).emit(CALL_STARTED) }) @@ -51,12 +53,15 @@ module.exports = function (io, state) { }) socket.on(JOIN_CALL, function (data) { + // convert this so that it broadcasts to all sockets and includes the map id + // and info about who joined socket.broadcast.in(mapRoom(data.mapid)).emit(MAPPER_JOINED_CALL, data.id) }) socket.on(LEAVE_CALL, function (data) { + // convert this so that it broadcasts to all sockets and includes the map id + // and info about who joined socket.broadcast.in(mapRoom(data.mapid)).emit(MAPPER_LEFT_CALL, data.id) }) }) } - diff --git a/realtime/map.js b/realtime/map.js index d0c85a10..5e153209 100644 --- a/realtime/map.js +++ b/realtime/map.js @@ -1,5 +1,4 @@ - -import { +const { MAPPER_LIST_UPDATED, NEW_MAPPER, LOST_MAPPER, @@ -13,19 +12,19 @@ import { JOIN_MAP, LEAVE_MAP, - SEND_MAPPER_INFO, SEND_COORDS, + SEND_MAPPER_INFO, CREATE_MESSAGE, DRAG_TOPIC, CREATE_TOPIC, REMOVE_TOPIC, CREATE_SYNAPSE, REMOVE_SYNAPSE -} from '../frontend/src/Metamaps/Realtime/events' +} = require('../frontend/src/Metamaps/Realtime/events') const { mapRoom, userMapRoom } = require('./rooms') -module.exports = function (io, state) { +module.exports = function (io, store) { io.on('connection', function (socket) { // this will ping everyone on a map that there's a person just joined the map @@ -33,11 +32,11 @@ module.exports = function (io, state) { socket.mapid = data.mapid socket.userid = data.userid socket.username = data.username - socket.userimage = data.userimage + socket.avatar = data.avatar var newUser = { userid: data.userid, username: data.username, - userimage: data.userimage + avatar: data.avatar } socket.join(mapRoom(data.mapid)) socket.join(userMapRoom(data.userid, data.mapid)) @@ -63,7 +62,7 @@ module.exports = function (io, state) { userid: data.userid, username: data.username, userinconversation: data.userinconversation, - userimage: data.userimage + avatar: data.avatar } socket.broadcast.in(userMapRoom(data.userToNotify, data.mapid)).emit(MAPPER_LIST_UPDATED, existingUser) }) diff --git a/realtime/realtime-server.js b/realtime/realtime-server.js index 7cdcd6bf..c52b67ab 100644 --- a/realtime/realtime-server.js +++ b/realtime/realtime-server.js @@ -6,13 +6,14 @@ map = require('./map'), global = require('./global'), stunservers = [{"url": "stun:stun.l.google.com:19302"}] -var state = { - connectedPeople: {}, - liveMaps: {} -} -signalling(io, stunservers, state) -junto(io, state) -map(io, state) -global(io, state) -io.listen(5001) +const { createStore } = require('redux') +const { reducer } = require('./reducer') +let store = createStore(reducer) + +global(io, store) +signalling(io, stunservers, store) +junto(io, store) +map(io, store) + +io.listen(5001) diff --git a/realtime/reducer.js b/realtime/reducer.js new file mode 100644 index 00000000..57178733 --- /dev/null +++ b/realtime/reducer.js @@ -0,0 +1,75 @@ +const { omit, omitBy, isNil, mapValues } = require('lodash') +const { + JOIN_MAP, + LEAVE_MAP, + JOIN_CALL, + LEAVE_CALL +} = require('../frontend/src/Metamaps/Realtime/events') + +const NOT_IN_CONVERSATION = 0 +const IN_CONVERSATION = 1 + +const addMapperToMap = (map, userId) => { return { ...map, [userId]: NOT_IN_CONVERSATION }} + +const reducer = (state = { connectedPeople: {}, liveMaps: {} }, action) => { + const { type, payload } = action + const { connectedPeople, liveMaps } = state + const map = payload && liveMaps[payload.mapid] + const mapWillEmpty = map && Object.keys(map).length === 1 + const callWillFinish = map && (type === LEAVE_CALL || type === 'DISCONNECT') && Object.keys(map).length === 2 + + switch (type) { + case JOIN_MAP: + return Object.assign({}, state, { + connectedPeople: Object.assign({}, connectedPeople, { + [payload.userid]: { + id: payload.userid, + username: payload.username, + avatar: payload.avatar + } + }), + liveMaps: Object.assign({}, liveMaps, { + [payload.mapid]: addMapperToMap(map || {}, payload.userid) + }) + }) + case LEAVE_MAP: + // if the map will empty, remove it from liveMaps, if the map will not empty, just remove the mapper + const newLiveMaps = mapWillEmpty + ? omit(liveMaps, payload.mapid) + : Object.assign({}, liveMaps, { [payload.mapid]: omit(map, payload.userid) }) + + return { + connectedPeople: omit(connectedPeople, payload.userid), + liveMaps: omitBy(newLiveMaps, isNil) + } + case JOIN_CALL: + // update the user (payload.id is user id) in the given map to be marked in the conversation + return Object.assign({}, state, { + liveMaps: Object.assign({}, liveMaps, { + [payload.mapid]: Object.assign({}, map, { + [payload.id]: IN_CONVERSATION + }) + }) + }) + case LEAVE_CALL: + const newMap = callWillFinish + ? mapValues(map, () => NOT_IN_CONVERSATION) + : Object.assign({}, map, { [payload.userid]: NOT_IN_CONVERSATION }) + + return Object.assign({}, state, { + liveMaps: Object.assign({}, liveMaps, { map: newMap }) + }) + case 'DISCONNECT': + const mapWithoutUser = omit(map, payload.userid) + const newMap = callWillFinish ? mapValues(mapWithoutUser, () => NOT_IN_CONVERSATION) : mapWithoutUser + const newLiveMaps = mapWillEmpty ? omit(liveMaps, payload.mapid) : Object.assign({}, liveMaps, { [payload.mapid]: newMap }) + return { + connectedPeople: omit(connectedPeople, payload.userid), + liveMaps: omitBy(newLiveMaps, isNil) + } + default: + return state + } +} + +module.exports = reducer diff --git a/realtime/rooms.js b/realtime/rooms.js index 6276e3f9..30d56d1b 100644 --- a/realtime/rooms.js +++ b/realtime/rooms.js @@ -1,4 +1,3 @@ - module.exports = { mapRoom: mapId => `maps/${mapId}`, userMapRoom: (mapperId, mapId) => `mappers/${mapperId}/maps/${mapId}`, diff --git a/realtime/signal.js b/realtime/signal.js index c14ce392..39283709 100644 --- a/realtime/signal.js +++ b/realtime/signal.js @@ -1,4 +1,4 @@ -var uuid = require('node-uuid') +const uuid = require('node-uuid') // based off of https://github.com/andyet/signalmaster // since it was updated to socket.io 1.3.7 @@ -12,7 +12,6 @@ function safeCb(cb) { } module.exports = function(io, stunservers, state) { - io.on('connection', function (socket) { socket.resources = { screen: false,