-
+
+
diff --git a/packages/frontend-core/src/components/grid/layout/Avatar.svelte b/packages/frontend-core/src/components/grid/layout/Avatar.svelte
deleted file mode 100644
index e3eec9f830..0000000000
--- a/packages/frontend-core/src/components/grid/layout/Avatar.svelte
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
- {user.email[0]}
-
-
-
diff --git a/packages/frontend-core/src/components/grid/layout/UserAvatars.svelte b/packages/frontend-core/src/components/grid/layout/UserAvatars.svelte
index 2968d3c27c..df705c97b4 100644
--- a/packages/frontend-core/src/components/grid/layout/UserAvatars.svelte
+++ b/packages/frontend-core/src/components/grid/layout/UserAvatars.svelte
@@ -1,13 +1,23 @@
- {#each $users as user}
-
+ {#each uniqueUsers as user}
+
{/each}
diff --git a/packages/frontend-core/src/components/grid/stores/users.js b/packages/frontend-core/src/components/grid/stores/users.js
index a160330590..5368c414ce 100644
--- a/packages/frontend-core/src/components/grid/stores/users.js
+++ b/packages/frontend-core/src/components/grid/stores/users.js
@@ -1,10 +1,22 @@
import { writable, get, derived } from "svelte/store"
+import { helpers } from "@budibase/shared-core"
export const createStores = () => {
const users = writable([])
+ const enrichedUsers = derived(users, $users => {
+ return $users.map(user => ({
+ ...user,
+ color: helpers.getUserColor(user),
+ label: helpers.getUserLabel(user),
+ }))
+ })
+
return {
- users,
+ users: {
+ ...users,
+ subscribe: enrichedUsers.subscribe,
+ },
}
}
@@ -28,11 +40,11 @@ export const deriveStores = context => {
const updateUser = user => {
const $users = get(users)
- if (!$users.some(x => x.id === user.id)) {
+ if (!$users.some(x => x.sessionId === user.sessionId)) {
users.set([...$users, user])
} else {
users.update(state => {
- const index = state.findIndex(x => x.id === user.id)
+ const index = state.findIndex(x => x.sessionId === user.sessionId)
state[index] = user
return state.slice()
})
@@ -41,7 +53,7 @@ export const deriveStores = context => {
const removeUser = user => {
users.update(state => {
- return state.filter(x => x.id !== user.id)
+ return state.filter(x => x.sessionId !== user.sessionId)
})
}
diff --git a/packages/frontend-core/src/components/index.js b/packages/frontend-core/src/components/index.js
index 88107da535..3005c85d01 100644
--- a/packages/frontend-core/src/components/index.js
+++ b/packages/frontend-core/src/components/index.js
@@ -1,4 +1,5 @@
export { default as SplitPage } from "./SplitPage.svelte"
export { default as TestimonialPage } from "./TestimonialPage.svelte"
export { default as Testimonial } from "./Testimonial.svelte"
+export { default as UserAvatar } from "./UserAvatar.svelte"
export { Grid } from "./grid"
diff --git a/packages/server/src/websockets/builder.ts b/packages/server/src/websockets/builder.ts
index ae32a7de18..de8709129d 100644
--- a/packages/server/src/websockets/builder.ts
+++ b/packages/server/src/websockets/builder.ts
@@ -5,6 +5,7 @@ import http from "http"
import Koa from "koa"
import { Datasource, Table } from "@budibase/types"
import { gridSocket } from "./index"
+import { clearLock } from "../utilities/redis"
export default class BuilderSocket extends Socket {
constructor(app: Koa, server: http.Server) {
@@ -15,8 +16,7 @@ export default class BuilderSocket extends Socket {
const user = socket.data.user
const appId = socket.data.appId
socket.join(appId)
- socket.to(appId).emit("user-update", socket.data.user)
- console.log(`Builder user connected: ${user?.id}`)
+ socket.to(appId).emit("user-update", user)
// Initial identification of connected spreadsheet
socket.on("get-users", async (payload, callback) => {
@@ -27,8 +27,22 @@ export default class BuilderSocket extends Socket {
})
// Disconnection cleanup
- socket.on("disconnect", () => {
- socket.to(appId).emit("user-disconnect", socket.data.user)
+ socket.on("disconnect", async () => {
+ socket.to(appId).emit("user-disconnect", user)
+
+ // Remove app lock from this user if they have no other connections
+ try {
+ const sockets = await this.io.in(appId).fetchSockets()
+ const hasOtherConnection = sockets.some(socket => {
+ const { _id, sessionId } = socket.data.user
+ return _id === user._id && sessionId !== user.sessionId
+ })
+ if (!hasOtherConnection) {
+ await clearLock(appId, user)
+ }
+ } catch (e) {
+ // This is fine, just means this user didn't hold the lock
+ }
})
})
}
diff --git a/packages/server/src/websockets/grid.ts b/packages/server/src/websockets/grid.ts
index 09b9ee0e3d..a0272be33c 100644
--- a/packages/server/src/websockets/grid.ts
+++ b/packages/server/src/websockets/grid.ts
@@ -12,7 +12,6 @@ export default class GridSocket extends Socket {
this.io.on("connection", socket => {
const user = socket.data.user
- console.log(`Spreadsheet user connected: ${user?.id}`)
// Socket state
let currentRoom: string
@@ -21,14 +20,14 @@ export default class GridSocket extends Socket {
socket.on("select-table", async (tableId, callback) => {
// Leave current room
if (currentRoom) {
- socket.to(currentRoom).emit("user-disconnect", socket.data.user)
+ socket.to(currentRoom).emit("user-disconnect", user)
socket.leave(currentRoom)
}
// Join new room
currentRoom = tableId
socket.join(currentRoom)
- socket.to(currentRoom).emit("user-update", socket.data.user)
+ socket.to(currentRoom).emit("user-update", user)
// Reply with all users in current room
const sockets = await this.io.in(currentRoom).fetchSockets()
@@ -41,14 +40,14 @@ export default class GridSocket extends Socket {
socket.on("select-cell", cellId => {
socket.data.user.focusedCellId = cellId
if (currentRoom) {
- socket.to(currentRoom).emit("user-update", socket.data.user)
+ socket.to(currentRoom).emit("user-update", user)
}
})
// Disconnection cleanup
socket.on("disconnect", () => {
if (currentRoom) {
- socket.to(currentRoom).emit("user-disconnect", socket.data.user)
+ socket.to(currentRoom).emit("user-disconnect", user)
}
})
})
diff --git a/packages/server/src/websockets/websocket.ts b/packages/server/src/websockets/websocket.ts
index c3a1019932..5cac959099 100644
--- a/packages/server/src/websockets/websocket.ts
+++ b/packages/server/src/websockets/websocket.ts
@@ -7,6 +7,7 @@ import { auth } from "@budibase/backend-core"
import currentApp from "../middleware/currentapp"
import { createAdapter } from "@socket.io/redis-adapter"
import { getSocketPubSubClients } from "../utilities/redis"
+import uuid from "uuid"
export default class Socket {
io: Server
@@ -64,29 +65,14 @@ export default class Socket {
// Middlewares are finished
// Extract some data from our enriched koa context to persist
// as metadata for the socket
-
- // Add user info, including a deterministic color and friendly
- // label
+ // Add user info, including a deterministic color and label
const { _id, email, firstName, lastName } = ctx.user
- let hue = 1
- for (let i = 0; i < email.length && i < 5; i++) {
- hue *= email.charCodeAt(i + 1)
- hue /= 17
- }
- hue = hue % 360
- const color = `hsl(${hue}, 50%, 40%)`
- let label = email
- if (firstName) {
- label = firstName
- if (lastName) {
- label += ` ${lastName}`
- }
- }
socket.data.user = {
- id: _id,
+ _id,
email,
- color,
- label,
+ firstName,
+ lastName,
+ sessionId: uuid.v4(),
}
// Add app ID to help split sockets into rooms
diff --git a/packages/shared-core/src/helpers/helpers.ts b/packages/shared-core/src/helpers/helpers.ts
index 8ecfc24b56..8b8f5a3918 100644
--- a/packages/shared-core/src/helpers/helpers.ts
+++ b/packages/shared-core/src/helpers/helpers.ts
@@ -1,3 +1,5 @@
+import { User } from "@budibase/types"
+
/**
* Gets a key within an object. The key supports dot syntax for retrieving deep
* fields - e.g. "a.b.c".
@@ -21,3 +23,60 @@ export const deepGet = (obj: { [x: string]: any }, key: string) => {
}
return obj
}
+
+/**
+ * Gets the initials to show in a user avatar.
+ * @param user the user
+ */
+export const getUserInitials = (user: User) => {
+ if (!user) {
+ return "?"
+ }
+ let initials = ""
+ initials += user.firstName ? user.firstName[0] : ""
+ initials += user.lastName ? user.lastName[0] : ""
+ return initials === "" ? user.email[0] : initials
+}
+
+/**
+ * Gets a deterministic colour for a particular user
+ * @param user the user
+ */
+export const getUserColor = (user: User) => {
+ let id = user?._id
+ if (!id) {
+ return "var(--spectrum-global-color-blue-400)"
+ }
+
+ // In order to generate the same color for global users as app users, we need
+ // to remove the app-specific table prefix
+ id = id.replace("ro_ta_users_", "")
+
+ // Generate a hue based on the ID
+ let hue = 1
+ for (let i = 0; i < id.length; i++) {
+ hue += id.charCodeAt(i)
+ hue = hue % 36
+ }
+ return `hsl(${hue * 10}, 50%, 40%)`
+}
+
+/**
+ * Gets a friendly label to describe who a user is.
+ * @param user the user
+ */
+export const getUserLabel = (user: User) => {
+ if (!user) {
+ return ""
+ }
+ const { firstName, lastName, email } = user
+ if (firstName && lastName) {
+ return `${firstName} ${lastName}`
+ } else if (firstName) {
+ return firstName
+ } else if (lastName) {
+ return lastName
+ } else {
+ return email
+ }
+}