mirror of
				https://github.com/node-red/node-red.git
				synced 2025-03-01 10:36:34 +00:00 
			
		
		
		
	Add user presence indication to tabs and nodes
This commit is contained in:
		| @@ -1,114 +1,92 @@ | ||||
| RED.multiplayer = (function () { | ||||
|  | ||||
|     // sessionId - used to identify sessions across websocket reconnects | ||||
|     let sessionId | ||||
|     // activeSessionId - used to identify sessions across websocket reconnects | ||||
|     let activeSessionId | ||||
|  | ||||
|     let headerWidget | ||||
|     // Map of session id to { session:'', user:{}, location:{}} | ||||
|     let connections = {} | ||||
|     // Map of username to { user:{}, connections:[] } | ||||
|     let sessions = {} | ||||
|     // Map of username to { user:{}, sessions:[] } | ||||
|     let users = {} | ||||
|  | ||||
|     function addUserConnection (connection) { | ||||
|         if (connections[connection.session]) { | ||||
|     function addUserSession (session) { | ||||
|         if (sessions[session.session]) { | ||||
|             // This is an existing connection that has been authenticated | ||||
|             const existingConnection = connections[connection.session] | ||||
|             if (existingConnection.user.username !== connection.user.username) { | ||||
|                 removeUserButton(users[existingConnection.user.username]) | ||||
|             const existingSession = sessions[session.session] | ||||
|             if (existingSession.user.username !== session.user.username) { | ||||
|                 removeUserHeaderButton(users[existingSession.user.username]) | ||||
|             } | ||||
|         } | ||||
|         connections[connection.session] = connection | ||||
|         const user = users[connection.user.username] = users[connection.user.username] || { | ||||
|             user: connection.user, | ||||
|             connections: [] | ||||
|         sessions[session.session] = session | ||||
|         const user = users[session.user.username] = users[session.user.username] || { | ||||
|             user: session.user, | ||||
|             sessions: [] | ||||
|         } | ||||
|         connection.location = connection.location || {} | ||||
|         user.connections.push(connection) | ||||
|         if (session.user.profileColor === undefined) { | ||||
|             session.user.profileColor = (1 + Math.floor(Math.random() * 5)) | ||||
|         } | ||||
|         session.location = session.location || {} | ||||
|         user.sessions.push(session) | ||||
|  | ||||
|         if (connection.user.username === RED.settings.user?.username || | ||||
|             connection.session === sessionId | ||||
|         ) { | ||||
|             // This is the current user - do not add a extra button for them | ||||
|         if (session.session === activeSessionId) { | ||||
|             // This is the current user session - do not add a extra button for them | ||||
|         } else { | ||||
|             if (user.connections.length === 1) { | ||||
|             if (user.sessions.length === 1) { | ||||
|                 if (user.button) { | ||||
|                     clearTimeout(user.inactiveTimeout) | ||||
|                     clearTimeout(user.removeTimeout) | ||||
|                     user.button.removeClass('inactive') | ||||
|                 } else { | ||||
|                     addUserButton(user) | ||||
|                     addUserHeaderButton(user) | ||||
|                 } | ||||
|             } | ||||
|             sessions[session.session].location = session.location | ||||
|             updateUserLocation(session.session) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function removeUserConnection (session, isDisconnected) { | ||||
|         const connection = connections[session] | ||||
|         delete connections[session] | ||||
|         const user = users[connection.user.username] | ||||
|         const i = user.connections.indexOf(connection) | ||||
|         user.connections.splice(i, 1) | ||||
|     function removeUserSession (sessionId, isDisconnected) { | ||||
|         removeUserLocation(sessionId) | ||||
|         const session = sessions[sessionId] | ||||
|         delete sessions[sessionId] | ||||
|         const user = users[session.user.username] | ||||
|         const i = user.sessions.indexOf(session) | ||||
|         user.sessions.splice(i, 1) | ||||
|         if (isDisconnected) { | ||||
|             removeUserButton(user) | ||||
|             removeUserHeaderButton(user) | ||||
|         } else { | ||||
|             if (user.connections.length === 0) { | ||||
|             if (user.sessions.length === 0) { | ||||
|                 // Give the user 5s to reconnect before marking inactive | ||||
|                 user.inactiveTimeout = setTimeout(() => { | ||||
|                     user.button.addClass('inactive') | ||||
|                     // Give the user further 20 seconds to reconnect before removing them | ||||
|                     // from the user toolbar entirely | ||||
|                     user.removeTimeout = setTimeout(() => { | ||||
|                         removeUserButton(user) | ||||
|                         removeUserHeaderButton(user) | ||||
|                     }, 20000) | ||||
|                 }, 5000) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function addUserButton (user) { | ||||
|         user.button = $('<li class="red-ui-multiplayer-user"><button type="button" class="red-ui-multiplayer-user-icon" href="#"></button></li>') | ||||
|     function addUserHeaderButton (user) { | ||||
|         user.button = $('<li class="red-ui-multiplayer-user"><button type="button" class="red-ui-multiplayer-user-icon"></button></li>') | ||||
|             .attr('data-username', user.user.username) | ||||
|             .prependTo("#red-ui-multiplayer-user-list"); | ||||
|         var button = user.button.find("button") | ||||
|         RED.popover.tooltip(button, user.user.username) | ||||
|         button.on('click', function () { | ||||
|             RED.popover.create({ | ||||
|                 target:button, | ||||
|                 trigger: 'modal', | ||||
|                 interactive: true, | ||||
|                 width: "250px", | ||||
|                 direction: 'bottom', | ||||
|                 content: () => {  | ||||
|                     const content = $('<div>') | ||||
|                     $('<div style="text-align: center">').text(user.user.username).appendTo(content) | ||||
|  | ||||
|                     const location = user.connections[0].location | ||||
|                     if (location.workspace) { | ||||
|                         const ws = RED.nodes.workspace(location.workspace) || RED.nodes.subflow(location.workspace) | ||||
|                         if (ws) { | ||||
|                             $('<div>').text(`${ws.type}: ${ws.label||ws.name||ws.id}`).appendTo(content) | ||||
|                         } else { | ||||
|                             $('<div>').text(`tab: unknown`).appendTo(content) | ||||
|                         } | ||||
|                     } | ||||
|                     if (location.node) { | ||||
|                         const node = RED.nodes.node(location.node) | ||||
|                         if (node) { | ||||
|                             $('<div>').text(`node: ${node.id}`).appendTo(content) | ||||
|                         } else { | ||||
|                             $('<div>').text(`node: unknown`).appendTo(content) | ||||
|                         } | ||||
|                     } | ||||
|                     return content | ||||
|                 }, | ||||
|             }).open() | ||||
|             const location = user.sessions[0].location | ||||
|             revealUser(location) | ||||
|         }) | ||||
|         if (!user.user.image) { | ||||
|             $('<i class="fa fa-user"></i>').appendTo(button); | ||||
|         } else { | ||||
|             $('<span class="user-profile"></span>').css({ | ||||
|                 backgroundImage: "url("+user.user.image+")", | ||||
|             }).appendTo(button); | ||||
|         } | ||||
|  | ||||
|         const userProfile = RED.user.generateUserIcon(user.user) | ||||
|         userProfile.appendTo(button) | ||||
|     } | ||||
|  | ||||
|     function removeUserHeaderButton (user) { | ||||
|         user.button.remove() | ||||
|         delete user.button | ||||
|     } | ||||
|  | ||||
|     function getLocation () { | ||||
| @@ -124,7 +102,7 @@ RED.multiplayer = (function () { | ||||
|         } | ||||
|         return location | ||||
|     } | ||||
|     function updateLocation () { | ||||
|     function publishLocation () { | ||||
|         const location = getLocation() | ||||
|         if (location.workspace !== 0) { | ||||
|             log('send', 'multiplayer/location', location) | ||||
| @@ -132,31 +110,314 @@ RED.multiplayer = (function () { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function removeUserButton (user) { | ||||
|         user.button.remove() | ||||
|         delete user.button | ||||
|     function revealUser(location, skipWorkspace) { | ||||
|         if (location.node) { | ||||
|             // Need to check if this is a known node, so we can fall back to revealing | ||||
|             // the workspace instead | ||||
|             const node = RED.nodes.node(location.node) | ||||
|             if (node) { | ||||
|                 RED.view.reveal(location.node) | ||||
|             } else if (!skipWorkspace && location.workspace) { | ||||
|                 RED.view.reveal(location.workspace) | ||||
|             } | ||||
|         } else if (!skipWorkspace && location.workspace) { | ||||
|             RED.view.reveal(location.workspace) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function updateUserLocation (data) { | ||||
|         connections[data.session].location = data | ||||
|         delete data.session | ||||
|     const workspaceTrays = {} | ||||
|     function getWorkspaceTray(workspaceId) { | ||||
|         // console.log('get tray for',workspaceId) | ||||
|         if (!workspaceTrays[workspaceId]) { | ||||
|             const tray = $('<div class="red-ui-multiplayer-users-tray"></div>') | ||||
|             const users = [] | ||||
|             const userIcons = {} | ||||
|  | ||||
|             const userCountIcon = $(`<div class="red-ui-multiplayer-user-location"><span class="red-ui-user-profile red-ui-multiplayer-user-count"><span></span></span></div>`) | ||||
|             const userCountSpan = userCountIcon.find('span span') | ||||
|             userCountIcon.hide() | ||||
|             userCountSpan.text('') | ||||
|             userCountIcon.appendTo(tray) | ||||
|             const userCountTooltip = RED.popover.tooltip(userCountIcon, function () { | ||||
|                     const content = $('<div>') | ||||
|                     users.forEach(sessionId => { | ||||
|                         $('<div>').append($('<a href="#">').text(sessions[sessionId].user.username).on('click', function (evt) { | ||||
|                             evt.preventDefault() | ||||
|                             revealUser(sessions[sessionId].location, true) | ||||
|                             userCountTooltip.close() | ||||
|                         })).appendTo(content) | ||||
|                     }) | ||||
|                     return content | ||||
|                 }, | ||||
|                 null, | ||||
|                 true | ||||
|             ) | ||||
|  | ||||
|             function updateUserCount () { | ||||
|                 const maxShown = 2 | ||||
|                 const children = tray.children() | ||||
|                 children.each(function (index, element) { | ||||
|                     const i = users.length - index | ||||
|                     if (i > maxShown) { | ||||
|                         $(this).hide() | ||||
|                     } else if (i >= 0) { | ||||
|                         $(this).show() | ||||
|                     } | ||||
|                 }) | ||||
|                 if (users.length < maxShown + 1) {  | ||||
|                     userCountIcon.hide() | ||||
|                 } else { | ||||
|                     userCountSpan.text('+'+(users.length - maxShown)) | ||||
|                     userCountIcon.show() | ||||
|                 } | ||||
|             } | ||||
|             workspaceTrays[workspaceId] = { | ||||
|                 attached: false, | ||||
|                 tray, | ||||
|                 users, | ||||
|                 userIcons, | ||||
|                 addUser: function (sessionId) { | ||||
|                     if (users.indexOf(sessionId) === -1) { | ||||
|                         // console.log(`addUser ws:${workspaceId} session:${sessionId}`) | ||||
|                         users.push(sessionId) | ||||
|                         const userLocationId = `red-ui-multiplayer-user-location-${sessionId}` | ||||
|                         const userLocationIcon = $(`<div class="red-ui-multiplayer-user-location" id="${userLocationId}"></div>`) | ||||
|                         RED.user.generateUserIcon(sessions[sessionId].user).appendTo(userLocationIcon) | ||||
|                         userLocationIcon.prependTo(tray) | ||||
|                         RED.popover.tooltip(userLocationIcon, sessions[sessionId].user.username) | ||||
|                         userIcons[sessionId] = userLocationIcon | ||||
|                         updateUserCount() | ||||
|                     } | ||||
|                 }, | ||||
|                 removeUser: function (sessionId) { | ||||
|                     // console.log(`removeUser ws:${workspaceId} session:${sessionId}`) | ||||
|                     const userLocationId = `red-ui-multiplayer-user-location-${sessionId}` | ||||
|                     const index = users.indexOf(sessionId) | ||||
|                     if (index > -1) { | ||||
|                         users.splice(index, 1) | ||||
|                         userIcons[sessionId].remove() | ||||
|                         delete userIcons[sessionId] | ||||
|                     } | ||||
|                     updateUserCount() | ||||
|                 }, | ||||
|                 updateUserCount | ||||
|             } | ||||
|         } | ||||
|         const trayDef = workspaceTrays[workspaceId] | ||||
|         if (!trayDef.attached) { | ||||
|             const workspaceTab = $(`#red-ui-tab-${workspaceId}`) | ||||
|             if (workspaceTab.length > 0) { | ||||
|                 trayDef.attached = true | ||||
|                 trayDef.tray.appendTo(workspaceTab) | ||||
|                 trayDef.users.forEach(sessionId => { | ||||
|                     trayDef.userIcons[sessionId].on('click', function (evt) { | ||||
|                         revealUser(sessions[sessionId].location, true) | ||||
|                     }) | ||||
|                 }) | ||||
|             } | ||||
|         } | ||||
|         return workspaceTrays[workspaceId] | ||||
|     } | ||||
|     function attachWorkspaceTrays () { | ||||
|         let viewTouched = false | ||||
|         for (let sessionId of Object.keys(sessions)) { | ||||
|             const location = sessions[sessionId].location | ||||
|             if (location) { | ||||
|                 if (location.workspace) { | ||||
|                     getWorkspaceTray(location.workspace).updateUserCount() | ||||
|                 } | ||||
|                 if (location.node) { | ||||
|                     addUserToNode(sessionId, location.node) | ||||
|                     viewTouched = true | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         if (viewTouched) { | ||||
|             RED.view.redraw() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function addUserToNode(sessionId, nodeId) { | ||||
|         const node = RED.nodes.node(nodeId) | ||||
|         if (node) { | ||||
|             if (!node._multiplayer) { | ||||
|                 node._multiplayer = { | ||||
|                     users: [sessionId] | ||||
|                 } | ||||
|                 node._multiplayer_refresh = true | ||||
|             } else { | ||||
|                 if (node._multiplayer.users.indexOf(sessionId) === -1) { | ||||
|                     node._multiplayer.users.push(sessionId) | ||||
|                     node._multiplayer_refresh = true | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     function removeUserFromNode(sessionId, nodeId) { | ||||
|         const node = RED.nodes.node(nodeId) | ||||
|         if (node && node._multiplayer) { | ||||
|             const i = node._multiplayer.users.indexOf(sessionId) | ||||
|             if (i > -1) { | ||||
|                 node._multiplayer.users.splice(i, 1) | ||||
|             } | ||||
|             if (node._multiplayer.users.length === 0) { | ||||
|                 delete node._multiplayer | ||||
|             } else { | ||||
|                 node._multiplayer_refresh = true | ||||
|             } | ||||
|         } | ||||
|  | ||||
|     } | ||||
|  | ||||
|     function removeUserLocation (sessionId) { | ||||
|         updateUserLocation(sessionId, {}) | ||||
|     } | ||||
|     function updateUserLocation (sessionId, location) { | ||||
|         let viewTouched = false | ||||
|         const oldLocation = sessions[sessionId].location | ||||
|         if (location) { | ||||
|             if (oldLocation.workspace !== location.workspace) { | ||||
|                 // console.log('removing', sessionId, oldLocation.workspace) | ||||
|                 workspaceTrays[oldLocation.workspace]?.removeUser(sessionId) | ||||
|             } | ||||
|             if (oldLocation.node !== location.node) { | ||||
|                 removeUserFromNode(sessionId, oldLocation.node) | ||||
|                 viewTouched = true | ||||
|             } | ||||
|             sessions[sessionId].location = location | ||||
|         } else { | ||||
|             location = sessions[sessionId].location | ||||
|         } | ||||
|         // console.log(`updateUserLocation sessionId:${sessionId} oldWS:${oldLocation?.workspace} newWS:${location.workspace}`) | ||||
|         if (location.workspace) { | ||||
|             getWorkspaceTray(location.workspace).addUser(sessionId) | ||||
|         } | ||||
|         if (location.node) { | ||||
|             addUserToNode(sessionId, location.node) | ||||
|             viewTouched = true | ||||
|         } | ||||
|         if (viewTouched) { | ||||
|             RED.view.redraw() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // function refreshUserLocations () { | ||||
|     //     for (const session of Object.keys(sessions)) { | ||||
|     //         if (session !== activeSessionId) { | ||||
|     //             updateUserLocation(session) | ||||
|     //         } | ||||
|     //     } | ||||
|     // } | ||||
|  | ||||
|     return { | ||||
|         init: function () { | ||||
|              | ||||
|  | ||||
|             sessionId = RED.settings.getLocal('multiplayer:sessionId') | ||||
|             if (!sessionId) { | ||||
|                 sessionId = RED.nodes.id() | ||||
|                 RED.settings.setLocal('multiplayer:sessionId', sessionId) | ||||
|             function createAnnotationUser(user) { | ||||
|  | ||||
|                 const group = document.createElementNS("http://www.w3.org/2000/svg","g"); | ||||
|                 const badge = document.createElementNS("http://www.w3.org/2000/svg","circle"); | ||||
|                 const radius = 20 | ||||
|                 badge.setAttribute("cx",radius/2); | ||||
|                 badge.setAttribute("cy",radius/2); | ||||
|                 badge.setAttribute("r",radius/2); | ||||
|                 badge.setAttribute("class", "red-ui-multiplayer-annotation-background") | ||||
|                 group.appendChild(badge) | ||||
|                 if (user && user.profileColor !== undefined) { | ||||
|                     badge.setAttribute("class", "red-ui-multiplayer-annotation-background red-ui-user-profile-color-" + user.profileColor) | ||||
|                 } | ||||
|                 if (user && user.image) { | ||||
|                     const image = document.createElementNS("http://www.w3.org/2000/svg","image"); | ||||
|                     image.setAttribute("width", radius) | ||||
|                     image.setAttribute("height", radius) | ||||
|                     image.setAttribute("href", user.image) | ||||
|                     image.setAttribute("clip-path", "circle("+Math.floor(radius/2)+")") | ||||
|                     group.appendChild(image) | ||||
|                 } else if (user && user.anonymous) { | ||||
|                     const anonIconHead = document.createElementNS("http://www.w3.org/2000/svg","circle"); | ||||
|                     anonIconHead.setAttribute("cx", radius/2) | ||||
|                     anonIconHead.setAttribute("cy", radius/2 - 2) | ||||
|                     anonIconHead.setAttribute("r", 2.4) | ||||
|                     anonIconHead.setAttribute("class","red-ui-multiplayer-annotation-anon-label"); | ||||
|                     group.appendChild(anonIconHead) | ||||
|                     const anonIconBody = document.createElementNS("http://www.w3.org/2000/svg","path"); | ||||
|                     anonIconBody.setAttribute("class","red-ui-multiplayer-annotation-anon-label"); | ||||
|                     // anonIconBody.setAttribute("d",`M ${radius/2 - 4} ${radius/2 + 1} h 8 v4 h -8 z`); | ||||
|                     anonIconBody.setAttribute("d",`M ${radius/2} ${radius/2 + 5} h -2.5 c -2 1 -2 -5 0.5 -4.5 c 2 1 2 1 4 0 c 2.5 -0.5  2.5 5.5  0 4.5  z`); | ||||
|                     group.appendChild(anonIconBody) | ||||
|                 } else { | ||||
|                     const labelText = user.username ? user.username.substring(0,2) : user | ||||
|                     const label = document.createElementNS("http://www.w3.org/2000/svg","text"); | ||||
|                     if (user.username) { | ||||
|                         label.setAttribute("class","red-ui-multiplayer-annotation-label"); | ||||
|                         label.textContent = user.username.substring(0,2) | ||||
|                     } else { | ||||
|                         label.setAttribute("class","red-ui-multiplayer-annotation-label red-ui-multiplayer-user-count") | ||||
|                         label.textContent = user | ||||
|                     } | ||||
|                     label.setAttribute("text-anchor", "middle") | ||||
|                     label.setAttribute("x",radius/2); | ||||
|                     label.setAttribute("y",radius/2 + 3); | ||||
|                     group.appendChild(label) | ||||
|                 } | ||||
|                 const border = document.createElementNS("http://www.w3.org/2000/svg","circle"); | ||||
|                 border.setAttribute("cx",radius/2); | ||||
|                 border.setAttribute("cy",radius/2); | ||||
|                 border.setAttribute("r",radius/2); | ||||
|                 border.setAttribute("class", "red-ui-multiplayer-annotation-border") | ||||
|                 group.appendChild(border) | ||||
|            | ||||
|  | ||||
|  | ||||
|                 return group | ||||
|             } | ||||
|              | ||||
|             RED.view.annotations.register("red-ui-multiplayer",{ | ||||
|                 type: 'badge', | ||||
|                 align: 'left', | ||||
|                 class: "red-ui-multiplayer-annotation", | ||||
|                 show: "_multiplayer", | ||||
|                 refresh: "_multiplayer_refresh", | ||||
|                 element: function(node) { | ||||
|                     const containerGroup = document.createElementNS("http://www.w3.org/2000/svg","g"); | ||||
|                     containerGroup.setAttribute("transform","translate(0,-4)") | ||||
|                     if (node._multiplayer) { | ||||
|                         let y = 0 | ||||
|                         for (let i = Math.min(1, node._multiplayer.users.length - 1); i >= 0; i--) { | ||||
|                             const user = sessions[node._multiplayer.users[i]].user | ||||
|                             const group = createAnnotationUser(user) | ||||
|                             group.setAttribute("transform","translate("+y+",0)") | ||||
|                             y += 15 | ||||
|                             containerGroup.appendChild(group) | ||||
|                         } | ||||
|                         if (node._multiplayer.users.length > 2) { | ||||
|                             const group = createAnnotationUser('+'+(node._multiplayer.users.length - 2)) | ||||
|                             group.setAttribute("transform","translate("+y+",0)") | ||||
|                             y += 12 | ||||
|                             containerGroup.appendChild(group) | ||||
|                         } | ||||
|  | ||||
|                     } | ||||
|                     return containerGroup; | ||||
|                 }, | ||||
|                 tooltip: node => { return node._multiplayer.users.map(u => sessions[u].user.username).join('\n') } | ||||
|             }); | ||||
|  | ||||
|  | ||||
|             // activeSessionId = RED.settings.getLocal('multiplayer:sessionId') | ||||
|             // if (!activeSessionId) { | ||||
|                 activeSessionId = RED.nodes.id() | ||||
|             //     RED.settings.setLocal('multiplayer:sessionId', activeSessionId) | ||||
|             //     log('Session ID (new)', activeSessionId) | ||||
|             // } else { | ||||
|                 log('Session ID', activeSessionId) | ||||
|             // } | ||||
|              | ||||
|             headerWidget = $('<li><ul id="red-ui-multiplayer-user-list"></ul></li>').prependTo('.red-ui-header-toolbar') | ||||
|  | ||||
|             RED.comms.on('connect', () => { | ||||
|                 const location = getLocation() | ||||
|                 const connectInfo = { | ||||
|                     session: sessionId | ||||
|                     session: activeSessionId | ||||
|                 } | ||||
|                 if (location.workspace !== 0) { | ||||
|                     connectInfo.location = location | ||||
| @@ -168,40 +429,52 @@ RED.multiplayer = (function () { | ||||
|                 if (topic === 'multiplayer/init') { | ||||
|                     // We have just reconnected, runtime has sent state to | ||||
|                     // initialise the world | ||||
|                     connections = {} | ||||
|                     sessions = {} | ||||
|                     users = {} | ||||
|                     $('#red-ui-multiplayer-user-list').empty() | ||||
|  | ||||
|                     msg.forEach(connection => { | ||||
|                         addUserConnection(connection) | ||||
|                     msg.sessions.forEach(session => { | ||||
|                         addUserSession(session) | ||||
|                     }) | ||||
|                 } else if (topic === 'multiplayer/connection-added') { | ||||
|                     addUserConnection(msg) | ||||
|                     addUserSession(msg) | ||||
|                 } else if (topic === 'multiplayer/connection-removed') { | ||||
|                     removeUserConnection(msg.session, msg.disconnected) | ||||
|                     removeUserSession(msg.session, msg.disconnected) | ||||
|                 } else if (topic === 'multiplayer/location') { | ||||
|                     updateUserLocation(msg) | ||||
|                     const session = msg.session | ||||
|                     delete msg.session | ||||
|                     updateUserLocation(session, msg) | ||||
|                 } | ||||
|             }) | ||||
|  | ||||
|             RED.events.on('workspace:change', (event) => { | ||||
|                 updateLocation() | ||||
|                 getWorkspaceTray(event.workspace) | ||||
|                 publishLocation() | ||||
|             }) | ||||
|             RED.events.on('editor:open', () => { | ||||
|                 updateLocation() | ||||
|                 publishLocation() | ||||
|             }) | ||||
|             RED.events.on('editor:close', () => { | ||||
|                 updateLocation() | ||||
|                 publishLocation() | ||||
|             }) | ||||
|             RED.events.on('editor:change', () => { | ||||
|                 updateLocation() | ||||
|                 publishLocation() | ||||
|             }) | ||||
|             RED.events.on('login', () => { | ||||
|                 updateLocation() | ||||
|                 publishLocation() | ||||
|             }) | ||||
|             RED.events.on('flows:loaded', () => { | ||||
|                 attachWorkspaceTrays() | ||||
|             }) | ||||
|             RED.events.on('workspace:close', (event) => { | ||||
|                 // A subflow tab has been closed. Need to mark its tray as detached | ||||
|                 if (workspaceTrays[event.workspace]) { | ||||
|                     workspaceTrays[event.workspace].attached = false | ||||
|                 } | ||||
|             }) | ||||
|             RED.events.on('logout', () => { | ||||
|                 const disconnectInfo = { | ||||
|                     session: sessionId | ||||
|                     session: activeSessionId | ||||
|                 } | ||||
|                 RED.comms.send('multiplayer/disconnect', disconnectInfo) | ||||
|                 RED.settings.removeLocal('multiplayer:sessionId') | ||||
|   | ||||
| @@ -298,6 +298,7 @@ var RED = (function() { | ||||
|                                 RED.workspaces.show(workspaces[0]); | ||||
|                             } | ||||
|                         } | ||||
|                         RED.events.emit('flows:loaded') | ||||
|                     } catch(err) { | ||||
|                         console.warn(err); | ||||
|                         RED.notify( | ||||
|   | ||||
| @@ -445,9 +445,12 @@ RED.popover = (function() { | ||||
|  | ||||
|     return { | ||||
|         create: createPopover, | ||||
|         tooltip: function(target,content, action) { | ||||
|         tooltip: function(target,content, action, interactive) { | ||||
|             var label = function() { | ||||
|                 var label = content; | ||||
|                 if (typeof content === 'function') { | ||||
|                     label = content() | ||||
|                 } | ||||
|                 if (action) { | ||||
|                     var shortcut = RED.keyboard.getShortcut(action); | ||||
|                     if (shortcut && shortcut.key) { | ||||
| @@ -463,6 +466,7 @@ RED.popover = (function() { | ||||
|                 size: "small", | ||||
|                 direction: "bottom", | ||||
|                 content: label, | ||||
|                 interactive, | ||||
|                 delay: { show: 750, hide: 50 } | ||||
|             }); | ||||
|             popover.setContent = function(newContent) { | ||||
|   | ||||
| @@ -112,16 +112,23 @@ RED.deploy = (function() { | ||||
|             RED.actions.add("core:set-deploy-type-to-modified-nodes",function() { RED.menu.setSelected("deploymenu-item-node",true); }); | ||||
|         } | ||||
|  | ||||
|  | ||||
|         window.addEventListener('beforeunload', function (event) { | ||||
|             if (RED.nodes.dirty()) { | ||||
|                 event.preventDefault(); | ||||
|                 event.stopImmediatePropagation() | ||||
|                 event.returnValue = RED._("deploy.confirm.undeployedChanges"); | ||||
|                 return | ||||
|             } | ||||
|         }) | ||||
|  | ||||
|         RED.events.on('workspace:dirty',function(state) { | ||||
|             if (state.dirty) { | ||||
|                 window.onbeforeunload = function() { | ||||
|                     return RED._("deploy.confirm.undeployedChanges"); | ||||
|                 } | ||||
|                 // window.onbeforeunload = function() { | ||||
|                 //     return  | ||||
|                 // } | ||||
|                 $("#red-ui-header-button-deploy").removeClass("disabled"); | ||||
|             } else { | ||||
|                 window.onbeforeunload = null; | ||||
|                 // window.onbeforeunload = null; | ||||
|                 $("#red-ui-header-button-deploy").addClass("disabled"); | ||||
|             } | ||||
|         }); | ||||
|   | ||||
| @@ -9,14 +9,27 @@ RED.view.annotations = (function() { | ||||
|                     addAnnotation(evt.node.__pendingAnnotation__,evt); | ||||
|                     delete evt.node.__pendingAnnotation__; | ||||
|                 } | ||||
|                 var badgeDX = 0; | ||||
|                 var controlDX = 0; | ||||
|                 for (var i=0,l=evt.el.__annotations__.length;i<l;i++) { | ||||
|                     var annotation = evt.el.__annotations__[i]; | ||||
|                 let badgeRDX = 0; | ||||
|                 let badgeLDX = 0; | ||||
|                  | ||||
|                 for (let i=0,l=evt.el.__annotations__.length;i<l;i++) { | ||||
|                     const annotation = evt.el.__annotations__[i]; | ||||
|                     if (annotations.hasOwnProperty(annotation.id)) { | ||||
|                         var opts = annotations[annotation.id]; | ||||
|                         var showAnnotation = true; | ||||
|                         var isBadge = opts.type === 'badge'; | ||||
|                         const opts = annotations[annotation.id]; | ||||
|                         let showAnnotation = true; | ||||
|                         const isBadge = opts.type === 'badge'; | ||||
|                         if (opts.refresh !== undefined) { | ||||
|                             let refreshAnnotation = false | ||||
|                             if (typeof opts.refresh === "string") { | ||||
|                                 refreshAnnotation = !!evt.node[opts.refresh] | ||||
|                                 delete evt.node[opts.refresh] | ||||
|                             } else if (typeof opts.refresh === "function") { | ||||
|                                 refreshAnnotation = opts.refresh(evnt.node) | ||||
|                             } | ||||
|                             if (refreshAnnotation) { | ||||
|                                 refreshAnnotationElement(annotation.id, annotation.node, annotation.element) | ||||
|                             } | ||||
|                         } | ||||
|                         if (opts.show !== undefined) { | ||||
|                             if (typeof opts.show === "string") { | ||||
|                                 showAnnotation = !!evt.node[opts.show] | ||||
| @@ -29,17 +42,24 @@ RED.view.annotations = (function() { | ||||
|                         } | ||||
|                         if (isBadge) { | ||||
|                             if (showAnnotation) { | ||||
|                                 var rect = annotation.element.getBoundingClientRect(); | ||||
|                                 badgeDX += rect.width; | ||||
|                                 annotation.element.setAttribute("transform", "translate("+(evt.node.w-3-badgeDX)+", -8)"); | ||||
|                                 badgeDX += 4; | ||||
|                             } | ||||
|                         } else { | ||||
|                             if (showAnnotation) { | ||||
|                                 var rect = annotation.element.getBoundingClientRect(); | ||||
|                                 annotation.element.setAttribute("transform", "translate("+(3+controlDX)+", -12)"); | ||||
|                                 controlDX += rect.width + 4; | ||||
|                                 const rect = annotation.element.getBoundingClientRect(); | ||||
|                                 let annotationX | ||||
|                                 if (!opts.align || opts.align === 'right') { | ||||
|                                     annotationX = evt.node.w - 3 - badgeRDX - rect.width | ||||
|                                     badgeRDX += rect.width + 4; | ||||
|  | ||||
|                                 } else if (opts.align === 'left') { | ||||
|                                     annotationX = 3 + badgeLDX | ||||
|                                     badgeLDX += rect.width + 4; | ||||
|                                 } | ||||
|                                 annotation.element.setAttribute("transform", "translate("+annotationX+", -8)"); | ||||
|                             } | ||||
|                         // } else { | ||||
|                         //     if (showAnnotation) { | ||||
|                         //         var rect = annotation.element.getBoundingClientRect(); | ||||
|                         //         annotation.element.setAttribute("transform", "translate("+(3+controlDX)+", -12)"); | ||||
|                         //         controlDX += rect.width + 4; | ||||
|                         //     } | ||||
|                         } | ||||
|                     } else { | ||||
|                         annotation.element.parentNode.removeChild(annotation.element); | ||||
| @@ -95,15 +115,25 @@ RED.view.annotations = (function() { | ||||
|         annotationGroup.setAttribute("class",opts.class || ""); | ||||
|         evt.el.__annotations__.push({ | ||||
|             id:id, | ||||
|             node: evt.node, | ||||
|             element: annotationGroup | ||||
|         }); | ||||
|         var annotation = opts.element(evt.node); | ||||
|         refreshAnnotationElement(id, evt.node, annotationGroup) | ||||
|         evt.el.appendChild(annotationGroup); | ||||
|     } | ||||
|  | ||||
|     function refreshAnnotationElement(id, node, annotationGroup) { | ||||
|         const opts = annotations[id]; | ||||
|         const annotation = opts.element(node); | ||||
|         if (opts.tooltip) { | ||||
|             annotation.addEventListener("mouseenter", getAnnotationMouseEnter(annotation,evt.node,opts.tooltip)); | ||||
|             annotation.addEventListener("mouseenter", getAnnotationMouseEnter(annotation, node, opts.tooltip)); | ||||
|             annotation.addEventListener("mouseleave", annotationMouseLeave); | ||||
|         } | ||||
|         if (annotationGroup.hasChildNodes()) { | ||||
|             annotationGroup.removeChild(annotationGroup.firstChild) | ||||
|         } | ||||
|         annotationGroup.appendChild(annotation); | ||||
|         evt.el.appendChild(annotationGroup); | ||||
|  | ||||
|     } | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -401,6 +401,7 @@ RED.workspaces = (function() { | ||||
|                 if (tab.type === "tab") { | ||||
|                     workspaceTabCount--; | ||||
|                 } else { | ||||
|                     RED.events.emit("workspace:close",{workspace: tab.id}) | ||||
|                     hideStack.push(tab.id); | ||||
|                 } | ||||
|                 RED.menu.setDisabled("menu-item-workspace-delete",activeWorkspace === 0 || workspaceTabCount <= 1); | ||||
|   | ||||
| @@ -212,6 +212,8 @@ RED.user = (function() { | ||||
|  | ||||
|     function updateUserMenu() { | ||||
|         $("#red-ui-header-button-user-submenu li").remove(); | ||||
|         const userMenu = $("#red-ui-header-button-user") | ||||
|         userMenu.empty() | ||||
|         if (RED.settings.user.anonymous) { | ||||
|             RED.menu.addItem("red-ui-header-button-user",{ | ||||
|                 id:"usermenu-item-login", | ||||
| @@ -226,7 +228,6 @@ RED.user = (function() { | ||||
|                     }); | ||||
|                 } | ||||
|             }); | ||||
|             $('<i class="fa fa-user"></i>').appendTo("#red-ui-header-button-user"); | ||||
|         } else { | ||||
|             RED.menu.addItem("red-ui-header-button-user",{ | ||||
|                 id:"usermenu-item-username", | ||||
| @@ -239,17 +240,9 @@ RED.user = (function() { | ||||
|                     RED.user.logout(); | ||||
|                 } | ||||
|             }); | ||||
|             const userMenu = $("#red-ui-header-button-user") | ||||
|             userMenu.empty() | ||||
|             if (RED.settings.user.image) { | ||||
|                 $('<span class="user-profile"></span>').css({ | ||||
|                     backgroundImage: "url("+RED.settings.user.image+")", | ||||
|                 }).appendTo(userMenu); | ||||
|             } else { | ||||
|                 $('<i class="fa fa-user"></i>').appendTo(userMenu); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         const userIcon = generateUserIcon(RED.settings.user) | ||||
|         userIcon.appendTo(userMenu); | ||||
|     } | ||||
|  | ||||
|     function init() { | ||||
| @@ -320,12 +313,30 @@ RED.user = (function() { | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     function generateUserIcon(user) { | ||||
|         const userIcon = $('<span class="red-ui-user-profile"></span>') | ||||
|         if (user.image) { | ||||
|             userIcon.addClass('has_profile_image') | ||||
|             userIcon.css({ | ||||
|                 backgroundImage: "url("+user.image+")", | ||||
|             }) | ||||
|         } else if (user.anonymous) { | ||||
|             $('<i class="fa fa-user"></i>').appendTo(userIcon); | ||||
|         } else { | ||||
|             $('<span>').text(user.username.substring(0,2)).appendTo(userIcon); | ||||
|         } | ||||
|         if (user.profileColor !== undefined) { | ||||
|             userIcon.addClass('red-ui-user-profile-color-' + user.profileColor) | ||||
|         } | ||||
|         return userIcon | ||||
|     } | ||||
|  | ||||
|     return { | ||||
|         init: init, | ||||
|         login: login, | ||||
|         logout: logout, | ||||
|         hasPermission: hasPermission | ||||
|         hasPermission: hasPermission, | ||||
|         generateUserIcon | ||||
|     } | ||||
|  | ||||
| })(); | ||||
|   | ||||
| @@ -314,6 +314,16 @@ $spinner-color: #999; | ||||
|  | ||||
| $tab-icon-color: #dedede; | ||||
|  | ||||
| // Anonymous User Colors | ||||
|  | ||||
| $user-profile-colors: ( | ||||
|     1: #822e81, | ||||
|     2: #955e42, | ||||
|     3: #9c914f, | ||||
|     4: #748e54, | ||||
|     5: #06bcc1 | ||||
| ); | ||||
|  | ||||
| // Deprecated | ||||
| $text-color-green: $text-color-success; | ||||
| $info-text-code-color: $text-color-code; | ||||
|   | ||||
| @@ -274,18 +274,44 @@ | ||||
|     #usermenu-item-username > .red-ui-menu-label { | ||||
|         color: var(--red-ui-header-menu-heading-color); | ||||
|     } | ||||
| } | ||||
|  | ||||
|     .user-profile { | ||||
|         background-position: center center; | ||||
|         background-repeat: no-repeat; | ||||
|         background-size: contain; | ||||
|         display: inline-block; | ||||
|         width: 30px; | ||||
|         height: 30px; | ||||
|         vertical-align: middle; | ||||
|  | ||||
| .red-ui-user-profile { | ||||
|     background-color: var(--red-ui-header-background); | ||||
|     border: 2px solid var(--red-ui-header-menu-color); | ||||
|     border-radius: 30px; | ||||
|     overflow: hidden; | ||||
|  | ||||
|     background-position: center center; | ||||
|     background-repeat: no-repeat; | ||||
|     background-size: contain; | ||||
|     display: inline-flex; | ||||
|     justify-content: center; | ||||
|     align-items: center; | ||||
|     vertical-align: middle; | ||||
|     width: 30px; | ||||
|     height: 30px; | ||||
|     font-size: 20px; | ||||
|  | ||||
|     &.red-ui-user-profile-color-1 { | ||||
|         background-color: var(--red-ui-user-profile-colors-1); | ||||
|     } | ||||
|     &.red-ui-user-profile-color-2 { | ||||
|         background-color: var(--red-ui-user-profile-colors-2); | ||||
|     } | ||||
|     &.red-ui-user-profile-color-3 { | ||||
|         background-color: var(--red-ui-user-profile-colors-3); | ||||
|     } | ||||
|     &.red-ui-user-profile-color-4 { | ||||
|         background-color: var(--red-ui-user-profile-colors-4); | ||||
|     } | ||||
|     &.red-ui-user-profile-color-5 { | ||||
|         background-color: var(--red-ui-user-profile-colors-5); | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
| @media only screen and (max-width: 450px) { | ||||
|     span.red-ui-header-logo > span { | ||||
|         display: none; | ||||
|   | ||||
| @@ -5,23 +5,18 @@ | ||||
|     li { | ||||
|         display: inline-flex; | ||||
|         align-items: center; | ||||
|         width: 30px; | ||||
|         margin: 0 2px; | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| .red-ui-multiplayer-user-icon { | ||||
|     background: var(--red-ui-header-background); | ||||
|     border: 2px solid var(--red-ui-header-menu-color); | ||||
|     border-radius: 30px; | ||||
|     background: none; | ||||
|     border: none; | ||||
|     display: inline-flex; | ||||
|     justify-content: center; | ||||
|     align-items: center; | ||||
|     width: 28px; | ||||
|     height: 28px; | ||||
|     text-align: center; | ||||
|     overflow: hidden; | ||||
|     box-sizing: border-box; | ||||
|     text-decoration: none; | ||||
|     color: var(--red-ui-header-menu-color); | ||||
| @@ -36,13 +31,86 @@ | ||||
|     .red-ui-multiplayer-user.inactive & { | ||||
|         opacity: 0.5; | ||||
|     } | ||||
|     .user-profile { | ||||
|         background-position: center center; | ||||
|         background-repeat: no-repeat; | ||||
|         background-size: contain; | ||||
|         display: inline-block; | ||||
|         vertical-align: middle; | ||||
|         width: 28px; | ||||
|         height: 28px; | ||||
|     .red-ui-user-profile { | ||||
|         width: 20px; | ||||
|         border-radius: 20px; | ||||
|         height: 20px; | ||||
|         font-size: 12px | ||||
|     } | ||||
| } | ||||
| .red-ui-multiplayer-users-tray { | ||||
|     position: absolute; | ||||
|     top: 5px; | ||||
|     right: 20px; | ||||
|     line-height: normal; | ||||
|     cursor: pointer; | ||||
|     // &:hover { | ||||
|     //     .red-ui-multiplayer-user-location { | ||||
|     //         margin-left: 1px; | ||||
|     //     } | ||||
|     // } | ||||
| } | ||||
| $multiplayer-user-icon-background: var(--red-ui-primary-background); | ||||
| $multiplayer-user-icon-border: var(--red-ui-view-background); | ||||
| $multiplayer-user-icon-text-color: var(--red-ui-header-menu-color); | ||||
| $multiplayer-user-icon-count-text-color: var(--red-ui-primary-color); | ||||
| $multiplayer-user-icon-shadow: 0px 0px 4px var(--red-ui-shadow); | ||||
| .red-ui-multiplayer-user-location { | ||||
|     display: inline-block; | ||||
|     margin-left: -6px; | ||||
|     transition: margin-left 0.2s; | ||||
|     .red-ui-user-profile { | ||||
|         border: 1px solid $multiplayer-user-icon-border; | ||||
|         color: $multiplayer-user-icon-text-color; | ||||
|         width: 18px; | ||||
|         height: 18px; | ||||
|         border-radius: 18px; | ||||
|         font-size: 10px; | ||||
|         font-weight: normal; | ||||
|         box-shadow: $multiplayer-user-icon-shadow; | ||||
|         &.red-ui-multiplayer-user-count { | ||||
|             color: $multiplayer-user-icon-count-text-color; | ||||
|             background-color: $multiplayer-user-icon-background; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| .red-ui-multiplayer-annotation { | ||||
|     .red-ui-multiplayer-annotation-background { | ||||
|         filter: drop-shadow($multiplayer-user-icon-shadow); | ||||
|         fill: $multiplayer-user-icon-background; | ||||
|         &.red-ui-user-profile-color-1 { | ||||
|             fill: var(--red-ui-user-profile-colors-1); | ||||
|         } | ||||
|         &.red-ui-user-profile-color-2 { | ||||
|             fill: var(--red-ui-user-profile-colors-2); | ||||
|         } | ||||
|         &.red-ui-user-profile-color-3 { | ||||
|             fill: var(--red-ui-user-profile-colors-3); | ||||
|         } | ||||
|         &.red-ui-user-profile-color-4 { | ||||
|             fill: var(--red-ui-user-profile-colors-4); | ||||
|         } | ||||
|         &.red-ui-user-profile-color-5 { | ||||
|             fill: var(--red-ui-user-profile-colors-5); | ||||
|         } | ||||
|     } | ||||
|     .red-ui-multiplayer-annotation-border { | ||||
|         stroke: $multiplayer-user-icon-border; | ||||
|         stroke-width: 1px; | ||||
|         fill: none; | ||||
|     } | ||||
|     .red-ui-multiplayer-annotation-anon-label { | ||||
|         fill: $multiplayer-user-icon-text-color; | ||||
|         stroke: none; | ||||
|     } | ||||
|     text { | ||||
|         user-select: none; | ||||
|         fill: $multiplayer-user-icon-text-color; | ||||
|         stroke: none; | ||||
|         font-size: 10px;    | ||||
|         &.red-ui-multiplayer-user-count { | ||||
|             fill: $multiplayer-user-icon-count-text-color; | ||||
|         }      | ||||
|     } | ||||
| } | ||||
| @@ -299,4 +299,7 @@ | ||||
|  | ||||
|     --red-ui-tab-icon-color: #{$tab-icon-color}; | ||||
|  | ||||
|     @each $current-color in 1 2 3 4 5 { | ||||
|         --red-ui-user-profile-colors-#{"" + $current-color}: #{map-get($user-profile-colors, $current-color)}; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -69,7 +69,7 @@ module.exports = { | ||||
|             // Send init info to new connection | ||||
|             const initPacket = { | ||||
|                 topic: "multiplayer/init", | ||||
|                 data: getSessionsList(), | ||||
|                 data: { sessions: getSessionsList() }, | ||||
|                 session: opts.session | ||||
|             } | ||||
|             // console.log('<<', initPacket) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user