diff --git a/app/assets/images/junto.gif b/app/assets/images/junto.gif new file mode 100644 index 00000000..e4a72d4b Binary files /dev/null and b/app/assets/images/junto.gif differ diff --git a/app/assets/stylesheets/mapcard.scss.erb b/app/assets/stylesheets/mapcard.scss.erb index 871b6baf..57fc315a 100644 --- a/app/assets/stylesheets/mapcard.scss.erb +++ b/app/assets/stylesheets/mapcard.scss.erb @@ -1,144 +1,259 @@ /* Map Cards */ .map { - 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); -} -.map.newMap { - float: left; - position: relative; -} -.map.newMap:hover { - background: #dcdcdc; -} -.map.newMap a { - height: 340px; - display: block; - position: relative; -} -.newMap .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; -} -.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; + + &:hover { + .dropdownMenu .menuToggle .circle { + background-color: #FFF; + } + .dropdownMenu .menuToggle:hover .circle { + background-color: #DDD; + } + + .mainContent { + filter: blur(2px); + } + + .mapMetadata { + display: block; + } + } + + .mapHasMapper, .mapHasConversation { + position: absolute; + top: 8px; + left: 8px; + min-width: 32px; + min-height: 32px; + + &:hover { + background-color: #FFF; + border-radius: 2px; + + .mapperList { + display: block; + } + } + + .mapperList { + display: none; + padding: 8px; + list-style-type: none; + + li { + &.live { + height: 32px; + padding-left: 32px; + font-size: 16px; + } + + img { + width: 24px; + height: 24px; + border-radius: 12px; + display: inline-block; + vertical-align: middle; + } + span { + padding-left: 10px; + font-size: 14px; + } + } + } + } + .mapHasMapper { + background: url('<%= asset_path('junto.png') %>') no-repeat 4px 0; + } + .mapHasConversation { + background: url('<%= asset_path('junto.gif') %>') no-repeat 4px 0; + } + + .dropdownMenu { + position: absolute; + top: 8px; + right: 8px; + cursor: pointer; + + .menuToggle { + width: 30px; + height: 10px; + + .circle { + display: inline-block; + background-color: #454545; + width: 6px; + height: 6px; + border-radius: 3px; + margin: 2px; + } + + &:hover .circle { + background-color: #222; + } + } + + .menuItems { + position: absolute; + top: 18px; + right: 0px; + background: #FFF; + border-radius: 2px; + list-style-type: none; + color: #454545; + + li { + white-space: nowrap; + padding: 6px; + + &:hover { + background-color: #DDD; + } + } + } + } + + .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; + } + + .cardViewOnly { + float: right; + line-height: 32px; + padding-right: 10px; + color: #454545; + } + + .scroll { + display:block; + font-family: helvetica, sans-serif; + font-size: 12px; + word-wrap: break-word; + text-align: center; + margin-top: 16px; + } + + + + .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/app/views/shared/_cheatsheet.html.erb b/app/views/shared/_cheatsheet.html.erb index bb12fd7f..52b95804 100644 --- a/app/views/shared/_cheatsheet.html.erb +++ b/app/views/shared/_cheatsheet.html.erb @@ -4,14 +4,14 @@ #%>

HELP

- +
- +
-
+
Recenter Topics around chosen Topic: Alt + click on the topic OR Alt + E
Reveal the siblings for a Topic: Right-click and choose 'Reveal siblings' OR Alt + R
Center topic and reveal siblings: Alt + T
- -
+ +
Double-click on canvas: Bring up the metacode spinner
Scroll: change metacode spinner selection
Tab: rotate spinner counter-clockwise
@@ -39,10 +39,10 @@
Esc: Hides auto-suggestion results
Enter: create a new topic
Gear Icon: open up metacode settings
- +
-
+
Open 'Topic' card: Double-click on topic icon
@@ -71,11 +71,11 @@ Open 'Context Menu': Right-click/alt+click on topic icon or synapse
*Hide/Remove/Delete topic within context menu
- +
-
+
Open 'Create Synapse' prompt: Right-click & drag from one topic to another
Enter or Tab: Create synapse
Esc or Delete: Cancel synapse creation
@@ -83,11 +83,11 @@
Create new Topic with Synapse: Right-click + drag from topic to open canvas
Enter: Create topic
Enter: Create synapse
- +
-
+
Open 'Synapse' card: Double-click on Synapse
Edit Synapse description: Click on current description text
Save Synapse description: Hit enter
@@ -96,19 +96,19 @@
Browse synapses / change visible synapse click on arrow icon and select desired synapse
Open 'Context Menu': Right-click/alt-click on Synapse
*Hide/Remove/Delete synapse within context menu
- +
-
+
Move around Canvas: Click and drag
Zoom in/out: Scroll OR click on
&
Zoom to see all: Click
OR Ctrl + E
- +
-
+
Select/Deselect Topic: Click on topic icon
Select/Deselect Synapse: Click on synapse
Select multiple Topics/Synapses: Shift + click
@@ -120,9 +120,9 @@
Deselect all topics & Synapses: Click on background or Esc
-
+
Open 'Search' prompt: Ctrl + /
-
Close 'Search' prompt: Esc
+
Close 'Search' prompt: Esc
<% if controller_name == "maps" && action_name == "show" %>
Add to current Map: Click "+" on a topic result
<% end %> @@ -131,9 +131,9 @@
Search for mapper: type "mapper:", then your search query. i.e. mapper:Robert
-
+
Ctrl + /: Open 'Search' prompt
-
Ctrl + H: Hide selection on map
+
Ctrl + H: Hide selection on map
Ctrl + M: Remove selection from map
Ctrl + D: Delete selection
Ctrl + A: Select all topics
@@ -145,9 +145,6 @@
-
1. GETTING STARTED
-
2. UP YOUR SKILLZ
-
3. ADVANCED MAPPING
diff --git a/frontend/src/Metamaps/JIT.js b/frontend/src/Metamaps/JIT.js index 6549d823..73c1b600 100644 --- a/frontend/src/Metamaps/JIT.js +++ b/frontend/src/Metamaps/JIT.js @@ -37,6 +37,10 @@ const JIT = { tempInit: false, tempNode: null, tempNode2: null, + mouseDownPix: {}, + dragFlag : 0, + dragTolerance: 0, + virtualPointer: {}, events: { topicDrag: 'Metamaps:JIT:events:topicDrag', @@ -754,77 +758,127 @@ const JIT = { Control.deselectAllNodes() }, // escKeyHandler onDragMoveTopicHandler: function (node, eventInfo, e) { - const self = JIT + var self = JIT - // this is used to send nodes that are moving to - // other realtime collaborators on the same map - const positionsToSend = {} - let topic - - const authorized = Active.Map && Active.Map.authorizeToEdit(Active.Mapper) + var authorized = Active.Map && Active.Map.authorizeToEdit(Active.Mapper) if (node && !node.nodeFrom) { - const pos = eventInfo.getPos() + var pos = eventInfo.getPos(), + EDGE_THICKNESS = 30 /** Metamaps.Visualize.mGraph.canvas.scaleOffsetX*/, + SHIFT = 2 / Metamaps.Visualize.mGraph.canvas.scaleOffsetX, + PERIOD = 5; + + //self.virtualPointer = pos; // if it's a left click, or a touch, move the node - if (e.touches || (e.button === 0 && !e.altKey && !e.ctrlKey && !e.shiftKey && (e.buttons === 0 || e.buttons === 1 || e.buttons === undefined))) { + if (e.touches || (e.button === 0 && !e.altKey && !e.ctrlKey && (e.buttons === 0 || e.buttons === 1 || e.buttons === undefined))) { + + var width = Metamaps.Visualize.mGraph.canvas.getSize().width, + height = Metamaps.Visualize.mGraph.canvas.getSize().height, + xPix = Metamaps.Util.coordsToPixels(pos).x, + yPix = Metamaps.Util.coordsToPixels(pos).y; + + if(self.dragFlag === 0){ + self.mouseDownPix = Metamaps.Util.coordsToPixels(eventInfo.getPos()); + self.dragFlag = 1; + } + + if(Metamaps.Util.getDistance(Metamaps.Util.coordsToPixels(pos),self.mouseDownPix) > 2 && !self.dragTolerance){ + self.dragTolerance = 1; + } + + if(xPix < EDGE_THICKNESS && self.dragTolerance ){ + clearInterval(self.dragLeftEdge); + clearInterval(self.dragRightEdge); + clearInterval(self.dragTopEdge); + clearInterval(self.dragBottomEdge); + self.virtualPointer = {x:Metamaps.Util.pixelsToCoords({x:EDGE_THICKNESS,y:yPix}).x - SHIFT,y:pos.y}; + Metamaps.Visualize.mGraph.canvas.translate(SHIFT,0); + self.updateTopicPositions(node, self.virtualPointer); + Visualize.mGraph.plot(); + + self.dragLeftEdge = setInterval( function(){ + self.virtualPointer = {x:Metamaps.Util.pixelsToCoords({x:EDGE_THICKNESS,y:yPix}).x - SHIFT,y:pos.y}; + Metamaps.Visualize.mGraph.canvas.translate(SHIFT,0); + self.updateTopicPositions(node,self.virtualPointer); + Visualize.mGraph.plot(); + } , PERIOD); + + } + if(width - xPix < EDGE_THICKNESS && self.dragTolerance){ + clearInterval(self.dragLeftEdge); + clearInterval(self.dragRightEdge); + clearInterval(self.dragTopEdge); + clearInterval(self.dragBottomEdge); + self.virtualPointer = {x:Metamaps.Util.pixelsToCoords({x:width - EDGE_THICKNESS,y:yPix}).x + SHIFT,y:pos.y}; + Metamaps.Visualize.mGraph.canvas.translate(-SHIFT,0); + self.updateTopicPositions(node, self.virtualPointer); + Visualize.mGraph.plot(); + + self.dragRightEdge = setInterval( function(){ + self.virtualPointer = {x:Metamaps.Util.pixelsToCoords({x:width - EDGE_THICKNESS,y:yPix}).x + SHIFT,y:pos.y}; + Metamaps.Visualize.mGraph.canvas.translate(-SHIFT,0); + self.updateTopicPositions(node, self.virtualPointer); + Visualize.mGraph.plot(); + } , PERIOD); + } + if(yPix < EDGE_THICKNESS && self.dragTolerance){ + clearInterval(self.dragLeftEdge); + clearInterval(self.dragRightEdge); + clearInterval(self.dragTopEdge); + clearInterval(self.dragBottomEdge); + self.virtualPointer = {x:pos.x,y:Metamaps.Util.pixelsToCoords({x:xPix,y:EDGE_THICKNESS}).y - SHIFT}; + Metamaps.Visualize.mGraph.canvas.translate(0,SHIFT); + self.updateTopicPositions(node, self.virtualPointer); + Visualize.mGraph.plot(); + + self.dragTopEdge = setInterval( function(){ + self.virtualPointer = {x:pos.x,y:Metamaps.Util.pixelsToCoords({x:xPix,y:EDGE_THICKNESS}).y - SHIFT}; + Metamaps.Visualize.mGraph.canvas.translate(0,SHIFT); + self.updateTopicPositions(node, self.virtualPointer); + Visualize.mGraph.plot(); + } , PERIOD); + } + if(height - yPix < EDGE_THICKNESS && self.dragTolerance){ + clearInterval(self.dragLeftEdge); + clearInterval(self.dragRightEdge); + clearInterval(self.dragTopEdge); + clearInterval(self.dragBottomEdge); + self.virtualPointer = {x:pos.x,y:Metamaps.Util.pixelsToCoords({x:xPix,y:height - EDGE_THICKNESS}).y + SHIFT}; + Metamaps.Visualize.mGraph.canvas.translate(0,-SHIFT); + self.updateTopicPositions(node, self.virtualPointer); + Visualize.mGraph.plot(); + + self.dragBottomEdge = setInterval( function(){ + self.virtualPointer = {x:pos.x,y:Metamaps.Util.pixelsToCoords({x:xPix,y:height - EDGE_THICKNESS}).y + SHIFT}; + Metamaps.Visualize.mGraph.canvas.translate(0,-SHIFT); + self.updateTopicPositions(node, self.virtualPointer); + Visualize.mGraph.plot(); + } , PERIOD); + } + + if(xPix >= EDGE_THICKNESS && width - xPix >= EDGE_THICKNESS && yPix >= EDGE_THICKNESS && height - yPix >= EDGE_THICKNESS){ + clearInterval(self.dragLeftEdge); + clearInterval(self.dragRightEdge); + clearInterval(self.dragTopEdge); + clearInterval(self.dragBottomEdge); + + self.updateTopicPositions(node,pos); + Visualize.mGraph.plot() + } + // if the node dragged isn't already selected, select it - const whatToDo = self.handleSelectionBeforeDragging(node, e) + var whatToDo = self.handleSelectionBeforeDragging(node, e) if (node.pos.rho || node.pos.rho === 0) { // this means we're in topic view - const rho = Math.sqrt(pos.x * pos.x + pos.y * pos.y) - const theta = Math.atan2(pos.y, pos.x) + var rho = Math.sqrt(pos.x * pos.x + pos.y * pos.y) + var theta = Math.atan2(pos.y, pos.x) node.pos.setp(theta, rho) - } else if (whatToDo === 'only-drag-this-one') { - node.pos.setc(pos.x, pos.y) - - if (Active.Map) { - topic = node.getData('topic') - // we use the topic ID not the node id - // because we can't depend on the node id - // to be the same as on other collaborators - // maps - positionsToSend[topic.id] = pos - $(document).trigger(JIT.events.topicDrag, [positionsToSend]) - } } else { - const len = Selected.Nodes.length - - // first define offset for each node - const xOffset = [] - const yOffset = [] - for (let i = 0; i < len; i += 1) { - const n = Selected.Nodes[i] - xOffset[i] = n.pos.x - node.pos.x - yOffset[i] = n.pos.y - node.pos.y - } // for - - for (let i = 0; i < len; i += 1) { - const n = Selected.Nodes[i] - const x = pos.x + xOffset[i] - const y = pos.y + yOffset[i] - n.pos.setc(x, y) - - if (Active.Map) { - topic = n.getData('topic') - // we use the topic ID not the node id - // because we can't depend on the node id - // to be the same as on other collaborators - // maps - positionsToSend[topic.id] = n.pos - } - } // for - - if (Active.Map) { - $(document).trigger(JIT.events.topicDrag, [positionsToSend]) - } - } // if - - if (whatToDo === 'deselect') { - Control.deselectNode(node) + //self.updateTopicPositions(node,pos); } - Visualize.mGraph.plot() - } else if ((e.button === 2 || (e.button === 0 && e.altKey) || e.buttons === 2) && authorized) { - // if it's a right click or holding down alt, start synapse creation ->third option is for firefox + } + // if it's a right click or holding down alt, start synapse creation ->third option is for firefox + else if ((e.button === 2 || (e.button === 0 && e.altKey) || e.buttons === 2) && authorized) { if (JIT.tempInit === false) { JIT.tempNode = node JIT.tempInit = true @@ -832,7 +886,7 @@ const JIT = { Create.newTopic.hide() Create.newSynapse.hide() // set the draw synapse start positions - const l = Selected.Nodes.length + var l = Selected.Nodes.length if (l > 0) { for (let i = l - 1; i >= 0; i -= 1) { const n = Selected.Nodes[i] @@ -874,8 +928,8 @@ const JIT = { n.setData('dim', 25, 'current') }) // pop up node creation :) - const myX = e.clientX - 110 - const myY = e.clientY - 30 + var myX = e.clientX - 110 + var myY = e.clientY - 30 $('#new_topic').css('left', myX + 'px') $('#new_topic').css('top', myY + 'px') Create.newTopic.x = eventInfo.getPos().x @@ -887,9 +941,11 @@ const JIT = { y: pos.y } } - } else if ((e.button === 2 || (e.button === 0 && e.altKey) || e.buttons === 2) && Active.Topic) { + } + else if ((e.button === 2 || (e.button === 0 && e.altKey) || e.buttons === 2) && Active.Topic) { GlobalUI.notifyUser('Cannot create in Topic view.') - } else if ((e.button === 2 || (e.button === 0 && e.altKey) || e.buttons === 2) && !authorized) { + } + else if ((e.button === 2 || (e.button === 0 && e.altKey) || e.buttons === 2) && !authorized) { GlobalUI.notifyUser('Cannot edit Public map.') } } @@ -905,9 +961,21 @@ const JIT = { Visualize.mGraph.plot() }, // onDragCancelHandler onDragEndTopicHandler: function (node, eventInfo, e) { - let midpoint = {} - let pixelPos - let mapping + var self = JIT; + var midpoint = {}, pixelPos, mapping + + clearInterval(self.dragLeftEdge); + clearInterval(self.dragRightEdge); + clearInterval(self.dragTopEdge); + clearInterval(self.dragBottomEdge); + + delete self.dragLeftEdge; + delete self.dragRightEdge; + delete self.dragTopEdge; + delete self.dragBottomEdge; + + self.dragFlag = 0; + self.dragTolerance = 0; if (JIT.tempInit && JIT.tempNode2 === null) { // this means you want to add a new topic, and then a synapse @@ -1002,7 +1070,44 @@ const JIT = { Control.deselectAllNodes() } } - }, // canvasClickHandler + }, // canvasClickHandler + updateTopicPositions: function (node, pos){ + var len = Selected.Nodes.length; + var topic; + // this is used to send nodes that are moving to + // other realtime collaborators on the same map + var positionsToSend = {}; + + // first define offset for each node + var xOffset = [] + var yOffset = [] + for (var i = 0; i < len; i += 1) { + var n = Selected.Nodes[i] + xOffset[i] = n.pos.x - node.pos.x + yOffset[i] = n.pos.y - node.pos.y + } // for + + for (var i = 0; i < len; i += 1) { + var n = Selected.Nodes[i] + var x = pos.x + xOffset[i] + var y = pos.y + yOffset[i] + n.pos.setc(x, y) + + if (Active.Map) { + topic = n.getData('topic') + // we use the topic ID not the node id + // because we can't depend on the node id + // to be the same as on other collaborators + // maps + positionsToSend[topic.id] = n.pos + } + } // for + + if (Active.Map) { + $(document).trigger(JIT.events.topicDrag, [positionsToSend]) + } + }, + nodeDoubleClickHandler: function (node, e) { TopicCard.showCard(node) }, // nodeDoubleClickHandler @@ -1027,19 +1132,23 @@ const JIT = { // 2 others are selected only and shift, so additionally select this one // 3 others are selected only, no shift: drag only this one // 4 this node and others were selected, so drag them (just return false) - // return value: deselect node again after? if (Selected.Nodes.length === 0) { + Control.selectNode(node, e) return 'only-drag-this-one' } if (Selected.Nodes.indexOf(node) === -1) { if (e.shiftKey) { Control.selectNode(node, e) - return 'nothing' + return 'move-all-incuding-this-one' } else { + Control.deselectAllEdges() + Control.deselectAllNodes() + Control.selectNode(node, e) return 'only-drag-this-one' } } - return 'nothing' // case 4? + return 'move-all'; // case 4? + }, // handleSelectionBeforeDragging getNodeXY: function (node) { if (typeof node.pos.x === 'number' && typeof node.pos.y === 'number') { diff --git a/frontend/src/Metamaps/Listeners.js b/frontend/src/Metamaps/Listeners.js index cbd138fd..8b4d6616 100644 --- a/frontend/src/Metamaps/Listeners.js +++ b/frontend/src/Metamaps/Listeners.js @@ -7,6 +7,7 @@ import Mobile from './Mobile' import Realtime from './Realtime' import Selected from './Selected' import Topic from './Topic' +import Util from './Util' import Visualize from './Visualize' import { Search } from './GlobalUI' @@ -108,16 +109,19 @@ const Listeners = { }) $(window).resize(function () { - if (Visualize && Visualize.mGraph) Visualize.mGraph.canvas.resize($(window).width(), $(window).height()) + if (Visualize && Visualize.mGraph) { + Util.resizeCanvas(Visualize.mGraph.canvas) + } + if (Active.Map && Realtime.inConversation) Realtime.positionVideos() Mobile.resizeTitle() }) }, - centerAndReveal: function(nodes, opts) { + centerAndReveal: function (nodes, opts) { if (nodes.length < 1) return var node = nodes[nodes.length - 1] if (opts.center && opts.reveal) { - Topic.centerOn(node.id, function() { + Topic.centerOn(node.id, function () { Topic.fetchRelatives(nodes) }) } else if (opts.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/Util.js b/frontend/src/Metamaps/Util.js index 48a89b1d..49797ab4 100644 --- a/frontend/src/Metamaps/Util.js +++ b/frontend/src/Metamaps/Util.js @@ -1,3 +1,5 @@ +/* global $ */ + import { Parser, HtmlRenderer } from 'commonmark' import Visualize from './Visualize' @@ -138,6 +140,25 @@ const Util = { // use safe: true to filter xss return new HtmlRenderer({ safe: true }) .render(new Parser().parse(text)) + }, + logCanvasAttributes: function(canvas){ + return { + scaleX: canvas.scaleOffsetX, + scaleY: canvas.scaleOffsetY, + centreCoords: Util.pixelsToCoords({ x: canvas.canvases[0].size.width / 2, y: canvas.canvases[0].size.height / 2 }), + }; + }, + resizeCanvas: function(canvas){ + // Store the current canvas attributes, i.e. scale and map-coordinate at the centre of the user's screen + const oldAttr = Util.logCanvasAttributes(canvas); + + // Resize the canvas to fill the new window size. Based on how JIT works, this also resets the map back to scale 1 and tranlations = 0 + canvas.resize($(window).width(), $(window).height()) + + // Return the map to the original scale, and then put the previous central map-coordinate back to the centre of user's newly resized screen + canvas.scale(oldAttr.scaleX, oldAttr.scaleY) + const newAttr = Util.logCanvasAttributes(canvas); + canvas.translate(newAttr.centreCoords.x - oldAttr.centreCoords.x, newAttr.centreCoords.y - oldAttr.centreCoords.y) } } 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..441f3ca2 100644 --- a/frontend/src/Metamaps/Views/ExploreMaps.js +++ b/frontend/src/Metamaps/Views/ExploreMaps.js @@ -4,6 +4,8 @@ import React from 'react' import ReactDOM from 'react-dom' // TODO ensure this isn't a double import import Active from '../Active' +import GlobalUI from '../GlobalUI' +import Realtime from '../Realtime' import Maps from '../../components/Maps' /* @@ -11,6 +13,7 @@ import Maps from '../../components/Maps' */ const ExploreMaps = { + pending: false, setCollection: function (collection) { var self = ExploreMaps @@ -27,58 +30,79 @@ const ExploreMaps = { render: function (mapperObj, cb) { var self = ExploreMaps + if (!self.collection) return + if (typeof mapperObj === 'function') { cb = mapperObj mapperObj = null } - - var exploreObj = { + + var exploreObj = { currentUser: Active.Mapper, section: self.collection.id, maps: self.collection, + juntoState: Realtime.juntoState, moreToLoad: self.collection.page != 'loadedAll', user: mapperObj, - loadMore: self.loadMore + loadMore: self.loadMore, + pending: self.pending, + onStar: function (map) { + $.post('/maps/' + map.id + '/star') + map.set('star_count', map.get('star_count') + 1) + if (Metamaps.Stars) Metamaps.Stars.push({ user_id: Active.Mapper.id, map_id: map.id }) + Metamaps.Maps.Starred.add(map) + GlobalUI.notifyUser('Map is now starred') + self.render() + }, + onRequest: function (map) { + $.post({ + url: `/maps/${map.id}/access_request` + }) + GlobalUI.notifyUser('You will be notified by email if request accepted') + } } ReactDOM.render( React.createElement(Maps, exploreObj), document.getElementById('explore') - ) - + ).resize() + if (cb) cb() - Metamaps.Loading.hide() }, loadMore: function () { var self = ExploreMaps - if (self.collection.page != "loadedAll") { self.collection.getMaps() + self.pending = true } - else self.render() + self.render() }, handleSuccess: function (cb) { var self = ExploreMaps - + self.pending = false if (self.collection && self.collection.id === 'mapper') { self.fetchUserThenRender(cb) } else { self.render(cb) + Metamaps.Loading.hide() } }, handleError: function () { console.log('error loading maps!') // TODO + Metamaps.Loading.hide() }, fetchUserThenRender: function (cb) { var self = ExploreMaps - + // first load the mapper object and then call the render function $.ajax({ url: '/users/' + self.collection.mapperId + '/details.json', success: function (response) { self.render(response, cb) + Metamaps.Loading.hide() }, error: function () { self.render(cb) + Metamaps.Loading.hide() } }) } 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..efa3d59f 100644 --- a/frontend/src/components/Maps/MapCard.js +++ b/frontend/src/components/Maps/MapCard.js @@ -1,8 +1,60 @@ import React, { Component, PropTypes } from 'react' +import { find, values } from 'lodash' + +const IN_CONVERSATION = 1 // shared with /realtime/reducer.js + +const MapperList = (props) => { + return
    +
  • LIVE
  • + { props.mappers.map(mapper =>
  • { mapper.username }
  • ) } +
+} + +class Menu extends Component { + + constructor(props) { + super(props) + this.state = { open: false } + } + + toggle = () => { + this.setState({ open: !this.state.open }) + return true + } + + render = () => { + const { currentUser, map, onStar, onRequest } = this.props + const style = { display: this.state.open ? 'block' : 'none' } + + return
+
+
+
+
+
+
    +
  • { this.toggle() && onStar(map) }}>Star Map
  • + { !map.authorizeToEdit(currentUser) &&
  • { this.toggle() && onRequest(map) }}>Request Access
  • } +
+
+ } +} +Menu.propTypes = { + currentUser: PropTypes.object.isRequired, + map: PropTypes.object.isRequired, + onStar: PropTypes.func.isRequired, + onRequest: PropTypes.func.isRequired +} + class MapCard extends Component { render = () => { - const { map, currentUser } = this.props + const { map, juntoState, currentUser, onRequest, onStar } = this.props + + const hasMap = juntoState.liveMaps[map.id] + const hasConversation = hasMap && find(values(hasMap), v => v === IN_CONVERSATION) + const hasMapper = hasMap && !hasConversation + const mapperList = hasMap && Object.keys(hasMap).map(id => juntoState.connectedPeople[id]) function capitalize (string) { return string.charAt(0).toUpperCase() + string.slice(1) @@ -19,49 +71,51 @@ class MapCard extends Component { return ( ) } @@ -69,7 +123,10 @@ class MapCard extends Component { MapCard.propTypes = { map: PropTypes.object.isRequired, - currentUser: PropTypes.object + juntoState: PropTypes.object, + currentUser: PropTypes.object, + onStar: PropTypes.func.isRequired, + onRequest: PropTypes.func.isRequired } export default MapCard diff --git a/frontend/src/components/Maps/index.js b/frontend/src/components/Maps/index.js index 670f5aaf..df59b446 100644 --- a/frontend/src/components/Maps/index.js +++ b/frontend/src/components/Maps/index.js @@ -1,4 +1,5 @@ import React, { Component, PropTypes } from 'react' +import { throttle } from 'lodash' import Header from './Header' import MapperCard from './MapperCard' import MapCard from './MapCard' @@ -15,19 +16,10 @@ class Maps extends Component { componentDidMount() { window && window.addEventListener('resize', this.resize) + this.refs.maps.addEventListener('scroll', throttle(this.scroll, 500, { leading: true, trailing: false })) this.resize() } - componentDidUpdate(oldProps) { - const { maps, user, currentUser } = this.props - const oldMaps = oldProps.maps - const oldUser = oldProps.user - const oldCurrentUser = oldProps.currentUser - const numCards = maps.length + (user || currentUser ? 1 : 0) - const oldNumCards = oldMaps.length + (oldUser || oldCurrentUser ? 1 : 0) - if (numCards !== oldNumCards) this.resize() - } - componentWillUnmount() { window && window.removeEventListener('resize', this.resize) } @@ -40,22 +32,26 @@ class Maps extends Component { this.setState({ mapsWidth }) } + scroll = () => { + const { loadMore, moreToLoad, pending } = this.props + const { maps } = this.refs + if (moreToLoad && !pending && maps.scrollTop + maps.offsetHeight > maps.scrollHeight - 300 ) { + loadMore() + } + } + render = () => { - const { maps, currentUser, section, user, moreToLoad, loadMore } = this.props + const { maps, currentUser, juntoState, section, user, moreToLoad, loadMore, onStar, onRequest } = this.props const style = { width: this.state.mapsWidth + 'px' } return (
-
+
{ user ? : null } { currentUser && !user ? : null } - { maps.models.map(map => ) } + { maps.models.map(map => ) }
- {!moreToLoad ? null : [ - , -
- ]}
{ - 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..9b07cfc3 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..183ee081 --- /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 Object.assign({}, 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 newMapWithoutUser = callWillFinish ? mapValues(mapWithoutUser, () => NOT_IN_CONVERSATION) : mapWithoutUser + const newLiveMapsWithoutUser = mapWillEmpty ? omit(liveMaps, payload.mapid) : Object.assign({}, liveMaps, { [payload.mapid]: newMapWithoutUser }) + return { + connectedPeople: omit(connectedPeople, payload.userid), + liveMaps: omitBy(newLiveMapsWithoutUser, 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,