@jupyter/collaboration 3.1.0 → 4.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -49,15 +49,19 @@ export class CollaboratorsPanel extends Panel {
49
49
  */
50
50
  this._onAwarenessChanged = () => {
51
51
  const state = this._awareness.getStates();
52
- const collaborators = [];
52
+ const collaboratorsMap = new Map();
53
53
  state.forEach((value, key) => {
54
54
  if (this._currentUser.isReady &&
55
55
  value.user &&
56
56
  value.user.username !== this._currentUser.identity.username) {
57
- collaborators.push(value);
57
+ const uniqueKey = `${value.user.username}-${value.current || 'no-current'}`;
58
+ if (!collaboratorsMap.has(uniqueKey)) {
59
+ collaboratorsMap.set(uniqueKey, value);
60
+ }
58
61
  }
59
62
  });
60
- this._collaboratorsChanged.emit(collaborators);
63
+ // Convert map to array to maintain the same emit interface
64
+ this._collaboratorsChanged.emit(Array.from(collaboratorsMap.values()));
61
65
  };
62
66
  this._collaboratorsChanged = new Signal(this);
63
67
  this._awareness = awareness;
@@ -72,8 +76,9 @@ export function CollaboratorsBody(props) {
72
76
  props.collaboratorsChanged.connect((_, value) => {
73
77
  setCollaborators(value);
74
78
  });
75
- return (React.createElement("div", { className: COLLABORATORS_LIST_CLASS }, collaborators.map((collaborator, i) => {
76
- return (React.createElement(Collaborator, { collaborator: collaborator, fileopener: props.fileopener, docRegistry: props.docRegistry }));
79
+ return (React.createElement("div", { className: COLLABORATORS_LIST_CLASS }, collaborators.map(collaborator => {
80
+ const uniqueKey = `${collaborator.user.username}-${collaborator.current || 'no-current'}`;
81
+ return (React.createElement(Collaborator, { key: uniqueKey, collaborator: collaborator, fileopener: props.fileopener, docRegistry: props.docRegistry }));
77
82
  })));
78
83
  }
79
84
  export function Collaborator(props) {
@@ -81,26 +86,25 @@ export function Collaborator(props) {
81
86
  const { collaborator, fileopener } = props;
82
87
  let currentMain = '';
83
88
  if (collaborator.current) {
89
+ // Discard widget tracker prefix (e.g. `notebook:` or `editor:`)
84
90
  const path = collaborator.current.split(':');
85
- currentMain = `${path[1]}:${path[2]}`;
91
+ currentMain = `${path[1]}`;
86
92
  }
87
93
  const documents = collaborator.documents || [];
88
94
  const docs = documents.map(document => {
89
95
  var _a, _b;
90
- const path = document.split(':');
91
- const fileTypes = (_b = (_a = props.docRegistry) === null || _a === void 0 ? void 0 : _a.getFileTypesForPath(path[1])) === null || _b === void 0 ? void 0 : _b.filter(ft => ft.icon !== undefined);
92
- const icon = fileTypes ? fileTypes[0].icon : fileIcon;
96
+ const fileTypes = (_b = (_a = props.docRegistry) === null || _a === void 0 ? void 0 : _a.getFileTypesForPath(document)) === null || _b === void 0 ? void 0 : _b.filter(ft => ft.icon !== undefined);
97
+ const icon = (fileTypes === null || fileTypes === void 0 ? void 0 : fileTypes.length) ? fileTypes[0].icon : fileIcon;
93
98
  const iconClass = fileTypes
94
99
  ? fileTypes[0].iconClass
95
100
  : undefined;
96
101
  return {
97
- filepath: path[1],
98
- filename: path[1].length > 40
99
- ? path[1]
102
+ filename: document.length > 40
103
+ ? document
100
104
  .slice(0, 10)
101
105
  .concat('…')
102
- .concat(path[1].slice(path[1].length - 15))
103
- : path[1],
106
+ .concat(document.slice(document.length - 15))
107
+ : document,
104
108
  fileLocation: document,
105
109
  icon,
106
110
  iconClass
@@ -127,6 +131,6 @@ export function Collaborator(props) {
127
131
  ? `${COLLABORATOR_FILE_CLASS} jp-mod-running`
128
132
  : COLLABORATOR_FILE_CLASS), key: doc.filename, onClick: () => fileopener(doc.fileLocation) },
129
133
  React.createElement(LabIcon.resolveReact, { icon: doc.icon, iconClass: doc.iconClass, tag: 'span', className: 'jp-DirListing-itemIcon', stylesheet: 'listing' }),
130
- React.createElement("span", { className: 'jp-DirListing-itemText', title: doc.filepath }, doc.filename)));
134
+ React.createElement("span", { className: 'jp-DirListing-itemText', title: doc.fileLocation }, doc.filename)));
131
135
  })))));
132
136
  }
package/lib/index.d.ts CHANGED
@@ -8,3 +8,4 @@ export * from './cursors';
8
8
  export * from './menu';
9
9
  export * from './sharedlink';
10
10
  export * from './userinfopanel';
11
+ export * from './users-item';
package/lib/index.js CHANGED
@@ -10,3 +10,4 @@ export * from './cursors';
10
10
  export * from './menu';
11
11
  export * from './sharedlink';
12
12
  export * from './userinfopanel';
13
+ export * from './users-item';
@@ -0,0 +1,85 @@
1
+ import { DocumentRegistry } from '@jupyterlab/docregistry';
2
+ import { User } from '@jupyterlab/services';
3
+ import { ReactWidget } from '@jupyterlab/ui-components';
4
+ import * as React from 'react';
5
+ /**
6
+ * The namespace for the UsersItem component.
7
+ */
8
+ export declare namespace UsersItem {
9
+ /**
10
+ * Properties of the component.
11
+ */
12
+ interface IProps {
13
+ /**
14
+ * The model of the document.
15
+ */
16
+ model: DocumentRegistry.IModel | null;
17
+ /**
18
+ * A function to display the user icons, optional.
19
+ * This function will overwrite the default one, and can be used to handle event on
20
+ * icons.
21
+ */
22
+ iconRenderer?: (props: UsersItem.IIconRendererProps) => JSX.Element;
23
+ }
24
+ /**
25
+ * The state of the component.
26
+ */
27
+ interface IState {
28
+ /**
29
+ * The user list.
30
+ */
31
+ usersList: IUserData[];
32
+ }
33
+ /**
34
+ * Properties send to the iconRenderer function.
35
+ */
36
+ interface IIconRendererProps extends React.HTMLAttributes<HTMLElement> {
37
+ /**
38
+ * The user.
39
+ */
40
+ user: IUserData;
41
+ /**
42
+ * The document's model.
43
+ */
44
+ model?: DocumentRegistry.IModel;
45
+ }
46
+ /**
47
+ * The user data type.
48
+ */
49
+ type IUserData = {
50
+ /**
51
+ * User id (the client id of the awareness).
52
+ */
53
+ userId: number;
54
+ /**
55
+ * User data.
56
+ */
57
+ userData: User.IIdentity;
58
+ };
59
+ }
60
+ /**
61
+ * A component displaying the collaborative users of a document.
62
+ */
63
+ export declare class UsersItem extends React.Component<UsersItem.IProps, UsersItem.IState> {
64
+ constructor(props: UsersItem.IProps);
65
+ /**
66
+ * Static method to create a widget.
67
+ */
68
+ static createWidget(options: UsersItem.IProps): ReactWidget;
69
+ componentDidMount(): void;
70
+ /**
71
+ * Filter out the duplicated users, which can happen temporary on reload.
72
+ */
73
+ private filterDuplicated;
74
+ render(): React.ReactNode;
75
+ /**
76
+ * Triggered when a change occurs in the document awareness, to build again the users list.
77
+ */
78
+ private _awarenessChange;
79
+ private _model;
80
+ private _iconRenderer;
81
+ }
82
+ /**
83
+ * Default renderer for the user icon.
84
+ */
85
+ export declare function DefaultIconRenderer(props: UsersItem.IIconRendererProps): JSX.Element;
@@ -0,0 +1,90 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ import { classes, ReactWidget } from '@jupyterlab/ui-components';
6
+ import * as React from 'react';
7
+ const USERS_ITEM_CLASS = 'jp-toolbar-users-item';
8
+ /**
9
+ * A component displaying the collaborative users of a document.
10
+ */
11
+ export class UsersItem extends React.Component {
12
+ constructor(props) {
13
+ var _a;
14
+ super(props);
15
+ /**
16
+ * Triggered when a change occurs in the document awareness, to build again the users list.
17
+ */
18
+ this._awarenessChange = () => {
19
+ var _a;
20
+ const clients = (_a = this._model) === null || _a === void 0 ? void 0 : _a.sharedModel.awareness.getStates();
21
+ const users = [];
22
+ if (clients) {
23
+ clients.forEach((val, key) => {
24
+ if (val.user) {
25
+ users.push({ userId: key, userData: val.user });
26
+ }
27
+ });
28
+ }
29
+ this.setState(old => ({ ...old, usersList: users }));
30
+ };
31
+ this._model = props.model;
32
+ this._iconRenderer = (_a = props.iconRenderer) !== null && _a !== void 0 ? _a : null;
33
+ this.state = { usersList: [] };
34
+ }
35
+ /**
36
+ * Static method to create a widget.
37
+ */
38
+ static createWidget(options) {
39
+ return ReactWidget.create(React.createElement(UsersItem, { ...options }));
40
+ }
41
+ componentDidMount() {
42
+ var _a;
43
+ (_a = this._model) === null || _a === void 0 ? void 0 : _a.sharedModel.awareness.on('change', this._awarenessChange);
44
+ this._awarenessChange();
45
+ }
46
+ /**
47
+ * Filter out the duplicated users, which can happen temporary on reload.
48
+ */
49
+ filterDuplicated(usersList) {
50
+ var _a;
51
+ const newList = [];
52
+ const selected = new Set();
53
+ for (const element of usersList) {
54
+ if (((_a = element === null || element === void 0 ? void 0 : element.userData) === null || _a === void 0 ? void 0 : _a.username) &&
55
+ !selected.has(element.userData.username)) {
56
+ selected.add(element.userData.username);
57
+ newList.push(element);
58
+ }
59
+ }
60
+ return newList;
61
+ }
62
+ render() {
63
+ var _a;
64
+ const IconRenderer = (_a = this._iconRenderer) !== null && _a !== void 0 ? _a : DefaultIconRenderer;
65
+ return (React.createElement("div", { className: USERS_ITEM_CLASS }, this.filterDuplicated(this.state.usersList).map(user => {
66
+ if (this._model &&
67
+ user.userId !== this._model.sharedModel.awareness.clientID) {
68
+ return IconRenderer({ user, model: this._model });
69
+ }
70
+ })));
71
+ }
72
+ }
73
+ /**
74
+ * Default renderer for the user icon.
75
+ */
76
+ export function DefaultIconRenderer(props) {
77
+ let el;
78
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
79
+ const { user, model, ...htmlProps } = props;
80
+ const iconClasses = classes('lm-MenuBar-itemIcon', props.className || '');
81
+ if (user.userData.avatar_url) {
82
+ el = (React.createElement("div", { ...htmlProps, key: user.userId, title: user.userData.display_name, className: classes(iconClasses, 'jp-MenuBar-imageIcon') },
83
+ React.createElement("img", { src: user.userData.avatar_url, alt: "" })));
84
+ }
85
+ else {
86
+ el = (React.createElement("div", { ...htmlProps, key: user.userId, title: user.userData.display_name, className: classes(iconClasses, 'jp-MenuBar-anonymousIcon'), style: { backgroundColor: user.userData.color } },
87
+ React.createElement("span", null, user.userData.initials)));
88
+ }
89
+ return el;
90
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jupyter/collaboration",
3
- "version": "3.1.0",
3
+ "version": "4.0.0-beta.0",
4
4
  "description": "JupyterLab - Real-Time Collaboration Widgets",
5
5
  "homepage": "https://github.com/jupyterlab/jupyter-collaboration",
6
6
  "bugs": {
@@ -41,11 +41,11 @@
41
41
  "dependencies": {
42
42
  "@codemirror/state": "^6.2.0",
43
43
  "@codemirror/view": "^6.7.0",
44
- "@jupyterlab/apputils": "^4.0.5",
45
- "@jupyterlab/coreutils": "^6.0.5",
46
- "@jupyterlab/docregistry": "^4.0.5",
47
- "@jupyterlab/services": "^7.0.5",
48
- "@jupyterlab/ui-components": "^4.0.5",
44
+ "@jupyterlab/apputils": "^4.4.0-alpha.2",
45
+ "@jupyterlab/coreutils": "^6.4.0-alpha.2",
46
+ "@jupyterlab/docregistry": "^4.4.0-alpha.2",
47
+ "@jupyterlab/services": "^7.4.0-alpha.2",
48
+ "@jupyterlab/ui-components": "^4.4.0-alpha.2",
49
49
  "@lumino/coreutils": "^2.1.0",
50
50
  "@lumino/signaling": "^2.0.0",
51
51
  "@lumino/virtualdom": "^2.0.0",
@@ -57,7 +57,7 @@
57
57
  "devDependencies": {
58
58
  "@types/react": "~18.3.1",
59
59
  "rimraf": "^4.1.2",
60
- "typescript": "~5.0.4"
60
+ "typescript": "~5.1.6"
61
61
  },
62
62
  "publishConfig": {
63
63
  "access": "public"
package/style/base.css CHANGED
@@ -5,6 +5,7 @@
5
5
 
6
6
  @import url('./menu.css');
7
7
  @import url('./sidepanel.css');
8
+ @import url('./users-item.css');
8
9
 
9
10
  .jp-shared-link-body {
10
11
  user-select: none;
@@ -0,0 +1,20 @@
1
+ /* -----------------------------------------------------------------------------
2
+ | Copyright (c) Jupyter Development Team.
3
+ | Distributed under the terms of the Modified BSD License.
4
+ |---------------------------------------------------------------------------- */
5
+
6
+ .jp-toolbar-users-item {
7
+ flex-grow: 1;
8
+ display: flex;
9
+ flex-direction: row;
10
+ }
11
+
12
+ .jp-toolbar-users-item .jp-MenuBar-anonymousIcon,
13
+ .jp-toolbar-users-item .jp-MenuBar-imageIcon {
14
+ position: relative;
15
+ left: 0;
16
+ height: 22px;
17
+ width: 22px;
18
+ box-sizing: border-box;
19
+ cursor: default;
20
+ }