send events from server to client

This commit is contained in:
Connor Turland 2016-12-29 05:52:00 +00:00
parent 829e3ad29b
commit 36449cce47
11 changed files with 233 additions and 218 deletions

View file

@ -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

View file

@ -33,6 +33,7 @@ class Map < ApplicationRecord
# Validate the attached image is image/jpg, image/png, etc # Validate the attached image is image/jpg, image/png, etc
validates_attachment_content_type :screenshot, content_type: /\Aimage\/.*\Z/ validates_attachment_content_type :screenshot, content_type: /\Aimage\/.*\Z/
after_update :after_updated
after_save :update_deferring_topics_and_synapses, if: :permission_changed? after_save :update_deferring_topics_and_synapses, if: :permission_changed?
delegate :count, to: :topics, prefix: :topic # same as `def topic_count; topics.count; end` delegate :count, to: :topics, prefix: :topic # same as `def topic_count; topics.count; end`
@ -119,6 +120,13 @@ class Map < ApplicationRecord
end end
removed.compact removed.compact
end 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 def update_deferring_topics_and_synapses
Topic.where(defer_to_map_id: id).update_all(permission: permission) Topic.where(defer_to_map_id: id).update_all(permission: permission)

View file

@ -33,8 +33,10 @@ class Mapping < ApplicationRecord
if mappable_type == 'Topic' if mappable_type == 'Topic'
meta = {'x': xloc, 'y': yloc, 'mapping_id': id} meta = {'x': xloc, 'y': yloc, 'mapping_id': id}
Events::TopicAddedToMap.publish!(mappable, map, user, meta) Events::TopicAddedToMap.publish!(mappable, map, user, meta)
ActionCable.server.broadcast 'map_' + map.id.to_s, type: 'topicAdded', topic: mappable.filtered
elsif mappable_type == 'Synapse' elsif mappable_type == 'Synapse'
Events::SynapseAddedToMap.publish!(mappable, map, user, meta) Events::SynapseAddedToMap.publish!(mappable, map, user, meta)
ActionCable.server.broadcast 'map_' + map.id.to_s, type: 'synapseAdded', synapse: synapse.filtered
end end
end end
@ -42,6 +44,8 @@ class Mapping < ApplicationRecord
if mappable_type == 'Topic' and (xloc_changed? or yloc_changed?) if mappable_type == 'Topic' and (xloc_changed? or yloc_changed?)
meta = {'x': xloc, 'y': yloc, 'mapping_id': id} meta = {'x': xloc, 'y': yloc, 'mapping_id': id}
Events::TopicMovedOnMap.publish!(mappable, map, updated_by, meta) 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
end end
@ -55,8 +59,10 @@ class Mapping < ApplicationRecord
meta = {'mapping_id': id} meta = {'mapping_id': id}
if mappable_type == 'Topic' if mappable_type == 'Topic'
Events::TopicRemovedFromMap.publish!(mappable, map, updated_by, meta) 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' elsif mappable_type == 'Synapse'
Events::SynapseRemovedFromMap.publish!(mappable, map, updated_by, meta) Events::SynapseRemovedFromMap.publish!(mappable, map, updated_by, meta)
ActionCable.server.broadcast 'map_' + map.id.to_s, type: 'synapseRemoved', id: mappable.id
end end
end end
end end

View file

@ -4,6 +4,8 @@ class Message < ApplicationRecord
belongs_to :resource, polymorphic: true belongs_to :resource, polymorphic: true
delegate :name, to: :user, prefix: true delegate :name, to: :user, prefix: true
after_create :after_created
def user_image def user_image
user.image.url user.image.url
@ -13,4 +15,8 @@ class Message < ApplicationRecord
json = super(methods: [:user_name, :user_image]) json = super(methods: [:user_name, :user_image])
json json
end end
def after_created
ActionCable.server.broadcast 'map_' + map.id.to_s, type: 'messageCreated', message: self.as_json
end
end end

View file

@ -59,6 +59,9 @@ class Synapse < ApplicationRecord
meta = new.merge(old) # we are prioritizing the old values, keeping them meta = new.merge(old) # we are prioritizing the old values, keeping them
meta['changed'] = changed_attributes.keys.select {|k| attrs.include?(k) } meta['changed'] = changed_attributes.keys.select {|k| attrs.include?(k) }
Events::SynapseUpdated.publish!(self, user, meta) Events::SynapseUpdated.publish!(self, user, meta)
maps.each {|map|
ActionCable.server.broadcast 'map_' + map.id.to_s, type: 'synapseUpdated', id: id
}
end end
end end
end end

View file

@ -154,6 +154,9 @@ class Topic < ApplicationRecord
meta = new.merge(old) # we are prioritizing the old values, keeping them meta = new.merge(old) # we are prioritizing the old values, keeping them
meta['changed'] = changed_attributes.keys.select {|k| attrs.include?(k) } meta['changed'] = changed_attributes.keys.select {|k| attrs.include?(k) }
Events::TopicUpdated.publish!(self, user, meta) Events::TopicUpdated.publish!(self, user, meta)
maps.each {|map|
ActionCable.server.broadcast 'map_' + map.id.to_s, type: 'topicUpdated', id: id
}
end end
end end
end end

View file

@ -1,39 +1,204 @@
/* global $, ActionCable */ /* global $, ActionCable */
import Active from './Active' import Active from './Active'
import Control from './Control'
import DataModel from './DataModel' import DataModel from './DataModel'
import Map from './Map'
import Mapper from './Mapper'
import Synapse from './Synapse'
import Topic from './Topic' import Topic from './Topic'
import { ChatView } from './Views'
const Cable = { const Cable = {
init: () => { init: () => {
let self = Cable let self = Cable
self.cable = ActionCable.createConsumer() self.cable = ActionCable.createConsumer()
}, },
subTopic: id => { subscribeToMap: id => {
let self = Cable let self = Cable
self.topicSubs[id] = self.cable.subscriptions.create({ self.sub = self.cable.subscriptions.create({
channel: 'TopicChannel', channel: 'MapChannel',
id: id id: id
}, { }, {
received: event => self[event.type](event.data) received: event => self[event.type](event)
})
},
unsubAllTopics: () => {
let self = Cable
Object.keys(self.topicSubs).forEach(id => {
self.topicSubs[id].unsubscribe()
}) })
self.topicSubs = {}
}, },
newSynapse: data => { unsubscribeFromMap: () => {
let self = Cable
self.sub.unsubscribe()
delete self.sub
},
synapseAdded: event => {
const m = Active.Mapper const m = Active.Mapper
const s = new DataModel.Synapse(data.synapse) const s = new DataModel.Synapse(event.synapse)
const t1 = new DataModel.Topic(data.topic1) const t1 = new DataModel.Topic(event.topic1)
const t2 = new DataModel.Topic(data.topic2) const t2 = new DataModel.Topic(event.topic2)
if (t1.authorizeToShow(m) && t2.authorizeToShow(m) && s.authorizeToShow(m)) { 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 export default Cable

View file

@ -5,6 +5,7 @@ import { find as _find } from 'lodash'
import Active from '../Active' import Active from '../Active'
import AutoLayout from '../AutoLayout' import AutoLayout from '../AutoLayout'
import Cable from '../Cable'
import Create from '../Create' import Create from '../Create'
import DataModel from '../DataModel' import DataModel from '../DataModel'
import DataModelMap from '../DataModel/Map' import DataModelMap from '../DataModel/Map'
@ -124,6 +125,7 @@ const Map = {
Filter.checkMappers() Filter.checkMappers()
Realtime.startActiveMap() Realtime.startActiveMap()
Cable.subscribeToMap(id)
Loading.hide() Loading.hide()
// for mobile // for mobile
@ -148,6 +150,7 @@ const Map = {
Filter.close() Filter.close()
InfoBox.close() InfoBox.close()
Realtime.endActiveMap() Realtime.endActiveMap()
Cable.unsubscribeFromMap()
$('.viewOnly').removeClass('isViewOnly') $('.viewOnly').removeClass('isViewOnly')
} }
}, },

View file

@ -33,13 +33,11 @@ module.exports = {
/* EVENTS RECEIVABLE FROM RAILS SERVER THROUGH ACTIONCABLE */ /* EVENTS RECEIVABLE FROM RAILS SERVER THROUGH ACTIONCABLE */
MESSAGE_CREATED: 'MESSAGE_CREATED', MESSAGE_CREATED: 'MESSAGE_CREATED',
TOPIC_CREATED: 'TOPIC_CREATED', TOPIC_ADDED: 'TOPIC_ADDED',
TOPIC_UPDATED: 'TOPIC_UPDATED', TOPIC_UPDATED: 'TOPIC_UPDATED',
TOPIC_REMOVED: 'TOPIC_REMOVED', TOPIC_REMOVED: 'TOPIC_REMOVED',
TOPIC_DELETED: 'TOPIC_DELETED', SYNAPSE_ADDED: 'SYNAPSE_ADDED',
SYNAPSE_CREATED: 'SYNAPSE_CREATED',
SYNAPSE_UPDATED: 'SYNAPSE_UPDATED', SYNAPSE_UPDATED: 'SYNAPSE_UPDATED',
SYNAPSE_REMOVED: 'SYNAPSE_REMOVED', SYNAPSE_REMOVED: 'SYNAPSE_REMOVED',
SYNAPSE_DELETED: 'SYNAPSE_DELETED',
MAP_UPDATED: 'MAP_UPDATED' MAP_UPDATED: 'MAP_UPDATED'
} }

View file

@ -26,16 +26,15 @@ import {
NEW_MAPPER, NEW_MAPPER,
LOST_MAPPER, LOST_MAPPER,
PEER_COORDS_UPDATED, PEER_COORDS_UPDATED,
MESSAGE_CREATED,
TOPIC_DRAGGED, TOPIC_DRAGGED,
TOPIC_CREATED,
MESSAGE_CREATED,
TOPIC_ADDED,
TOPIC_UPDATED, TOPIC_UPDATED,
TOPIC_REMOVED, TOPIC_REMOVED,
TOPIC_DELETED, SYNAPSE_ADDED,
SYNAPSE_CREATED,
SYNAPSE_UPDATED, SYNAPSE_UPDATED,
SYNAPSE_REMOVED, SYNAPSE_REMOVED,
SYNAPSE_DELETED,
MAP_UPDATED MAP_UPDATED
} from './events' } from './events'
@ -54,16 +53,15 @@ import {
peerCoordsUpdated, peerCoordsUpdated,
newMapper, newMapper,
lostMapper, lostMapper,
messageCreated,
topicDragged, topicDragged,
topicCreated,
messageCreated,
topicAdded,
topicUpdated, topicUpdated,
topicRemoved, topicRemoved,
topicDeleted, synapseAdded,
synapseCreated,
synapseUpdated, synapseUpdated,
synapseRemoved, synapseRemoved,
synapseDeleted,
mapUpdated mapUpdated
} from './receivable' } from './receivable'
@ -479,14 +477,12 @@ const subscribeToEvents = (Realtime, socket) => {
/* /*
socket.on(MESSAGE_CREATED, messageCreated(Realtime)) 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_UPDATED, topicUpdated(Realtime))
socket.on(TOPIC_REMOVED, topicRemoved(Realtime)) socket.on(TOPIC_REMOVED, topicRemoved(Realtime))
socket.on(TOPIC_DELETED, topicDeleted(Realtime)) socket.on(SYNAPSE_ADDED, synapseAdded(Realtime))
socket.on(SYNAPSE_CREATED, synapseCreated(Realtime))
socket.on(SYNAPSE_UPDATED, synapseUpdated(Realtime)) socket.on(SYNAPSE_UPDATED, synapseUpdated(Realtime))
socket.on(SYNAPSE_REMOVED, synapseRemoved(Realtime)) socket.on(SYNAPSE_REMOVED, synapseRemoved(Realtime))
socket.on(SYNAPSE_DELETED, synapseDeleted(Realtime))
socket.on(MAP_UPDATED, mapUpdated(Realtime)) socket.on(MAP_UPDATED, mapUpdated(Realtime))
*/ */
} }

View file

@ -13,7 +13,6 @@ import { ChatView } from '../Views'
import DataModel from '../DataModel' import DataModel from '../DataModel'
import GlobalUI from '../GlobalUI' import GlobalUI from '../GlobalUI'
import Control from '../Control' import Control from '../Control'
import Map from '../Map'
import Mapper from '../Mapper' import Mapper from '../Mapper'
import Topic from '../Topic' import Topic from '../Topic'
import Synapse from '../Synapse' import Synapse from '../Synapse'
@ -25,188 +24,8 @@ export const juntoUpdated = self => state => {
$(document).trigger(JUNTO_UPDATED) $(document).trigger(JUNTO_UPDATED)
} }
export const synapseRemoved = self => data => { /* All the following events are received through the nodejs realtime server
var synapse = DataModel.Synapses.get(data.mappableid) and are done this way because they are transient data, not persisted to the server */
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')
}
})
}
}
export const topicDragged = self => positions => { export const topicDragged = self => positions => {
var topic var topic
var node var node