@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.
- package/README.md +6 -0
- package/api.md +9 -5
- package/assets/img/bubble_default.svg +1 -0
- package/assets/less/message.less +83 -0
- package/assets/less/widget.less +12 -0
- package/compiled_src/chatbox/AudioAttachments.js +5 -0
- package/compiled_src/chatbox/ChatButton.js +19 -17
- package/compiled_src/chatbox/Event.js +9 -2
- package/compiled_src/chatbox/FileAttachments.js +9 -3
- package/compiled_src/chatbox/ImageAttachments.js +16 -6
- package/compiled_src/chatbox/UploadProgress.js +44 -0
- package/compiled_src/chatbox/VideoAttachments.js +5 -0
- package/compiled_src/store/api.js +77 -78
- package/compiled_src/store/api_fake.js +1 -1
- package/compiled_src/store/attachments.js +5 -1
- package/compiled_src/store/store.js +25 -4
- package/dist/{1a12a5ed6a9.278.js → 1a12ed33347.278.js} +1 -1
- package/dist/1a12ed33347.app.js +1 -0
- package/dist/{1a12a5ed6a9.chat.html → 1a12ed33347.chat.html} +1 -1
- package/dist/1a12ed33347.chat.js +1 -0
- package/dist/{1a12a5ed6a9.form.html → 1a12ed33347.form.html} +1 -1
- package/dist/{1a12a5ed6a9.form.js → 1a12ed33347.form.js} +1 -1
- package/dist/{1a12a5ed6a9.index.html → 1a12ed33347.index.html} +1 -1
- package/dist/{1a12a5ed6a9.library.html → 1a12ed33347.library.html} +1 -1
- package/dist/1a12ed33347.library.js +1 -0
- package/dist/{1a12a5ed6a9.preview.html → 1a12ed33347.preview.html} +1 -1
- package/dist/1a12ed33347.preview.js +1 -0
- package/dist/1a12ed33347.webview.js +1 -0
- package/dist/HASH +1 -0
- package/dist/assets/img/{bubble_default.7d5e4ab.svg → bubble_default.6f6eec8.svg} +1 -0
- package/dist/screenshoot.js +25 -20
- package/package.json +11 -10
- package/src/chatbox/AudioAttachments.js +2 -0
- package/src/chatbox/ChatButton.js +22 -20
- package/src/chatbox/Event.js +8 -4
- package/src/chatbox/FileAttachments.js +5 -2
- package/src/chatbox/ImageAttachments.js +6 -2
- package/src/chatbox/UploadProgress.js +21 -0
- package/src/chatbox/VideoAttachments.js +2 -0
- package/src/store/api.js +66 -81
- package/src/store/api_fake.js +1 -1
- package/src/store/attachments.js +5 -1
- package/src/store/store.js +13 -5
- package/up +18 -0
- package/dist/.HASH +0 -1
- package/dist/1a12a5ed6a9.app.js +0 -1
- package/dist/1a12a5ed6a9.chat.js +0 -1
- package/dist/1a12a5ed6a9.library.js +0 -1
- package/dist/1a12a5ed6a9.preview.js +0 -1
- package/dist/1a12a5ed6a9.webview.js +0 -1
- /package/dist/{1a12a5ed6a9.127.js → 1a12ed33347.127.js} +0 -0
- /package/dist/{1a12a5ed6a9.132.js → 1a12ed33347.132.js} +0 -0
- /package/dist/{1a12a5ed6a9.185.js → 1a12ed33347.185.js} +0 -0
- /package/dist/{1a12a5ed6a9.27.js → 1a12ed33347.27.js} +0 -0
- /package/dist/{1a12a5ed6a9.339.js → 1a12ed33347.339.js} +0 -0
- /package/dist/{1a12a5ed6a9.459.js → 1a12ed33347.459.js} +0 -0
- /package/dist/{1a12a5ed6a9.490.js → 1a12ed33347.490.js} +0 -0
package/package.json
CHANGED
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@subiz/widget-v4",
|
|
3
|
-
"version": "4.1.
|
|
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.
|
|
39
|
-
"puppeteer": "^25.
|
|
40
|
-
"tape": "^5.10.
|
|
41
|
-
"webpack": "^5.
|
|
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>
|
|
@@ -391,6 +391,7 @@ export default class ChatButton extends Com {
|
|
|
391
391
|
|
|
392
392
|
let imgurl = sb.get(accountSetting, 'desktop_button.background_image_url')
|
|
393
393
|
let maskurl = sb.get(accountSetting, 'desktop_button.background_mask_url')
|
|
394
|
+
// Keep configured dimensions even when the image fails; default only missing dimensions to 70px.
|
|
394
395
|
let imgwidth = sb.get(accountSetting, 'desktop_button.width') || 70
|
|
395
396
|
let imgheight = sb.get(accountSetting, 'desktop_button.height') || 70
|
|
396
397
|
|
|
@@ -450,18 +451,10 @@ export default class ChatButton extends Com {
|
|
|
450
451
|
appearance.maskAsset ? prepareButtonMask(appearance.maskAsset) : Promise.resolve(null),
|
|
451
452
|
])
|
|
452
453
|
const stale = () => !this._isMounted || this._buttonImageVersion !== version
|
|
453
|
-
if (stale())
|
|
454
|
-
if (mask) URL.revokeObjectURL(mask)
|
|
455
|
-
return
|
|
456
|
-
}
|
|
454
|
+
if (stale()) return
|
|
457
455
|
const fallback = loaded.some((success) => !success) || (!!appearance.maskAsset && !mask)
|
|
458
456
|
const ready = !fallback || (await preloadButtonImage(defaultBubble))
|
|
459
|
-
if (stale())
|
|
460
|
-
if (mask) URL.revokeObjectURL(mask)
|
|
461
|
-
return
|
|
462
|
-
}
|
|
463
|
-
if (this._buttonMaskUrl) URL.revokeObjectURL(this._buttonMaskUrl)
|
|
464
|
-
this._buttonMaskUrl = mask
|
|
457
|
+
if (stale()) return
|
|
465
458
|
// Commit the hidden style before revealing even when images are cached.
|
|
466
459
|
// Otherwise older browsers can paint only the final state and skip the transition.
|
|
467
460
|
if (this._buttonElement) window.getComputedStyle(this._buttonElement).transform
|
|
@@ -470,7 +463,6 @@ export default class ChatButton extends Com {
|
|
|
470
463
|
}
|
|
471
464
|
|
|
472
465
|
componentWillUnmount() {
|
|
473
|
-
if (this._buttonMaskUrl) URL.revokeObjectURL(this._buttonMaskUrl)
|
|
474
466
|
if (this._animateTimeout) clearTimeout(this._animateTimeout)
|
|
475
467
|
}
|
|
476
468
|
|
|
@@ -478,12 +470,13 @@ export default class ChatButton extends Com {
|
|
|
478
470
|
if (typeof sbz_info != 'undefined') sbz_info.render_chatbox_button = sb.Date.now()
|
|
479
471
|
const appearance = this.getButtonAppearance()
|
|
480
472
|
let {imgsrc, imgurl, maskurl, background, position, imgwidth, imgheight} = appearance
|
|
473
|
+
const fallback = this.state.readyImageKey === appearance.key && this.state.fallbackButtonImage
|
|
481
474
|
// Never attach a remote CSS mask while the local copy is being prepared.
|
|
482
475
|
if (appearance.maskAsset) {
|
|
483
476
|
maskurl = this.state.readyImageKey === appearance.key ? this.state.buttonMaskUrl : ''
|
|
484
477
|
if (imgsrc === appearance.maskAsset) imgsrc = maskurl || ''
|
|
485
478
|
}
|
|
486
|
-
if (
|
|
479
|
+
if (fallback) {
|
|
487
480
|
imgsrc = imgurl = defaultBubble
|
|
488
481
|
maskurl = ''
|
|
489
482
|
}
|
|
@@ -548,7 +541,12 @@ export default class ChatButton extends Com {
|
|
|
548
541
|
`
|
|
549
542
|
}
|
|
550
543
|
|
|
544
|
+
if (fallback) {
|
|
545
|
+
imgcontainerstyle += `width: ${imgwidth}px !important; height: ${imgheight}px !important;`
|
|
546
|
+
}
|
|
547
|
+
|
|
551
548
|
let cls = 'chat-button chat-button--' + position
|
|
549
|
+
if (fallback) cls += ' chat-button--fallback'
|
|
552
550
|
// dont render till all image is loaded
|
|
553
551
|
|
|
554
552
|
let whiteBgStyle = `
|
|
@@ -679,18 +677,22 @@ function preloadButtonImage(src, {crossOrigin = false} = {}) {
|
|
|
679
677
|
})
|
|
680
678
|
}
|
|
681
679
|
|
|
682
|
-
//
|
|
683
|
-
//
|
|
680
|
+
// Pin the mask bytes with a data URL: some sites allow data: images but block blob:.
|
|
681
|
+
// If inline images or fetch are blocked, preload the original URL with CORS for CSS masks.
|
|
684
682
|
async function prepareButtonMask(src) {
|
|
685
|
-
let url
|
|
686
683
|
try {
|
|
687
684
|
const response = await fetch(src)
|
|
688
685
|
if (!response.ok) return null
|
|
689
|
-
|
|
690
|
-
|
|
686
|
+
const blob = await response.blob()
|
|
687
|
+
const url = await new Promise((resolve) => {
|
|
688
|
+
const reader = new FileReader()
|
|
689
|
+
reader.onload = () => resolve(reader.result)
|
|
690
|
+
reader.onerror = reader.onabort = () => resolve('')
|
|
691
|
+
reader.readAsDataURL(blob)
|
|
692
|
+
})
|
|
693
|
+
if (url && await preloadButtonImage(url)) return url
|
|
691
694
|
} catch (_) {
|
|
692
|
-
//
|
|
695
|
+
// A blocked fetch or failed conversion does not mean the original image is unavailable.
|
|
693
696
|
}
|
|
694
|
-
|
|
695
|
-
return null
|
|
697
|
+
return await preloadButtonImage(src, {crossOrigin: true}) ? src : null
|
|
696
698
|
}
|
package/src/chatbox/Event.js
CHANGED
|
@@ -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
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
.
|
|
375
|
-
.
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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')
|
package/src/store/api_fake.js
CHANGED
|
@@ -66,7 +66,7 @@ export default class {
|
|
|
66
66
|
sendLostFocus = (data) => OK200
|
|
67
67
|
sendContentView = (data) => OK200
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
uploadAttachment = async (file) => OK200
|
|
70
70
|
|
|
71
71
|
time = (_) => Promise.resolve({code: 200, body: new sb.Date().toISOString()})
|
|
72
72
|
updateUser = (data) => OK200
|
package/src/store/attachments.js
CHANGED
|
@@ -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
|
}
|
package/src/store/store.js
CHANGED
|
@@ -959,9 +959,10 @@ export default class Store {
|
|
|
959
959
|
await this.readyGate.entry()
|
|
960
960
|
const key = websiteFormStorageKey(this.api, formid)
|
|
961
961
|
const kv = this.kv
|
|
962
|
+
const values = sb.cloneDeep(payload.values)
|
|
962
963
|
const result = formResult(await this.api.submitWebsiteForm(formid, payload))
|
|
963
964
|
try {
|
|
964
|
-
kv.put(key, {conversation_id: result.conversation_id, expires_at: sb.Date.now() + 7 * 24 * 60 * 60 * 1000})
|
|
965
|
+
kv.put(key, {conversation_id: result.conversation_id, values, expires_at: sb.Date.now() + 7 * 24 * 60 * 60 * 1000})
|
|
965
966
|
} catch (_) { /* Storage failure must not make callers retry a successful submission. */ }
|
|
966
967
|
return result
|
|
967
968
|
}
|
|
@@ -979,7 +980,7 @@ export default class Store {
|
|
|
979
980
|
this.kv.del(key)
|
|
980
981
|
return {submitted: false, status: 'expired'}
|
|
981
982
|
}
|
|
982
|
-
return {submitted: true, status: 'found', conversation_id: receipt.conversation_id}
|
|
983
|
+
return {submitted: true, status: 'found', conversation_id: receipt.conversation_id, values: Array.isArray(receipt.values) ? sb.cloneDeep(receipt.values) : []}
|
|
983
984
|
} catch (_) {
|
|
984
985
|
return {submitted: false, status: 'storage_unavailable'}
|
|
985
986
|
}
|
|
@@ -1449,7 +1450,7 @@ export default class Store {
|
|
|
1449
1450
|
if (att.type !== 'file') return
|
|
1450
1451
|
if (!att._file) return
|
|
1451
1452
|
files.push(att)
|
|
1452
|
-
if (ImageTypes.includes(att._file.type)) {
|
|
1453
|
+
if (!att._upload?.file && ImageTypes.includes(att._file.type)) {
|
|
1453
1454
|
let {width, height, url} = await loadImage(att._file)
|
|
1454
1455
|
att.url = url
|
|
1455
1456
|
att.width = width
|
|
@@ -1462,14 +1463,21 @@ export default class Store {
|
|
|
1462
1463
|
await this.convoGate.entry()
|
|
1463
1464
|
// upload file first
|
|
1464
1465
|
|
|
1465
|
-
const uploaded = await uploadAttachments(files, (file) => this.api.
|
|
1466
|
+
const uploaded = await uploadAttachments(files, (file, state) => this.api.uploadAttachment(file, state, () => this.pubsub.publish('events', event)))
|
|
1466
1467
|
event._error = uploaded.error
|
|
1467
1468
|
if (uploaded.errorMessage) event._error_message = uploaded.errorMessage
|
|
1468
1469
|
|
|
1469
1470
|
if (event._error) return this.onWs(event) // dont send error msg
|
|
1470
1471
|
if (cid && cid.startsWith('cslight')) return
|
|
1471
1472
|
|
|
1472
|
-
|
|
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
|
|
1473
1481
|
if (error) {
|
|
1474
1482
|
event._error = error
|
|
1475
1483
|
this.onWs(event)
|
package/up
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
2
|
set -e # exit when any command fails
|
|
3
3
|
|
|
4
|
+
# The storage client still references ssl.PROTOCOL_TLSv1; the local Python 3.14
|
|
5
|
+
# uses OpenSSL 4, which removed the underlying legacy API.
|
|
6
|
+
# Respect an explicit interpreter; otherwise prefer a compatible installed Python.
|
|
7
|
+
if [ -z "${CLOUDSDK_PYTHON:-}" ]; then
|
|
8
|
+
for python in python3.13 python3.12 python3.11 python3.10; do
|
|
9
|
+
if command -v "$python" >/dev/null 2>&1; then
|
|
10
|
+
CLOUDSDK_PYTHON=$(command -v "$python")
|
|
11
|
+
export CLOUDSDK_PYTHON
|
|
12
|
+
break
|
|
13
|
+
fi
|
|
14
|
+
done
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
# Fail before rebuilding if gcloud cannot load the upload command.
|
|
18
|
+
gcloud storage cp --help >/dev/null
|
|
19
|
+
|
|
4
20
|
# safety first
|
|
5
21
|
echo "Clean"
|
|
6
22
|
#rm -rf node_modules && rm -rf package-lock.json && npm i --no-optional
|
|
@@ -17,8 +33,10 @@ echo "Build OK. New version hash: $(cat dist/HASH)"
|
|
|
17
33
|
# DEPLOY TO WIDGET SERVER
|
|
18
34
|
# (do not deploy dist/HASH)
|
|
19
35
|
mv dist/HASH dist/.HASH
|
|
36
|
+
trap 'if [ -f dist/.HASH ]; then mv dist/.HASH dist/HASH; fi' 0
|
|
20
37
|
gcloud storage cp -r --cache-control="public, max-age=31536000" --gzip-local="html,js,css,svg,jpg,png" dist/* gs://public-gcs.subiz-cdn.com/widget-v4/public/
|
|
21
38
|
mv dist/.HASH dist/HASH
|
|
39
|
+
trap - 0
|
|
22
40
|
|
|
23
41
|
if [ "$1" = 'prod' ]; then
|
|
24
42
|
gcloud storage cp --cache-control="public, max-age=10" --content-type="text/plain" dist/HASH gs://public-gcs.subiz-cdn.com/widget-v4/public/
|
package/dist/.HASH
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
1a12a5ed6a9
|