@flashphoner/sfusdk 1.0.1-36 → 1.0.41

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.
package/Gruntfile.js CHANGED
@@ -50,6 +50,18 @@ module.exports = function(grunt) {
50
50
  release: [
51
51
  'release'
52
52
  ]
53
+ },
54
+ run: {
55
+ options: {
56
+ // ...
57
+ },
58
+ test: {
59
+ cmd: 'npm',
60
+ args: [
61
+ 'run',
62
+ 'test'
63
+ ]
64
+ }
53
65
  }
54
66
  });
55
67
 
@@ -57,6 +69,7 @@ module.exports = function(grunt) {
57
69
  grunt.loadNpmTasks('grunt-contrib-copy');
58
70
  grunt.loadNpmTasks('grunt-contrib-clean');
59
71
  grunt.loadNpmTasks('grunt-jsdoc');
72
+ grunt.loadNpmTasks('grunt-run');
60
73
  grunt.registerTask('build', [
61
74
  'clean:build',
62
75
  'copy',
package/README.md CHANGED
@@ -87,7 +87,7 @@ cp -r out/* /var/www/html/flashphoner-sfu-test
87
87
 
88
88
  [SFU functions description](https://docs.flashphoner.com/display/WCS52EN/SFU+functions+with+Simulcast)
89
89
  [SFU SDK documentation](https://docs.flashphoner.com/display/SS1E/SFU+SDK+1.0+-+EN)
90
- [SFU client example description](https://docs.flashphoner.com/display/SS1E/SFU+client)
90
+ [SFU examples description](https://docs.flashphoner.com/display/SS1E/SFU+SDK+Examples)
91
91
  [API documentation](http://flashphoner.com/docs/api/WCS5/client/sfu-sdk/latest)
92
92
 
93
93
  ## Known issues
@@ -1,4 +1,4 @@
1
- SFU SDK - 1.0.1-36
1
+ SFU SDK - 1.0.41
2
2
 
3
3
  [Download builds](https://docs.flashphoner.com/display/SS1E/Release+notes)
4
4
 
package/package.json CHANGED
@@ -1,7 +1,10 @@
1
1
  {
2
2
  "name": "@flashphoner/sfusdk",
3
3
  "description": "Official Flashphoner WebCallServer SFU SDK package",
4
- "version": "1.0.1-36",
4
+ "version": "1.0.41",
5
+ "scripts": {
6
+ "test": "jest --runInBand"
7
+ },
5
8
  "dependencies": {
6
9
  "bootstrap": "^4.6.0",
7
10
  "datatables.net": "^1.10.24",
@@ -10,15 +13,17 @@
10
13
  "jquery-ui": "^1.12.1",
11
14
  "popper.js": "^1.16.1",
12
15
  "sdp-transform": "^2.14.0",
13
- "webrtc-adapter": "^7.2.6",
14
- "uuid": "^8.3.0"
16
+ "uuid": "^8.3.0",
17
+ "webrtc-adapter": "^7.2.6"
15
18
  },
16
19
  "devDependencies": {
17
20
  "grunt": "^1.0.1",
18
21
  "grunt-browserify": "^5.0.0",
19
22
  "grunt-contrib-clean": "^1.0.0",
20
23
  "grunt-contrib-copy": "^1.0.0",
21
- "grunt-jsdoc": "^2.4.0"
24
+ "grunt-jsdoc": "^2.4.0",
25
+ "grunt-run": "^0.8.1",
26
+ "jest": "^27.4.7"
22
27
  },
23
28
  "keywords": [
24
29
  "Flashphoner",
@@ -26,5 +31,8 @@
26
31
  "SFU SDK"
27
32
  ],
28
33
  "author": "Flashphoner",
29
- "license": "MIT"
34
+ "license": "MIT",
35
+ "jest" : {
36
+ "testEnvironment": "jsdom"
37
+ }
30
38
  }
@@ -100,6 +100,9 @@ const connect = function(state) {
100
100
  roomConfig.url = $("#url").val();
101
101
  roomConfig.roomName = $("#roomName").val();
102
102
  roomConfig.nickname = $("#" + state.inputId()).val();
103
+ // clean state display items
104
+ setStatus(state.statusId(), "");
105
+ setStatus(state.errInfoId(), "");
103
106
  // connect to server and create a room if not
104
107
  const session = sfu.createRoom(roomConfig);
105
108
  session.on(constants.SFU_EVENT.CONNECTED, function(room) {
@@ -56,16 +56,37 @@ const CurrentState = function(prefix) {
56
56
  pc: null,
57
57
  session: null,
58
58
  room: null,
59
+ timer: null,
59
60
  set: function(pc, session, room) {
60
61
  state.pc = pc;
61
62
  state.session = session;
62
63
  state.room = room;
63
64
  },
64
65
  clear: function() {
66
+ state.stopWaiting();
65
67
  state.room = null;
66
68
  state.session = null;
67
69
  state.pc = null;
68
70
  },
71
+ waitFor: function(div, timeout) {
72
+ state.stopWaiting();
73
+ state.timer = setTimeout(function () {
74
+ if (div.innerHTML !== "") {
75
+ // Enable stop button
76
+ $("#" + state.buttonId()).prop('disabled', false);
77
+ }
78
+ else if (state.isConnected()) {
79
+ setStatus(state.errInfoId(), "No media capturing started in " + timeout + " ms, stopping", "red");
80
+ onStopClick(state);
81
+ }
82
+ }, timeout);
83
+ },
84
+ stopWaiting: function() {
85
+ if (state.timer) {
86
+ clearTimeout(state.timer);
87
+ state.timer = null;
88
+ }
89
+ },
69
90
  buttonId: function() {
70
91
  return state.prefix + "Btn";
71
92
  },
@@ -86,6 +107,12 @@ const CurrentState = function(prefix) {
86
107
  },
87
108
  is: function(value) {
88
109
  return (prefix === value);
110
+ },
111
+ isActive: function() {
112
+ return (state.room && state.pc);
113
+ },
114
+ isConnected: function() {
115
+ return (state.session && state.session.state() == constants.SFU_STATE.CONNECTED);
89
116
  }
90
117
  };
91
118
  return state;
@@ -165,14 +192,12 @@ const onConnected = function(state) {
165
192
  // Add errors displaying
166
193
  state.room.on(constants.SFU_ROOM_EVENT.FAILED, function(e) {
167
194
  setStatus(state.errInfoId(), e, "red");
195
+ stopStreaming(state);
168
196
  }).on(constants.SFU_ROOM_EVENT.OPERATION_FAILED, function (e) {
169
197
  setStatus(state.errInfoId(), e.operation + " failed: " + e.error, "red");
198
+ stopStreaming(state);
170
199
  });
171
- if (state.is(PUBLISH)) {
172
- publishStreams(state);
173
- } else if (state.is(PLAY)) {
174
- playStreams(state);
175
- }
200
+ startStreaming(state);
176
201
  }
177
202
 
178
203
  const onDisconnected = function(state) {
@@ -204,65 +229,67 @@ const onStartClick = function(state) {
204
229
 
205
230
  const onStopClick = function(state) {
206
231
  $("#" + state.buttonId()).prop('disabled', true);
232
+ stopStreaming(state);
233
+ if (state.isConnected()) {
234
+ state.session.disconnect();
235
+ }
236
+ }
237
+
238
+ const startStreaming = function(state) {
207
239
  if (state.is(PUBLISH)) {
208
- unPublishStreams(state);
240
+ publishStreams(state);
209
241
  } else if (state.is(PLAY)) {
210
- stopStreams(state);
211
- }
212
- state.session.disconnect();
242
+ playStreams(state);
243
+ }
213
244
  }
214
245
 
215
- const publishStreams = async function(state) {
216
- let timerId;
217
- //create local display item to show local streams
218
- localDisplay = initLocalDisplay(document.getElementById("localVideo"));
219
- try {
220
- //get configured local video streams
221
- let streams = await getVideoStreams(mainConfig);
222
- let audioStreams = await getAudioStreams(mainConfig);
223
- //combine local video streams with audio streams
224
- streams.push.apply(streams, audioStreams);
225
- let config = {};
226
- //add our local streams to the room (to PeerConnection)
227
- streams.forEach(function (s) {
228
- //add local stream to local display
229
- localDisplay.add(s.stream.id, $("#" + state.inputId()).val(), s.stream);
230
- //add each track to PeerConnection
231
- s.stream.getTracks().forEach((track) => {
232
- if (s.source === "screen") {
233
- config[track.id] = s.source;
234
- }
235
- addTrackToPeerConnection(state.pc, s.stream, track, s.encodings);
236
- subscribeTrackToEndedEvent(state.room, track, state.pc);
237
- });
238
- });
239
- state.room.join(config);
240
- // TODO: Use room state or promises to detect if publishing started to enable stop button
241
- timerId = waitFor(document.getElementById("localVideo"), 3000, state);
242
- } catch(e) {
243
- console.error("Failed to capture streams: " + e);
244
- setStatus(state.errInfoId(), e.name, "red");
245
- if (timerId) {
246
- clearTimeout(timerId);
247
- timerId = null;
248
- }
249
- onStopClick(state);
246
+ const stopStreaming = function(state) {
247
+ state.stopWaiting();
248
+ if (state.is(PUBLISH)) {
249
+ unPublishStreams(state);
250
+ } else if (state.is(PLAY)) {
251
+ stopStreams(state);
250
252
  }
251
253
  }
252
254
 
253
- // A workaround to check if publishing or playback is started
254
- const waitFor = function (div, timeout, state) {
255
- let timerId = setTimeout(function () {
256
- if (div.innerHTML !== "") {
257
- // Enable stop button
258
- $("#" + state.buttonId()).prop('disabled', false);
259
- }
260
- else {
261
- setStatus(state.errInfoId(), "Something went wrong, stopping", "red");
262
- onStopClick(state);
255
+ const publishStreams = async function(state) {
256
+ if (state.isConnected()) {
257
+ //create local display item to show local streams
258
+ localDisplay = initLocalDisplay(document.getElementById("localVideo"));
259
+ try {
260
+ //get configured local video streams
261
+ let streams = await getVideoStreams(mainConfig);
262
+ let audioStreams = await getAudioStreams(mainConfig);
263
+ if (state.isConnected() && state.isActive()) {
264
+ //combine local video streams with audio streams
265
+ streams.push.apply(streams, audioStreams);
266
+ let config = {};
267
+ //add our local streams to the room (to PeerConnection)
268
+ streams.forEach(function (s) {
269
+ //add local stream to local display
270
+ localDisplay.add(s.stream.id, $("#" + state.inputId()).val(), s.stream);
271
+ //add each track to PeerConnection
272
+ s.stream.getTracks().forEach((track) => {
273
+ if (s.source === "screen") {
274
+ config[track.id] = s.source;
275
+ }
276
+ addTrackToPeerConnection(state.pc, s.stream, track, s.encodings);
277
+ subscribeTrackToEndedEvent(state.room, track, state.pc);
278
+ });
279
+ });
280
+ state.room.join(config);
281
+ // TODO: Use room state or promises to detect if publishing started to enable stop button
282
+ state.waitFor(document.getElementById("localVideo"), 3000);
283
+ }
284
+ } catch(e) {
285
+ console.error("Failed to capture streams: " + e);
286
+ setStatus(state.errInfoId(), e.name, "red");
287
+ state.stopWaiting();
288
+ if (state.isConnected()) {
289
+ onStopClick(state);
290
+ }
263
291
  }
264
- }, timeout);
265
- return timerId;
292
+ }
266
293
  }
267
294
 
268
295
  const unPublishStreams = function(state) {
@@ -272,9 +299,11 @@ const unPublishStreams = function(state) {
272
299
  }
273
300
 
274
301
  const playStreams = function(state) {
275
- //create remote display item to show remote streams
276
- remoteDisplay = initRemoteDisplay(document.getElementById("remoteVideo"), state.room, state.pc);
277
- state.room.join();
302
+ if (state.isConnected() && state.isActive()) {
303
+ //create remote display item to show remote streams
304
+ remoteDisplay = initRemoteDisplay(document.getElementById("remoteVideo"), state.room, state.pc);
305
+ state.room.join();
306
+ }
278
307
  $("#" + state.buttonId()).prop('disabled', false);
279
308
  }
280
309
 
@@ -56,6 +56,14 @@ const constants = Object.freeze({
56
56
  */
57
57
  USER_LIST: "USER_LIST",
58
58
 
59
+ //TODO(naz): documentation
60
+ USER_CHATS: "USER_CHATS",
61
+ CHAT_LOADED: "CHAT_LOADED",
62
+ NEW_CHAT: "NEW_CHAT",
63
+ CHAT_DELETED: "CHAT_DELETED",
64
+ CHAT_UPDATED: "CHAT_UPDATED",
65
+ MESSAGE_STATE: "MESSAGE_STATE",
66
+
59
67
  /**
60
68
  * @typedef {Object} UserCalendar
61
69
  * @property events {UserCalendarEvents} object with events
@@ -400,6 +408,11 @@ const constants = Object.freeze({
400
408
  MESSAGE_STATE: "SFU_MESSAGE_STATE",
401
409
  USER_LIST: "SFU_USER_LIST",
402
410
  USER_CALENDAR: "SFU_USER_CALENDAR",
411
+ USER_CHATS: "SFU_USER_CHATS",
412
+ CHAT_LOADED: "SFU_CHAT_LOADED",
413
+ NEW_CHAT: "SFU_NEW_CHAT",
414
+ CHAT_DELETED: "SFU_CHAT_DELETED",
415
+ CHAT_UPDATED: "SFU_UPDATE_CHAT",
403
416
  GET_USER_LIST: "getUserList",
404
417
  GET_USER_CALENDAR: "getUserCalendar",
405
418
  ADD_CALENDAR_EVENT: "addCalendarEvent",
@@ -411,7 +424,14 @@ const constants = Object.freeze({
411
424
  UNSUBSCRIBE_FROM_WAITING_PARTICIPANT: "unsubscribeFromWaitingParticipant",
412
425
  MOVE_TO_WAITING_ROOM: "moveToWaitingRoom",
413
426
  CONFIGURE_WAITING_ROOM: "configureWaitingRoom",
414
- TRACK_CONTENT_HEADER: "a=content:"
427
+ TRACK_CONTENT_HEADER: "a=content:",
428
+ GET_USER_CHATS: "getUserChats",
429
+ LOAD_CHAT: "loadChat",
430
+ CREATE_CHAT: "createChat",
431
+ DELETE_CHAT: "deleteChat",
432
+ RENAME_CHAT: "renameChat",
433
+ ADD_MEMBER_TO_CHAT: "addMemberToChat",
434
+ REMOVE_MEMBER_FROM_CHAT: "removeMemberFromChat"
415
435
  }),
416
436
  /**
417
437
  * @namespace FlashphonerSFUExtended.SFU_OPERATIONS
@@ -430,7 +450,15 @@ const constants = Object.freeze({
430
450
  ASSIGN_ROLE: "ASSIGN_ROLE",
431
451
  SUBSCRIBE_TO_WAITING_PARTICIPANT: "SUBSCRIBE_TO_WAITING_PARTICIPANT",
432
452
  UNSUBSCRIBE_FROM_WAITING_PARTICIPANT: "UNSUBSCRIBE_FROM_WAITING_PARTICIPANT",
433
- MOVE_TO_WAITING_ROOM: "MOVE_TO_WAITING_ROOM"
453
+ MOVE_TO_WAITING_ROOM: "MOVE_TO_WAITING_ROOM",
454
+ CREATE_CHAT: "CREATE_CHAT",
455
+ DELETE_CHAT: "DELETE_CHAT",
456
+ RENAME_CHAT: "RENAME_CHAT",
457
+ LOAD_CHAT: "LOAD_CHAT",
458
+ ADD_MEMBER_TO_CHAT: "ADD_MEMBER_TO_CHAT",
459
+ REMOVE_MEMBER_FROM_CHAT: "REMOVE_MEMBER_FROM_CHAT",
460
+ UPDATE_CHAT_PREFERENCES: "UPDATE_CHAT_PREFERENCES",
461
+ GET_USER_CHATS: "GET_USER_CHATS"
434
462
  }),
435
463
  /**
436
464
  * @namespace FlashphonerSFU.SFU_PARTICIPANT_ROLE
@@ -6,20 +6,31 @@ const create = function(options) {
6
6
 
7
7
  /*
8
8
  * @param {String} msg.to Recipient's id
9
+ * @param {String} msg.chatId Indicates chat this message belongs to
9
10
  * @param {String} msg.body Message body
10
11
  */
11
12
  const sendMessage = function(msg) {
12
13
  return new Promise(function(resolve, reject) {
13
- const message = {
14
- id: uuidv4(),
15
- to: msg.to,
16
- body: msg.body
17
- };
18
- connection.send("sendMessage", message);
19
- pendingMessages[message.id] = {
20
- resolve: resolve,
21
- reject: reject
22
- };
14
+ //validate
15
+ if (!msg) {
16
+ reject(new Error("Can't send null message"));
17
+ } else if (!msg.body || msg.body === "") {
18
+ reject(new Error("Can't send message without a body"));
19
+ } else if (!msg.chatId || msg.chatId === "") {
20
+ reject(new Error("Can't send message without a chatId"));
21
+ } else {
22
+ const message = {
23
+ id: uuidv4(),
24
+ to: msg.to,
25
+ body: msg.body,
26
+ chatId: msg.chatId
27
+ };
28
+ connection.send("sendMessage", message);
29
+ pendingMessages[message.id] = {
30
+ resolve: resolve,
31
+ reject: reject
32
+ };
33
+ }
23
34
  });
24
35
  };
25
36
 
@@ -28,7 +39,7 @@ const create = function(options) {
28
39
  if (promise) {
29
40
  delete pendingMessages[msg[0].status.id];
30
41
  if (msg[0].status.delivered) {
31
- promise.resolve();
42
+ promise.resolve(msg[0].status);
32
43
  } else {
33
44
  promise.reject();
34
45
  }
@@ -8,11 +8,21 @@ function add(id, resolve, reject) {
8
8
  }
9
9
 
10
10
  function resolve(id, args) {
11
- getAndRemovePromise(id).resolve(args);
11
+ const promise = getAndRemovePromise(id);
12
+ if (!promise) {
13
+ return false;
14
+ }
15
+ promise.resolve(args);
16
+ return true;
12
17
  }
13
18
 
14
19
  function reject(id, args) {
15
- getAndRemovePromise(id).reject(args);
20
+ const promise = getAndRemovePromise(id);
21
+ if (!promise) {
22
+ return false;
23
+ }
24
+ promise.reject(args);
25
+ return true;
16
26
  }
17
27
 
18
28
  function promised(id) {
@@ -22,7 +32,7 @@ function promised(id) {
22
32
  function getAndRemovePromise(id) {
23
33
  const promise = promises[id];
24
34
  if (!promise) {
25
- throw new Error("Don't have promise with id " + id)
35
+ return;
26
36
  }
27
37
  delete promises[id];
28
38
  return promise;
@@ -22,7 +22,6 @@ let state = SFU_STATE.NEW;
22
22
  let connectionConfig;
23
23
  let user;
24
24
  let im;
25
- let pendingUserList = [];
26
25
 
27
26
  function setupConnection(connection) {
28
27
  connection.onMessage = function(name, msg){
@@ -33,15 +32,28 @@ function setupConnection(connection) {
33
32
  notify(SFU_EVENT.MESSAGE, msg[0].message);
34
33
  } else if (msg[0].type === SFU_INTERNAL.MESSAGE_STATE) {
35
34
  im.onMessageState(msg);
35
+ notify(SFU_EVENT.MESSAGE_STATE, msg[0].status);
36
36
  } else if (msg[0].type === SFU_INTERNAL.USER_LIST) {
37
- while (pendingUserList.length > 0) {
38
- const promise = pendingUserList.pop();
39
- promise.resolve(msg[0].list);
40
- }
37
+ promises.resolve(msg[0].internalMessageId, msg[0].list);
41
38
  notify(SFU_EVENT.USER_LIST, msg[0].list);
42
39
  } else if (msg[0].type === SFU_INTERNAL.USER_CALENDAR) {
43
40
  promises.resolve(msg[0].internalMessageId, msg[0].calendar);
44
41
  notify(SFU_EVENT.USER_CALENDAR, msg[0].calendar);
42
+ } else if (msg[0].type === SFU_INTERNAL.NEW_CHAT) {
43
+ if(!promises.resolve(msg[0].internalMessageId, msg[0].info)) {
44
+ notify(SFU_EVENT.NEW_CHAT, msg[0].info);
45
+ }
46
+ } else if (msg[0].type === SFU_INTERNAL.CHAT_DELETED) {
47
+ notify(SFU_EVENT.CHAT_DELETED, msg[0].info);
48
+ } else if (msg[0].type === SFU_INTERNAL.CHAT_UPDATED) {
49
+ promises.resolve(msg[0].internalMessageId, msg[0].info);
50
+ notify(SFU_EVENT.CHAT_UPDATED, msg[0].info);
51
+ } else if (msg[0].type === SFU_INTERNAL.USER_CHATS) {
52
+ promises.resolve(msg[0].internalMessageId, msg[0].chats);
53
+ notify(SFU_EVENT.USER_CHATS, msg[0].chats);
54
+ } else if (msg[0].type === SFU_INTERNAL.CHAT_LOADED) {
55
+ promises.resolve(msg[0].internalMessageId, msg[0].chat);
56
+ notify(SFU_EVENT.CHAT_LOADED, msg[0].chat);
45
57
  } else if (msg[0].type === constants.SFU_ROOM_EVENT.OPERATION_FAILED && promises.promised(msg[0].internalMessageId)) {
46
58
  promises.reject(msg[0].internalMessageId, msg[0]);
47
59
  } else if (msg[0].type === constants.SFU_EVENT.ACK && promises.promised(msg[0].internalMessageId)) {
@@ -128,7 +140,8 @@ const connect = function(options) {
128
140
  /**
129
141
  * Send message
130
142
  * @param {Object} msg Message
131
- * @param {String} msg.to Recipient's id
143
+ * @param {String} msg.to Recipient's id (deprecated, use chatId instead)
144
+ * @param {String} msg.chatId Indicates chat this message belongs to
132
145
  * @param {String} msg.body Message body
133
146
  * @returns {Promise} Promise will resolve upon message delivery and reject if delivery was unsuccessful
134
147
  * @throws {Error} error if api isn't connected
@@ -144,26 +157,18 @@ const sendMessage = function(msg) {
144
157
  /**
145
158
  * Fetch available user list from server
146
159
  * @returns {Promise<Array<FlashphonerSFUExtended.UserListEntry>>}
147
- * @throws {Error} error if api isn't connected
148
160
  * @memberOf FlashphonerSFUExtended
149
161
 
150
162
  */
151
163
  const getUserList = function() {
152
- if (state !== SFU_STATE.AUTHENTICATED) {
153
- throw new Error("Can't get user list while in " + state + " state");
154
- }
155
164
  return new Promise(function (resolve, reject){
156
165
  if (state !== SFU_STATE.AUTHENTICATED) {
157
166
  reject(new Error("Can't get user list while in " + state + " state"));
158
167
  return;
159
168
  }
160
- if (pendingUserList.length > 0) {
161
- pendingUserList.push({resolve: resolve, reject: reject});
162
- return;
163
- } else {
164
- pendingUserList.push({resolve: resolve, reject: reject});
165
- }
166
- connection.send(constants.SFU_INTERNAL_API.GET_USER_LIST);
169
+ const id = uuidv4();
170
+ promises.add(id, resolve, reject);
171
+ connection.send(constants.SFU_INTERNAL_API.GET_USER_LIST, {internalMessageId: id});
167
172
  });
168
173
  };
169
174
 
@@ -229,6 +234,126 @@ const removeCalendarEvent = function(event) {
229
234
  });
230
235
  };
231
236
 
237
+ /**
238
+ * Fetch available chats from server
239
+ * @returns {Promise<Array<FlashphonerSFUExtended.UserCalendar>>}
240
+ * @memberOf FlashphonerSFUExtended
241
+ */
242
+ const getUserChats = function() {
243
+ return new Promise(function (resolve, reject){
244
+ if (state !== SFU_STATE.AUTHENTICATED) {
245
+ reject(new Error("Can't get user chats while in " + state + " state"));
246
+ return;
247
+ }
248
+ const id = uuidv4();
249
+ promises.add(id, resolve, reject);
250
+ connection.send(constants.SFU_INTERNAL_API.GET_USER_CHATS, {internalMessageId: id});
251
+ });
252
+ };
253
+
254
+ /**
255
+ * Fetch chat data from server
256
+ * @returns {Promise<Array<FlashphonerSFUExtended.UserCalendar>>}
257
+ * @memberOf FlashphonerSFUExtended
258
+ */
259
+ const loadChat = function(chat) {
260
+ return new Promise(function (resolve, reject){
261
+ if (state !== SFU_STATE.AUTHENTICATED) {
262
+ reject(new Error("Can't load chats while in " + state + " state"));
263
+ return;
264
+ }
265
+ const id = uuidv4();
266
+ promises.add(id, resolve, reject);
267
+ connection.send(constants.SFU_INTERNAL_API.LOAD_CHAT, {id: chat.id, internalMessageId: id});
268
+ });
269
+ };
270
+
271
+ /**
272
+ * Create chat
273
+ * @returns {Promise<Array<FlashphonerSFUExtended.UserCalendar>>}
274
+ * @memberOf FlashphonerSFUExtended
275
+ */
276
+ const createChat = function(chat) {
277
+ return new Promise(function (resolve, reject){
278
+ if (state !== SFU_STATE.AUTHENTICATED) {
279
+ reject(new Error("Can't create chats while in " + state + " state"));
280
+ return;
281
+ }
282
+ const id = uuidv4();
283
+ promises.add(id, resolve, reject);
284
+ connection.send(constants.SFU_INTERNAL_API.CREATE_CHAT, {id: chat.id, name: chat.name, members: chat.members, internalMessageId: id});
285
+ });
286
+ };
287
+
288
+ /**
289
+ * Delete chat
290
+ * @returns {Promise<Array<FlashphonerSFUExtended.UserCalendar>>}
291
+ * @memberOf FlashphonerSFUExtended
292
+ */
293
+ const deleteChat = function(chat) {
294
+ return new Promise(function (resolve, reject){
295
+ if (state !== SFU_STATE.AUTHENTICATED) {
296
+ reject(new Error("Can't delete chats while in " + state + " state"));
297
+ return;
298
+ }
299
+ const id = uuidv4();
300
+ promises.add(id, resolve, reject);
301
+ connection.send(constants.SFU_INTERNAL_API.DELETE_CHAT, {id: chat.id, internalMessageId: id});
302
+ });
303
+ };
304
+
305
+ /**
306
+ * Rename chat
307
+ * @returns {Promise<Array<FlashphonerSFUExtended.UserCalendar>>}
308
+ * @memberOf FlashphonerSFUExtended
309
+ */
310
+ const renameChat = function(chat) {
311
+ return new Promise(function (resolve, reject){
312
+ if (state !== SFU_STATE.AUTHENTICATED) {
313
+ reject(new Error("Can't rename chats while in " + state + " state"));
314
+ return;
315
+ }
316
+ const id = uuidv4();
317
+ promises.add(id, resolve, reject);
318
+ connection.send(constants.SFU_INTERNAL_API.RENAME_CHAT, {id: chat.id, name: chat.name, internalMessageId: id});
319
+ });
320
+ };
321
+
322
+ /**
323
+ * Add member to chat
324
+ * @returns {Promise<Array<FlashphonerSFUExtended.UserCalendar>>}
325
+ * @memberOf FlashphonerSFUExtended
326
+ */
327
+ const addMemberToChat = function(chat) {
328
+ return new Promise(function (resolve, reject){
329
+ if (state !== SFU_STATE.AUTHENTICATED) {
330
+ reject(new Error("Can't add member to chat while in " + state + " state"));
331
+ return;
332
+ }
333
+ const id = uuidv4();
334
+ promises.add(id, resolve, reject);
335
+ connection.send(constants.SFU_INTERNAL_API.ADD_MEMBER_TO_CHAT, {id: chat.id, member: chat.member, internalMessageId: id});
336
+ });
337
+ };
338
+
339
+ /**
340
+ * Remove member from chat
341
+ * @returns {Promise<Array<FlashphonerSFUExtended.UserCalendar>>}
342
+ * @memberOf FlashphonerSFUExtended
343
+ */
344
+ const removeMemberFromChat = function(chat) {
345
+ return new Promise(function (resolve, reject){
346
+ if (state !== SFU_STATE.AUTHENTICATED) {
347
+ reject(new Error("Can't remove member to chat while in " + state + " state"));
348
+ return;
349
+ }
350
+ const id = uuidv4();
351
+ promises.add(id, resolve, reject);
352
+ connection.send(constants.SFU_INTERNAL_API.REMOVE_MEMBER_FROM_CHAT, {id: chat.id, member: chat.member, internalMessageId: id});
353
+ });
354
+ };
355
+
356
+
232
357
  /**
233
358
  * Create room
234
359
  *
@@ -326,8 +451,6 @@ const disconnect = function() {
326
451
  value.leaveRoom();
327
452
  }
328
453
  user = undefined;
329
- pendingUserList = [];
330
- pendingUserCalendar = [];
331
454
  connection.close();
332
455
  connection = ws.createConnection();
333
456
  state = SFU_STATE.DISCONNECTED;
@@ -341,6 +464,13 @@ sfu.getUserList = getUserList;
341
464
  sfu.getUserCalendar = getUserCalendar;
342
465
  sfu.addCalendarEvent = addCalendarEvent;
343
466
  sfu.removeCalendarEvent = removeCalendarEvent;
467
+ sfu.getUserChats = getUserChats;
468
+ sfu.loadChat = loadChat;
469
+ sfu.createChat = createChat;
470
+ sfu.deleteChat = deleteChat;
471
+ sfu.renameChat = renameChat;
472
+ sfu.addMemberToChat = addMemberToChat;
473
+ sfu.removeMemberFromChat = removeMemberFromChat;
344
474
  sfu.room = room;
345
475
 
346
476
 
@@ -0,0 +1,151 @@
1
+ const sfu = require("../../sdk/sfu-extended");
2
+ const constants = require("../../sdk/constants");
3
+
4
+ const SERVER = "ws://127.0.0.1:8080";
5
+ const USER = "bob@flashphoner.com";
6
+ const PASSWD = "123456";
7
+ const CHAT_ID = "test-chat-id";
8
+ const CHAT_NAME = "test-chat";
9
+ const CHAT_NAME2 = "test-chat2";
10
+ const USER2 = "alice@flashphoner.com";
11
+ const MESSAGE_BODY = "This is a test message";
12
+ const MEMBERS = [
13
+ "bob@flashphoner.com",
14
+ "alice@flashphoner.com",
15
+ "kiri@flashphoner.com"
16
+ ];
17
+
18
+ beforeAll( async () => {
19
+ await sfu.connect({
20
+ url: SERVER,
21
+ username: USER,
22
+ password: PASSWD
23
+ });
24
+ const chats = await sfu.getUserChats();
25
+ if (chats) {
26
+ Object.entries(chats).map(async ([key, value]) => {
27
+ await sfu.deleteChat({id: value.id});
28
+ });
29
+ }
30
+ });
31
+
32
+ afterAll(() => {
33
+ return sfu.disconnect();
34
+ });
35
+
36
+
37
+ describe("connection", () => {
38
+ test("should connect to server", () => {
39
+ expect(sfu.state()).toBe(constants.SFU_STATE.AUTHENTICATED);
40
+ });
41
+ });
42
+
43
+ describe("contacts", () => {
44
+ test("should load user list", () => {
45
+ return sfu.getUserList();
46
+ });
47
+ });
48
+
49
+ describe("calendar", () => {
50
+ test("should load calendar", () => {
51
+ return sfu.getUserCalendar().then((calendar) => {
52
+ expect(calendar).toHaveProperty("events");
53
+ })
54
+ })
55
+ });
56
+
57
+ describe("chat", () => {
58
+ test("should create new chat", () => {
59
+ return sfu.createChat({
60
+ id: CHAT_ID,
61
+ name: CHAT_NAME
62
+ });
63
+ });
64
+ test("should load chats", () => {
65
+ return sfu.getUserChats().then((chats) => {
66
+ expect(chats).toBeTruthy();
67
+ });
68
+ });
69
+ test("should add new member", () => {
70
+ return sfu.addMemberToChat({
71
+ id: CHAT_ID,
72
+ member: USER2
73
+ });
74
+ });
75
+ test("should send a message", () => {
76
+ return sfu.sendMessage({
77
+ chatId: CHAT_ID,
78
+ body: MESSAGE_BODY
79
+ })
80
+ });
81
+ test("should load chat", () => {
82
+ return sfu.loadChat({
83
+ id: CHAT_ID
84
+ }).then((chat) => {
85
+ expect(chat.id).toBe(CHAT_ID);
86
+ expect(chat.name).toBe(CHAT_NAME);
87
+ expect(chat.members).toContain(USER);
88
+ expect(Array.isArray(chat.messages)).toBe(true);
89
+ expect(chat.messages.length).toBeGreaterThan(0);
90
+ });
91
+ });
92
+ test("should remove member", () => {
93
+ return sfu.removeMemberFromChat({
94
+ id: CHAT_ID,
95
+ member: USER2
96
+ });
97
+ });
98
+ test("should rename chat", () => {
99
+ return sfu.renameChat({
100
+ id: CHAT_ID,
101
+ name: CHAT_NAME2
102
+ });
103
+ });
104
+ test.skip("should receive message", (done) => {
105
+ sfu.on(constants.SFU_EVENT.MESSAGE, (msg) => {
106
+ done();
107
+ })
108
+ sfu.sendMessage({
109
+ chatId: CHAT_ID,
110
+ body: MESSAGE_BODY
111
+ });
112
+ });
113
+ test("should receive ACK message", (done) => {
114
+ sfu.on(constants.SFU_EVENT.MESSAGE_STATE, (state) => {
115
+ done();
116
+ })
117
+ sfu.sendMessage({
118
+ chatId: CHAT_ID,
119
+ body: MESSAGE_BODY
120
+ });
121
+ });
122
+ test("should create new chat without id", () => {
123
+ return sfu.createChat({
124
+ name: CHAT_NAME
125
+ });
126
+ });
127
+ test("should create new chat with member list", () => {
128
+ return sfu.createChat({
129
+ name: CHAT_NAME,
130
+ members: MEMBERS
131
+ });
132
+ });
133
+ test("should create new chat without name based on members", async () => {
134
+ const chat = await sfu.createChat({
135
+ members: MEMBERS
136
+ });
137
+ expect(chat.name).toBeTruthy();
138
+ });
139
+ test("should delete chat", () => {
140
+ return sfu.deleteChat({
141
+ id: CHAT_ID
142
+ });
143
+ });
144
+ test("should change name after member was added", async () => {
145
+ const chat = await sfu.createChat({});
146
+ expect(chat.name).toBeTruthy();
147
+ const name1 = chat.name;
148
+ const sameChat = await sfu.addMemberToChat({id: chat.id, member: USER2});
149
+ expect(sameChat.name !== name1).toBeTruthy();
150
+ });
151
+ });