@pocketping/widget 1.0.2 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1259 -113
- package/dist/index.d.cts +80 -4
- package/dist/index.d.ts +80 -4
- package/dist/index.js +1256 -112
- package/dist/pocketping.min.global.js +493 -7
- package/package.json +10 -3
package/dist/index.js
CHANGED
|
@@ -190,6 +190,12 @@ function styles(primaryColor, theme) {
|
|
|
190
190
|
border-radius: 4px;
|
|
191
191
|
opacity: 0.8;
|
|
192
192
|
transition: opacity 0.2s;
|
|
193
|
+
flex-shrink: 0;
|
|
194
|
+
width: 28px;
|
|
195
|
+
height: 28px;
|
|
196
|
+
display: flex;
|
|
197
|
+
align-items: center;
|
|
198
|
+
justify-content: center;
|
|
193
199
|
}
|
|
194
200
|
|
|
195
201
|
.pp-close-btn:hover {
|
|
@@ -197,8 +203,8 @@ function styles(primaryColor, theme) {
|
|
|
197
203
|
}
|
|
198
204
|
|
|
199
205
|
.pp-close-btn svg {
|
|
200
|
-
width:
|
|
201
|
-
height:
|
|
206
|
+
width: 16px;
|
|
207
|
+
height: 16px;
|
|
202
208
|
}
|
|
203
209
|
|
|
204
210
|
.pp-messages {
|
|
@@ -372,6 +378,486 @@ function styles(primaryColor, theme) {
|
|
|
372
378
|
.pp-footer a:hover {
|
|
373
379
|
text-decoration: underline;
|
|
374
380
|
}
|
|
381
|
+
|
|
382
|
+
/* Attachment Styles */
|
|
383
|
+
.pp-file-input {
|
|
384
|
+
/* Use offscreen positioning instead of display:none for better browser compatibility */
|
|
385
|
+
position: absolute;
|
|
386
|
+
width: 1px;
|
|
387
|
+
height: 1px;
|
|
388
|
+
padding: 0;
|
|
389
|
+
margin: -1px;
|
|
390
|
+
overflow: hidden;
|
|
391
|
+
clip: rect(0, 0, 0, 0);
|
|
392
|
+
white-space: nowrap;
|
|
393
|
+
border: 0;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
.pp-attach-btn {
|
|
397
|
+
width: 40px;
|
|
398
|
+
height: 40px;
|
|
399
|
+
border-radius: 50%;
|
|
400
|
+
background: transparent;
|
|
401
|
+
color: ${colors.textSecondary};
|
|
402
|
+
border: 1px solid ${colors.border};
|
|
403
|
+
cursor: pointer;
|
|
404
|
+
display: flex;
|
|
405
|
+
align-items: center;
|
|
406
|
+
justify-content: center;
|
|
407
|
+
transition: color 0.2s, border-color 0.2s;
|
|
408
|
+
flex-shrink: 0;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
.pp-attach-btn:hover:not(:disabled) {
|
|
412
|
+
color: ${primaryColor};
|
|
413
|
+
border-color: ${primaryColor};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
.pp-attach-btn:disabled {
|
|
417
|
+
opacity: 0.5;
|
|
418
|
+
cursor: not-allowed;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
.pp-attach-btn svg {
|
|
422
|
+
width: 18px;
|
|
423
|
+
height: 18px;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
.pp-attachments-preview {
|
|
427
|
+
display: flex;
|
|
428
|
+
gap: 8px;
|
|
429
|
+
padding: 8px 12px;
|
|
430
|
+
border-top: 1px solid ${colors.border};
|
|
431
|
+
overflow-x: auto;
|
|
432
|
+
background: ${colors.bgSecondary};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
.pp-attachment-preview {
|
|
436
|
+
position: relative;
|
|
437
|
+
width: 60px;
|
|
438
|
+
height: 60px;
|
|
439
|
+
border-radius: 8px;
|
|
440
|
+
overflow: hidden;
|
|
441
|
+
flex-shrink: 0;
|
|
442
|
+
background: ${colors.bg};
|
|
443
|
+
border: 1px solid ${colors.border};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
.pp-preview-img {
|
|
447
|
+
width: 100%;
|
|
448
|
+
height: 100%;
|
|
449
|
+
object-fit: cover;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
.pp-preview-file {
|
|
453
|
+
width: 100%;
|
|
454
|
+
height: 100%;
|
|
455
|
+
display: flex;
|
|
456
|
+
align-items: center;
|
|
457
|
+
justify-content: center;
|
|
458
|
+
color: ${colors.textSecondary};
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
.pp-preview-file svg {
|
|
462
|
+
width: 24px;
|
|
463
|
+
height: 24px;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
.pp-remove-attachment {
|
|
467
|
+
position: absolute;
|
|
468
|
+
top: 2px;
|
|
469
|
+
right: 2px;
|
|
470
|
+
width: 18px;
|
|
471
|
+
height: 18px;
|
|
472
|
+
border-radius: 50%;
|
|
473
|
+
background: rgba(0, 0, 0, 0.6);
|
|
474
|
+
color: white;
|
|
475
|
+
border: none;
|
|
476
|
+
cursor: pointer;
|
|
477
|
+
display: flex;
|
|
478
|
+
align-items: center;
|
|
479
|
+
justify-content: center;
|
|
480
|
+
padding: 0;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
.pp-remove-attachment svg {
|
|
484
|
+
width: 10px;
|
|
485
|
+
height: 10px;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
.pp-upload-progress {
|
|
489
|
+
position: absolute;
|
|
490
|
+
bottom: 0;
|
|
491
|
+
left: 0;
|
|
492
|
+
height: 3px;
|
|
493
|
+
background: ${primaryColor};
|
|
494
|
+
transition: width 0.1s;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
.pp-upload-error {
|
|
498
|
+
position: absolute;
|
|
499
|
+
top: 50%;
|
|
500
|
+
left: 50%;
|
|
501
|
+
transform: translate(-50%, -50%);
|
|
502
|
+
width: 24px;
|
|
503
|
+
height: 24px;
|
|
504
|
+
border-radius: 50%;
|
|
505
|
+
background: #ef4444;
|
|
506
|
+
color: white;
|
|
507
|
+
font-weight: bold;
|
|
508
|
+
display: flex;
|
|
509
|
+
align-items: center;
|
|
510
|
+
justify-content: center;
|
|
511
|
+
font-size: 14px;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
.pp-attachment-uploading {
|
|
515
|
+
opacity: 0.7;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
.pp-attachment-error {
|
|
519
|
+
border-color: #ef4444;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/* Message Attachments */
|
|
523
|
+
.pp-message-attachments {
|
|
524
|
+
display: flex;
|
|
525
|
+
flex-direction: column;
|
|
526
|
+
gap: 8px;
|
|
527
|
+
margin-top: 4px;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
.pp-attachment {
|
|
531
|
+
display: block;
|
|
532
|
+
text-decoration: none;
|
|
533
|
+
color: inherit;
|
|
534
|
+
border-radius: 8px;
|
|
535
|
+
overflow: hidden;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
.pp-attachment-image img {
|
|
539
|
+
max-width: 200px;
|
|
540
|
+
max-height: 200px;
|
|
541
|
+
border-radius: 8px;
|
|
542
|
+
display: block;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
.pp-attachment-audio {
|
|
546
|
+
display: flex;
|
|
547
|
+
flex-direction: column;
|
|
548
|
+
gap: 4px;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
.pp-attachment-audio audio {
|
|
552
|
+
width: 200px;
|
|
553
|
+
height: 36px;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
.pp-attachment-audio .pp-attachment-name {
|
|
557
|
+
font-size: 11px;
|
|
558
|
+
opacity: 0.7;
|
|
559
|
+
white-space: nowrap;
|
|
560
|
+
overflow: hidden;
|
|
561
|
+
text-overflow: ellipsis;
|
|
562
|
+
max-width: 200px;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
.pp-attachment-video video {
|
|
566
|
+
max-width: 200px;
|
|
567
|
+
max-height: 200px;
|
|
568
|
+
border-radius: 8px;
|
|
569
|
+
display: block;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
.pp-attachment-file {
|
|
573
|
+
display: flex;
|
|
574
|
+
align-items: center;
|
|
575
|
+
gap: 8px;
|
|
576
|
+
padding: 8px 12px;
|
|
577
|
+
background: ${isDark ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.05)"};
|
|
578
|
+
border-radius: 8px;
|
|
579
|
+
transition: background 0.2s;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
.pp-attachment-file:hover {
|
|
583
|
+
background: ${isDark ? "rgba(255,255,255,0.15)" : "rgba(0,0,0,0.08)"};
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
.pp-attachment-file svg {
|
|
587
|
+
width: 24px;
|
|
588
|
+
height: 24px;
|
|
589
|
+
flex-shrink: 0;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
.pp-attachment-info {
|
|
593
|
+
display: flex;
|
|
594
|
+
flex-direction: column;
|
|
595
|
+
min-width: 0;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
.pp-attachment-name {
|
|
599
|
+
font-size: 13px;
|
|
600
|
+
font-weight: 500;
|
|
601
|
+
white-space: nowrap;
|
|
602
|
+
overflow: hidden;
|
|
603
|
+
text-overflow: ellipsis;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
.pp-attachment-size {
|
|
607
|
+
font-size: 11px;
|
|
608
|
+
opacity: 0.7;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/* Drag & Drop */
|
|
612
|
+
.pp-dragging {
|
|
613
|
+
position: relative;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
.pp-drop-overlay {
|
|
617
|
+
position: absolute;
|
|
618
|
+
inset: 0;
|
|
619
|
+
background: ${isDark ? "rgba(0,0,0,0.9)" : "rgba(255,255,255,0.95)"};
|
|
620
|
+
display: flex;
|
|
621
|
+
flex-direction: column;
|
|
622
|
+
align-items: center;
|
|
623
|
+
justify-content: center;
|
|
624
|
+
gap: 12px;
|
|
625
|
+
z-index: 100;
|
|
626
|
+
border: 3px dashed ${primaryColor};
|
|
627
|
+
border-radius: 16px;
|
|
628
|
+
margin: 4px;
|
|
629
|
+
pointer-events: none;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
.pp-drop-icon svg {
|
|
633
|
+
width: 48px;
|
|
634
|
+
height: 48px;
|
|
635
|
+
color: ${primaryColor};
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
.pp-drop-text {
|
|
639
|
+
font-size: 16px;
|
|
640
|
+
font-weight: 500;
|
|
641
|
+
color: ${colors.text};
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/* Message Context Menu */
|
|
645
|
+
.pp-message-menu {
|
|
646
|
+
position: fixed;
|
|
647
|
+
background: ${colors.bg};
|
|
648
|
+
border: 1px solid ${colors.border};
|
|
649
|
+
border-radius: 8px;
|
|
650
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
651
|
+
padding: 4px;
|
|
652
|
+
z-index: 200;
|
|
653
|
+
min-width: 120px;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
.pp-message-menu button {
|
|
657
|
+
display: flex;
|
|
658
|
+
align-items: center;
|
|
659
|
+
gap: 8px;
|
|
660
|
+
width: 100%;
|
|
661
|
+
padding: 8px 12px;
|
|
662
|
+
border: none;
|
|
663
|
+
background: transparent;
|
|
664
|
+
color: ${colors.text};
|
|
665
|
+
font-size: 13px;
|
|
666
|
+
cursor: pointer;
|
|
667
|
+
border-radius: 4px;
|
|
668
|
+
text-align: left;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
.pp-message-menu button:hover {
|
|
672
|
+
background: ${isDark ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.05)"};
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
.pp-message-menu button svg {
|
|
676
|
+
width: 16px;
|
|
677
|
+
height: 16px;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
.pp-menu-delete {
|
|
681
|
+
color: #ef4444 !important;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/* Edit Modal */
|
|
685
|
+
.pp-edit-modal {
|
|
686
|
+
position: absolute;
|
|
687
|
+
bottom: 80px;
|
|
688
|
+
left: 12px;
|
|
689
|
+
right: 12px;
|
|
690
|
+
background: ${colors.bg};
|
|
691
|
+
border: 1px solid ${colors.border};
|
|
692
|
+
border-radius: 12px;
|
|
693
|
+
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
|
|
694
|
+
z-index: 150;
|
|
695
|
+
overflow: hidden;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
.pp-edit-header {
|
|
699
|
+
display: flex;
|
|
700
|
+
justify-content: space-between;
|
|
701
|
+
align-items: center;
|
|
702
|
+
padding: 12px 16px;
|
|
703
|
+
border-bottom: 1px solid ${colors.border};
|
|
704
|
+
font-weight: 500;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
.pp-edit-header button {
|
|
708
|
+
background: transparent;
|
|
709
|
+
border: none;
|
|
710
|
+
color: ${colors.textSecondary};
|
|
711
|
+
cursor: pointer;
|
|
712
|
+
padding: 4px;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
.pp-edit-header button svg {
|
|
716
|
+
width: 18px;
|
|
717
|
+
height: 18px;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
.pp-edit-input {
|
|
721
|
+
width: 100%;
|
|
722
|
+
padding: 12px 16px;
|
|
723
|
+
border: none;
|
|
724
|
+
background: transparent;
|
|
725
|
+
color: ${colors.text};
|
|
726
|
+
font-size: 14px;
|
|
727
|
+
resize: none;
|
|
728
|
+
min-height: 80px;
|
|
729
|
+
outline: none;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
.pp-edit-actions {
|
|
733
|
+
display: flex;
|
|
734
|
+
justify-content: flex-end;
|
|
735
|
+
gap: 8px;
|
|
736
|
+
padding: 12px 16px;
|
|
737
|
+
border-top: 1px solid ${colors.border};
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
.pp-edit-cancel {
|
|
741
|
+
padding: 8px 16px;
|
|
742
|
+
border: 1px solid ${colors.border};
|
|
743
|
+
border-radius: 6px;
|
|
744
|
+
background: transparent;
|
|
745
|
+
color: ${colors.text};
|
|
746
|
+
font-size: 13px;
|
|
747
|
+
cursor: pointer;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
.pp-edit-save {
|
|
751
|
+
padding: 8px 16px;
|
|
752
|
+
border: none;
|
|
753
|
+
border-radius: 6px;
|
|
754
|
+
background: ${primaryColor};
|
|
755
|
+
color: white;
|
|
756
|
+
font-size: 13px;
|
|
757
|
+
cursor: pointer;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
.pp-edit-save:disabled {
|
|
761
|
+
opacity: 0.5;
|
|
762
|
+
cursor: not-allowed;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/* Reply Preview */
|
|
766
|
+
.pp-reply-preview {
|
|
767
|
+
display: flex;
|
|
768
|
+
align-items: center;
|
|
769
|
+
gap: 8px;
|
|
770
|
+
padding: 8px 12px;
|
|
771
|
+
background: ${isDark ? "rgba(255,255,255,0.05)" : "rgba(0,0,0,0.03)"};
|
|
772
|
+
border-top: 1px solid ${colors.border};
|
|
773
|
+
border-left: 3px solid ${primaryColor};
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
.pp-reply-preview-content {
|
|
777
|
+
flex: 1;
|
|
778
|
+
min-width: 0;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
.pp-reply-label {
|
|
782
|
+
display: block;
|
|
783
|
+
font-size: 11px;
|
|
784
|
+
color: ${primaryColor};
|
|
785
|
+
font-weight: 500;
|
|
786
|
+
margin-bottom: 2px;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
.pp-reply-text {
|
|
790
|
+
display: block;
|
|
791
|
+
font-size: 12px;
|
|
792
|
+
color: ${colors.textSecondary};
|
|
793
|
+
white-space: nowrap;
|
|
794
|
+
overflow: hidden;
|
|
795
|
+
text-overflow: ellipsis;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
.pp-reply-cancel {
|
|
799
|
+
background: transparent;
|
|
800
|
+
border: none;
|
|
801
|
+
color: ${colors.textSecondary};
|
|
802
|
+
cursor: pointer;
|
|
803
|
+
padding: 4px;
|
|
804
|
+
flex-shrink: 0;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
.pp-reply-cancel svg {
|
|
808
|
+
width: 16px;
|
|
809
|
+
height: 16px;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/* Reply Quote in Message */
|
|
813
|
+
.pp-reply-quote {
|
|
814
|
+
background: ${isDark ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.05)"};
|
|
815
|
+
border-left: 2px solid ${primaryColor};
|
|
816
|
+
padding: 4px 8px;
|
|
817
|
+
margin-bottom: 6px;
|
|
818
|
+
border-radius: 0 4px 4px 0;
|
|
819
|
+
font-size: 12px;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
.pp-reply-sender {
|
|
823
|
+
display: block;
|
|
824
|
+
font-weight: 500;
|
|
825
|
+
color: ${primaryColor};
|
|
826
|
+
margin-bottom: 2px;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
.pp-reply-content {
|
|
830
|
+
display: block;
|
|
831
|
+
color: ${colors.textSecondary};
|
|
832
|
+
white-space: nowrap;
|
|
833
|
+
overflow: hidden;
|
|
834
|
+
text-overflow: ellipsis;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/* Deleted Message */
|
|
838
|
+
.pp-message-deleted {
|
|
839
|
+
opacity: 0.6;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
.pp-deleted-content {
|
|
843
|
+
font-style: italic;
|
|
844
|
+
color: ${colors.textSecondary};
|
|
845
|
+
display: flex;
|
|
846
|
+
align-items: center;
|
|
847
|
+
gap: 4px;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
.pp-deleted-icon {
|
|
851
|
+
font-size: 12px;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/* Edited Badge */
|
|
855
|
+
.pp-edited-badge {
|
|
856
|
+
font-size: 10px;
|
|
857
|
+
color: ${colors.textSecondary};
|
|
858
|
+
margin-left: 4px;
|
|
859
|
+
font-style: italic;
|
|
860
|
+
}
|
|
375
861
|
`;
|
|
376
862
|
}
|
|
377
863
|
|
|
@@ -385,9 +871,18 @@ function ChatWidget({ client: client2, config: initialConfig }) {
|
|
|
385
871
|
const [operatorOnline, setOperatorOnline] = useState(false);
|
|
386
872
|
const [isConnected, setIsConnected] = useState(false);
|
|
387
873
|
const [unreadCount, setUnreadCount] = useState(0);
|
|
874
|
+
const [pendingAttachments, setPendingAttachments] = useState([]);
|
|
875
|
+
const [isUploading, setIsUploading] = useState(false);
|
|
876
|
+
const [replyingTo, setReplyingTo] = useState(null);
|
|
877
|
+
const [editingMessage, setEditingMessage] = useState(null);
|
|
878
|
+
const [editContent, setEditContent] = useState("");
|
|
879
|
+
const [messageMenu, setMessageMenu] = useState(null);
|
|
880
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
388
881
|
const [config, setConfig] = useState(initialConfig);
|
|
389
882
|
const messagesEndRef = useRef(null);
|
|
390
883
|
const inputRef = useRef(null);
|
|
884
|
+
const fileInputRef = useRef(null);
|
|
885
|
+
const messagesContainerRef = useRef(null);
|
|
391
886
|
useEffect(() => {
|
|
392
887
|
const unsubOpen = client2.on("openChange", setIsOpen);
|
|
393
888
|
const unsubMessage = client2.on("message", () => {
|
|
@@ -484,11 +979,17 @@ function ChatWidget({ client: client2, config: initialConfig }) {
|
|
|
484
979
|
if (!shouldShow) return null;
|
|
485
980
|
const handleSubmit = async (e) => {
|
|
486
981
|
e.preventDefault();
|
|
487
|
-
|
|
982
|
+
const hasContent = inputValue.trim().length > 0;
|
|
983
|
+
const readyAttachments = pendingAttachments.filter((a) => a.status === "ready" && a.attachment);
|
|
984
|
+
if (!hasContent && readyAttachments.length === 0) return;
|
|
488
985
|
const content = inputValue;
|
|
986
|
+
const attachmentIds = readyAttachments.map((a) => a.attachment.id);
|
|
987
|
+
const replyToId = replyingTo?.id;
|
|
489
988
|
setInputValue("");
|
|
989
|
+
setPendingAttachments([]);
|
|
990
|
+
setReplyingTo(null);
|
|
490
991
|
try {
|
|
491
|
-
await client2.sendMessage(content);
|
|
992
|
+
await client2.sendMessage(content, attachmentIds, replyToId);
|
|
492
993
|
} catch (err) {
|
|
493
994
|
console.error("[PocketPing] Failed to send message:", err);
|
|
494
995
|
}
|
|
@@ -498,6 +999,190 @@ function ChatWidget({ client: client2, config: initialConfig }) {
|
|
|
498
999
|
setInputValue(target.value);
|
|
499
1000
|
client2.sendTyping(true);
|
|
500
1001
|
};
|
|
1002
|
+
const handleFileSelect = async (e) => {
|
|
1003
|
+
const target = e.target;
|
|
1004
|
+
const files = target.files;
|
|
1005
|
+
if (!files || files.length === 0) return;
|
|
1006
|
+
const newPending = [];
|
|
1007
|
+
for (let i = 0; i < files.length; i++) {
|
|
1008
|
+
const file = files[i];
|
|
1009
|
+
const id = `pending-${Date.now()}-${i}`;
|
|
1010
|
+
let preview;
|
|
1011
|
+
if (file.type.startsWith("image/")) {
|
|
1012
|
+
preview = URL.createObjectURL(file);
|
|
1013
|
+
}
|
|
1014
|
+
newPending.push({
|
|
1015
|
+
id,
|
|
1016
|
+
file,
|
|
1017
|
+
preview,
|
|
1018
|
+
progress: 0,
|
|
1019
|
+
status: "pending"
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
setPendingAttachments((prev) => [...prev, ...newPending]);
|
|
1023
|
+
target.value = "";
|
|
1024
|
+
setIsUploading(true);
|
|
1025
|
+
for (const pending of newPending) {
|
|
1026
|
+
try {
|
|
1027
|
+
setPendingAttachments(
|
|
1028
|
+
(prev) => prev.map((a) => a.id === pending.id ? { ...a, status: "uploading" } : a)
|
|
1029
|
+
);
|
|
1030
|
+
const attachment = await client2.uploadFile(pending.file, (progress) => {
|
|
1031
|
+
setPendingAttachments(
|
|
1032
|
+
(prev) => prev.map((a) => a.id === pending.id ? { ...a, progress } : a)
|
|
1033
|
+
);
|
|
1034
|
+
});
|
|
1035
|
+
setPendingAttachments(
|
|
1036
|
+
(prev) => prev.map(
|
|
1037
|
+
(a) => a.id === pending.id ? { ...a, status: "ready", progress: 100, attachment } : a
|
|
1038
|
+
)
|
|
1039
|
+
);
|
|
1040
|
+
} catch (err) {
|
|
1041
|
+
console.error("[PocketPing] Failed to upload file:", err);
|
|
1042
|
+
setPendingAttachments(
|
|
1043
|
+
(prev) => prev.map(
|
|
1044
|
+
(a) => a.id === pending.id ? { ...a, status: "error", error: "Upload failed" } : a
|
|
1045
|
+
)
|
|
1046
|
+
);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
setIsUploading(false);
|
|
1050
|
+
};
|
|
1051
|
+
const handleRemoveAttachment = (id) => {
|
|
1052
|
+
setPendingAttachments((prev) => {
|
|
1053
|
+
const removed = prev.find((a) => a.id === id);
|
|
1054
|
+
if (removed?.preview) {
|
|
1055
|
+
URL.revokeObjectURL(removed.preview);
|
|
1056
|
+
}
|
|
1057
|
+
return prev.filter((a) => a.id !== id);
|
|
1058
|
+
});
|
|
1059
|
+
};
|
|
1060
|
+
const handleReply = (message) => {
|
|
1061
|
+
setReplyingTo(message);
|
|
1062
|
+
setMessageMenu(null);
|
|
1063
|
+
inputRef.current?.focus();
|
|
1064
|
+
};
|
|
1065
|
+
const handleCancelReply = () => {
|
|
1066
|
+
setReplyingTo(null);
|
|
1067
|
+
};
|
|
1068
|
+
const handleStartEdit = (message) => {
|
|
1069
|
+
if (message.sender !== "visitor") return;
|
|
1070
|
+
setEditingMessage(message);
|
|
1071
|
+
setEditContent(message.content);
|
|
1072
|
+
setMessageMenu(null);
|
|
1073
|
+
};
|
|
1074
|
+
const handleCancelEdit = () => {
|
|
1075
|
+
setEditingMessage(null);
|
|
1076
|
+
setEditContent("");
|
|
1077
|
+
};
|
|
1078
|
+
const handleSaveEdit = async () => {
|
|
1079
|
+
if (!editingMessage || !editContent.trim()) return;
|
|
1080
|
+
try {
|
|
1081
|
+
await client2.editMessage(editingMessage.id, editContent.trim());
|
|
1082
|
+
setEditingMessage(null);
|
|
1083
|
+
setEditContent("");
|
|
1084
|
+
} catch (err) {
|
|
1085
|
+
console.error("[PocketPing] Failed to edit message:", err);
|
|
1086
|
+
}
|
|
1087
|
+
};
|
|
1088
|
+
const handleDelete = async (message) => {
|
|
1089
|
+
if (message.sender !== "visitor") return;
|
|
1090
|
+
setMessageMenu(null);
|
|
1091
|
+
if (confirm("Delete this message?")) {
|
|
1092
|
+
try {
|
|
1093
|
+
await client2.deleteMessage(message.id);
|
|
1094
|
+
} catch (err) {
|
|
1095
|
+
console.error("[PocketPing] Failed to delete message:", err);
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
};
|
|
1099
|
+
const handleMessageContextMenu = (e, message) => {
|
|
1100
|
+
e.preventDefault();
|
|
1101
|
+
const mouseEvent = e;
|
|
1102
|
+
setMessageMenu({
|
|
1103
|
+
message,
|
|
1104
|
+
x: mouseEvent.clientX,
|
|
1105
|
+
y: mouseEvent.clientY
|
|
1106
|
+
});
|
|
1107
|
+
};
|
|
1108
|
+
useEffect(() => {
|
|
1109
|
+
if (!messageMenu) return;
|
|
1110
|
+
const handleClickOutside = () => setMessageMenu(null);
|
|
1111
|
+
document.addEventListener("click", handleClickOutside);
|
|
1112
|
+
return () => document.removeEventListener("click", handleClickOutside);
|
|
1113
|
+
}, [messageMenu]);
|
|
1114
|
+
const dragCounterRef = useRef(0);
|
|
1115
|
+
const handleDragEnter = (e) => {
|
|
1116
|
+
e.preventDefault();
|
|
1117
|
+
e.stopPropagation();
|
|
1118
|
+
dragCounterRef.current++;
|
|
1119
|
+
if (dragCounterRef.current === 1) {
|
|
1120
|
+
setIsDragging(true);
|
|
1121
|
+
}
|
|
1122
|
+
};
|
|
1123
|
+
const handleDragOver = (e) => {
|
|
1124
|
+
e.preventDefault();
|
|
1125
|
+
e.stopPropagation();
|
|
1126
|
+
};
|
|
1127
|
+
const handleDragLeave = (e) => {
|
|
1128
|
+
e.preventDefault();
|
|
1129
|
+
e.stopPropagation();
|
|
1130
|
+
dragCounterRef.current--;
|
|
1131
|
+
if (dragCounterRef.current === 0) {
|
|
1132
|
+
setIsDragging(false);
|
|
1133
|
+
}
|
|
1134
|
+
};
|
|
1135
|
+
const handleDrop = async (e) => {
|
|
1136
|
+
e.preventDefault();
|
|
1137
|
+
e.stopPropagation();
|
|
1138
|
+
dragCounterRef.current = 0;
|
|
1139
|
+
setIsDragging(false);
|
|
1140
|
+
const files = e.dataTransfer?.files;
|
|
1141
|
+
if (!files || files.length === 0) return;
|
|
1142
|
+
const newPending = [];
|
|
1143
|
+
for (let i = 0; i < files.length; i++) {
|
|
1144
|
+
const file = files[i];
|
|
1145
|
+
const id = `pending-${Date.now()}-${i}`;
|
|
1146
|
+
let preview;
|
|
1147
|
+
if (file.type.startsWith("image/")) {
|
|
1148
|
+
preview = URL.createObjectURL(file);
|
|
1149
|
+
}
|
|
1150
|
+
newPending.push({
|
|
1151
|
+
id,
|
|
1152
|
+
file,
|
|
1153
|
+
preview,
|
|
1154
|
+
progress: 0,
|
|
1155
|
+
status: "pending"
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
setPendingAttachments((prev) => [...prev, ...newPending]);
|
|
1159
|
+
setIsUploading(true);
|
|
1160
|
+
for (const pending of newPending) {
|
|
1161
|
+
try {
|
|
1162
|
+
setPendingAttachments(
|
|
1163
|
+
(prev) => prev.map((a) => a.id === pending.id ? { ...a, status: "uploading" } : a)
|
|
1164
|
+
);
|
|
1165
|
+
const attachment = await client2.uploadFile(pending.file, (progress) => {
|
|
1166
|
+
setPendingAttachments(
|
|
1167
|
+
(prev) => prev.map((a) => a.id === pending.id ? { ...a, progress } : a)
|
|
1168
|
+
);
|
|
1169
|
+
});
|
|
1170
|
+
setPendingAttachments(
|
|
1171
|
+
(prev) => prev.map(
|
|
1172
|
+
(a) => a.id === pending.id ? { ...a, status: "ready", progress: 100, attachment } : a
|
|
1173
|
+
)
|
|
1174
|
+
);
|
|
1175
|
+
} catch (err) {
|
|
1176
|
+
console.error("[PocketPing] Failed to upload dropped file:", err);
|
|
1177
|
+
setPendingAttachments(
|
|
1178
|
+
(prev) => prev.map(
|
|
1179
|
+
(a) => a.id === pending.id ? { ...a, status: "error", error: "Upload failed" } : a
|
|
1180
|
+
)
|
|
1181
|
+
);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
setIsUploading(false);
|
|
1185
|
+
};
|
|
501
1186
|
const position = config.position ?? "bottom-right";
|
|
502
1187
|
const theme = getTheme(config.theme ?? "auto");
|
|
503
1188
|
const primaryColor = config.primaryColor ?? "#6366f1";
|
|
@@ -516,84 +1201,211 @@ function ChatWidget({ client: client2, config: initialConfig }) {
|
|
|
516
1201
|
]
|
|
517
1202
|
}
|
|
518
1203
|
),
|
|
519
|
-
isOpen && /* @__PURE__ */ jsxs(
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
/* @__PURE__ */ jsxs("div", { class: "pp-messages", children: [
|
|
545
|
-
config.welcomeMessage && messages.length === 0 && /* @__PURE__ */ jsx("div", { class: "pp-welcome", children: config.welcomeMessage }),
|
|
546
|
-
messages.map((msg) => /* @__PURE__ */ jsxs(
|
|
547
|
-
"div",
|
|
548
|
-
{
|
|
549
|
-
class: `pp-message pp-message-${msg.sender}`,
|
|
550
|
-
children: [
|
|
551
|
-
/* @__PURE__ */ jsx("div", { class: "pp-message-content", children: msg.content }),
|
|
552
|
-
/* @__PURE__ */ jsxs("div", { class: "pp-message-time", children: [
|
|
553
|
-
formatTime(msg.timestamp),
|
|
554
|
-
msg.sender === "ai" && /* @__PURE__ */ jsx("span", { class: "pp-ai-badge", children: "AI" }),
|
|
555
|
-
msg.sender === "visitor" && /* @__PURE__ */ jsx("span", { class: `pp-status pp-status-${msg.status ?? "sent"}`, children: /* @__PURE__ */ jsx(StatusIcon, { status: msg.status }) })
|
|
1204
|
+
isOpen && /* @__PURE__ */ jsxs(
|
|
1205
|
+
"div",
|
|
1206
|
+
{
|
|
1207
|
+
class: `pp-window pp-${position} pp-theme-${theme} ${isDragging ? "pp-dragging" : ""}`,
|
|
1208
|
+
onDragEnter: handleDragEnter,
|
|
1209
|
+
onDragOver: handleDragOver,
|
|
1210
|
+
onDragLeave: handleDragLeave,
|
|
1211
|
+
onDrop: handleDrop,
|
|
1212
|
+
children: [
|
|
1213
|
+
isDragging && /* @__PURE__ */ jsxs("div", { class: "pp-drop-overlay", children: [
|
|
1214
|
+
/* @__PURE__ */ jsx("div", { class: "pp-drop-icon", children: /* @__PURE__ */ jsx(AttachIcon, {}) }),
|
|
1215
|
+
/* @__PURE__ */ jsx("div", { class: "pp-drop-text", children: "Drop files to upload" })
|
|
1216
|
+
] }),
|
|
1217
|
+
/* @__PURE__ */ jsxs("div", { class: "pp-header", children: [
|
|
1218
|
+
/* @__PURE__ */ jsxs("div", { class: "pp-header-info", children: [
|
|
1219
|
+
config.operatorAvatar && /* @__PURE__ */ jsx("img", { src: config.operatorAvatar, alt: "", class: "pp-avatar" }),
|
|
1220
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
1221
|
+
/* @__PURE__ */ jsx("div", { class: "pp-header-title", children: config.operatorName ?? "Support" }),
|
|
1222
|
+
/* @__PURE__ */ jsx("div", { class: "pp-header-status", children: operatorOnline ? /* @__PURE__ */ jsxs(Fragment2, { children: [
|
|
1223
|
+
/* @__PURE__ */ jsx("span", { class: "pp-status-dot pp-online" }),
|
|
1224
|
+
" Online"
|
|
1225
|
+
] }) : /* @__PURE__ */ jsxs(Fragment2, { children: [
|
|
1226
|
+
/* @__PURE__ */ jsx("span", { class: "pp-status-dot" }),
|
|
1227
|
+
" Away"
|
|
1228
|
+
] }) })
|
|
556
1229
|
] })
|
|
557
|
-
]
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
1230
|
+
] }),
|
|
1231
|
+
/* @__PURE__ */ jsx(
|
|
1232
|
+
"button",
|
|
1233
|
+
{
|
|
1234
|
+
class: "pp-close-btn",
|
|
1235
|
+
onClick: () => client2.setOpen(false),
|
|
1236
|
+
"aria-label": "Close chat",
|
|
1237
|
+
children: /* @__PURE__ */ jsx(CloseIcon, {})
|
|
1238
|
+
}
|
|
1239
|
+
)
|
|
1240
|
+
] }),
|
|
1241
|
+
/* @__PURE__ */ jsxs("div", { class: "pp-messages", ref: messagesContainerRef, children: [
|
|
1242
|
+
config.welcomeMessage && messages.length === 0 && /* @__PURE__ */ jsx("div", { class: "pp-welcome", children: config.welcomeMessage }),
|
|
1243
|
+
messages.map((msg) => {
|
|
1244
|
+
const isDeleted = !!msg.deletedAt;
|
|
1245
|
+
const isEdited = !!msg.editedAt;
|
|
1246
|
+
const replyToMsg = msg.replyTo ? messages.find((m) => m.id === msg.replyTo) : null;
|
|
1247
|
+
return /* @__PURE__ */ jsxs(
|
|
1248
|
+
"div",
|
|
1249
|
+
{
|
|
1250
|
+
class: `pp-message pp-message-${msg.sender} ${isDeleted ? "pp-message-deleted" : ""}`,
|
|
1251
|
+
onContextMenu: (e) => handleMessageContextMenu(e, msg),
|
|
1252
|
+
children: [
|
|
1253
|
+
replyToMsg && /* @__PURE__ */ jsxs("div", { class: "pp-reply-quote", children: [
|
|
1254
|
+
/* @__PURE__ */ jsx("span", { class: "pp-reply-sender", children: replyToMsg.sender === "visitor" ? "You" : "Support" }),
|
|
1255
|
+
/* @__PURE__ */ jsxs("span", { class: "pp-reply-content", children: [
|
|
1256
|
+
replyToMsg.deletedAt ? "Message deleted" : replyToMsg.content.slice(0, 50),
|
|
1257
|
+
replyToMsg.content.length > 50 ? "..." : ""
|
|
1258
|
+
] })
|
|
1259
|
+
] }),
|
|
1260
|
+
isDeleted ? /* @__PURE__ */ jsxs("div", { class: "pp-message-content pp-deleted-content", children: [
|
|
1261
|
+
/* @__PURE__ */ jsx("span", { class: "pp-deleted-icon", children: "\u{1F5D1}\uFE0F" }),
|
|
1262
|
+
" Message deleted"
|
|
1263
|
+
] }) : /* @__PURE__ */ jsxs(Fragment2, { children: [
|
|
1264
|
+
msg.content && /* @__PURE__ */ jsx("div", { class: "pp-message-content", children: msg.content }),
|
|
1265
|
+
msg.attachments && msg.attachments.length > 0 && /* @__PURE__ */ jsx("div", { class: "pp-message-attachments", children: msg.attachments.map((att) => /* @__PURE__ */ jsx(AttachmentDisplay, { attachment: att }, att.id)) })
|
|
1266
|
+
] }),
|
|
1267
|
+
/* @__PURE__ */ jsxs("div", { class: "pp-message-time", children: [
|
|
1268
|
+
formatTime(msg.timestamp),
|
|
1269
|
+
isEdited && !isDeleted && /* @__PURE__ */ jsx("span", { class: "pp-edited-badge", children: "edited" }),
|
|
1270
|
+
msg.sender === "ai" && /* @__PURE__ */ jsx("span", { class: "pp-ai-badge", children: "AI" }),
|
|
1271
|
+
msg.sender === "visitor" && !isDeleted && /* @__PURE__ */ jsx("span", { class: `pp-status pp-status-${msg.status ?? "sent"}`, children: /* @__PURE__ */ jsx(StatusIcon, { status: msg.status }) })
|
|
1272
|
+
] })
|
|
1273
|
+
]
|
|
1274
|
+
},
|
|
1275
|
+
msg.id
|
|
1276
|
+
);
|
|
1277
|
+
}),
|
|
1278
|
+
isTyping && /* @__PURE__ */ jsxs("div", { class: "pp-message pp-message-operator pp-typing", children: [
|
|
1279
|
+
/* @__PURE__ */ jsx("span", {}),
|
|
1280
|
+
/* @__PURE__ */ jsx("span", {}),
|
|
1281
|
+
/* @__PURE__ */ jsx("span", {})
|
|
1282
|
+
] }),
|
|
1283
|
+
/* @__PURE__ */ jsx("div", { ref: messagesEndRef })
|
|
1284
|
+
] }),
|
|
1285
|
+
messageMenu && /* @__PURE__ */ jsxs(
|
|
1286
|
+
"div",
|
|
1287
|
+
{
|
|
1288
|
+
class: "pp-message-menu",
|
|
1289
|
+
style: { top: `${messageMenu.y}px`, left: `${messageMenu.x}px` },
|
|
1290
|
+
children: [
|
|
1291
|
+
/* @__PURE__ */ jsxs("button", { onClick: () => handleReply(messageMenu.message), children: [
|
|
1292
|
+
/* @__PURE__ */ jsx(ReplyIcon, {}),
|
|
1293
|
+
" Reply"
|
|
1294
|
+
] }),
|
|
1295
|
+
messageMenu.message.sender === "visitor" && !messageMenu.message.deletedAt && /* @__PURE__ */ jsxs(Fragment2, { children: [
|
|
1296
|
+
/* @__PURE__ */ jsxs("button", { onClick: () => handleStartEdit(messageMenu.message), children: [
|
|
1297
|
+
/* @__PURE__ */ jsx(EditIcon, {}),
|
|
1298
|
+
" Edit"
|
|
1299
|
+
] }),
|
|
1300
|
+
/* @__PURE__ */ jsxs("button", { class: "pp-menu-delete", onClick: () => handleDelete(messageMenu.message), children: [
|
|
1301
|
+
/* @__PURE__ */ jsx(DeleteIcon, {}),
|
|
1302
|
+
" Delete"
|
|
1303
|
+
] })
|
|
1304
|
+
] })
|
|
1305
|
+
]
|
|
1306
|
+
}
|
|
1307
|
+
),
|
|
1308
|
+
editingMessage && /* @__PURE__ */ jsxs("div", { class: "pp-edit-modal", children: [
|
|
1309
|
+
/* @__PURE__ */ jsxs("div", { class: "pp-edit-header", children: [
|
|
1310
|
+
/* @__PURE__ */ jsx("span", { children: "Edit message" }),
|
|
1311
|
+
/* @__PURE__ */ jsx("button", { onClick: handleCancelEdit, children: /* @__PURE__ */ jsx(CloseIcon, {}) })
|
|
1312
|
+
] }),
|
|
1313
|
+
/* @__PURE__ */ jsx(
|
|
1314
|
+
"textarea",
|
|
1315
|
+
{
|
|
1316
|
+
class: "pp-edit-input",
|
|
1317
|
+
value: editContent,
|
|
1318
|
+
onInput: (e) => setEditContent(e.target.value),
|
|
1319
|
+
autoFocus: true
|
|
1320
|
+
}
|
|
1321
|
+
),
|
|
1322
|
+
/* @__PURE__ */ jsxs("div", { class: "pp-edit-actions", children: [
|
|
1323
|
+
/* @__PURE__ */ jsx("button", { class: "pp-edit-cancel", onClick: handleCancelEdit, children: "Cancel" }),
|
|
1324
|
+
/* @__PURE__ */ jsx("button", { class: "pp-edit-save", onClick: handleSaveEdit, disabled: !editContent.trim(), children: "Save" })
|
|
1325
|
+
] })
|
|
1326
|
+
] }),
|
|
1327
|
+
replyingTo && /* @__PURE__ */ jsxs("div", { class: "pp-reply-preview", children: [
|
|
1328
|
+
/* @__PURE__ */ jsxs("div", { class: "pp-reply-preview-content", children: [
|
|
1329
|
+
/* @__PURE__ */ jsx("span", { class: "pp-reply-label", children: "Replying to" }),
|
|
1330
|
+
/* @__PURE__ */ jsxs("span", { class: "pp-reply-text", children: [
|
|
1331
|
+
replyingTo.content.slice(0, 50),
|
|
1332
|
+
replyingTo.content.length > 50 ? "..." : ""
|
|
1333
|
+
] })
|
|
1334
|
+
] }),
|
|
1335
|
+
/* @__PURE__ */ jsx("button", { class: "pp-reply-cancel", onClick: handleCancelReply, children: /* @__PURE__ */ jsx(CloseIcon, {}) })
|
|
1336
|
+
] }),
|
|
1337
|
+
pendingAttachments.length > 0 && /* @__PURE__ */ jsx("div", { class: "pp-attachments-preview", children: pendingAttachments.map((pending) => /* @__PURE__ */ jsxs("div", { class: `pp-attachment-preview pp-attachment-${pending.status}`, children: [
|
|
1338
|
+
pending.preview ? /* @__PURE__ */ jsx("img", { src: pending.preview, alt: pending.file.name, class: "pp-preview-img" }) : /* @__PURE__ */ jsx("div", { class: "pp-preview-file", children: /* @__PURE__ */ jsx(FileIcon, { mimeType: pending.file.type }) }),
|
|
1339
|
+
/* @__PURE__ */ jsx(
|
|
1340
|
+
"button",
|
|
1341
|
+
{
|
|
1342
|
+
class: "pp-remove-attachment",
|
|
1343
|
+
onClick: () => handleRemoveAttachment(pending.id),
|
|
1344
|
+
"aria-label": "Remove attachment",
|
|
1345
|
+
type: "button",
|
|
1346
|
+
children: /* @__PURE__ */ jsx(CloseIcon, {})
|
|
1347
|
+
}
|
|
1348
|
+
),
|
|
1349
|
+
pending.status === "uploading" && /* @__PURE__ */ jsx("div", { class: "pp-upload-progress", style: { width: `${pending.progress}%` } }),
|
|
1350
|
+
pending.status === "error" && /* @__PURE__ */ jsx("div", { class: "pp-upload-error", title: pending.error, children: "!" })
|
|
1351
|
+
] }, pending.id)) }),
|
|
1352
|
+
/* @__PURE__ */ jsxs("form", { class: "pp-input-form", onSubmit: handleSubmit, children: [
|
|
1353
|
+
/* @__PURE__ */ jsx(
|
|
1354
|
+
"input",
|
|
1355
|
+
{
|
|
1356
|
+
ref: (el) => {
|
|
1357
|
+
fileInputRef.current = el;
|
|
1358
|
+
if (el) {
|
|
1359
|
+
el.onchange = handleFileSelect;
|
|
1360
|
+
}
|
|
1361
|
+
},
|
|
1362
|
+
type: "file",
|
|
1363
|
+
class: "pp-file-input",
|
|
1364
|
+
accept: "image/*,audio/*,video/*,.pdf,.doc,.docx,.xls,.xlsx,.txt",
|
|
1365
|
+
multiple: true
|
|
1366
|
+
}
|
|
1367
|
+
),
|
|
1368
|
+
/* @__PURE__ */ jsx(
|
|
1369
|
+
"button",
|
|
1370
|
+
{
|
|
1371
|
+
type: "button",
|
|
1372
|
+
class: "pp-attach-btn",
|
|
1373
|
+
onClick: () => fileInputRef.current?.click(),
|
|
1374
|
+
disabled: !isConnected || isUploading,
|
|
1375
|
+
"aria-label": "Attach file",
|
|
1376
|
+
children: /* @__PURE__ */ jsx(AttachIcon, {})
|
|
1377
|
+
}
|
|
1378
|
+
),
|
|
1379
|
+
/* @__PURE__ */ jsx(
|
|
1380
|
+
"input",
|
|
1381
|
+
{
|
|
1382
|
+
ref: inputRef,
|
|
1383
|
+
type: "text",
|
|
1384
|
+
class: "pp-input",
|
|
1385
|
+
placeholder: config.placeholder ?? "Type a message...",
|
|
1386
|
+
value: inputValue,
|
|
1387
|
+
onInput: handleInputChange,
|
|
1388
|
+
disabled: !isConnected
|
|
1389
|
+
}
|
|
1390
|
+
),
|
|
1391
|
+
/* @__PURE__ */ jsx(
|
|
1392
|
+
"button",
|
|
1393
|
+
{
|
|
1394
|
+
type: "submit",
|
|
1395
|
+
class: "pp-send-btn",
|
|
1396
|
+
disabled: !inputValue.trim() && pendingAttachments.filter((a) => a.status === "ready").length === 0 || !isConnected || isUploading,
|
|
1397
|
+
"aria-label": "Send message",
|
|
1398
|
+
children: /* @__PURE__ */ jsx(SendIcon, {})
|
|
1399
|
+
}
|
|
1400
|
+
)
|
|
1401
|
+
] }),
|
|
1402
|
+
/* @__PURE__ */ jsxs("div", { class: "pp-footer", children: [
|
|
1403
|
+
"Powered by ",
|
|
1404
|
+
/* @__PURE__ */ jsx("a", { href: "https://pocketping.io", target: "_blank", rel: "noopener", children: "PocketPing" })
|
|
1405
|
+
] })
|
|
1406
|
+
]
|
|
1407
|
+
}
|
|
1408
|
+
)
|
|
597
1409
|
] });
|
|
598
1410
|
}
|
|
599
1411
|
function checkPageVisibility(config) {
|
|
@@ -649,15 +1461,96 @@ function StatusIcon({ status }) {
|
|
|
649
1461
|
}
|
|
650
1462
|
return null;
|
|
651
1463
|
}
|
|
1464
|
+
function AttachIcon() {
|
|
1465
|
+
return /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: /* @__PURE__ */ jsx("path", { d: "M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" }) });
|
|
1466
|
+
}
|
|
1467
|
+
function ReplyIcon() {
|
|
1468
|
+
return /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: [
|
|
1469
|
+
/* @__PURE__ */ jsx("polyline", { points: "9 17 4 12 9 7" }),
|
|
1470
|
+
/* @__PURE__ */ jsx("path", { d: "M20 18v-2a4 4 0 0 0-4-4H4" })
|
|
1471
|
+
] });
|
|
1472
|
+
}
|
|
1473
|
+
function EditIcon() {
|
|
1474
|
+
return /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: /* @__PURE__ */ jsx("path", { d: "M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z" }) });
|
|
1475
|
+
}
|
|
1476
|
+
function DeleteIcon() {
|
|
1477
|
+
return /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: [
|
|
1478
|
+
/* @__PURE__ */ jsx("polyline", { points: "3 6 5 6 21 6" }),
|
|
1479
|
+
/* @__PURE__ */ jsx("path", { d: "M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" })
|
|
1480
|
+
] });
|
|
1481
|
+
}
|
|
1482
|
+
function FileIcon({ mimeType }) {
|
|
1483
|
+
if (mimeType === "application/pdf") {
|
|
1484
|
+
return /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: [
|
|
1485
|
+
/* @__PURE__ */ jsx("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }),
|
|
1486
|
+
/* @__PURE__ */ jsx("polyline", { points: "14 2 14 8 20 8" }),
|
|
1487
|
+
/* @__PURE__ */ jsx("path", { d: "M9 15h6" }),
|
|
1488
|
+
/* @__PURE__ */ jsx("path", { d: "M9 11h6" })
|
|
1489
|
+
] });
|
|
1490
|
+
}
|
|
1491
|
+
if (mimeType.startsWith("audio/")) {
|
|
1492
|
+
return /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: [
|
|
1493
|
+
/* @__PURE__ */ jsx("path", { d: "M9 18V5l12-2v13" }),
|
|
1494
|
+
/* @__PURE__ */ jsx("circle", { cx: "6", cy: "18", r: "3" }),
|
|
1495
|
+
/* @__PURE__ */ jsx("circle", { cx: "18", cy: "16", r: "3" })
|
|
1496
|
+
] });
|
|
1497
|
+
}
|
|
1498
|
+
if (mimeType.startsWith("video/")) {
|
|
1499
|
+
return /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: [
|
|
1500
|
+
/* @__PURE__ */ jsx("rect", { x: "2", y: "2", width: "20", height: "20", rx: "2.18", ry: "2.18" }),
|
|
1501
|
+
/* @__PURE__ */ jsx("line", { x1: "7", y1: "2", x2: "7", y2: "22" }),
|
|
1502
|
+
/* @__PURE__ */ jsx("line", { x1: "17", y1: "2", x2: "17", y2: "22" }),
|
|
1503
|
+
/* @__PURE__ */ jsx("line", { x1: "2", y1: "12", x2: "22", y2: "12" }),
|
|
1504
|
+
/* @__PURE__ */ jsx("line", { x1: "2", y1: "7", x2: "7", y2: "7" }),
|
|
1505
|
+
/* @__PURE__ */ jsx("line", { x1: "2", y1: "17", x2: "7", y2: "17" }),
|
|
1506
|
+
/* @__PURE__ */ jsx("line", { x1: "17", y1: "17", x2: "22", y2: "17" }),
|
|
1507
|
+
/* @__PURE__ */ jsx("line", { x1: "17", y1: "7", x2: "22", y2: "7" })
|
|
1508
|
+
] });
|
|
1509
|
+
}
|
|
1510
|
+
return /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: [
|
|
1511
|
+
/* @__PURE__ */ jsx("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }),
|
|
1512
|
+
/* @__PURE__ */ jsx("polyline", { points: "14 2 14 8 20 8" })
|
|
1513
|
+
] });
|
|
1514
|
+
}
|
|
1515
|
+
function AttachmentDisplay({ attachment }) {
|
|
1516
|
+
const isImage = attachment.mimeType.startsWith("image/");
|
|
1517
|
+
const isAudio = attachment.mimeType.startsWith("audio/");
|
|
1518
|
+
const isVideo = attachment.mimeType.startsWith("video/");
|
|
1519
|
+
const formatSize = (bytes) => {
|
|
1520
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
1521
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
1522
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
1523
|
+
};
|
|
1524
|
+
if (isImage) {
|
|
1525
|
+
return /* @__PURE__ */ jsx("a", { href: attachment.url, target: "_blank", rel: "noopener", class: "pp-attachment pp-attachment-image", children: /* @__PURE__ */ jsx("img", { src: attachment.thumbnailUrl || attachment.url, alt: attachment.filename }) });
|
|
1526
|
+
}
|
|
1527
|
+
if (isAudio) {
|
|
1528
|
+
return /* @__PURE__ */ jsxs("div", { class: "pp-attachment pp-attachment-audio", children: [
|
|
1529
|
+
/* @__PURE__ */ jsx("audio", { controls: true, preload: "metadata", children: /* @__PURE__ */ jsx("source", { src: attachment.url, type: attachment.mimeType }) }),
|
|
1530
|
+
/* @__PURE__ */ jsx("span", { class: "pp-attachment-name", children: attachment.filename })
|
|
1531
|
+
] });
|
|
1532
|
+
}
|
|
1533
|
+
if (isVideo) {
|
|
1534
|
+
return /* @__PURE__ */ jsx("div", { class: "pp-attachment pp-attachment-video", children: /* @__PURE__ */ jsx("video", { controls: true, preload: "metadata", children: /* @__PURE__ */ jsx("source", { src: attachment.url, type: attachment.mimeType }) }) });
|
|
1535
|
+
}
|
|
1536
|
+
return /* @__PURE__ */ jsxs("a", { href: attachment.url, target: "_blank", rel: "noopener", class: "pp-attachment pp-attachment-file", children: [
|
|
1537
|
+
/* @__PURE__ */ jsx(FileIcon, { mimeType: attachment.mimeType }),
|
|
1538
|
+
/* @__PURE__ */ jsxs("div", { class: "pp-attachment-info", children: [
|
|
1539
|
+
/* @__PURE__ */ jsx("span", { class: "pp-attachment-name", children: attachment.filename }),
|
|
1540
|
+
/* @__PURE__ */ jsx("span", { class: "pp-attachment-size", children: formatSize(attachment.size) })
|
|
1541
|
+
] })
|
|
1542
|
+
] });
|
|
1543
|
+
}
|
|
652
1544
|
|
|
653
1545
|
// src/version.ts
|
|
654
|
-
var VERSION = "
|
|
1546
|
+
var VERSION = "0.3.6";
|
|
655
1547
|
|
|
656
1548
|
// src/client.ts
|
|
657
1549
|
var PocketPingClient = class {
|
|
658
1550
|
constructor(config) {
|
|
659
1551
|
this.session = null;
|
|
660
1552
|
this.ws = null;
|
|
1553
|
+
this.sse = null;
|
|
661
1554
|
this.isOpen = false;
|
|
662
1555
|
this.listeners = /* @__PURE__ */ new Map();
|
|
663
1556
|
this.customEventHandlers = /* @__PURE__ */ new Map();
|
|
@@ -670,7 +1563,7 @@ var PocketPingClient = class {
|
|
|
670
1563
|
this.wsConnectedAt = 0;
|
|
671
1564
|
this.quickFailureThreshold = 2e3;
|
|
672
1565
|
// If WS fails within 2s, assume serverless
|
|
673
|
-
this.
|
|
1566
|
+
this.connectionMode = "none";
|
|
674
1567
|
this.trackedElementCleanups = [];
|
|
675
1568
|
this.currentTrackedElements = [];
|
|
676
1569
|
this.inspectorMode = false;
|
|
@@ -731,7 +1624,7 @@ var PocketPingClient = class {
|
|
|
731
1624
|
welcomeMessage: this.config.welcomeMessage
|
|
732
1625
|
});
|
|
733
1626
|
this.storeSessionId(response.sessionId);
|
|
734
|
-
this.
|
|
1627
|
+
this.connectRealtime();
|
|
735
1628
|
if (response.inspectorMode) {
|
|
736
1629
|
this.enableInspectorMode();
|
|
737
1630
|
} else if (response.trackedElements?.length) {
|
|
@@ -742,9 +1635,20 @@ var PocketPingClient = class {
|
|
|
742
1635
|
return this.session;
|
|
743
1636
|
}
|
|
744
1637
|
disconnect() {
|
|
745
|
-
this.ws
|
|
746
|
-
|
|
1638
|
+
if (this.ws) {
|
|
1639
|
+
this.ws.onclose = null;
|
|
1640
|
+
this.ws.onmessage = null;
|
|
1641
|
+
this.ws.onerror = null;
|
|
1642
|
+
this.ws.onopen = null;
|
|
1643
|
+
this.ws.close();
|
|
1644
|
+
this.ws = null;
|
|
1645
|
+
}
|
|
1646
|
+
if (this.sse) {
|
|
1647
|
+
this.sse.close();
|
|
1648
|
+
this.sse = null;
|
|
1649
|
+
}
|
|
747
1650
|
this.session = null;
|
|
1651
|
+
this.connectionMode = "none";
|
|
748
1652
|
if (this.reconnectTimeout) {
|
|
749
1653
|
clearTimeout(this.reconnectTimeout);
|
|
750
1654
|
}
|
|
@@ -752,7 +1656,7 @@ var PocketPingClient = class {
|
|
|
752
1656
|
this.cleanupTrackedElements();
|
|
753
1657
|
this.disableInspectorMode();
|
|
754
1658
|
}
|
|
755
|
-
async sendMessage(content) {
|
|
1659
|
+
async sendMessage(content, attachmentIds, replyTo) {
|
|
756
1660
|
if (!this.session) {
|
|
757
1661
|
throw new Error("Not connected");
|
|
758
1662
|
}
|
|
@@ -763,7 +1667,8 @@ var PocketPingClient = class {
|
|
|
763
1667
|
content,
|
|
764
1668
|
sender: "visitor",
|
|
765
1669
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
766
|
-
status: "sending"
|
|
1670
|
+
status: "sending",
|
|
1671
|
+
replyTo
|
|
767
1672
|
};
|
|
768
1673
|
this.session.messages.push(tempMessage);
|
|
769
1674
|
this.emit("message", tempMessage);
|
|
@@ -773,7 +1678,9 @@ var PocketPingClient = class {
|
|
|
773
1678
|
body: JSON.stringify({
|
|
774
1679
|
sessionId: this.session.sessionId,
|
|
775
1680
|
content,
|
|
776
|
-
sender: "visitor"
|
|
1681
|
+
sender: "visitor",
|
|
1682
|
+
attachmentIds: attachmentIds || [],
|
|
1683
|
+
replyTo
|
|
777
1684
|
})
|
|
778
1685
|
});
|
|
779
1686
|
const messageIndex = this.session.messages.findIndex((m) => m.id === tempId);
|
|
@@ -781,6 +1688,9 @@ var PocketPingClient = class {
|
|
|
781
1688
|
this.session.messages[messageIndex].id = response.messageId;
|
|
782
1689
|
this.session.messages[messageIndex].timestamp = response.timestamp;
|
|
783
1690
|
this.session.messages[messageIndex].status = "sent";
|
|
1691
|
+
if (response.attachments && response.attachments.length > 0) {
|
|
1692
|
+
this.session.messages[messageIndex].attachments = response.attachments;
|
|
1693
|
+
}
|
|
784
1694
|
this.emit("message", this.session.messages[messageIndex]);
|
|
785
1695
|
}
|
|
786
1696
|
const message = this.session.messages[messageIndex] || {
|
|
@@ -789,7 +1699,8 @@ var PocketPingClient = class {
|
|
|
789
1699
|
content,
|
|
790
1700
|
sender: "visitor",
|
|
791
1701
|
timestamp: response.timestamp,
|
|
792
|
-
status: "sent"
|
|
1702
|
+
status: "sent",
|
|
1703
|
+
attachments: response.attachments
|
|
793
1704
|
};
|
|
794
1705
|
this.config.onMessage?.(message);
|
|
795
1706
|
return message;
|
|
@@ -802,6 +1713,110 @@ var PocketPingClient = class {
|
|
|
802
1713
|
throw error;
|
|
803
1714
|
}
|
|
804
1715
|
}
|
|
1716
|
+
/**
|
|
1717
|
+
* Upload a file attachment
|
|
1718
|
+
* Returns the attachment data after successful upload
|
|
1719
|
+
* @param file - File object to upload
|
|
1720
|
+
* @param onProgress - Optional callback for upload progress (0-100)
|
|
1721
|
+
* @example
|
|
1722
|
+
* const attachment = await PocketPing.uploadFile(file, (progress) => {
|
|
1723
|
+
* console.log(`Upload ${progress}% complete`)
|
|
1724
|
+
* })
|
|
1725
|
+
* await PocketPing.sendMessage('Check this file', [attachment.id])
|
|
1726
|
+
*/
|
|
1727
|
+
async uploadFile(file, onProgress) {
|
|
1728
|
+
if (!this.session) {
|
|
1729
|
+
throw new Error("Not connected");
|
|
1730
|
+
}
|
|
1731
|
+
this.emit("uploadStart", { filename: file.name, size: file.size });
|
|
1732
|
+
try {
|
|
1733
|
+
const initResponse = await this.fetch("/upload", {
|
|
1734
|
+
method: "POST",
|
|
1735
|
+
body: JSON.stringify({
|
|
1736
|
+
sessionId: this.session.sessionId,
|
|
1737
|
+
filename: file.name,
|
|
1738
|
+
mimeType: file.type || "application/octet-stream",
|
|
1739
|
+
size: file.size
|
|
1740
|
+
})
|
|
1741
|
+
});
|
|
1742
|
+
onProgress?.(10);
|
|
1743
|
+
this.emit("uploadProgress", { filename: file.name, progress: 10 });
|
|
1744
|
+
await this.uploadToPresignedUrl(initResponse.uploadUrl, file, (progress) => {
|
|
1745
|
+
const mappedProgress = 10 + progress * 0.8;
|
|
1746
|
+
onProgress?.(mappedProgress);
|
|
1747
|
+
this.emit("uploadProgress", { filename: file.name, progress: mappedProgress });
|
|
1748
|
+
});
|
|
1749
|
+
const completeResponse = await this.fetch("/upload/complete", {
|
|
1750
|
+
method: "POST",
|
|
1751
|
+
body: JSON.stringify({
|
|
1752
|
+
sessionId: this.session.sessionId,
|
|
1753
|
+
attachmentId: initResponse.attachmentId
|
|
1754
|
+
})
|
|
1755
|
+
});
|
|
1756
|
+
onProgress?.(100);
|
|
1757
|
+
this.emit("uploadComplete", completeResponse);
|
|
1758
|
+
return {
|
|
1759
|
+
id: completeResponse.id,
|
|
1760
|
+
filename: completeResponse.filename,
|
|
1761
|
+
mimeType: completeResponse.mimeType,
|
|
1762
|
+
size: completeResponse.size,
|
|
1763
|
+
url: completeResponse.url,
|
|
1764
|
+
thumbnailUrl: completeResponse.thumbnailUrl,
|
|
1765
|
+
status: completeResponse.status
|
|
1766
|
+
};
|
|
1767
|
+
} catch (error) {
|
|
1768
|
+
this.emit("uploadError", { filename: file.name, error });
|
|
1769
|
+
throw error;
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
/**
|
|
1773
|
+
* Upload multiple files at once
|
|
1774
|
+
* @param files - Array of File objects to upload
|
|
1775
|
+
* @param onProgress - Optional callback for overall progress (0-100)
|
|
1776
|
+
* @returns Array of uploaded attachments
|
|
1777
|
+
*/
|
|
1778
|
+
async uploadFiles(files, onProgress) {
|
|
1779
|
+
const attachments = [];
|
|
1780
|
+
const totalFiles = files.length;
|
|
1781
|
+
for (let i = 0; i < totalFiles; i++) {
|
|
1782
|
+
const file = files[i];
|
|
1783
|
+
const baseProgress = i / totalFiles * 100;
|
|
1784
|
+
const fileProgress = 100 / totalFiles;
|
|
1785
|
+
const attachment = await this.uploadFile(file, (progress) => {
|
|
1786
|
+
const totalProgress = baseProgress + progress / 100 * fileProgress;
|
|
1787
|
+
onProgress?.(totalProgress);
|
|
1788
|
+
});
|
|
1789
|
+
attachments.push(attachment);
|
|
1790
|
+
}
|
|
1791
|
+
return attachments;
|
|
1792
|
+
}
|
|
1793
|
+
/**
|
|
1794
|
+
* Upload file to presigned URL with progress tracking
|
|
1795
|
+
*/
|
|
1796
|
+
uploadToPresignedUrl(url, file, onProgress) {
|
|
1797
|
+
return new Promise((resolve, reject) => {
|
|
1798
|
+
const xhr = new XMLHttpRequest();
|
|
1799
|
+
xhr.upload.addEventListener("progress", (event) => {
|
|
1800
|
+
if (event.lengthComputable) {
|
|
1801
|
+
const progress = event.loaded / event.total * 100;
|
|
1802
|
+
onProgress?.(progress);
|
|
1803
|
+
}
|
|
1804
|
+
});
|
|
1805
|
+
xhr.addEventListener("load", () => {
|
|
1806
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
1807
|
+
resolve();
|
|
1808
|
+
} else {
|
|
1809
|
+
reject(new Error(`Upload failed with status ${xhr.status}`));
|
|
1810
|
+
}
|
|
1811
|
+
});
|
|
1812
|
+
xhr.addEventListener("error", () => {
|
|
1813
|
+
reject(new Error("Upload failed"));
|
|
1814
|
+
});
|
|
1815
|
+
xhr.open("PUT", url);
|
|
1816
|
+
xhr.setRequestHeader("Content-Type", file.type || "application/octet-stream");
|
|
1817
|
+
xhr.send(file);
|
|
1818
|
+
});
|
|
1819
|
+
}
|
|
805
1820
|
async fetchMessages(after) {
|
|
806
1821
|
if (!this.session) {
|
|
807
1822
|
throw new Error("Not connected");
|
|
@@ -855,6 +1870,54 @@ var PocketPingClient = class {
|
|
|
855
1870
|
console.error("[PocketPing] Failed to send read status:", err);
|
|
856
1871
|
}
|
|
857
1872
|
}
|
|
1873
|
+
/**
|
|
1874
|
+
* Edit a message (visitor can only edit their own messages)
|
|
1875
|
+
* @param messageId - ID of the message to edit
|
|
1876
|
+
* @param content - New content for the message
|
|
1877
|
+
*/
|
|
1878
|
+
async editMessage(messageId, content) {
|
|
1879
|
+
if (!this.session) {
|
|
1880
|
+
throw new Error("Not connected");
|
|
1881
|
+
}
|
|
1882
|
+
const response = await this.fetch(`/message/${messageId}`, {
|
|
1883
|
+
method: "PATCH",
|
|
1884
|
+
body: JSON.stringify({
|
|
1885
|
+
sessionId: this.session.sessionId,
|
|
1886
|
+
content
|
|
1887
|
+
})
|
|
1888
|
+
});
|
|
1889
|
+
const messageIndex = this.session.messages.findIndex((m) => m.id === messageId);
|
|
1890
|
+
if (messageIndex >= 0) {
|
|
1891
|
+
this.session.messages[messageIndex].content = response.message.content;
|
|
1892
|
+
this.session.messages[messageIndex].editedAt = response.message.editedAt;
|
|
1893
|
+
this.emit("messageEdited", this.session.messages[messageIndex]);
|
|
1894
|
+
}
|
|
1895
|
+
return this.session.messages[messageIndex];
|
|
1896
|
+
}
|
|
1897
|
+
/**
|
|
1898
|
+
* Delete a message (soft delete - visitor can only delete their own messages)
|
|
1899
|
+
* @param messageId - ID of the message to delete
|
|
1900
|
+
*/
|
|
1901
|
+
async deleteMessage(messageId) {
|
|
1902
|
+
if (!this.session) {
|
|
1903
|
+
throw new Error("Not connected");
|
|
1904
|
+
}
|
|
1905
|
+
const response = await this.fetch(`/message/${messageId}`, {
|
|
1906
|
+
method: "DELETE",
|
|
1907
|
+
body: JSON.stringify({
|
|
1908
|
+
sessionId: this.session.sessionId
|
|
1909
|
+
})
|
|
1910
|
+
});
|
|
1911
|
+
if (response.deleted) {
|
|
1912
|
+
const messageIndex = this.session.messages.findIndex((m) => m.id === messageId);
|
|
1913
|
+
if (messageIndex >= 0) {
|
|
1914
|
+
this.session.messages[messageIndex].deletedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1915
|
+
this.session.messages[messageIndex].content = "";
|
|
1916
|
+
this.emit("messageDeleted", this.session.messages[messageIndex]);
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
return response.deleted;
|
|
1920
|
+
}
|
|
858
1921
|
async getPresence() {
|
|
859
1922
|
return this.fetch("/presence", { method: "GET" });
|
|
860
1923
|
}
|
|
@@ -1348,19 +2411,40 @@ var PocketPingClient = class {
|
|
|
1348
2411
|
return this.inspectorMode;
|
|
1349
2412
|
}
|
|
1350
2413
|
// ─────────────────────────────────────────────────────────────────
|
|
1351
|
-
// WebSocket
|
|
2414
|
+
// Real-time Connection (WebSocket → SSE → Polling)
|
|
1352
2415
|
// ─────────────────────────────────────────────────────────────────
|
|
1353
|
-
|
|
2416
|
+
connectRealtime() {
|
|
1354
2417
|
if (!this.session) return;
|
|
1355
|
-
if (this.
|
|
2418
|
+
if (this.connectionMode === "polling") {
|
|
1356
2419
|
this.startPolling();
|
|
1357
2420
|
return;
|
|
1358
2421
|
}
|
|
2422
|
+
if (this.connectionMode === "sse") {
|
|
2423
|
+
this.connectSSE();
|
|
2424
|
+
return;
|
|
2425
|
+
}
|
|
2426
|
+
this.connectWebSocket();
|
|
2427
|
+
}
|
|
2428
|
+
connectWebSocket() {
|
|
2429
|
+
if (!this.session) return;
|
|
1359
2430
|
const wsUrl = this.config.endpoint.replace(/^http/, "ws").replace(/\/$/, "") + `/stream?sessionId=${this.session.sessionId}`;
|
|
1360
2431
|
try {
|
|
1361
2432
|
this.ws = new WebSocket(wsUrl);
|
|
1362
2433
|
this.wsConnectedAt = Date.now();
|
|
2434
|
+
const connectionTimeout = setTimeout(() => {
|
|
2435
|
+
console.warn("[PocketPing] \u23F1\uFE0F WebSocket timeout - trying SSE");
|
|
2436
|
+
if (this.ws && this.ws.readyState !== WebSocket.OPEN) {
|
|
2437
|
+
this.ws.onclose = null;
|
|
2438
|
+
this.ws.onerror = null;
|
|
2439
|
+
this.ws.onopen = null;
|
|
2440
|
+
this.ws.close();
|
|
2441
|
+
this.ws = null;
|
|
2442
|
+
this.connectSSE();
|
|
2443
|
+
}
|
|
2444
|
+
}, 5e3);
|
|
1363
2445
|
this.ws.onopen = () => {
|
|
2446
|
+
clearTimeout(connectionTimeout);
|
|
2447
|
+
this.connectionMode = "ws";
|
|
1364
2448
|
this.reconnectAttempts = 0;
|
|
1365
2449
|
this.wsConnectedAt = Date.now();
|
|
1366
2450
|
this.emit("wsConnected", null);
|
|
@@ -1368,36 +2452,81 @@ var PocketPingClient = class {
|
|
|
1368
2452
|
this.ws.onmessage = (event) => {
|
|
1369
2453
|
try {
|
|
1370
2454
|
const wsEvent = JSON.parse(event.data);
|
|
1371
|
-
this.
|
|
2455
|
+
this.handleRealtimeEvent(wsEvent);
|
|
1372
2456
|
} catch (err) {
|
|
1373
2457
|
console.error("[PocketPing] Failed to parse WS message:", err);
|
|
1374
2458
|
}
|
|
1375
2459
|
};
|
|
1376
2460
|
this.ws.onclose = () => {
|
|
2461
|
+
clearTimeout(connectionTimeout);
|
|
1377
2462
|
this.emit("wsDisconnected", null);
|
|
1378
2463
|
this.handleWsFailure();
|
|
1379
2464
|
};
|
|
1380
2465
|
this.ws.onerror = () => {
|
|
2466
|
+
clearTimeout(connectionTimeout);
|
|
1381
2467
|
};
|
|
1382
|
-
} catch
|
|
1383
|
-
console.warn("[PocketPing] WebSocket unavailable
|
|
1384
|
-
this.
|
|
2468
|
+
} catch {
|
|
2469
|
+
console.warn("[PocketPing] WebSocket unavailable - trying SSE");
|
|
2470
|
+
this.connectSSE();
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2473
|
+
connectSSE() {
|
|
2474
|
+
if (!this.session) return;
|
|
2475
|
+
const sseUrl = this.config.endpoint.replace(/\/$/, "") + `/stream?sessionId=${this.session.sessionId}`;
|
|
2476
|
+
try {
|
|
2477
|
+
this.sse = new EventSource(sseUrl);
|
|
2478
|
+
const connectionTimeout = setTimeout(() => {
|
|
2479
|
+
console.warn("[PocketPing] \u23F1\uFE0F SSE timeout - falling back to polling");
|
|
2480
|
+
if (this.sse && this.sse.readyState !== EventSource.OPEN) {
|
|
2481
|
+
this.sse.close();
|
|
2482
|
+
this.sse = null;
|
|
2483
|
+
this.connectionMode = "polling";
|
|
2484
|
+
this.startPolling();
|
|
2485
|
+
}
|
|
2486
|
+
}, 5e3);
|
|
2487
|
+
this.sse.onopen = () => {
|
|
2488
|
+
clearTimeout(connectionTimeout);
|
|
2489
|
+
this.connectionMode = "sse";
|
|
2490
|
+
this.emit("sseConnected", null);
|
|
2491
|
+
};
|
|
2492
|
+
this.sse.addEventListener("message", (event) => {
|
|
2493
|
+
try {
|
|
2494
|
+
const data = JSON.parse(event.data);
|
|
2495
|
+
this.handleRealtimeEvent(data);
|
|
2496
|
+
} catch (err) {
|
|
2497
|
+
console.error("[PocketPing] Failed to parse SSE message:", err);
|
|
2498
|
+
}
|
|
2499
|
+
});
|
|
2500
|
+
this.sse.addEventListener("connected", () => {
|
|
2501
|
+
});
|
|
2502
|
+
this.sse.onerror = () => {
|
|
2503
|
+
clearTimeout(connectionTimeout);
|
|
2504
|
+
console.warn("[PocketPing] \u274C SSE error - falling back to polling");
|
|
2505
|
+
if (this.sse) {
|
|
2506
|
+
this.sse.close();
|
|
2507
|
+
this.sse = null;
|
|
2508
|
+
}
|
|
2509
|
+
this.connectionMode = "polling";
|
|
2510
|
+
this.startPolling();
|
|
2511
|
+
};
|
|
2512
|
+
} catch {
|
|
2513
|
+
console.warn("[PocketPing] SSE unavailable - falling back to polling");
|
|
2514
|
+
this.connectionMode = "polling";
|
|
1385
2515
|
this.startPolling();
|
|
1386
2516
|
}
|
|
1387
2517
|
}
|
|
1388
2518
|
handleWsFailure() {
|
|
1389
2519
|
const timeSinceConnect = Date.now() - this.wsConnectedAt;
|
|
1390
2520
|
if (timeSinceConnect < this.quickFailureThreshold) {
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
this.usePollingFallback = true;
|
|
1395
|
-
this.startPolling();
|
|
1396
|
-
return;
|
|
1397
|
-
}
|
|
2521
|
+
console.info("[PocketPing] WebSocket failed quickly - trying SSE");
|
|
2522
|
+
this.connectSSE();
|
|
2523
|
+
return;
|
|
1398
2524
|
}
|
|
1399
2525
|
this.scheduleReconnect();
|
|
1400
2526
|
}
|
|
2527
|
+
handleRealtimeEvent(event) {
|
|
2528
|
+
this.handleWebSocketEvent(event);
|
|
2529
|
+
}
|
|
1401
2530
|
handleWebSocketEvent(event) {
|
|
1402
2531
|
switch (event.type) {
|
|
1403
2532
|
case "message":
|
|
@@ -1509,8 +2638,8 @@ var PocketPingClient = class {
|
|
|
1509
2638
|
}
|
|
1510
2639
|
scheduleReconnect() {
|
|
1511
2640
|
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
1512
|
-
console.warn("[PocketPing] Max reconnect attempts reached,
|
|
1513
|
-
this.
|
|
2641
|
+
console.warn("[PocketPing] Max reconnect attempts reached, trying SSE");
|
|
2642
|
+
this.connectSSE();
|
|
1514
2643
|
return;
|
|
1515
2644
|
}
|
|
1516
2645
|
const delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), 3e4);
|
|
@@ -1535,6 +2664,7 @@ var PocketPingClient = class {
|
|
|
1535
2664
|
}
|
|
1536
2665
|
} catch (err) {
|
|
1537
2666
|
this.pollingFailures++;
|
|
2667
|
+
console.error(`[PocketPing] \u274C Polling error:`, err);
|
|
1538
2668
|
if (this.pollingFailures <= 3 || this.pollingFailures % 3 === 0) {
|
|
1539
2669
|
console.warn(`[PocketPing] Polling failed (${this.pollingFailures}/${this.maxPollingFailures})`);
|
|
1540
2670
|
}
|
|
@@ -1545,11 +2675,11 @@ var PocketPingClient = class {
|
|
|
1545
2675
|
}
|
|
1546
2676
|
}
|
|
1547
2677
|
if (this.session) {
|
|
1548
|
-
const delay = this.pollingFailures > 0 ? Math.min(
|
|
2678
|
+
const delay = this.pollingFailures > 0 ? Math.min(2e3 * Math.pow(2, this.pollingFailures - 1), 3e4) : 2e3;
|
|
1549
2679
|
this.pollingTimeout = setTimeout(poll, delay);
|
|
1550
2680
|
}
|
|
1551
2681
|
};
|
|
1552
|
-
poll
|
|
2682
|
+
this.pollingTimeout = setTimeout(poll, 500);
|
|
1553
2683
|
}
|
|
1554
2684
|
stopPolling() {
|
|
1555
2685
|
if (this.pollingTimeout) {
|
|
@@ -1691,11 +2821,23 @@ function close() {
|
|
|
1691
2821
|
function toggle() {
|
|
1692
2822
|
client?.toggleOpen();
|
|
1693
2823
|
}
|
|
1694
|
-
function sendMessage(content) {
|
|
2824
|
+
function sendMessage(content, attachmentIds) {
|
|
2825
|
+
if (!client) {
|
|
2826
|
+
throw new Error("[PocketPing] Not initialized");
|
|
2827
|
+
}
|
|
2828
|
+
return client.sendMessage(content, attachmentIds);
|
|
2829
|
+
}
|
|
2830
|
+
async function uploadFile(file, onProgress) {
|
|
2831
|
+
if (!client) {
|
|
2832
|
+
throw new Error("[PocketPing] Not initialized");
|
|
2833
|
+
}
|
|
2834
|
+
return client.uploadFile(file, onProgress);
|
|
2835
|
+
}
|
|
2836
|
+
async function uploadFiles(files, onProgress) {
|
|
1695
2837
|
if (!client) {
|
|
1696
2838
|
throw new Error("[PocketPing] Not initialized");
|
|
1697
2839
|
}
|
|
1698
|
-
return client.
|
|
2840
|
+
return client.uploadFiles(files, onProgress);
|
|
1699
2841
|
}
|
|
1700
2842
|
function trigger(eventName, data, options) {
|
|
1701
2843
|
if (!client) {
|
|
@@ -1764,7 +2906,7 @@ if (typeof document !== "undefined") {
|
|
|
1764
2906
|
}
|
|
1765
2907
|
}
|
|
1766
2908
|
}
|
|
1767
|
-
var index_default = { init, destroy, open, close, toggle, sendMessage, trigger, onEvent, offEvent, on, identify, reset, getIdentity, setupTrackedElements, getTrackedElements };
|
|
2909
|
+
var index_default = { init, destroy, open, close, toggle, sendMessage, uploadFile, uploadFiles, trigger, onEvent, offEvent, on, identify, reset, getIdentity, setupTrackedElements, getTrackedElements };
|
|
1768
2910
|
export {
|
|
1769
2911
|
close,
|
|
1770
2912
|
index_default as default,
|
|
@@ -1781,5 +2923,7 @@ export {
|
|
|
1781
2923
|
sendMessage,
|
|
1782
2924
|
setupTrackedElements,
|
|
1783
2925
|
toggle,
|
|
1784
|
-
trigger
|
|
2926
|
+
trigger,
|
|
2927
|
+
uploadFile,
|
|
2928
|
+
uploadFiles
|
|
1785
2929
|
};
|