@subiz/widget-v4 4.1.215 → 4.1.217

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.
Files changed (57) hide show
  1. package/README.md +6 -0
  2. package/api.md +9 -5
  3. package/assets/img/bubble_default.svg +1 -0
  4. package/assets/less/message.less +83 -0
  5. package/assets/less/widget.less +12 -0
  6. package/compiled_src/chatbox/AudioAttachments.js +5 -0
  7. package/compiled_src/chatbox/ChatButton.js +19 -17
  8. package/compiled_src/chatbox/Event.js +9 -2
  9. package/compiled_src/chatbox/FileAttachments.js +9 -3
  10. package/compiled_src/chatbox/ImageAttachments.js +16 -6
  11. package/compiled_src/chatbox/UploadProgress.js +44 -0
  12. package/compiled_src/chatbox/VideoAttachments.js +5 -0
  13. package/compiled_src/store/api.js +77 -78
  14. package/compiled_src/store/api_fake.js +1 -1
  15. package/compiled_src/store/attachments.js +5 -1
  16. package/compiled_src/store/store.js +25 -4
  17. package/dist/{1a12a5ed6a9.278.js → 1a12ed33347.278.js} +1 -1
  18. package/dist/1a12ed33347.app.js +1 -0
  19. package/dist/{1a12a5ed6a9.chat.html → 1a12ed33347.chat.html} +1 -1
  20. package/dist/1a12ed33347.chat.js +1 -0
  21. package/dist/{1a12a5ed6a9.form.html → 1a12ed33347.form.html} +1 -1
  22. package/dist/{1a12a5ed6a9.form.js → 1a12ed33347.form.js} +1 -1
  23. package/dist/{1a12a5ed6a9.index.html → 1a12ed33347.index.html} +1 -1
  24. package/dist/{1a12a5ed6a9.library.html → 1a12ed33347.library.html} +1 -1
  25. package/dist/1a12ed33347.library.js +1 -0
  26. package/dist/{1a12a5ed6a9.preview.html → 1a12ed33347.preview.html} +1 -1
  27. package/dist/1a12ed33347.preview.js +1 -0
  28. package/dist/1a12ed33347.webview.js +1 -0
  29. package/dist/HASH +1 -0
  30. package/dist/assets/img/{bubble_default.7d5e4ab.svg → bubble_default.6f6eec8.svg} +1 -0
  31. package/dist/screenshoot.js +25 -20
  32. package/package.json +11 -10
  33. package/src/chatbox/AudioAttachments.js +2 -0
  34. package/src/chatbox/ChatButton.js +22 -20
  35. package/src/chatbox/Event.js +8 -4
  36. package/src/chatbox/FileAttachments.js +5 -2
  37. package/src/chatbox/ImageAttachments.js +6 -2
  38. package/src/chatbox/UploadProgress.js +21 -0
  39. package/src/chatbox/VideoAttachments.js +2 -0
  40. package/src/store/api.js +66 -81
  41. package/src/store/api_fake.js +1 -1
  42. package/src/store/attachments.js +5 -1
  43. package/src/store/store.js +13 -5
  44. package/up +18 -0
  45. package/dist/.HASH +0 -1
  46. package/dist/1a12a5ed6a9.app.js +0 -1
  47. package/dist/1a12a5ed6a9.chat.js +0 -1
  48. package/dist/1a12a5ed6a9.library.js +0 -1
  49. package/dist/1a12a5ed6a9.preview.js +0 -1
  50. package/dist/1a12a5ed6a9.webview.js +0 -1
  51. /package/dist/{1a12a5ed6a9.127.js → 1a12ed33347.127.js} +0 -0
  52. /package/dist/{1a12a5ed6a9.132.js → 1a12ed33347.132.js} +0 -0
  53. /package/dist/{1a12a5ed6a9.185.js → 1a12ed33347.185.js} +0 -0
  54. /package/dist/{1a12a5ed6a9.27.js → 1a12ed33347.27.js} +0 -0
  55. /package/dist/{1a12a5ed6a9.339.js → 1a12ed33347.339.js} +0 -0
  56. /package/dist/{1a12a5ed6a9.459.js → 1a12ed33347.459.js} +0 -0
  57. /package/dist/{1a12a5ed6a9.490.js → 1a12ed33347.490.js} +0 -0
package/README.md CHANGED
@@ -6,6 +6,12 @@ $ npm run dev
6
6
 
7
7
  visit: http://localhost:8082/?account_id=acqcsmrppbftadjzxnvo
8
8
 
9
+ The fallback button keeps the configured width and height and has an opaque
10
+ white backing. Each dimension defaults to 70px only when it is not configured.
11
+
12
+ Masks use data URLs rather than blob URLs. If data URLs are blocked, the widget
13
+ tries the original image URL with CORS before using the fallback button.
14
+
9
15
  ### To test widget in injection mode
10
16
 
11
17
  ``` sh
package/api.md CHANGED
@@ -1264,6 +1264,7 @@ nghiệp vụ của form do website thực hiện.
1264
1264
  subiz('waitReady', async function () {
1265
1265
  try {
1266
1266
  const result = await subiz('getFormStatus', 'website_contact')
1267
+ // result.values: [{key: 'fullname', label: 'Họ tên', value: 'Nguyễn Văn An'}, ...]
1267
1268
  if (result.submitted) document.getElementById('contact-form').hidden = true
1268
1269
  } catch (error) {
1269
1270
  // Không ẩn form khi kiểm tra thất bại.
@@ -1272,11 +1273,13 @@ subiz('waitReady', async function () {
1272
1273
  ```
1273
1274
 
1274
1275
  `getFormStatus(formId)` đọc qua store KV/LSDB của widget, không gọi API trạng thái
1275
- hay lấy hội thoại. Sau khi submit thành công, SDK lưu dấu đã gửi trong 7 ngày,
1276
- tách theo account, ID khách và ID form. Chỉ lưu mã hội thoại và thời điểm hết hạn,
1277
- không lưu các giá trị khách nhập. Đọc trạng thái không gia hạn thời gian lưu.
1276
+ hay lấy hội thoại. Sau khi submit thành công, SDK lưu dấu đã gửi và nội dung form
1277
+ trong 7 ngày, tách theo account, ID khách và ID form. Bản ghi gồm mã hội thoại,
1278
+ thời điểm hết hạn và `values` của lần gửi thành công gần nhất, theo dạng
1279
+ `[{key, label, value}]` với giá trị đã chuẩn hóa thành chuỗi như khi gửi.
1280
+ Đọc trạng thái không gia hạn thời gian lưu. Lần gửi thất bại không ghi đè dữ liệu cũ.
1278
1281
 
1279
- Kết quả: `{submitted: true, status: 'found', conversation_id}` nếu còn hạn;
1282
+ Kết quả: `{submitted: true, status: 'found', conversation_id, values}` nếu còn hạn;
1280
1283
  `{submitted: false, status: 'not_found'}` nếu chưa lưu; `status: 'expired'` khi
1281
1284
  hết hạn (đồng thời xóa bản ghi); `status: 'storage_unavailable'` khi trình duyệt
1282
1285
  không cho truy cập localStorage. Lỗi lưu localStorage không biến một submit đã
@@ -1286,7 +1289,8 @@ SDK bỏ qua sự kiện realtime của hội thoại kênh form trước khi l
1286
1289
  thoại; không mở hội thoại form trong chat widget. Luồng chat thông thường giữ
1287
1290
  nguyên. Trạng thái chỉ có trên cùng website/trình duyệt và cùng ID khách, không
1288
1291
  đồng bộ thiết bị khác, mất khi xóa localStorage. Các lần gửi trước khi bổ sung
1289
- cơ chế lưu này không được tự khôi phục từ server.
1292
+ cơ chế lưu này không được tự khôi phục từ server. Bản ghi cũ chỉ lưu dấu đã gửi
1293
+ vẫn trả `submitted: true` nếu còn hạn, nhưng `values: []` vì chưa lưu nội dung.
1290
1294
 
1291
1295
  SDK không truy cập localStorage trực tiếp: store dùng `kv.put/match/del` và
1292
1296
  LSDB hiện có. Khi widget dùng chế độ in-memory hoặc trình duyệt chặn lưu trữ,
@@ -7,6 +7,7 @@
7
7
  </style>
8
8
  </defs>
9
9
  <title>Asset 5</title>
10
+ <circle cx="30" cy="30" r="30" fill="white"/>
10
11
  <g id="Layer_2" data-name="Layer 2">
11
12
  <g id="Layer_1-2" data-name="Layer 1">
12
13
  <g>
@@ -303,6 +303,89 @@ a.message_error {
303
303
  border: 1px solid @gray-200;
304
304
  }
305
305
 
306
+ .image-attachment__preview {
307
+ position: relative;
308
+ display: inline-block;
309
+ vertical-align: top;
310
+ margin-top: 5px;
311
+ border-radius: 7px;
312
+ overflow: hidden;
313
+ max-width: 100%;
314
+
315
+ .image-attachment__img {
316
+ display: block;
317
+ margin: 0;
318
+ }
319
+ }
320
+
321
+ .image-attachment__sending {
322
+ position: absolute;
323
+ inset: 0;
324
+ display: flex;
325
+ align-items: center;
326
+ justify-content: center;
327
+ cursor: wait;
328
+ background: rgba(0, 0, 0, 0.28);
329
+
330
+ &::before {
331
+ content: '';
332
+ position: absolute;
333
+ inset: 0;
334
+ background: @primary-color;
335
+ opacity: 0.3;
336
+ }
337
+ }
338
+
339
+ .image-attachment__progress {
340
+ position: relative;
341
+ width: 96px;
342
+ max-width: 80%;
343
+ color: #fff;
344
+ text-align: center;
345
+ }
346
+
347
+ .image-attachment__progress-label {
348
+ display: block;
349
+ font-size: 13px;
350
+ font-weight: 600;
351
+ margin-bottom: 6px;
352
+ font-variant-numeric: tabular-nums;
353
+ }
354
+
355
+ .image-attachment__progress-track {
356
+ display: block;
357
+ height: 4px;
358
+ border-radius: 4px;
359
+ overflow: hidden;
360
+ background: rgba(255, 255, 255, 0.35);
361
+ }
362
+
363
+ .image-attachment__progress-fill {
364
+ display: block;
365
+ height: 100%;
366
+ border-radius: inherit;
367
+ background: #fff;
368
+ }
369
+
370
+ .file-attachment--sending {
371
+ cursor: wait;
372
+
373
+ .file-attachment--filename { text-decoration: none; }
374
+ }
375
+
376
+ .file-attachment__progress {
377
+ display: block;
378
+ width: 100%;
379
+ min-width: 120px;
380
+ max-width: 100%;
381
+ margin-top: 8px;
382
+ color: @primary-color;
383
+ text-align: right;
384
+
385
+ .image-attachment__progress-track { background: rgba(0, 0, 0, 0.1); }
386
+ .image-attachment__progress-fill { background: @primary-color; }
387
+ }
388
+
306
389
  .audio-attachment {
307
390
  width: 100%;
308
391
  margin: 5px 0 0 0;
@@ -438,6 +438,18 @@
438
438
  // min-height: @desktop-chat-button-height;
439
439
  }
440
440
 
441
+ .chat-button--fallback {
442
+ .chat-button--img-container {
443
+ background-size: 100% 100%;
444
+ }
445
+
446
+ .chat-button--img {
447
+ width: 100%;
448
+ min-width: 0;
449
+ height: 100%;
450
+ }
451
+ }
452
+
441
453
  .chat-button.chat-button--left {
442
454
  float: left;
443
455
  }
@@ -4,6 +4,7 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.default = AudioAttachments;
7
+ var _UploadProgress = require("./UploadProgress.js");
7
8
  var _index = _interopRequireDefault(require("../util/index.js"));
8
9
  var _preact = require("preact");
9
10
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
@@ -17,6 +18,10 @@ function AudioAttachments(props) {
17
18
  });
18
19
  if (audios.length === 0) return null;
19
20
  var $audios = _index.default.map(audios, (audio, index) => {
21
+ if (props.sending) return (0, _preact.h)(_UploadProgress.UploadingFile, {
22
+ key: index,
23
+ attachment: audio
24
+ });
20
25
  return (0, _preact.h)("div", {
21
26
  key: index,
22
27
  class: `audio-attachment audio-attachment--${byType}`
@@ -400,18 +400,10 @@ class ChatButton extends _common.Com {
400
400
  this.readyGate.close();
401
401
  const [loaded, mask] = await Promise.all([Promise.all(appearance.urls.filter(url => url !== appearance.maskAsset).map(url => preloadButtonImage(url))), appearance.maskAsset ? prepareButtonMask(appearance.maskAsset) : Promise.resolve(null)]);
402
402
  const stale = () => !this._isMounted || this._buttonImageVersion !== version;
403
- if (stale()) {
404
- if (mask) URL.revokeObjectURL(mask);
405
- return;
406
- }
403
+ if (stale()) return;
407
404
  const fallback = loaded.some(success => !success) || !!appearance.maskAsset && !mask;
408
405
  const ready = !fallback || (await preloadButtonImage(defaultBubble));
409
- if (stale()) {
410
- if (mask) URL.revokeObjectURL(mask);
411
- return;
412
- }
413
- if (this._buttonMaskUrl) URL.revokeObjectURL(this._buttonMaskUrl);
414
- this._buttonMaskUrl = mask;
406
+ if (stale()) return;
415
407
  if (this._buttonElement) window.getComputedStyle(this._buttonElement).transform;
416
408
  this.setState({
417
409
  ready,
@@ -422,7 +414,6 @@ class ChatButton extends _common.Com {
422
414
  this.readyGate.open();
423
415
  }
424
416
  componentWillUnmount() {
425
- if (this._buttonMaskUrl) URL.revokeObjectURL(this._buttonMaskUrl);
426
417
  if (this._animateTimeout) clearTimeout(this._animateTimeout);
427
418
  }
428
419
  renderButton() {
@@ -437,11 +428,12 @@ class ChatButton extends _common.Com {
437
428
  imgwidth,
438
429
  imgheight
439
430
  } = appearance;
431
+ const fallback = this.state.readyImageKey === appearance.key && this.state.fallbackButtonImage;
440
432
  if (appearance.maskAsset) {
441
433
  maskurl = this.state.readyImageKey === appearance.key ? this.state.buttonMaskUrl : '';
442
434
  if (imgsrc === appearance.maskAsset) imgsrc = maskurl || '';
443
435
  }
444
- if (this.state.readyImageKey === appearance.key && this.state.fallbackButtonImage) {
436
+ if (fallback) {
445
437
  imgsrc = imgurl = defaultBubble;
446
438
  maskurl = '';
447
439
  }
@@ -501,7 +493,11 @@ class ChatButton extends _common.Com {
501
493
  mask-size: cover !important;
502
494
  `;
503
495
  }
496
+ if (fallback) {
497
+ imgcontainerstyle += `width: ${imgwidth}px !important; height: ${imgheight}px !important;`;
498
+ }
504
499
  let cls = 'chat-button chat-button--' + position;
500
+ if (fallback) cls += ' chat-button--fallback';
505
501
  let whiteBgStyle = `
506
502
  width: ${imgwidth - 4}px !important;
507
503
  height: ${imgheight - 4}px !important;
@@ -624,13 +620,19 @@ function preloadButtonImage(src, {
624
620
  });
625
621
  }
626
622
  async function prepareButtonMask(src) {
627
- let url;
628
623
  try {
629
624
  const response = await fetch(src);
630
625
  if (!response.ok) return null;
631
- url = URL.createObjectURL(await response.blob());
632
- if (await preloadButtonImage(url)) return url;
626
+ const blob = await response.blob();
627
+ const url = await new Promise(resolve => {
628
+ const reader = new FileReader();
629
+ reader.onload = () => resolve(reader.result);
630
+ reader.onerror = reader.onabort = () => resolve('');
631
+ reader.readAsDataURL(blob);
632
+ });
633
+ if (url && (await preloadButtonImage(url))) return url;
633
634
  } catch (_) {}
634
- if (url) URL.revokeObjectURL(url);
635
- return null;
635
+ return (await preloadButtonImage(src, {
636
+ crossOrigin: true
637
+ })) ? src : null;
636
638
  }
@@ -379,6 +379,9 @@ class Event extends _common.Com {
379
379
  class: "sending_dot sending_dot__hide"
380
380
  });
381
381
  }
382
+ const hasFile = _index.default.find(attachments, att => att.type === 'file' || att._file);
383
+ const sendingAttachment = !ev._error && ev.source_event_id === ev.id;
384
+ if (hasFile) $dot = null;
382
385
  let $sender = null;
383
386
  if (this.props.event.mode === 'lead' && _index.default.get(ev, 'by.type') !== 'user') $sender = (0, _preact.h)("div", {
384
387
  class: "pt-3"
@@ -413,16 +416,20 @@ class Event extends _common.Com {
413
416
  }), (0, _preact.h)(_ImageAttachments.default, {
414
417
  store: this.props.store,
415
418
  ev: ev,
419
+ sending: sendingAttachment,
416
420
  ref: this.imageAttachmentRef
417
421
  }), (0, _preact.h)(_AudioAttachments.default, {
418
422
  store: this.props.store,
419
- ev: ev
423
+ ev: ev,
424
+ sending: sendingAttachment
420
425
  }), (0, _preact.h)(_VideoAttachments.default, {
421
426
  store: this.props.store,
422
- ev: ev
427
+ ev: ev,
428
+ sending: sendingAttachment
423
429
  }), (0, _preact.h)(_FileAttachments.default, {
424
430
  store: this.props.store,
425
431
  ev: ev,
432
+ sending: sendingAttachment,
426
433
  ref: this.fileAttachmentRef
427
434
  }), (0, _preact.h)(_ButtonAttachments.default, {
428
435
  store: this.props.store,
@@ -4,6 +4,7 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.default = void 0;
7
+ var _UploadProgress = _interopRequireDefault(require("./UploadProgress.js"));
7
8
  var _index = _interopRequireDefault(require("../util/index.js"));
8
9
  var _common = require("../common.js");
9
10
  var _preact = require("preact");
@@ -33,19 +34,24 @@ class FileAttachments extends _common.Com {
33
34
  if (files.length === 0) return null;
34
35
  let $files = _index.default.map(files, (file, index) => {
35
36
  let cls = `file-attachment file-attachment--${byType}`;
37
+ if (this.props.sending) cls += ' file-attachment--sending';
36
38
  if (file._true_idx === this.state.focusedIdx) cls += ` just-focused`;
37
39
  return (0, _preact.h)("div", {
38
40
  key: index,
39
- class: cls
41
+ class: cls,
42
+ "aria-busy": !!this.props.sending
40
43
  }, (0, _preact.h)("a", {
41
44
  class: "file-attachment--link",
42
- href: this.props.store.replaceFileUrl(file.url),
45
+ href: this.props.sending ? undefined : this.props.store.replaceFileUrl(file.url),
43
46
  target: "_blank"
44
47
  }, (0, _preact.h)("span", {
45
48
  class: "file-attachment--download mr-2"
46
49
  }), (0, _preact.h)("span", {
47
50
  class: "file-attachment--filename"
48
- }, file.name)));
51
+ }, file.name)), this.props.sending && (0, _preact.h)(_UploadProgress.default, {
52
+ attachment: file,
53
+ compact: true
54
+ }));
49
55
  });
50
56
  return (0, _preact.h)("div", {
51
57
  class: `message-${byType}`
@@ -4,6 +4,7 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.default = void 0;
7
+ var _UploadProgress = _interopRequireDefault(require("./UploadProgress.js"));
7
8
  var _index = _interopRequireDefault(require("../util/index.js"));
8
9
  var _common = require("../common.js");
9
10
  var _preact = require("preact");
@@ -95,18 +96,25 @@ class ImageAttachment extends _common.Com {
95
96
  if (byType !== 'user') byType = 'agent';
96
97
  let cls = `image-attachment image-attachment--${byType}`;
97
98
  if (image._true_idx === this.state.focusedIdx) cls += ' just-focused';
99
+ const $sending = this.props.sending ? (0, _preact.h)("span", {
100
+ class: "image-attachment__sending"
101
+ }, (0, _preact.h)(_UploadProgress.default, {
102
+ attachment: image
103
+ })) : null;
98
104
  if (!image.width || !image.height) {
99
105
  return (0, _preact.h)("div", {
100
106
  class: cls
101
107
  }, (0, _preact.h)("a", {
102
- href: this.props.store.replaceFileUrl(image.url),
103
- target: "_blank"
108
+ class: "image-attachment__preview",
109
+ href: this.props.sending ? undefined : this.props.store.replaceFileUrl(image.url),
110
+ target: "_blank",
111
+ "aria-busy": !!this.props.sending
104
112
  }, (0, _preact.h)("img", {
105
113
  alt: true,
106
114
  class: "image-attachment__img",
107
115
  src: this.props.store.replaceFileUrl(image.thumbnail_url || image.url),
108
116
  onload: e => this.props.onLoad && this.props.onLoad()
109
- })));
117
+ }), $sending));
110
118
  }
111
119
  let {
112
120
  width,
@@ -121,8 +129,10 @@ class ImageAttachment extends _common.Com {
121
129
  return (0, _preact.h)("div", {
122
130
  class: cls
123
131
  }, (0, _preact.h)("a", {
124
- href: trueUrl,
125
- target: "_blank"
132
+ class: "image-attachment__preview",
133
+ href: this.props.sending ? undefined : trueUrl,
134
+ target: "_blank",
135
+ "aria-busy": !!this.props.sending
126
136
  }, (0, _preact.h)("img", {
127
137
  alt: true,
128
138
  ref: el => {
@@ -135,7 +145,7 @@ class ImageAttachment extends _common.Com {
135
145
  src: url,
136
146
  onload: e => this.props.onLoad && this.props.onLoad(),
137
147
  onerror: e => this.onErrorImage(index)
138
- })));
148
+ }), $sending));
139
149
  }
140
150
  render() {
141
151
  var attachments = _index.default.get(this.props.ev, 'data.message.attachments', []);
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.UploadingFile = UploadingFile;
7
+ exports.default = UploadProgress;
8
+ var _preact = require("preact");
9
+ function UploadProgress({
10
+ attachment,
11
+ compact = false
12
+ }) {
13
+ const progress = attachment._upload?.putDone || attachment._upload?.file ? 100 : attachment._upload?.progress ?? 0;
14
+ return (0, _preact.h)("span", {
15
+ class: 'image-attachment__progress' + (compact ? ' file-attachment__progress' : '')
16
+ }, (0, _preact.h)("span", {
17
+ class: "image-attachment__progress-label"
18
+ }, progress, "%"), (0, _preact.h)("span", {
19
+ class: "image-attachment__progress-track",
20
+ role: "progressbar",
21
+ "aria-label": attachment.name || 'Uploading file',
22
+ "aria-valuemin": 0,
23
+ "aria-valuemax": 100,
24
+ "aria-valuenow": progress
25
+ }, (0, _preact.h)("span", {
26
+ class: "image-attachment__progress-fill",
27
+ ref: el => {
28
+ if (el) el.style.setProperty('width', `${progress}%`, 'important');
29
+ }
30
+ })));
31
+ }
32
+ function UploadingFile({
33
+ attachment
34
+ }) {
35
+ return (0, _preact.h)("div", {
36
+ class: "file-attachment file-attachment--user file-attachment--sending",
37
+ "aria-busy": "true"
38
+ }, (0, _preact.h)("span", {
39
+ class: "file-attachment--filename"
40
+ }, attachment.name), (0, _preact.h)(UploadProgress, {
41
+ attachment: attachment,
42
+ compact: true
43
+ }));
44
+ }
@@ -4,6 +4,7 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.default = void 0;
7
+ var _UploadProgress = require("./UploadProgress.js");
7
8
  var _index = _interopRequireDefault(require("../util/index.js"));
8
9
  var _common = require("../common.js");
9
10
  var _preact = require("preact");
@@ -22,6 +23,10 @@ class VideoAttachments extends _common.Com {
22
23
  });
23
24
  if (videos.length === 0) return null;
24
25
  var $videos = _index.default.map(videos, (video, index) => {
26
+ if (this.props.sending) return (0, _preact.h)(_UploadProgress.UploadingFile, {
27
+ key: index,
28
+ attachment: video
29
+ });
25
30
  return (0, _preact.h)("div", {
26
31
  key: index,
27
32
  class: `video-attachment video-attachment--${byType}`
@@ -84,89 +84,88 @@ class _default {
84
84
  }));
85
85
  _defineProperty(this, "sendTyping", cid => this.apireq.post(`/accounts/${this.accid}/conversations/${cid}/typing`));
86
86
  _defineProperty(this, "sendLostFocus", () => this.apireq.post(`/accounts/${this.accid}/focuses/${this.uid}/delete`));
87
- _defineProperty(this, "uploadToS3", async file => {
88
- let name = file.name.replace(/\s+/g, '_');
89
- var {
90
- body,
91
- error
92
- } = await this.apireq.setBaseUrl('https://api5.subiz.com.vn/4.1').setQuery({
93
- 'account-id': this.accid,
94
- description: 'upload\n user-client: web-widget',
95
- name: name,
96
- presign: true,
97
- size: file.size,
98
- 'content-type': file.type
99
- }).post('/files');
100
- if (error) return {
101
- body,
102
- error
103
- };
104
- let signedurl = body.signed_url;
105
- let fileid = body.id;
106
- let err = await new Promise(resolve => {
107
- const xhr = new _index.default.XMLHttpRequest();
108
- xhr.open('PUT', signedurl, true);
109
- xhr.setRequestHeader('Content-Type', file.type || '');
110
- xhr.onload = function () {
111
- if (xhr.status === 200) {
112
- resolve();
113
- } else {
114
- resolve(new Error(`Upload failed: ${xhr.statusText}`));
115
- }
116
- };
117
- xhr.onerror = function () {
118
- resolve(new Error('Network error during upload.'));
119
- };
120
- xhr.send(file);
121
- });
122
- if (err) return {
123
- error: err
124
- };
125
- var {
126
- body,
127
- error
128
- } = await this.apireq.setBaseUrl('https://api5.subiz.com.vn/4.1').setQuery({
129
- 'account-id': this.accid
130
- }).post(`/files/${fileid}/uploaded`);
131
- if (error) return {
132
- error
87
+ _defineProperty(this, "attachmentRequest", () => this.apireq.setBaseUrl(new URL('/4.1', this.ApiURL).href).addQuery('x-account-id', this.accid));
88
+ _defineProperty(this, "attachmentJSON", async (path, body, method = 'post') => {
89
+ const result = await this.attachmentRequest()[method](path, body);
90
+ if (result.error || result.body?.error || result.code < 200 || result.code >= 300) {
91
+ throw Object.assign(new Error('Attachment request failed'), {
92
+ code: result.body?.error?.code,
93
+ response: result
94
+ });
95
+ }
96
+ return result.body;
97
+ });
98
+ _defineProperty(this, "uploadAttachment", async (file, state = {}, onProgress = () => {}) => {
99
+ if (file.size > 25000000) return {
100
+ error: 'invalid_field_size',
101
+ body: {
102
+ code: 'invalid_field_size'
103
+ }
133
104
  };
134
- return {
135
- url: _index.default.get(body, 'file.url', '')
105
+ const identity = JSON.stringify([this.accid, this.uid, this.ref, this.mask, this.profile_id, this.tokensession]);
106
+ if (state.identity && state.identity !== identity) throw new Error('Upload identity changed');
107
+ state.identity = identity;
108
+ const checkIdentity = () => {
109
+ if (identity !== JSON.stringify([this.accid, this.uid, this.ref, this.mask, this.profile_id, this.tokensession])) throw new Error('Upload identity changed');
136
110
  };
137
- });
138
- _defineProperty(this, "uploadToS32", async file => {
139
- var {
140
- body,
141
- error
142
- } = await ajax.setBaseUrl('https://api5.subiz.com.vn/4.1').withCredentials(true).setQuery({
143
- 'account-id': this.accid,
144
- description: 'upload\n user-client: web-widget',
145
- name: file.name.replace(/\s+/g, '_'),
111
+ if (state.file) return state.file;
112
+ if (state.upload && !state.putDone && state.upload.upload_expires_at <= _index.default.Date.now()) {
113
+ delete state.upload;
114
+ delete state.key;
115
+ }
116
+ if (!state.key) state.key = _index.default.randomString(36);
117
+ if (!state.metadata) state.metadata = {
118
+ category: 'attachment',
119
+ name: file.name,
120
+ content_type: file.type || 'application/octet-stream',
146
121
  size: file.size,
147
- type: file.type
148
- }).setParser('json').beforeHook((param, done) => {
149
- if (this.token_generator) {
150
- let user_id = this.profile_id;
151
- this.token_generator().then(token => {
152
- if (user_id != this.profile_id) return done(true);
153
- if (!token) return done(false);
154
- if (token == 'error') return done(false);
155
- param.request = param.request.addQuery('x-access-token', token);
156
- return done(true);
157
- });
158
- return;
159
- }
160
- if (this.ref) param.request = param.request.addQuery('x-user-ref', this.ref);else if (this.mask) param.request = param.request.addQuery('x-user-mask', this.mask);
161
- return done(true);
162
- }).post('/files', file);
163
- if (error) return {
164
- body,
165
- error
122
+ ttl_seconds: 0
166
123
  };
167
- return {
168
- url: _index.default.get(body, 'file.url')
124
+ if (!state.upload) {
125
+ const created = await this.attachmentJSON('/uploads', {
126
+ ...state.metadata,
127
+ idempotency_key: state.key
128
+ });
129
+ checkIdentity();
130
+ if (!created.upload?.upload_id || !created.upload.upload_url) throw new Error('Missing upload session');
131
+ state.upload = created.upload;
132
+ }
133
+ if (!state.putDone) {
134
+ checkIdentity();
135
+ const report = percent => {
136
+ if (state.progress === percent) return;
137
+ state.progress = percent;
138
+ onProgress(percent);
139
+ };
140
+ report(0);
141
+ await new Promise((resolve, reject) => {
142
+ const xhr = new _index.default.XMLHttpRequest();
143
+ xhr.upload.onprogress = event => {
144
+ if (event.lengthComputable) report(Math.min(100, Math.floor(event.loaded / event.total * 100)));
145
+ };
146
+ xhr.open('PUT', state.upload.upload_url, true);
147
+ xhr.withCredentials = false;
148
+ Object.entries(state.upload.required_headers || {}).forEach(([key, value]) => xhr.setRequestHeader(key, value));
149
+ xhr.onload = () => xhr.status >= 200 && xhr.status < 300 ? resolve() : reject(new Error(`Upload failed: HTTP ${xhr.status}`));
150
+ xhr.onerror = () => reject(new Error('Network error during upload'));
151
+ xhr.onabort = () => reject(new Error('Upload aborted'));
152
+ xhr.ontimeout = () => reject(new Error('Upload timed out'));
153
+ xhr.send(file);
154
+ });
155
+ report(100);
156
+ state.putDone = true;
157
+ }
158
+ checkIdentity();
159
+ const finished = await this.attachmentJSON(`/uploads/${encodeURIComponent(state.upload.upload_id)}/finish`);
160
+ checkIdentity();
161
+ const f = finished.file;
162
+ if (!f?.id || !f.url || f.error_code) throw new Error('File is not available');
163
+ state.file = {
164
+ url: f.url,
165
+ v: 'cdn',
166
+ file_id: f.id
169
167
  };
168
+ return state.file;
170
169
  });
171
170
  _defineProperty(this, "time", _ => ajax.setBaseUrl(this.ApiURL).withCredentials(true).setParser('json').get('/time'));
172
171
  _defineProperty(this, "updateUser", data => this.apireq.post(`/accounts/${this.accid}/users/${this.uid}`, data));
@@ -55,7 +55,7 @@ class _default {
55
55
  _defineProperty(this, "sendContent", data => OK200);
56
56
  _defineProperty(this, "sendLostFocus", data => OK200);
57
57
  _defineProperty(this, "sendContentView", data => OK200);
58
- _defineProperty(this, "uploadToS3", async file => OK200);
58
+ _defineProperty(this, "uploadAttachment", async file => OK200);
59
59
  _defineProperty(this, "time", _ => Promise.resolve({
60
60
  code: 200,
61
61
  body: new _index.default.Date().toISOString()
@@ -8,7 +8,7 @@ async function uploadAttachments(attachments, upload) {
8
8
  let error, errorMessage;
9
9
  for (const attachment of attachments) {
10
10
  try {
11
- const result = await upload(attachment._file);
11
+ const result = await upload(attachment._file, attachment._upload || (attachment._upload = {}));
12
12
  if (result.body && result.body.code === 'invalid_field_size') {
13
13
  errorMessage = 'cannot_send_too_big_file';
14
14
  }
@@ -17,6 +17,10 @@ async function uploadAttachments(attachments, upload) {
17
17
  continue;
18
18
  }
19
19
  attachment.url = result.url;
20
+ if (result.file_id) {
21
+ attachment.v = 'cdn';
22
+ attachment.file_id = result.file_id;
23
+ }
20
24
  } catch (err) {
21
25
  error = error || err;
22
26
  }