Start real-time collaboration for wysiwyg; flitter sockets
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
parent
157283a1cc
commit
282331d788
@ -0,0 +1,264 @@
|
||||
const SocketController = require('flitter-socket/Controller')
|
||||
const uuid = require('uuid').v4
|
||||
|
||||
class NormEditorGroup {
|
||||
constructor(page, node) {
|
||||
this.id = `${page.UUID}-${node.UUID}`
|
||||
this.page = page
|
||||
this.node = node
|
||||
this.connections = {}
|
||||
this.connection_id_x_session = {}
|
||||
}
|
||||
|
||||
join(connection_manager) {
|
||||
this.connections[connection_manager.id] = connection_manager
|
||||
this.connection_id_x_session[connection_manager.id] = {}
|
||||
|
||||
connection_manager.on_close().then(() => {
|
||||
delete this.connection_id_x_session[connection_manager.id]
|
||||
delete this.connections[connection_manager.id]
|
||||
this.blast_editor_group_users()
|
||||
})
|
||||
}
|
||||
|
||||
has(connection_manager) {
|
||||
return !!this.connections[connection_manager.id]
|
||||
}
|
||||
|
||||
unique_users() {
|
||||
const users = {}
|
||||
const conns = Object.values(this.connections)
|
||||
|
||||
for ( const conn of conns ) {
|
||||
const session = this.session(conn)
|
||||
const user = conn.request.user
|
||||
if ( user && !users[user.uuid] ) {
|
||||
if ( !session.user_color ) {
|
||||
session.user_color = '#' + Math.floor(Math.random() * 16777215).toString(16)
|
||||
}
|
||||
|
||||
users[user.uuid] = { user, color: session.user_color }
|
||||
}
|
||||
}
|
||||
|
||||
return Object.values(users)
|
||||
}
|
||||
|
||||
session(conn) {
|
||||
return this.connection_id_x_session[conn.id]
|
||||
}
|
||||
|
||||
blast_editor_group_users() {
|
||||
const conns = Object.values(this.connections)
|
||||
const unique_users = this.unique_users()
|
||||
|
||||
for ( const conn of conns ) {
|
||||
const users = unique_users.filter(data => data.user.uuid !== conn.request.user.uuid)
|
||||
.map(data => {
|
||||
const { user, color } = data
|
||||
return {
|
||||
uuid: user.uuid,
|
||||
uid: user.uid,
|
||||
display: user.uid,
|
||||
color,
|
||||
}
|
||||
})
|
||||
|
||||
conn._request('setEditorGroupUsers', { users })
|
||||
}
|
||||
}
|
||||
|
||||
blast_selections() {
|
||||
const conns = Object.values(this.connections)
|
||||
|
||||
for ( const conn of conns ) {
|
||||
const selections = conns.filter(maybeConn => conn.id !== maybeConn.id)
|
||||
.map(otherConn => {
|
||||
const session = this.session(otherConn)
|
||||
const user_data = {
|
||||
uuid: otherConn.request.user.uuid,
|
||||
uid: otherConn.request.user.uid,
|
||||
display: otherConn.request.user.uid,
|
||||
}
|
||||
|
||||
if ( session.user_color ) {
|
||||
user_data.color = session.user_color
|
||||
}
|
||||
|
||||
return {...(this.session(otherConn).selection || {}), user_data}
|
||||
})
|
||||
.filter(Boolean)
|
||||
|
||||
conn._request('setEditorGroupSelections', { selections })
|
||||
}
|
||||
}
|
||||
|
||||
set_member_selection(conn, selection) {
|
||||
const session = this.session(conn)
|
||||
if ( session ) {
|
||||
session.selection = selection
|
||||
this.blast_selections()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NormEditorController extends SocketController {
|
||||
editor_groups = {}
|
||||
|
||||
static get services() {
|
||||
return [...super.services, 'output', 'models']
|
||||
}
|
||||
|
||||
ping(transaction, socket) {
|
||||
this.output.info('Got norm editor socket ping.')
|
||||
|
||||
transaction.status(200)
|
||||
.message('Pong!')
|
||||
.send(transaction.incoming)
|
||||
}
|
||||
|
||||
_get_editor_group_or_fail(transaction) {
|
||||
if ( !transaction.incoming.editor_group_id ) {
|
||||
transaction.status(400)
|
||||
.message('Missing editor_group_id.')
|
||||
.send()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const editor_group = this.editor_groups[transaction.incoming.editor_group_id]
|
||||
if ( !editor_group || !editor_group.has(transaction.cm) ) {
|
||||
transaction.status(400)
|
||||
.message('Invalid editor_group_id.')
|
||||
.send()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
return editor_group
|
||||
}
|
||||
|
||||
set_member_selection(transaction, socket) {
|
||||
const editor_group = this._get_editor_group_or_fail(transaction)
|
||||
if ( !editor_group ) return;
|
||||
|
||||
if ( !transaction.incoming.selection ) {
|
||||
return transaction.status(400)
|
||||
.message('Missing selection.')
|
||||
.send()
|
||||
}
|
||||
|
||||
editor_group.set_member_selection(transaction.cm, transaction.incoming.selection)
|
||||
transaction.status(200).send()
|
||||
}
|
||||
|
||||
get_selections(transaction, socket) {
|
||||
const editor_group = this._get_editor_group_or_fail(transaction)
|
||||
if ( !editor_group ) return;
|
||||
|
||||
const selections = Object.values(editor_group.connections)
|
||||
.filter(maybeConn => transaction.cm.id !== maybeConn.id)
|
||||
.map(otherConn => {
|
||||
const session = editor_group.session(otherConn)
|
||||
const user_data = {
|
||||
uuid: otherConn.request.user.uuid,
|
||||
uid: otherConn.request.user.uid,
|
||||
display: otherConn.request.user.uid,
|
||||
}
|
||||
|
||||
if ( session.user_color ) {
|
||||
user_data.color = session.user_color
|
||||
}
|
||||
|
||||
return {...(editor_group.session(otherConn).selection || {}), user_data}
|
||||
})
|
||||
.filter(Boolean)
|
||||
|
||||
transaction.status(200)
|
||||
.send({ selections })
|
||||
}
|
||||
|
||||
get_editor_group_users(transaction, socket) {
|
||||
const editor_group = this._get_editor_group_or_fail(transaction)
|
||||
if ( !editor_group ) return;
|
||||
|
||||
transaction.status(200)
|
||||
.send(
|
||||
editor_group.unique_users()
|
||||
.filter(data => data.user.uuid !== transaction.cm.request.user.uuid)
|
||||
.map(data => {
|
||||
const { user, color } = data
|
||||
return {
|
||||
uuid: user.uuid,
|
||||
uid: user.uid,
|
||||
display: user.uid,
|
||||
color,
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
async join_editor_group(transaction, socket) {
|
||||
// FIXME support versioning
|
||||
const Page = this.models.get('api:Page')
|
||||
const Node = this.models.get('api:Node')
|
||||
|
||||
if ( !transaction.incoming.PageId ) {
|
||||
return transaction.status(400)
|
||||
.message('Missing pageId.')
|
||||
.send()
|
||||
}
|
||||
|
||||
const page = await Page.findOne({ UUID: transaction.incoming.PageId, Active: true })
|
||||
if ( !page || !(await page.is_accessible_by(transaction.cm.request.user, 'update')) ) {
|
||||
return transaction.status(401)
|
||||
.message('Invalid PageId.')
|
||||
.send()
|
||||
}
|
||||
|
||||
if ( !transaction.incoming.NodeId ) {
|
||||
transaction.status(400)
|
||||
.message('Missing NodeId.')
|
||||
.send()
|
||||
}
|
||||
|
||||
const node = await Node.findOne({ UUID: transaction.incoming.NodeId, PageId: page.UUID })
|
||||
if ( !node ) {
|
||||
transaction.status(400)
|
||||
.message('Invalid NodeId.')
|
||||
.send()
|
||||
}
|
||||
|
||||
// Try to look up an existing editor group
|
||||
const editor_group_id = `${page.UUID}-${node.UUID}`
|
||||
let editor_group = this.editor_groups[editor_group_id]
|
||||
|
||||
if ( !editor_group ) {
|
||||
// create a new editor group
|
||||
editor_group = new NormEditorGroup(page, node)
|
||||
this.editor_groups[editor_group_id] = editor_group
|
||||
}
|
||||
|
||||
if ( !editor_group.has(transaction.cm) )
|
||||
editor_group.join(transaction.cm)
|
||||
|
||||
transaction.status(200)
|
||||
.message('Joined editor group.')
|
||||
.send({ editor_group_id })
|
||||
|
||||
editor_group.blast_editor_group_users()
|
||||
}
|
||||
|
||||
_client_connected(connection_manager) {
|
||||
super._client_connected(connection_manager)
|
||||
|
||||
this.output.debug('New client connected to norm editor socket.')
|
||||
|
||||
if ( !connection_manager.request?.session?.auth?.user_id ) {
|
||||
delete this.connections[connection_manager.id]
|
||||
connection_manager.socket.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = NormEditorController
|
@ -0,0 +1,14 @@
|
||||
module.exports = exports = {
|
||||
prefix: '/api/v1/socket',
|
||||
|
||||
middleware: [],
|
||||
|
||||
socket: {
|
||||
'/norm-editor': [
|
||||
'controller::socket:NormEditor',
|
||||
],
|
||||
'/norm-editor/.websocket': [
|
||||
'controller::socket:NormEditor',
|
||||
],
|
||||
}
|
||||
}
|
Loading…
Reference in new issue