diff --git a/packages/node_modules/@node-red/editor-client/src/js/multiplayer.js b/packages/node_modules/@node-red/editor-client/src/js/multiplayer.js index 926a118a9..576541a4d 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/multiplayer.js +++ b/packages/node_modules/@node-red/editor-client/src/js/multiplayer.js @@ -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 = $('
  • ') + function addUserHeaderButton (user) { + user.button = $('
  • ') .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 = $('
    ') - $('
    ').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) { - $('
    ').text(`${ws.type}: ${ws.label||ws.name||ws.id}`).appendTo(content) - } else { - $('
    ').text(`tab: unknown`).appendTo(content) - } - } - if (location.node) { - const node = RED.nodes.node(location.node) - if (node) { - $('
    ').text(`node: ${node.id}`).appendTo(content) - } else { - $('
    ').text(`node: unknown`).appendTo(content) - } - } - return content - }, - }).open() + const location = user.sessions[0].location + revealUser(location) }) - if (!user.user.image) { - $('').appendTo(button); - } else { - $('').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 = $('
    ') + const users = [] + const userIcons = {} + + const userCountIcon = $(`
    `) + const userCountSpan = userCountIcon.find('span span') + userCountIcon.hide() + userCountSpan.text('') + userCountIcon.appendTo(tray) + const userCountTooltip = RED.popover.tooltip(userCountIcon, function () { + const content = $('
    ') + users.forEach(sessionId => { + $('
    ').append($('').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 = $(`
    `) + 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 = $('
    • ').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') diff --git a/packages/node_modules/@node-red/editor-client/src/js/red.js b/packages/node_modules/@node-red/editor-client/src/js/red.js index f377e7349..f8130a686 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/red.js +++ b/packages/node_modules/@node-red/editor-client/src/js/red.js @@ -298,6 +298,7 @@ var RED = (function() { RED.workspaces.show(workspaces[0]); } } + RED.events.emit('flows:loaded') } catch(err) { console.warn(err); RED.notify( diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/common/popover.js b/packages/node_modules/@node-red/editor-client/src/js/ui/common/popover.js index 1a70839ae..381bb9d3a 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/common/popover.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/common/popover.js @@ -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) { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js b/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js index a09fdeb01..83c04d775 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js @@ -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"); } }); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/view-annotations.js b/packages/node_modules/@node-red/editor-client/src/js/ui/view-annotations.js index 1df8c4bd1..b08df80ae 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/view-annotations.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/view-annotations.js @@ -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').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) { - $('').css({ - backgroundImage: "url("+RED.settings.user.image+")", - }).appendTo(userMenu); - } else { - $('').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 = $('') + if (user.image) { + userIcon.addClass('has_profile_image') + userIcon.css({ + backgroundImage: "url("+user.image+")", + }) + } else if (user.anonymous) { + $('').appendTo(userIcon); + } else { + $('').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 } })(); diff --git a/packages/node_modules/@node-red/editor-client/src/sass/colors.scss b/packages/node_modules/@node-red/editor-client/src/sass/colors.scss index b561fde16..f0a69e00a 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/colors.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/colors.scss @@ -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; diff --git a/packages/node_modules/@node-red/editor-client/src/sass/header.scss b/packages/node_modules/@node-red/editor-client/src/sass/header.scss index 5ce9e877d..b8db40f81 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/header.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/header.scss @@ -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; diff --git a/packages/node_modules/@node-red/editor-client/src/sass/multiplayer.scss b/packages/node_modules/@node-red/editor-client/src/sass/multiplayer.scss index 58fb9472f..4aaab86b1 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/multiplayer.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/multiplayer.scss @@ -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; + } } } \ No newline at end of file diff --git a/packages/node_modules/@node-red/editor-client/src/sass/variables.scss b/packages/node_modules/@node-red/editor-client/src/sass/variables.scss index c04c26ff9..bc8f9da17 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/variables.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/variables.scss @@ -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)}; + } } diff --git a/packages/node_modules/@node-red/runtime/lib/multiplayer/index.js b/packages/node_modules/@node-red/runtime/lib/multiplayer/index.js index a4108e51f..adfa63c28 100644 --- a/packages/node_modules/@node-red/runtime/lib/multiplayer/index.js +++ b/packages/node_modules/@node-red/runtime/lib/multiplayer/index.js @@ -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)