@subiz/widget-v4 4.1.216 → 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 (42) hide show
  1. package/assets/less/message.less +83 -0
  2. package/compiled_src/chatbox/AudioAttachments.js +5 -0
  3. package/compiled_src/chatbox/Event.js +9 -2
  4. package/compiled_src/chatbox/FileAttachments.js +9 -3
  5. package/compiled_src/chatbox/ImageAttachments.js +16 -6
  6. package/compiled_src/chatbox/UploadProgress.js +44 -0
  7. package/compiled_src/chatbox/VideoAttachments.js +5 -0
  8. package/compiled_src/store/api.js +77 -78
  9. package/compiled_src/store/api_fake.js +1 -1
  10. package/compiled_src/store/attachments.js +5 -1
  11. package/compiled_src/store/store.js +21 -3
  12. package/dist/{1a12ecfb6db.app.js → 1a12ed33347.app.js} +1 -1
  13. package/dist/{1a12ecfb6db.chat.html → 1a12ed33347.chat.html} +1 -1
  14. package/dist/{1a12ecfb6db.chat.js → 1a12ed33347.chat.js} +1 -1
  15. package/dist/{1a12ecfb6db.form.html → 1a12ed33347.form.html} +1 -1
  16. package/dist/{1a12ecfb6db.form.js → 1a12ed33347.form.js} +1 -1
  17. package/dist/{1a12ecfb6db.index.html → 1a12ed33347.index.html} +1 -1
  18. package/dist/{1a12ecfb6db.library.html → 1a12ed33347.library.html} +1 -1
  19. package/dist/{1a12ecfb6db.library.js → 1a12ed33347.library.js} +1 -1
  20. package/dist/{1a12ecfb6db.preview.html → 1a12ed33347.preview.html} +1 -1
  21. package/dist/{1a12ecfb6db.preview.js → 1a12ed33347.preview.js} +1 -1
  22. package/dist/{1a12ecfb6db.webview.js → 1a12ed33347.webview.js} +1 -1
  23. package/dist/HASH +1 -1
  24. package/package.json +11 -10
  25. package/src/chatbox/AudioAttachments.js +2 -0
  26. package/src/chatbox/Event.js +8 -4
  27. package/src/chatbox/FileAttachments.js +5 -2
  28. package/src/chatbox/ImageAttachments.js +6 -2
  29. package/src/chatbox/UploadProgress.js +21 -0
  30. package/src/chatbox/VideoAttachments.js +2 -0
  31. package/src/store/api.js +66 -81
  32. package/src/store/api_fake.js +1 -1
  33. package/src/store/attachments.js +5 -1
  34. package/src/store/store.js +10 -3
  35. /package/dist/{1a12ecfb6db.127.js → 1a12ed33347.127.js} +0 -0
  36. /package/dist/{1a12ecfb6db.132.js → 1a12ed33347.132.js} +0 -0
  37. /package/dist/{1a12ecfb6db.185.js → 1a12ed33347.185.js} +0 -0
  38. /package/dist/{1a12ecfb6db.27.js → 1a12ed33347.27.js} +0 -0
  39. /package/dist/{1a12ecfb6db.278.js → 1a12ed33347.278.js} +0 -0
  40. /package/dist/{1a12ecfb6db.339.js → 1a12ed33347.339.js} +0 -0
  41. /package/dist/{1a12ecfb6db.459.js → 1a12ed33347.459.js} +0 -0
  42. /package/dist/{1a12ecfb6db.490.js → 1a12ed33347.490.js} +0 -0
package/dist/HASH CHANGED
@@ -1 +1 @@
1
- 1a12ecfb6db
1
+ 1a12ed33347
package/package.json CHANGED
@@ -1,16 +1,13 @@
1
1
  {
2
2
  "name": "@subiz/widget-v4",
3
- "version": "4.1.216",
3
+ "version": "4.1.217",
4
4
  "main": "./compiled_src/lib.js",
5
5
  "scripts": {
6
6
  "dev": "npx webpack serve --config webpack.dev.js",
7
7
  "build": "npm run lib && rm -rf dist && webpack --config webpack.screenshoot.js && webpack --config webpack.prod.js",
8
8
  "report": "npx webpack --config webpack.report.js",
9
9
  "lib": "babel src --out-dir compiled_src --ignore \"**/*.test.js,**/*.test2.js\"",
10
- "test": "node test.cjs",
11
- "lint": "eslint src",
12
- "check": "npm run lint && npm test",
13
- "prepack": "npm run check && npm run lib"
10
+ "test": "node test.cjs"
14
11
  },
15
12
  "dependencies": {
16
13
  "@subiz/ajax": "^3.0.22",
@@ -32,15 +29,19 @@
32
29
  "@babel/preset-env": "^7.29.7",
33
30
  "babel-loader": "^10.1.1",
34
31
  "babel-plugin-syntax-jsx": "^6.18.0",
35
- "eslint": "^10.12.0",
36
32
  "globals": "^17.13.0",
37
33
  "html-webpack-plugin": "^5.6.8",
38
- "jsdom": "^30.1.2",
39
- "puppeteer": "^25.12.0",
40
- "tape": "^5.10.2",
41
- "webpack": "^5.111.1",
34
+ "jsdom": "^30.1.3",
35
+ "puppeteer": "^25.14.0",
36
+ "tape": "^5.10.3",
37
+ "webpack": "^5.112.0",
42
38
  "webpack-bundle-analyzer": "^5.4.0",
43
39
  "webpack-cli": "^7.2.3",
44
40
  "webpack-dev-server": "^6.0.0"
41
+ },
42
+ "allowScripts": {
43
+ "puppeteer@25.14.0": true,
44
+ "fsevents@2.3.3": true,
45
+ "core-js@3.50.0": true
45
46
  }
46
47
  }
@@ -1,3 +1,4 @@
1
+ import {UploadingFile} from './UploadProgress.js'
1
2
  import sb from '../util/index.js'
2
3
  import {h} from 'preact'
3
4
 
@@ -12,6 +13,7 @@ export default function AudioAttachments(props) {
12
13
  if (audios.length === 0) return null
13
14
 
14
15
  var $audios = sb.map(audios, (audio, index) => {
16
+ if (props.sending) return <UploadingFile key={index} attachment={audio} />
15
17
  return (
16
18
  <div key={index} class={`audio-attachment audio-attachment--${byType}`}>
17
19
  <audio class='audio-attachment__audio' controls>
@@ -362,6 +362,10 @@ export default class Event extends Com {
362
362
  $dot = <div class='sending_dot sending_dot__hide'></div>
363
363
  }
364
364
 
365
+ const hasFile = sb.find(attachments, att => att.type === 'file' || att._file)
366
+ const sendingAttachment = !ev._error && ev.source_event_id === ev.id
367
+ if (hasFile) $dot = null
368
+
365
369
  let $sender = null
366
370
  if (this.props.event.mode === 'lead' && sb.get(ev, 'by.type') !== 'user') $sender = <div class='pt-3'></div>
367
371
 
@@ -392,10 +396,10 @@ export default class Event extends Com {
392
396
  refresh={this.state.refresh}
393
397
  onLink={this.props.onLink}
394
398
  />
395
- <ImageAttachments store={this.props.store} ev={ev} ref={this.imageAttachmentRef} />
396
- <AudioAttachments store={this.props.store} ev={ev} />
397
- <VideoAttachments store={this.props.store} ev={ev} />
398
- <FileAttachments store={this.props.store} ev={ev} ref={this.fileAttachmentRef} />
399
+ <ImageAttachments store={this.props.store} ev={ev} sending={sendingAttachment} ref={this.imageAttachmentRef} />
400
+ <AudioAttachments store={this.props.store} ev={ev} sending={sendingAttachment} />
401
+ <VideoAttachments store={this.props.store} ev={ev} sending={sendingAttachment} />
402
+ <FileAttachments store={this.props.store} ev={ev} sending={sendingAttachment} ref={this.fileAttachmentRef} />
399
403
  <ButtonAttachments store={this.props.store} ev={ev} t={this.props.t} />
400
404
  <FormAttachment store={this.props.store} t={this.props.t} ev={ev} />
401
405
  <ProductAttachments t={this.props.t} store={this.props.store} ev={ev} />
@@ -1,3 +1,4 @@
1
+ import UploadProgress from './UploadProgress.js'
1
2
  import sb from '../util/index.js'
2
3
  import {Com} from '../common.js'
3
4
  import {h} from 'preact'
@@ -33,14 +34,16 @@ export default class FileAttachments extends Com {
33
34
 
34
35
  let $files = sb.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
 
38
40
  return (
39
- <div key={index} class={cls}>
40
- <a class='file-attachment--link' href={this.props.store.replaceFileUrl(file.url)} target='_blank'>
41
+ <div key={index} class={cls} aria-busy={!!this.props.sending}>
42
+ <a class='file-attachment--link' href={this.props.sending ? undefined : this.props.store.replaceFileUrl(file.url)} target='_blank'>
41
43
  <span class='file-attachment--download mr-2'></span>
42
44
  <span class='file-attachment--filename'>{file.name}</span>
43
45
  </a>
46
+ {this.props.sending && <UploadProgress attachment={file} compact />}
44
47
  </div>
45
48
  )
46
49
  })
@@ -1,3 +1,4 @@
1
+ import UploadProgress from './UploadProgress.js'
1
2
  import sb from '../util/index.js'
2
3
  import {Com} from '../common.js'
3
4
  import {h, createRef} from 'preact'
@@ -82,17 +83,19 @@ export default class ImageAttachment extends Com {
82
83
 
83
84
  let cls = `image-attachment image-attachment--${byType}`
84
85
  if (image._true_idx === this.state.focusedIdx) cls += ' just-focused'
86
+ const $sending = this.props.sending ? <span class='image-attachment__sending'><UploadProgress attachment={image} /></span> : null
85
87
 
86
88
  if (!image.width || !image.height) {
87
89
  return (
88
90
  <div class={cls}>
89
- <a href={this.props.store.replaceFileUrl(image.url)} target='_blank'>
91
+ <a class='image-attachment__preview' href={this.props.sending ? undefined : this.props.store.replaceFileUrl(image.url)} target='_blank' aria-busy={!!this.props.sending}>
90
92
  <img
91
93
  alt
92
94
  class='image-attachment__img'
93
95
  src={this.props.store.replaceFileUrl(image.thumbnail_url || image.url)}
94
96
  onload={(e) => this.props.onLoad && this.props.onLoad()}
95
97
  />
98
+ {$sending}
96
99
  </a>
97
100
  </div>
98
101
  )
@@ -108,7 +111,7 @@ export default class ImageAttachment extends Com {
108
111
 
109
112
  return (
110
113
  <div class={cls}>
111
- <a href={trueUrl} target='_blank'>
114
+ <a class='image-attachment__preview' href={this.props.sending ? undefined : trueUrl} target='_blank' aria-busy={!!this.props.sending}>
112
115
  <img
113
116
  alt
114
117
  ref={(el) => {
@@ -123,6 +126,7 @@ export default class ImageAttachment extends Com {
123
126
  onload={(e) => this.props.onLoad && this.props.onLoad()}
124
127
  onerror={(e) => this.onErrorImage(index)}
125
128
  />
129
+ {$sending}
126
130
  </a>
127
131
  </div>
128
132
  )
@@ -0,0 +1,21 @@
1
+ import {h} from 'preact'
2
+
3
+ export default function UploadProgress({attachment, compact = false}) {
4
+ const progress = attachment._upload?.putDone || attachment._upload?.file ? 100 : (attachment._upload?.progress ?? 0)
5
+ return <span class={'image-attachment__progress' + (compact ? ' file-attachment__progress' : '')}>
6
+ <span class='image-attachment__progress-label'>{progress}%</span>
7
+ <span class='image-attachment__progress-track' role='progressbar' aria-label={attachment.name || 'Uploading file'} aria-valuemin={0} aria-valuemax={100} aria-valuenow={progress}>
8
+ <span class='image-attachment__progress-fill' ref={el => {
9
+ // Widget styles reset all properties with !important.
10
+ if (el) el.style.setProperty('width', `${progress}%`, 'important')
11
+ }} />
12
+ </span>
13
+ </span>
14
+ }
15
+
16
+ export function UploadingFile({attachment}) {
17
+ return <div class='file-attachment file-attachment--user file-attachment--sending' aria-busy='true'>
18
+ <span class='file-attachment--filename'>{attachment.name}</span>
19
+ <UploadProgress attachment={attachment} compact />
20
+ </div>
21
+ }
@@ -1,3 +1,4 @@
1
+ import {UploadingFile} from './UploadProgress.js'
1
2
  import sb from '../util/index.js'
2
3
  import {Com} from '../common.js'
3
4
  import {h} from 'preact'
@@ -19,6 +20,7 @@ export default class VideoAttachments extends Com {
19
20
  if (videos.length === 0) return null
20
21
 
21
22
  var $videos = sb.map(videos, (video, index) => {
23
+ if (this.props.sending) return <UploadingFile key={index} attachment={video} />
22
24
  return (
23
25
  <div key={index} class={`video-attachment video-attachment--${byType}`}>
24
26
  <video
package/src/store/api.js CHANGED
@@ -309,91 +309,76 @@ export default class {
309
309
  })
310
310
  }
311
311
 
312
- uploadToS3 = async (file) => {
313
- let name = file.name.replace(/\s+/g, '_')
314
-
315
- var {body, error} = await this.apireq
316
- .setBaseUrl('https://api5.subiz.com.vn/4.1')
317
- .setQuery({
318
- 'account-id': this.accid,
319
- description: 'upload\n user-client: web-widget',
320
- name: name,
321
- presign: true,
322
- size: file.size,
323
- // server reads 'content-type' (not 'type') and signs it into the presigned URL,
324
- // so the PUT below must send the exact same Content-Type value
325
- 'content-type': file.type,
326
- })
327
- .post('/files')
328
- if (error) return {body, error}
329
-
330
- let signedurl = body.signed_url
331
- let fileid = body.id
332
-
333
- // Step 2: Upload the file to the signed_url
334
- let err = await new Promise((resolve) => {
335
- const xhr = new sb.XMLHttpRequest()
336
- xhr.open('PUT', signedurl, true)
337
- xhr.setRequestHeader('Content-Type', file.type || '')
338
- xhr.onload = function () {
339
- if (xhr.status === 200) {
340
- resolve()
341
- } else {
342
- resolve(new Error(`Upload failed: ${xhr.statusText}`))
343
- }
344
- }
345
-
346
- xhr.onerror = function () {
347
- resolve(new Error('Network error during upload.'))
348
- }
349
-
350
- xhr.send(file)
351
- })
352
- if (err) return {error: err}
353
-
354
- // Step 3: Notify server of upload completion
355
- var {body, error} = await this.apireq
356
- .setBaseUrl('https://api5.subiz.com.vn/4.1')
357
- .setQuery({'account-id': this.accid})
358
- .post(`/files/${fileid}/uploaded`)
359
- if (error) return {error}
360
- return {url: sb.get(body, 'file.url', '')}
312
+ attachmentRequest = () => this.apireq
313
+ .setBaseUrl(new URL('/4.1', this.ApiURL).href)
314
+ .addQuery('x-account-id', this.accid)
315
+
316
+ attachmentJSON = async (path, body, method = 'post') => {
317
+ const result = await this.attachmentRequest()[method](path, body)
318
+ if (result.error || result.body?.error || result.code < 200 || result.code >= 300) {
319
+ throw Object.assign(new Error('Attachment request failed'), {code: result.body?.error?.code, response: result})
320
+ }
321
+ return result.body
361
322
  }
362
323
 
363
- uploadToS32 = async (file) => {
364
- var {body, error} = await ajax
365
- .setBaseUrl('https://api5.subiz.com.vn/4.1')
366
- .withCredentials(true)
367
- .setQuery({
368
- 'account-id': this.accid,
369
- description: 'upload\n user-client: web-widget',
370
- name: file.name.replace(/\s+/g, '_'),
371
- size: file.size,
372
- type: file.type,
373
- })
374
- .setParser('json')
375
- .beforeHook((param, done) => {
376
- if (this.token_generator) {
377
- let user_id = this.profile_id
378
- this.token_generator().then((token) => {
379
- if (user_id != this.profile_id) return done(true) // outdated
380
-
381
- if (!token) return done(false)
382
- if (token == 'error') return done(false)
383
- param.request = param.request.addQuery('x-access-token', token)
384
- return done(true)
385
- })
386
- return
324
+ uploadAttachment = async (file, state = {}, onProgress = () => {}) => {
325
+ if (file.size > 25000000) return {error: 'invalid_field_size', body: {code: 'invalid_field_size'}}
326
+ const identity = JSON.stringify([this.accid, this.uid, this.ref, this.mask, this.profile_id, this.tokensession])
327
+ if (state.identity && state.identity !== identity) throw new Error('Upload identity changed')
328
+ state.identity = identity
329
+ const checkIdentity = () => {
330
+ if (identity !== JSON.stringify([this.accid, this.uid, this.ref, this.mask, this.profile_id, this.tokensession])) throw new Error('Upload identity changed')
331
+ }
332
+ if (state.file) return state.file
333
+ // A completed PUT can still be finalized after the PUT URL expires.
334
+ if (state.upload && !state.putDone && state.upload.upload_expires_at <= sb.Date.now()) {
335
+ delete state.upload
336
+ delete state.key
337
+ }
338
+ if (!state.key) state.key = sb.randomString(36)
339
+ if (!state.metadata) state.metadata = {
340
+ category: 'attachment', name: file.name,
341
+ content_type: file.type || 'application/octet-stream', size: file.size, ttl_seconds: 0,
342
+ }
343
+ if (!state.upload) {
344
+ const created = await this.attachmentJSON('/uploads', {...state.metadata, idempotency_key: state.key})
345
+ checkIdentity()
346
+ if (!created.upload?.upload_id || !created.upload.upload_url) throw new Error('Missing upload session')
347
+ state.upload = created.upload
348
+ }
349
+ if (!state.putDone) {
350
+ checkIdentity()
351
+ const report = (percent) => {
352
+ if (state.progress === percent) return
353
+ state.progress = percent
354
+ onProgress(percent)
355
+ }
356
+ report(0)
357
+ // XHR exposes upload progress; this request still bypasses the API client.
358
+ await new Promise((resolve, reject) => {
359
+ const xhr = new sb.XMLHttpRequest()
360
+ xhr.upload.onprogress = (event) => {
361
+ if (event.lengthComputable) report(Math.min(100, Math.floor(event.loaded / event.total * 100)))
387
362
  }
388
-
389
- if (this.ref) param.request = param.request.addQuery('x-user-ref', this.ref)
390
- else if (this.mask) param.request = param.request.addQuery('x-user-mask', this.mask) // deprecating
391
-
392
- return done(true)
363
+ xhr.open('PUT', state.upload.upload_url, true)
364
+ xhr.withCredentials = false
365
+ Object.entries(state.upload.required_headers || {}).forEach(([key, value]) => xhr.setRequestHeader(key, value))
366
+ xhr.onload = () => xhr.status >= 200 && xhr.status < 300 ? resolve() : reject(new Error(`Upload failed: HTTP ${xhr.status}`))
367
+ xhr.onerror = () => reject(new Error('Network error during upload'))
368
+ xhr.onabort = () => reject(new Error('Upload aborted'))
369
+ xhr.ontimeout = () => reject(new Error('Upload timed out'))
370
+ xhr.send(file)
393
371
  })
394
- .post('/files', file)
395
- if (error) return {body, error}
396
- return {url: sb.get(body, 'file.url')}
372
+ report(100)
373
+ state.putDone = true
374
+ }
375
+ checkIdentity()
376
+ const finished = await this.attachmentJSON(`/uploads/${encodeURIComponent(state.upload.upload_id)}/finish`)
377
+ checkIdentity()
378
+ const f = finished.file
379
+ if (!f?.id || !f.url || f.error_code) throw new Error('File is not available')
380
+ state.file = {url: f.url, v: 'cdn', file_id: f.id}
381
+ return state.file
397
382
  }
398
383
 
399
384
  time = (_) => ajax.setBaseUrl(this.ApiURL).withCredentials(true).setParser('json').get('/time')
@@ -66,7 +66,7 @@ export default class {
66
66
  sendLostFocus = (data) => OK200
67
67
  sendContentView = (data) => OK200
68
68
 
69
- uploadToS3 = async (file) => OK200
69
+ uploadAttachment = async (file) => OK200
70
70
 
71
71
  time = (_) => Promise.resolve({code: 200, body: new sb.Date().toISOString()})
72
72
  updateUser = (data) => OK200
@@ -4,7 +4,7 @@ export async function uploadAttachments(attachments, upload) {
4
4
  let error, errorMessage
5
5
  for (const attachment of attachments) {
6
6
  try {
7
- const result = await upload(attachment._file)
7
+ const result = await upload(attachment._file, attachment._upload || (attachment._upload = {}))
8
8
  if (result.body && result.body.code === 'invalid_field_size') {
9
9
  errorMessage = 'cannot_send_too_big_file'
10
10
  }
@@ -13,6 +13,10 @@ export async function uploadAttachments(attachments, upload) {
13
13
  continue
14
14
  }
15
15
  attachment.url = result.url
16
+ if (result.file_id) {
17
+ attachment.v = 'cdn'
18
+ attachment.file_id = result.file_id
19
+ }
16
20
  } catch (err) {
17
21
  error = error || err
18
22
  }
@@ -1450,7 +1450,7 @@ export default class Store {
1450
1450
  if (att.type !== 'file') return
1451
1451
  if (!att._file) return
1452
1452
  files.push(att)
1453
- if (ImageTypes.includes(att._file.type)) {
1453
+ if (!att._upload?.file && ImageTypes.includes(att._file.type)) {
1454
1454
  let {width, height, url} = await loadImage(att._file)
1455
1455
  att.url = url
1456
1456
  att.width = width
@@ -1463,14 +1463,21 @@ export default class Store {
1463
1463
  await this.convoGate.entry()
1464
1464
  // upload file first
1465
1465
 
1466
- const uploaded = await uploadAttachments(files, (file) => this.api.uploadToS3(file))
1466
+ const uploaded = await uploadAttachments(files, (file, state) => this.api.uploadAttachment(file, state, () => this.pubsub.publish('events', event)))
1467
1467
  event._error = uploaded.error
1468
1468
  if (uploaded.errorMessage) event._error_message = uploaded.errorMessage
1469
1469
 
1470
1470
  if (event._error) return this.onWs(event) // dont send error msg
1471
1471
  if (cid && cid.startsWith('cslight')) return
1472
1472
 
1473
- let {body: ret, error} = await this.api.sendConvoEvent(cid, event)
1473
+ // Keep browser Files and upload sessions locally for retry, never in the event JSON.
1474
+ const outgoing = {...event, data: {...event.data}}
1475
+ if (event.data.message) outgoing.data.message = {...event.data.message, attachments: event.data.message.attachments?.map(att => {
1476
+ const {_file, _upload, ...attachment} = att
1477
+ return attachment
1478
+ })}
1479
+ let {body: ret, error} = await this.api.sendConvoEvent(cid, outgoing)
1480
+ error = error || ret?.error
1474
1481
  if (error) {
1475
1482
  event._error = error
1476
1483
  this.onWs(event)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes