From 36449cce47396e395df7dd718e69da01f872d738 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Thu, 29 Dec 2016 05:52:00 +0000 Subject: [PATCH] send events from server to client --- app/channels/map_channel.rb | 8 + app/models/map.rb | 8 + app/models/mapping.rb | 6 + app/models/message.rb | 6 + app/models/synapse.rb | 3 + app/models/topic.rb | 3 + frontend/src/Metamaps/Cable.js | 199 +++++++++++++++++-- frontend/src/Metamaps/Map/index.js | 3 + frontend/src/Metamaps/Realtime/events.js | 6 +- frontend/src/Metamaps/Realtime/index.js | 24 +-- frontend/src/Metamaps/Realtime/receivable.js | 185 +---------------- 11 files changed, 233 insertions(+), 218 deletions(-) create mode 100644 app/channels/map_channel.rb diff --git a/app/channels/map_channel.rb b/app/channels/map_channel.rb new file mode 100644 index 00000000..8f6ee77a --- /dev/null +++ b/app/channels/map_channel.rb @@ -0,0 +1,8 @@ +class MapChannel < ApplicationCable::Channel + # Called when the consumer has successfully + # become a subscriber of this channel. + def subscribed + # verify permission + stream_from "map_#{params[:id]}" + end +end \ No newline at end of file diff --git a/app/models/map.rb b/app/models/map.rb index 79b4ae35..4f86d6f8 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -33,6 +33,7 @@ class Map < ApplicationRecord # Validate the attached image is image/jpg, image/png, etc validates_attachment_content_type :screenshot, content_type: /\Aimage\/.*\Z/ + after_update :after_updated after_save :update_deferring_topics_and_synapses, if: :permission_changed? delegate :count, to: :topics, prefix: :topic # same as `def topic_count; topics.count; end` @@ -119,6 +120,13 @@ class Map < ApplicationRecord end removed.compact end + + def after_updated + attrs = ['name', 'desc', 'permission'] + if attrs.any? {|k| changed_attributes.key?(k)} + ActionCable.server.broadcast 'map_' + id.to_s, type: 'mapUpdated' + end + end def update_deferring_topics_and_synapses Topic.where(defer_to_map_id: id).update_all(permission: permission) diff --git a/app/models/mapping.rb b/app/models/mapping.rb index 99d23db0..7e694b6f 100644 --- a/app/models/mapping.rb +++ b/app/models/mapping.rb @@ -33,8 +33,10 @@ class Mapping < ApplicationRecord if mappable_type == 'Topic' meta = {'x': xloc, 'y': yloc, 'mapping_id': id} Events::TopicAddedToMap.publish!(mappable, map, user, meta) + ActionCable.server.broadcast 'map_' + map.id.to_s, type: 'topicAdded', topic: mappable.filtered elsif mappable_type == 'Synapse' Events::SynapseAddedToMap.publish!(mappable, map, user, meta) + ActionCable.server.broadcast 'map_' + map.id.to_s, type: 'synapseAdded', synapse: synapse.filtered end end @@ -42,6 +44,8 @@ class Mapping < ApplicationRecord if mappable_type == 'Topic' and (xloc_changed? or yloc_changed?) meta = {'x': xloc, 'y': yloc, 'mapping_id': id} Events::TopicMovedOnMap.publish!(mappable, map, updated_by, meta) + # should we add another actioncable event here? don't need it right now because sockets handles + # the moving/dragging of topics. it could be moved via the api or some other source though end end @@ -55,8 +59,10 @@ class Mapping < ApplicationRecord meta = {'mapping_id': id} if mappable_type == 'Topic' Events::TopicRemovedFromMap.publish!(mappable, map, updated_by, meta) + ActionCable.server.broadcast 'map_' + map.id.to_s, type: 'topicRemoved', id: mappable.id elsif mappable_type == 'Synapse' Events::SynapseRemovedFromMap.publish!(mappable, map, updated_by, meta) + ActionCable.server.broadcast 'map_' + map.id.to_s, type: 'synapseRemoved', id: mappable.id end end end diff --git a/app/models/message.rb b/app/models/message.rb index 682b7e51..e00b68ca 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -4,6 +4,8 @@ class Message < ApplicationRecord belongs_to :resource, polymorphic: true delegate :name, to: :user, prefix: true + + after_create :after_created def user_image user.image.url @@ -13,4 +15,8 @@ class Message < ApplicationRecord json = super(methods: [:user_name, :user_image]) json end + + def after_created + ActionCable.server.broadcast 'map_' + map.id.to_s, type: 'messageCreated', message: self.as_json + end end diff --git a/app/models/synapse.rb b/app/models/synapse.rb index 4f55fb51..be57dde1 100644 --- a/app/models/synapse.rb +++ b/app/models/synapse.rb @@ -59,6 +59,9 @@ class Synapse < ApplicationRecord meta = new.merge(old) # we are prioritizing the old values, keeping them meta['changed'] = changed_attributes.keys.select {|k| attrs.include?(k) } Events::SynapseUpdated.publish!(self, user, meta) + maps.each {|map| + ActionCable.server.broadcast 'map_' + map.id.to_s, type: 'synapseUpdated', id: id + } end end end diff --git a/app/models/topic.rb b/app/models/topic.rb index af88f342..90443862 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -154,6 +154,9 @@ class Topic < ApplicationRecord meta = new.merge(old) # we are prioritizing the old values, keeping them meta['changed'] = changed_attributes.keys.select {|k| attrs.include?(k) } Events::TopicUpdated.publish!(self, user, meta) + maps.each {|map| + ActionCable.server.broadcast 'map_' + map.id.to_s, type: 'topicUpdated', id: id + } end end end diff --git a/frontend/src/Metamaps/Cable.js b/frontend/src/Metamaps/Cable.js index ee279865..20d5f0ce 100644 --- a/frontend/src/Metamaps/Cable.js +++ b/frontend/src/Metamaps/Cable.js @@ -1,39 +1,204 @@ /* global $, ActionCable */ import Active from './Active' +import Control from './Control' import DataModel from './DataModel' +import Map from './Map' +import Mapper from './Mapper' +import Synapse from './Synapse' import Topic from './Topic' +import { ChatView } from './Views' const Cable = { init: () => { let self = Cable self.cable = ActionCable.createConsumer() }, - subTopic: id => { + subscribeToMap: id => { let self = Cable - self.topicSubs[id] = self.cable.subscriptions.create({ - channel: 'TopicChannel', + self.sub = self.cable.subscriptions.create({ + channel: 'MapChannel', id: id }, { - received: event => self[event.type](event.data) - }) - }, - unsubAllTopics: () => { - let self = Cable - Object.keys(self.topicSubs).forEach(id => { - self.topicSubs[id].unsubscribe() + received: event => self[event.type](event) }) - self.topicSubs = {} }, - newSynapse: data => { + unsubscribeFromMap: () => { + let self = Cable + self.sub.unsubscribe() + delete self.sub + }, + synapseAdded: event => { const m = Active.Mapper - const s = new DataModel.Synapse(data.synapse) - const t1 = new DataModel.Topic(data.topic1) - const t2 = new DataModel.Topic(data.topic2) + const s = new DataModel.Synapse(event.synapse) + const t1 = new DataModel.Topic(event.topic1) + const t2 = new DataModel.Topic(event.topic2) if (t1.authorizeToShow(m) && t2.authorizeToShow(m) && s.authorizeToShow(m)) { - Topic.fetchForTopicView(data.synapse.id) + console.log('permission to fetch new synapse') } + }, + synapseAdded2: event => { + var topic1, topic2, node1, node2, synapse, mapping, cancel, mapper + + function waitThenRenderSynapse() { + if (synapse && mapping && mapper) { + topic1 = synapse.getTopic1() + node1 = topic1.get('node') + topic2 = synapse.getTopic2() + node2 = topic2.get('node') + + Synapse.renderSynapse(mapping, synapse, node1, node2, false) + } else if (!cancel) { + setTimeout(waitThenRenderSynapse, 10) + } + } + + mapper = DataModel.Mappers.get(data.mapperid) + if (mapper === undefined) { + Mapper.get(data.mapperid, function(m) { + DataModel.Mappers.add(m) + mapper = m + }) + } + $.ajax({ + url: '/synapses/' + data.mappableid + '.json', + success: function(response) { + DataModel.Synapses.add(response) + synapse = DataModel.Synapses.get(response.id) + }, + error: function() { + cancel = true + } + }) + $.ajax({ + url: '/mappings/' + data.mappingid + '.json', + success: function(response) { + DataModel.Mappings.add(response) + mapping = DataModel.Mappings.get(response.id) + }, + error: function() { + cancel = true + } + }) + waitThenRenderSynapse() + }, + synapseUpdated: event => { + var synapse = DataModel.Synapses.get(event.id) + if (synapse) { + // edge reset necessary because fetch causes model reset + var edge = synapse.get('edge') + synapse.fetch({ + success: function(model) { + model.set({ edge: edge }) + model.trigger('changeByOther') + } + }) + } + }, + synapseRemoved: event => { + var synapse = DataModel.Synapses.get(event.id) + if (synapse) { + var edge = synapse.get('edge') + var mapping = synapse.getMapping() + if (edge.getData('mappings').length - 1 === 0) { + Control.hideEdge(edge) + } + + var index = indexOf(edge.getData('synapses'), synapse) + edge.getData('mappings').splice(index, 1) + edge.getData('synapses').splice(index, 1) + if (edge.getData('displayIndex')) { + delete edge.data.$displayIndex + } + DataModel.Synapses.remove(synapse) + DataModel.Mappings.remove(mapping) + } + }, + topicAdded: event => { + var topic, mapping, mapper, cancel + + function waitThenRenderTopic() { + if (topic && mapping && mapper) { + Topic.renderTopic(mapping, topic, false, false) + } else if (!cancel) { + setTimeout(waitThenRenderTopic, 10) + } + } + + mapper = DataModel.Mappers.get(data.mapperid) + if (mapper === undefined) { + Mapper.get(data.mapperid, function(m) { + DataModel.Mappers.add(m) + mapper = m + }) + } + $.ajax({ + url: '/topics/' + data.mappableid + '.json', + success: function(response) { + DataModel.Topics.add(response) + topic = DataModel.Topics.get(response.id) + }, + error: function() { + cancel = true + } + }) + $.ajax({ + url: '/mappings/' + data.mappingid + '.json', + success: function(response) { + DataModel.Mappings.add(response) + mapping = DataModel.Mappings.get(response.id) + }, + error: function() { + cancel = true + } + }) + + waitThenRenderTopic() + }, + topicUpdated: event => { + var topic = DataModel.Topics.get(event.id) + if (topic) { + var node = topic.get('node') + topic.fetch({ + success: function(model) { + model.set({ node: node }) + model.trigger('changeByOther') + } + }) + } + }, + topicRemoved: event => { + var topic = DataModel.Topics.get(event.id) + if (topic) { + var node = topic.get('node') + var mapping = topic.getMapping() + Control.hideNode(node.id) + DataModel.Topics.remove(topic) + DataModel.Mappings.remove(mapping) + } + }, + messageCreated: event => { + ChatView.addMessages(new DataModel.MessageCollection(event.message)) + }, + mapUpdated: event => { + var map = Active.Map + var couldEditBefore = map.authorizeToEdit(Active.Mapper) + var idBefore = map.id + map.fetch({ + success: function(model, response) { + var idNow = model.id + var canEditNow = model.authorizeToEdit(Active.Mapper) + if (idNow !== idBefore) { + Map.leavePrivateMap() // this means the map has been changed to private + } else if (couldEditBefore && !canEditNow) { + Map.cantEditNow() + } else if (!couldEditBefore && canEditNow) { + Map.canEditNow() + } else { + model.trigger('changeByOther') + } + } + }) } -} export default Cable diff --git a/frontend/src/Metamaps/Map/index.js b/frontend/src/Metamaps/Map/index.js index 51038b87..648d70b9 100644 --- a/frontend/src/Metamaps/Map/index.js +++ b/frontend/src/Metamaps/Map/index.js @@ -5,6 +5,7 @@ import { find as _find } from 'lodash' import Active from '../Active' import AutoLayout from '../AutoLayout' +import Cable from '../Cable' import Create from '../Create' import DataModel from '../DataModel' import DataModelMap from '../DataModel/Map' @@ -124,6 +125,7 @@ const Map = { Filter.checkMappers() Realtime.startActiveMap() + Cable.subscribeToMap(id) Loading.hide() // for mobile @@ -148,6 +150,7 @@ const Map = { Filter.close() InfoBox.close() Realtime.endActiveMap() + Cable.unsubscribeFromMap() $('.viewOnly').removeClass('isViewOnly') } }, diff --git a/frontend/src/Metamaps/Realtime/events.js b/frontend/src/Metamaps/Realtime/events.js index 8121a626..84295093 100644 --- a/frontend/src/Metamaps/Realtime/events.js +++ b/frontend/src/Metamaps/Realtime/events.js @@ -33,13 +33,11 @@ module.exports = { /* EVENTS RECEIVABLE FROM RAILS SERVER THROUGH ACTIONCABLE */ MESSAGE_CREATED: 'MESSAGE_CREATED', - TOPIC_CREATED: 'TOPIC_CREATED', + TOPIC_ADDED: 'TOPIC_ADDED', TOPIC_UPDATED: 'TOPIC_UPDATED', TOPIC_REMOVED: 'TOPIC_REMOVED', - TOPIC_DELETED: 'TOPIC_DELETED', - SYNAPSE_CREATED: 'SYNAPSE_CREATED', + SYNAPSE_ADDED: 'SYNAPSE_ADDED', SYNAPSE_UPDATED: 'SYNAPSE_UPDATED', SYNAPSE_REMOVED: 'SYNAPSE_REMOVED', - SYNAPSE_DELETED: 'SYNAPSE_DELETED', MAP_UPDATED: 'MAP_UPDATED' } diff --git a/frontend/src/Metamaps/Realtime/index.js b/frontend/src/Metamaps/Realtime/index.js index dcf44852..2c7d5b3e 100644 --- a/frontend/src/Metamaps/Realtime/index.js +++ b/frontend/src/Metamaps/Realtime/index.js @@ -26,16 +26,15 @@ import { NEW_MAPPER, LOST_MAPPER, PEER_COORDS_UPDATED, - MESSAGE_CREATED, TOPIC_DRAGGED, - TOPIC_CREATED, + + MESSAGE_CREATED, + TOPIC_ADDED, TOPIC_UPDATED, TOPIC_REMOVED, - TOPIC_DELETED, - SYNAPSE_CREATED, + SYNAPSE_ADDED, SYNAPSE_UPDATED, SYNAPSE_REMOVED, - SYNAPSE_DELETED, MAP_UPDATED } from './events' @@ -54,16 +53,15 @@ import { peerCoordsUpdated, newMapper, lostMapper, - messageCreated, topicDragged, - topicCreated, + + messageCreated, + topicAdded, topicUpdated, topicRemoved, - topicDeleted, - synapseCreated, + synapseAdded, synapseUpdated, synapseRemoved, - synapseDeleted, mapUpdated } from './receivable' @@ -479,14 +477,12 @@ const subscribeToEvents = (Realtime, socket) => { /* socket.on(MESSAGE_CREATED, messageCreated(Realtime)) - socket.on(TOPIC_CREATED, topicCreated(Realtime)) + socket.on(TOPIC_ADDED, topicAdded(Realtime)) socket.on(TOPIC_UPDATED, topicUpdated(Realtime)) socket.on(TOPIC_REMOVED, topicRemoved(Realtime)) - socket.on(TOPIC_DELETED, topicDeleted(Realtime)) - socket.on(SYNAPSE_CREATED, synapseCreated(Realtime)) + socket.on(SYNAPSE_ADDED, synapseAdded(Realtime)) socket.on(SYNAPSE_UPDATED, synapseUpdated(Realtime)) socket.on(SYNAPSE_REMOVED, synapseRemoved(Realtime)) - socket.on(SYNAPSE_DELETED, synapseDeleted(Realtime)) socket.on(MAP_UPDATED, mapUpdated(Realtime)) */ } diff --git a/frontend/src/Metamaps/Realtime/receivable.js b/frontend/src/Metamaps/Realtime/receivable.js index b0a8ddb4..d37f1b6f 100644 --- a/frontend/src/Metamaps/Realtime/receivable.js +++ b/frontend/src/Metamaps/Realtime/receivable.js @@ -13,7 +13,6 @@ import { ChatView } from '../Views' import DataModel from '../DataModel' import GlobalUI from '../GlobalUI' import Control from '../Control' -import Map from '../Map' import Mapper from '../Mapper' import Topic from '../Topic' import Synapse from '../Synapse' @@ -25,188 +24,8 @@ export const juntoUpdated = self => state => { $(document).trigger(JUNTO_UPDATED) } -export const synapseRemoved = self => data => { - var synapse = DataModel.Synapses.get(data.mappableid) - if (synapse) { - var edge = synapse.get('edge') - var mapping = synapse.getMapping() - if (edge.getData('mappings').length - 1 === 0) { - Control.hideEdge(edge) - } - - var index = indexOf(edge.getData('synapses'), synapse) - edge.getData('mappings').splice(index, 1) - edge.getData('synapses').splice(index, 1) - if (edge.getData('displayIndex')) { - delete edge.data.$displayIndex - } - DataModel.Synapses.remove(synapse) - DataModel.Mappings.remove(mapping) - } -} - -export const synapseDeleted = self => data => { - synapseRemoved(self)(data) -} - -export const synapseCreated = self => data => { - var topic1, topic2, node1, node2, synapse, mapping, cancel, mapper - - function waitThenRenderSynapse() { - if (synapse && mapping && mapper) { - topic1 = synapse.getTopic1() - node1 = topic1.get('node') - topic2 = synapse.getTopic2() - node2 = topic2.get('node') - - Synapse.renderSynapse(mapping, synapse, node1, node2, false) - } else if (!cancel) { - setTimeout(waitThenRenderSynapse, 10) - } - } - - mapper = DataModel.Mappers.get(data.mapperid) - if (mapper === undefined) { - Mapper.get(data.mapperid, function(m) { - DataModel.Mappers.add(m) - mapper = m - }) - } - $.ajax({ - url: '/synapses/' + data.mappableid + '.json', - success: function(response) { - DataModel.Synapses.add(response) - synapse = DataModel.Synapses.get(response.id) - }, - error: function() { - cancel = true - } - }) - $.ajax({ - url: '/mappings/' + data.mappingid + '.json', - success: function(response) { - DataModel.Mappings.add(response) - mapping = DataModel.Mappings.get(response.id) - }, - error: function() { - cancel = true - } - }) - waitThenRenderSynapse() -} - -export const topicRemoved = self => data => { - var topic = DataModel.Topics.get(data.mappableid) - if (topic) { - var node = topic.get('node') - var mapping = topic.getMapping() - Control.hideNode(node.id) - DataModel.Topics.remove(topic) - DataModel.Mappings.remove(mapping) - } -} - -export const topicDeleted = self => data => { - topicRemoved(self)(data) -} - -export const topicCreated = self => data => { - var topic, mapping, mapper, cancel - - function waitThenRenderTopic() { - if (topic && mapping && mapper) { - Topic.renderTopic(mapping, topic, false, false) - } else if (!cancel) { - setTimeout(waitThenRenderTopic, 10) - } - } - - mapper = DataModel.Mappers.get(data.mapperid) - if (mapper === undefined) { - Mapper.get(data.mapperid, function(m) { - DataModel.Mappers.add(m) - mapper = m - }) - } - $.ajax({ - url: '/topics/' + data.mappableid + '.json', - success: function(response) { - DataModel.Topics.add(response) - topic = DataModel.Topics.get(response.id) - }, - error: function() { - cancel = true - } - }) - $.ajax({ - url: '/mappings/' + data.mappingid + '.json', - success: function(response) { - DataModel.Mappings.add(response) - mapping = DataModel.Mappings.get(response.id) - }, - error: function() { - cancel = true - } - }) - - waitThenRenderTopic() -} - -export const messageCreated = self => data => { - ChatView.addMessages(new DataModel.MessageCollection(data)) -} - -export const mapUpdated = self => data => { - var map = Active.Map - var isActiveMap = map && data.mapId === map.id - if (isActiveMap) { - var couldEditBefore = map.authorizeToEdit(Active.Mapper) - var idBefore = map.id - map.fetch({ - success: function(model, response) { - var idNow = model.id - var canEditNow = model.authorizeToEdit(Active.Mapper) - if (idNow !== idBefore) { - Map.leavePrivateMap() // this means the map has been changed to private - } else if (couldEditBefore && !canEditNow) { - Map.cantEditNow() - } else if (!couldEditBefore && canEditNow) { - Map.canEditNow() - } else { - model.trigger('changeByOther') - } - } - }) - } -} - -export const topicUpdated = self => data => { - var topic = DataModel.Topics.get(data.topicId) - if (topic) { - var node = topic.get('node') - topic.fetch({ - success: function(model) { - model.set({ node: node }) - model.trigger('changeByOther') - } - }) - } -} - -export const synapseUpdated = self => data => { - var synapse = DataModel.Synapses.get(data.synapseId) - if (synapse) { - // edge reset necessary because fetch causes model reset - var edge = synapse.get('edge') - synapse.fetch({ - success: function(model) { - model.set({ edge: edge }) - model.trigger('changeByOther') - } - }) - } -} - +/* All the following events are received through the nodejs realtime server + and are done this way because they are transient data, not persisted to the server */ export const topicDragged = self => positions => { var topic var node