@pocketping/widget 1.1.0 → 1.3.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 CHANGED
@@ -35,7 +35,9 @@ __export(index_exports, {
35
35
  sendMessage: () => sendMessage,
36
36
  setupTrackedElements: () => setupTrackedElements,
37
37
  toggle: () => toggle,
38
- trigger: () => trigger
38
+ trigger: () => trigger,
39
+ uploadFile: () => uploadFile,
40
+ uploadFiles: () => uploadFiles
39
41
  });
40
42
  module.exports = __toCommonJS(index_exports);
41
43
  var import_preact2 = require("preact");
@@ -229,6 +231,12 @@ function styles(primaryColor, theme) {
229
231
  border-radius: 4px;
230
232
  opacity: 0.8;
231
233
  transition: opacity 0.2s;
234
+ flex-shrink: 0;
235
+ width: 28px;
236
+ height: 28px;
237
+ display: flex;
238
+ align-items: center;
239
+ justify-content: center;
232
240
  }
233
241
 
234
242
  .pp-close-btn:hover {
@@ -236,8 +244,8 @@ function styles(primaryColor, theme) {
236
244
  }
237
245
 
238
246
  .pp-close-btn svg {
239
- width: 20px;
240
- height: 20px;
247
+ width: 16px;
248
+ height: 16px;
241
249
  }
242
250
 
243
251
  .pp-messages {
@@ -261,6 +269,94 @@ function styles(primaryColor, theme) {
261
269
  padding: 10px 14px;
262
270
  border-radius: 16px;
263
271
  word-wrap: break-word;
272
+ position: relative;
273
+ user-select: text;
274
+ -webkit-user-select: text;
275
+ }
276
+
277
+ /* Hover actions container - positioned above message (Slack style) */
278
+ .pp-message-actions {
279
+ position: absolute;
280
+ top: -28px;
281
+ display: flex;
282
+ gap: 2px;
283
+ background: ${colors.bg};
284
+ border: 1px solid ${colors.border};
285
+ border-radius: 6px;
286
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
287
+ padding: 2px;
288
+ opacity: 0;
289
+ animation: pp-actions-fade-in 0.12s ease forwards;
290
+ z-index: 10;
291
+ /* Reset color inheritance from message */
292
+ color: ${colors.textSecondary};
293
+ }
294
+
295
+ @keyframes pp-actions-fade-in {
296
+ from { opacity: 0; transform: translateY(4px); }
297
+ to { opacity: 1; transform: translateY(0); }
298
+ }
299
+
300
+ /* Visitor messages: actions aligned right */
301
+ .pp-actions-left {
302
+ right: 0;
303
+ }
304
+
305
+ /* Operator messages: actions aligned left */
306
+ .pp-actions-right {
307
+ left: 0;
308
+ }
309
+
310
+ .pp-message-actions .pp-action-btn {
311
+ width: 24px;
312
+ height: 24px;
313
+ border: none;
314
+ background: transparent;
315
+ border-radius: 4px;
316
+ cursor: pointer;
317
+ display: flex;
318
+ align-items: center;
319
+ justify-content: center;
320
+ color: ${colors.textSecondary} !important;
321
+ transition: background 0.1s, color 0.1s;
322
+ }
323
+
324
+ .pp-message-actions .pp-action-btn:hover {
325
+ background: ${colors.bgSecondary};
326
+ color: ${colors.text} !important;
327
+ }
328
+
329
+ .pp-message-actions .pp-action-btn svg {
330
+ width: 14px;
331
+ height: 14px;
332
+ stroke: ${colors.textSecondary};
333
+ }
334
+
335
+ .pp-message-actions .pp-action-btn:hover svg {
336
+ stroke: ${colors.text};
337
+ }
338
+
339
+ .pp-message-actions .pp-action-delete:hover {
340
+ background: #fef2f2;
341
+ }
342
+
343
+ .pp-message-actions .pp-action-delete:hover svg {
344
+ stroke: #ef4444;
345
+ }
346
+
347
+ .pp-theme-dark .pp-message-actions .pp-action-delete:hover {
348
+ background: #7f1d1d;
349
+ }
350
+
351
+ .pp-theme-dark .pp-message-actions .pp-action-delete:hover svg {
352
+ stroke: #fca5a5;
353
+ }
354
+
355
+ /* Hide hover actions on mobile */
356
+ @media (hover: none) and (pointer: coarse) {
357
+ .pp-message-actions {
358
+ display: none;
359
+ }
264
360
  }
265
361
 
266
362
  .pp-message-visitor {
@@ -344,72 +440,565 @@ function styles(primaryColor, theme) {
344
440
  40% { transform: scale(1); }
345
441
  }
346
442
 
347
- .pp-input-form {
348
- display: flex;
349
- padding: 12px;
350
- gap: 8px;
351
- border-top: 1px solid ${colors.border};
443
+ .pp-input-form {
444
+ display: flex;
445
+ padding: 12px;
446
+ gap: 8px;
447
+ border-top: 1px solid ${colors.border};
448
+ }
449
+
450
+ .pp-input {
451
+ flex: 1;
452
+ padding: 10px 14px;
453
+ border: 1px solid ${colors.border};
454
+ border-radius: 20px;
455
+ background: ${colors.bg};
456
+ color: ${colors.text};
457
+ font-size: 14px;
458
+ outline: none;
459
+ transition: border-color 0.2s;
460
+ }
461
+
462
+ .pp-input:focus {
463
+ border-color: ${primaryColor};
464
+ }
465
+
466
+ .pp-input::placeholder {
467
+ color: ${colors.textSecondary};
468
+ }
469
+
470
+ .pp-send-btn {
471
+ width: 40px;
472
+ height: 40px;
473
+ border-radius: 50%;
474
+ background: ${primaryColor};
475
+ color: white;
476
+ border: none;
477
+ cursor: pointer;
478
+ display: flex;
479
+ align-items: center;
480
+ justify-content: center;
481
+ transition: opacity 0.2s;
482
+ }
483
+
484
+ .pp-send-btn:disabled {
485
+ opacity: 0.5;
486
+ cursor: not-allowed;
487
+ }
488
+
489
+ .pp-send-btn svg {
490
+ width: 18px;
491
+ height: 18px;
492
+ }
493
+
494
+ .pp-footer {
495
+ text-align: center;
496
+ padding: 8px;
497
+ font-size: 11px;
498
+ color: ${colors.textSecondary};
499
+ border-top: 1px solid ${colors.border};
500
+ }
501
+
502
+ .pp-footer a {
503
+ color: ${primaryColor};
504
+ text-decoration: none;
505
+ }
506
+
507
+ .pp-footer a:hover {
508
+ text-decoration: underline;
509
+ }
510
+
511
+ /* Attachment Styles */
512
+ .pp-file-input {
513
+ /* Use offscreen positioning instead of display:none for better browser compatibility */
514
+ position: absolute;
515
+ width: 1px;
516
+ height: 1px;
517
+ padding: 0;
518
+ margin: -1px;
519
+ overflow: hidden;
520
+ clip: rect(0, 0, 0, 0);
521
+ white-space: nowrap;
522
+ border: 0;
523
+ }
524
+
525
+ .pp-attach-btn {
526
+ width: 40px;
527
+ height: 40px;
528
+ border-radius: 50%;
529
+ background: transparent;
530
+ color: ${colors.textSecondary};
531
+ border: 1px solid ${colors.border};
532
+ cursor: pointer;
533
+ display: flex;
534
+ align-items: center;
535
+ justify-content: center;
536
+ transition: color 0.2s, border-color 0.2s;
537
+ flex-shrink: 0;
538
+ }
539
+
540
+ .pp-attach-btn:hover:not(:disabled) {
541
+ color: ${primaryColor};
542
+ border-color: ${primaryColor};
543
+ }
544
+
545
+ .pp-attach-btn:disabled {
546
+ opacity: 0.5;
547
+ cursor: not-allowed;
548
+ }
549
+
550
+ .pp-attach-btn svg {
551
+ width: 18px;
552
+ height: 18px;
553
+ }
554
+
555
+ .pp-attachments-preview {
556
+ display: flex;
557
+ gap: 8px;
558
+ padding: 8px 12px;
559
+ border-top: 1px solid ${colors.border};
560
+ overflow-x: auto;
561
+ background: ${colors.bgSecondary};
562
+ }
563
+
564
+ .pp-attachment-preview {
565
+ position: relative;
566
+ width: 60px;
567
+ height: 60px;
568
+ border-radius: 8px;
569
+ overflow: hidden;
570
+ flex-shrink: 0;
571
+ background: ${colors.bg};
572
+ border: 1px solid ${colors.border};
573
+ }
574
+
575
+ .pp-preview-img {
576
+ width: 100%;
577
+ height: 100%;
578
+ object-fit: cover;
579
+ }
580
+
581
+ .pp-preview-file {
582
+ width: 100%;
583
+ height: 100%;
584
+ display: flex;
585
+ align-items: center;
586
+ justify-content: center;
587
+ color: ${colors.textSecondary};
588
+ }
589
+
590
+ .pp-preview-file svg {
591
+ width: 24px;
592
+ height: 24px;
593
+ }
594
+
595
+ .pp-remove-attachment {
596
+ position: absolute;
597
+ top: 2px;
598
+ right: 2px;
599
+ width: 18px;
600
+ height: 18px;
601
+ border-radius: 50%;
602
+ background: rgba(0, 0, 0, 0.6);
603
+ color: white;
604
+ border: none;
605
+ cursor: pointer;
606
+ display: flex;
607
+ align-items: center;
608
+ justify-content: center;
609
+ padding: 0;
610
+ }
611
+
612
+ .pp-remove-attachment svg {
613
+ width: 10px;
614
+ height: 10px;
615
+ }
616
+
617
+ .pp-upload-progress {
618
+ position: absolute;
619
+ bottom: 0;
620
+ left: 0;
621
+ height: 3px;
622
+ background: ${primaryColor};
623
+ transition: width 0.1s;
624
+ }
625
+
626
+ .pp-upload-error {
627
+ position: absolute;
628
+ top: 50%;
629
+ left: 50%;
630
+ transform: translate(-50%, -50%);
631
+ width: 24px;
632
+ height: 24px;
633
+ border-radius: 50%;
634
+ background: #ef4444;
635
+ color: white;
636
+ font-weight: bold;
637
+ display: flex;
638
+ align-items: center;
639
+ justify-content: center;
640
+ font-size: 14px;
641
+ }
642
+
643
+ .pp-attachment-uploading {
644
+ opacity: 0.7;
645
+ }
646
+
647
+ .pp-attachment-error {
648
+ border-color: #ef4444;
649
+ }
650
+
651
+ /* Message Attachments */
652
+ .pp-message-attachments {
653
+ display: flex;
654
+ flex-direction: column;
655
+ gap: 8px;
656
+ margin-top: 4px;
657
+ }
658
+
659
+ .pp-attachment {
660
+ display: block;
661
+ text-decoration: none;
662
+ color: inherit;
663
+ border-radius: 8px;
664
+ overflow: hidden;
665
+ }
666
+
667
+ .pp-attachment-image img {
668
+ max-width: 200px;
669
+ max-height: 200px;
670
+ border-radius: 8px;
671
+ display: block;
672
+ }
673
+
674
+ .pp-attachment-audio {
675
+ display: flex;
676
+ flex-direction: column;
677
+ gap: 4px;
678
+ }
679
+
680
+ .pp-attachment-audio audio {
681
+ width: 200px;
682
+ height: 36px;
683
+ }
684
+
685
+ .pp-attachment-audio .pp-attachment-name {
686
+ font-size: 11px;
687
+ opacity: 0.7;
688
+ white-space: nowrap;
689
+ overflow: hidden;
690
+ text-overflow: ellipsis;
691
+ max-width: 200px;
692
+ }
693
+
694
+ .pp-attachment-video video {
695
+ max-width: 200px;
696
+ max-height: 200px;
697
+ border-radius: 8px;
698
+ display: block;
699
+ }
700
+
701
+ .pp-attachment-file {
702
+ display: flex;
703
+ align-items: center;
704
+ gap: 8px;
705
+ padding: 8px 12px;
706
+ background: ${isDark ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.05)"};
707
+ border-radius: 8px;
708
+ transition: background 0.2s;
709
+ }
710
+
711
+ .pp-attachment-file:hover {
712
+ background: ${isDark ? "rgba(255,255,255,0.15)" : "rgba(0,0,0,0.08)"};
713
+ }
714
+
715
+ .pp-attachment-file svg {
716
+ width: 24px;
717
+ height: 24px;
718
+ flex-shrink: 0;
719
+ }
720
+
721
+ .pp-attachment-info {
722
+ display: flex;
723
+ flex-direction: column;
724
+ min-width: 0;
725
+ }
726
+
727
+ .pp-attachment-name {
728
+ font-size: 13px;
729
+ font-weight: 500;
730
+ white-space: nowrap;
731
+ overflow: hidden;
732
+ text-overflow: ellipsis;
733
+ }
734
+
735
+ .pp-attachment-size {
736
+ font-size: 11px;
737
+ opacity: 0.7;
738
+ }
739
+
740
+ /* Drag & Drop */
741
+ .pp-dragging {
742
+ position: relative;
743
+ }
744
+
745
+ .pp-drop-overlay {
746
+ position: absolute;
747
+ inset: 0;
748
+ background: ${isDark ? "rgba(0,0,0,0.9)" : "rgba(255,255,255,0.95)"};
749
+ display: flex;
750
+ flex-direction: column;
751
+ align-items: center;
752
+ justify-content: center;
753
+ gap: 12px;
754
+ z-index: 100;
755
+ border: 3px dashed ${primaryColor};
756
+ border-radius: 16px;
757
+ margin: 4px;
758
+ pointer-events: none;
759
+ }
760
+
761
+ .pp-drop-icon svg {
762
+ width: 48px;
763
+ height: 48px;
764
+ color: ${primaryColor};
765
+ }
766
+
767
+ .pp-drop-text {
768
+ font-size: 16px;
769
+ font-weight: 500;
770
+ color: ${colors.text};
771
+ }
772
+
773
+ /* Message Context Menu */
774
+ .pp-message-menu {
775
+ position: fixed;
776
+ background: ${colors.bg};
777
+ border: 1px solid ${colors.border};
778
+ border-radius: 8px;
779
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
780
+ padding: 4px;
781
+ z-index: 200;
782
+ min-width: 120px;
783
+ }
784
+
785
+ .pp-message-menu button {
786
+ display: flex;
787
+ align-items: center;
788
+ gap: 8px;
789
+ width: 100%;
790
+ padding: 8px 12px;
791
+ border: none;
792
+ background: transparent;
793
+ color: ${colors.text};
794
+ font-size: 13px;
795
+ cursor: pointer;
796
+ border-radius: 4px;
797
+ text-align: left;
798
+ }
799
+
800
+ .pp-message-menu button:hover {
801
+ background: ${isDark ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.05)"};
802
+ }
803
+
804
+ .pp-message-menu button svg {
805
+ width: 16px;
806
+ height: 16px;
807
+ }
808
+
809
+ .pp-menu-delete {
810
+ color: #ef4444 !important;
811
+ }
812
+
813
+ /* Edit Modal */
814
+ .pp-edit-modal {
815
+ position: absolute;
816
+ bottom: 80px;
817
+ left: 12px;
818
+ right: 12px;
819
+ background: ${colors.bg};
820
+ border: 1px solid ${colors.border};
821
+ border-radius: 12px;
822
+ box-shadow: 0 4px 20px rgba(0,0,0,0.15);
823
+ z-index: 150;
824
+ overflow: hidden;
825
+ }
826
+
827
+ .pp-edit-header {
828
+ display: flex;
829
+ justify-content: space-between;
830
+ align-items: center;
831
+ padding: 12px 16px;
832
+ border-bottom: 1px solid ${colors.border};
833
+ font-weight: 500;
834
+ }
835
+
836
+ .pp-edit-header button {
837
+ background: transparent;
838
+ border: none;
839
+ color: ${colors.textSecondary};
840
+ cursor: pointer;
841
+ padding: 4px;
842
+ }
843
+
844
+ .pp-edit-header button svg {
845
+ width: 18px;
846
+ height: 18px;
847
+ }
848
+
849
+ .pp-edit-input {
850
+ width: 100%;
851
+ padding: 12px 16px;
852
+ border: none;
853
+ background: transparent;
854
+ color: ${colors.text};
855
+ font-size: 14px;
856
+ resize: none;
857
+ min-height: 80px;
858
+ outline: none;
859
+ }
860
+
861
+ .pp-edit-actions {
862
+ display: flex;
863
+ justify-content: flex-end;
864
+ gap: 8px;
865
+ padding: 12px 16px;
866
+ border-top: 1px solid ${colors.border};
867
+ }
868
+
869
+ .pp-edit-cancel {
870
+ padding: 8px 16px;
871
+ border: 1px solid ${colors.border};
872
+ border-radius: 6px;
873
+ background: transparent;
874
+ color: ${colors.text};
875
+ font-size: 13px;
876
+ cursor: pointer;
877
+ }
878
+
879
+ .pp-edit-save {
880
+ padding: 8px 16px;
881
+ border: none;
882
+ border-radius: 6px;
883
+ background: ${primaryColor};
884
+ color: white;
885
+ font-size: 13px;
886
+ cursor: pointer;
887
+ }
888
+
889
+ .pp-edit-save:disabled {
890
+ opacity: 0.5;
891
+ cursor: not-allowed;
892
+ }
893
+
894
+ /* Reply Preview */
895
+ .pp-reply-preview {
896
+ display: flex;
897
+ align-items: center;
898
+ gap: 8px;
899
+ padding: 8px 12px;
900
+ background: ${isDark ? "rgba(255,255,255,0.05)" : "rgba(0,0,0,0.03)"};
901
+ border-top: 1px solid ${colors.border};
902
+ border-left: 3px solid ${primaryColor};
903
+ }
904
+
905
+ .pp-reply-preview-content {
906
+ flex: 1;
907
+ min-width: 0;
908
+ }
909
+
910
+ .pp-reply-label {
911
+ display: block;
912
+ font-size: 11px;
913
+ color: ${primaryColor};
914
+ font-weight: 500;
915
+ margin-bottom: 2px;
916
+ }
917
+
918
+ .pp-reply-text {
919
+ display: block;
920
+ font-size: 12px;
921
+ color: ${colors.textSecondary};
922
+ white-space: nowrap;
923
+ overflow: hidden;
924
+ text-overflow: ellipsis;
925
+ }
926
+
927
+ .pp-reply-cancel {
928
+ background: transparent;
929
+ border: none;
930
+ color: ${colors.textSecondary};
931
+ cursor: pointer;
932
+ padding: 4px;
933
+ flex-shrink: 0;
934
+ }
935
+
936
+ .pp-reply-cancel svg {
937
+ width: 16px;
938
+ height: 16px;
352
939
  }
353
940
 
354
- .pp-input {
355
- flex: 1;
356
- padding: 10px 14px;
357
- border: 1px solid ${colors.border};
358
- border-radius: 20px;
359
- background: ${colors.bg};
360
- color: ${colors.text};
361
- font-size: 14px;
362
- outline: none;
363
- transition: border-color 0.2s;
941
+ /* Reply Quote in Message */
942
+ .pp-reply-quote {
943
+ background: ${isDark ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.05)"};
944
+ border-left: 2px solid ${primaryColor};
945
+ padding: 4px 8px;
946
+ margin-bottom: 6px;
947
+ border-radius: 0 4px 4px 0;
948
+ font-size: 12px;
949
+ position: relative;
950
+ z-index: 1;
364
951
  }
365
952
 
366
- .pp-input:focus {
367
- border-color: ${primaryColor};
953
+ .pp-reply-sender {
954
+ display: block;
955
+ font-weight: 500;
956
+ color: ${primaryColor};
957
+ margin-bottom: 2px;
368
958
  }
369
959
 
370
- .pp-input::placeholder {
960
+ .pp-reply-content {
961
+ display: block;
371
962
  color: ${colors.textSecondary};
963
+ white-space: nowrap;
964
+ overflow: hidden;
965
+ text-overflow: ellipsis;
372
966
  }
373
967
 
374
- .pp-send-btn {
375
- width: 40px;
376
- height: 40px;
377
- border-radius: 50%;
378
- background: ${primaryColor};
379
- color: white;
380
- border: none;
381
- cursor: pointer;
382
- display: flex;
383
- align-items: center;
384
- justify-content: center;
385
- transition: opacity 0.2s;
968
+ /* Reply quote in visitor message bubble needs higher contrast */
969
+ .pp-message-visitor .pp-reply-quote {
970
+ background: rgba(255, 255, 255, 0.18);
971
+ border-left-color: rgba(255, 255, 255, 0.7);
386
972
  }
387
973
 
388
- .pp-send-btn:disabled {
389
- opacity: 0.5;
390
- cursor: not-allowed;
974
+ .pp-message-visitor .pp-reply-sender,
975
+ .pp-message-visitor .pp-reply-content {
976
+ color: rgba(255, 255, 255, 0.9);
391
977
  }
392
978
 
393
- .pp-send-btn svg {
394
- width: 18px;
395
- height: 18px;
979
+ /* Deleted Message */
980
+ .pp-message-deleted {
981
+ opacity: 0.6;
396
982
  }
397
983
 
398
- .pp-footer {
399
- text-align: center;
400
- padding: 8px;
401
- font-size: 11px;
984
+ .pp-deleted-content {
985
+ font-style: italic;
402
986
  color: ${colors.textSecondary};
403
- border-top: 1px solid ${colors.border};
987
+ display: flex;
988
+ align-items: center;
989
+ gap: 4px;
404
990
  }
405
991
 
406
- .pp-footer a {
407
- color: ${primaryColor};
408
- text-decoration: none;
992
+ .pp-deleted-icon {
993
+ font-size: 12px;
409
994
  }
410
995
 
411
- .pp-footer a:hover {
412
- text-decoration: underline;
996
+ /* Edited Badge */
997
+ .pp-edited-badge {
998
+ font-size: 10px;
999
+ color: ${colors.textSecondary};
1000
+ margin-left: 4px;
1001
+ font-style: italic;
413
1002
  }
414
1003
  `;
415
1004
  }
@@ -424,9 +1013,20 @@ function ChatWidget({ client: client2, config: initialConfig }) {
424
1013
  const [operatorOnline, setOperatorOnline] = (0, import_hooks.useState)(false);
425
1014
  const [isConnected, setIsConnected] = (0, import_hooks.useState)(false);
426
1015
  const [unreadCount, setUnreadCount] = (0, import_hooks.useState)(0);
1016
+ const [pendingAttachments, setPendingAttachments] = (0, import_hooks.useState)([]);
1017
+ const [isUploading, setIsUploading] = (0, import_hooks.useState)(false);
1018
+ const [replyingTo, setReplyingTo] = (0, import_hooks.useState)(null);
1019
+ const [editingMessage, setEditingMessage] = (0, import_hooks.useState)(null);
1020
+ const [editContent, setEditContent] = (0, import_hooks.useState)("");
1021
+ const [messageMenu, setMessageMenu] = (0, import_hooks.useState)(null);
1022
+ const [isDragging, setIsDragging] = (0, import_hooks.useState)(false);
1023
+ const [hoveredMessageId, setHoveredMessageId] = (0, import_hooks.useState)(null);
1024
+ const [longPressTimer, setLongPressTimer] = (0, import_hooks.useState)(null);
427
1025
  const [config, setConfig] = (0, import_hooks.useState)(initialConfig);
428
1026
  const messagesEndRef = (0, import_hooks.useRef)(null);
429
1027
  const inputRef = (0, import_hooks.useRef)(null);
1028
+ const fileInputRef = (0, import_hooks.useRef)(null);
1029
+ const messagesContainerRef = (0, import_hooks.useRef)(null);
430
1030
  (0, import_hooks.useEffect)(() => {
431
1031
  const unsubOpen = client2.on("openChange", setIsOpen);
432
1032
  const unsubMessage = client2.on("message", () => {
@@ -523,11 +1123,17 @@ function ChatWidget({ client: client2, config: initialConfig }) {
523
1123
  if (!shouldShow) return null;
524
1124
  const handleSubmit = async (e) => {
525
1125
  e.preventDefault();
526
- if (!inputValue.trim()) return;
1126
+ const hasContent = inputValue.trim().length > 0;
1127
+ const readyAttachments = pendingAttachments.filter((a) => a.status === "ready" && a.attachment);
1128
+ if (!hasContent && readyAttachments.length === 0) return;
527
1129
  const content = inputValue;
1130
+ const attachmentIds = readyAttachments.map((a) => a.attachment.id);
1131
+ const replyToId = replyingTo?.id;
528
1132
  setInputValue("");
1133
+ setPendingAttachments([]);
1134
+ setReplyingTo(null);
529
1135
  try {
530
- await client2.sendMessage(content);
1136
+ await client2.sendMessage(content, attachmentIds, replyToId);
531
1137
  } catch (err) {
532
1138
  console.error("[PocketPing] Failed to send message:", err);
533
1139
  }
@@ -537,9 +1143,213 @@ function ChatWidget({ client: client2, config: initialConfig }) {
537
1143
  setInputValue(target.value);
538
1144
  client2.sendTyping(true);
539
1145
  };
1146
+ const handleFileSelect = async (e) => {
1147
+ const target = e.target;
1148
+ const files = target.files;
1149
+ if (!files || files.length === 0) return;
1150
+ const newPending = [];
1151
+ for (let i = 0; i < files.length; i++) {
1152
+ const file = files[i];
1153
+ const id = `pending-${Date.now()}-${i}`;
1154
+ let preview;
1155
+ if (file.type.startsWith("image/")) {
1156
+ preview = URL.createObjectURL(file);
1157
+ }
1158
+ newPending.push({
1159
+ id,
1160
+ file,
1161
+ preview,
1162
+ progress: 0,
1163
+ status: "pending"
1164
+ });
1165
+ }
1166
+ setPendingAttachments((prev) => [...prev, ...newPending]);
1167
+ target.value = "";
1168
+ setIsUploading(true);
1169
+ for (const pending of newPending) {
1170
+ try {
1171
+ setPendingAttachments(
1172
+ (prev) => prev.map((a) => a.id === pending.id ? { ...a, status: "uploading" } : a)
1173
+ );
1174
+ const attachment = await client2.uploadFile(pending.file, (progress) => {
1175
+ setPendingAttachments(
1176
+ (prev) => prev.map((a) => a.id === pending.id ? { ...a, progress } : a)
1177
+ );
1178
+ });
1179
+ setPendingAttachments(
1180
+ (prev) => prev.map(
1181
+ (a) => a.id === pending.id ? { ...a, status: "ready", progress: 100, attachment } : a
1182
+ )
1183
+ );
1184
+ } catch (err) {
1185
+ console.error("[PocketPing] Failed to upload file:", err);
1186
+ setPendingAttachments(
1187
+ (prev) => prev.map(
1188
+ (a) => a.id === pending.id ? { ...a, status: "error", error: "Upload failed" } : a
1189
+ )
1190
+ );
1191
+ }
1192
+ }
1193
+ setIsUploading(false);
1194
+ };
1195
+ const handleRemoveAttachment = (id) => {
1196
+ setPendingAttachments((prev) => {
1197
+ const removed = prev.find((a) => a.id === id);
1198
+ if (removed?.preview) {
1199
+ URL.revokeObjectURL(removed.preview);
1200
+ }
1201
+ return prev.filter((a) => a.id !== id);
1202
+ });
1203
+ };
1204
+ const handleReply = (message) => {
1205
+ setReplyingTo(message);
1206
+ setMessageMenu(null);
1207
+ inputRef.current?.focus();
1208
+ };
1209
+ const handleCancelReply = () => {
1210
+ setReplyingTo(null);
1211
+ };
1212
+ const handleStartEdit = (message) => {
1213
+ if (message.sender !== "visitor") return;
1214
+ setEditingMessage(message);
1215
+ setEditContent(message.content);
1216
+ setMessageMenu(null);
1217
+ };
1218
+ const handleCancelEdit = () => {
1219
+ setEditingMessage(null);
1220
+ setEditContent("");
1221
+ };
1222
+ const handleSaveEdit = async () => {
1223
+ if (!editingMessage || !editContent.trim()) return;
1224
+ try {
1225
+ await client2.editMessage(editingMessage.id, editContent.trim());
1226
+ setEditingMessage(null);
1227
+ setEditContent("");
1228
+ } catch (err) {
1229
+ console.error("[PocketPing] Failed to edit message:", err);
1230
+ }
1231
+ };
1232
+ const handleDelete = async (message) => {
1233
+ if (message.sender !== "visitor") return;
1234
+ setMessageMenu(null);
1235
+ if (confirm("Delete this message?")) {
1236
+ try {
1237
+ await client2.deleteMessage(message.id);
1238
+ } catch (err) {
1239
+ console.error("[PocketPing] Failed to delete message:", err);
1240
+ }
1241
+ }
1242
+ };
1243
+ const handleMessageContextMenu = (e, message) => {
1244
+ e.preventDefault();
1245
+ const mouseEvent = e;
1246
+ setMessageMenu({
1247
+ message,
1248
+ x: mouseEvent.clientX,
1249
+ y: mouseEvent.clientY
1250
+ });
1251
+ };
1252
+ const handleTouchStart = (message) => {
1253
+ const timer = setTimeout(() => {
1254
+ if (navigator.vibrate) navigator.vibrate(50);
1255
+ setMessageMenu({
1256
+ message,
1257
+ x: window.innerWidth / 2 - 60,
1258
+ // Center horizontally
1259
+ y: window.innerHeight / 2 - 50
1260
+ // Center vertically
1261
+ });
1262
+ }, 500);
1263
+ setLongPressTimer(timer);
1264
+ };
1265
+ const handleTouchEnd = () => {
1266
+ if (longPressTimer) {
1267
+ clearTimeout(longPressTimer);
1268
+ setLongPressTimer(null);
1269
+ }
1270
+ };
1271
+ (0, import_hooks.useEffect)(() => {
1272
+ if (!messageMenu) return;
1273
+ const handleClickOutside = () => setMessageMenu(null);
1274
+ document.addEventListener("click", handleClickOutside);
1275
+ return () => document.removeEventListener("click", handleClickOutside);
1276
+ }, [messageMenu]);
1277
+ const dragCounterRef = (0, import_hooks.useRef)(0);
1278
+ const handleDragEnter = (e) => {
1279
+ e.preventDefault();
1280
+ e.stopPropagation();
1281
+ dragCounterRef.current++;
1282
+ if (dragCounterRef.current === 1) {
1283
+ setIsDragging(true);
1284
+ }
1285
+ };
1286
+ const handleDragOver = (e) => {
1287
+ e.preventDefault();
1288
+ e.stopPropagation();
1289
+ };
1290
+ const handleDragLeave = (e) => {
1291
+ e.preventDefault();
1292
+ e.stopPropagation();
1293
+ dragCounterRef.current--;
1294
+ if (dragCounterRef.current === 0) {
1295
+ setIsDragging(false);
1296
+ }
1297
+ };
1298
+ const handleDrop = async (e) => {
1299
+ e.preventDefault();
1300
+ e.stopPropagation();
1301
+ dragCounterRef.current = 0;
1302
+ setIsDragging(false);
1303
+ const files = e.dataTransfer?.files;
1304
+ if (!files || files.length === 0) return;
1305
+ const newPending = [];
1306
+ for (let i = 0; i < files.length; i++) {
1307
+ const file = files[i];
1308
+ const id = `pending-${Date.now()}-${i}`;
1309
+ let preview;
1310
+ if (file.type.startsWith("image/")) {
1311
+ preview = URL.createObjectURL(file);
1312
+ }
1313
+ newPending.push({
1314
+ id,
1315
+ file,
1316
+ preview,
1317
+ progress: 0,
1318
+ status: "pending"
1319
+ });
1320
+ }
1321
+ setPendingAttachments((prev) => [...prev, ...newPending]);
1322
+ setIsUploading(true);
1323
+ for (const pending of newPending) {
1324
+ try {
1325
+ setPendingAttachments(
1326
+ (prev) => prev.map((a) => a.id === pending.id ? { ...a, status: "uploading" } : a)
1327
+ );
1328
+ const attachment = await client2.uploadFile(pending.file, (progress) => {
1329
+ setPendingAttachments(
1330
+ (prev) => prev.map((a) => a.id === pending.id ? { ...a, progress } : a)
1331
+ );
1332
+ });
1333
+ setPendingAttachments(
1334
+ (prev) => prev.map(
1335
+ (a) => a.id === pending.id ? { ...a, status: "ready", progress: 100, attachment } : a
1336
+ )
1337
+ );
1338
+ } catch (err) {
1339
+ console.error("[PocketPing] Failed to upload dropped file:", err);
1340
+ setPendingAttachments(
1341
+ (prev) => prev.map(
1342
+ (a) => a.id === pending.id ? { ...a, status: "error", error: "Upload failed" } : a
1343
+ )
1344
+ );
1345
+ }
1346
+ }
1347
+ setIsUploading(false);
1348
+ };
540
1349
  const position = config.position ?? "bottom-right";
541
1350
  const theme = getTheme(config.theme ?? "auto");
542
1351
  const primaryColor = config.primaryColor ?? "#6366f1";
1352
+ const actionIconColor = theme === "dark" ? "#9ca3af" : "#6b7280";
543
1353
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_preact.Fragment, { children: [
544
1354
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { children: styles(primaryColor, theme) }),
545
1355
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
@@ -555,84 +1365,263 @@ function ChatWidget({ client: client2, config: initialConfig }) {
555
1365
  ]
556
1366
  }
557
1367
  ),
558
- isOpen && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { class: `pp-window pp-${position} pp-theme-${theme}`, children: [
559
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { class: "pp-header", children: [
560
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { class: "pp-header-info", children: [
561
- config.operatorAvatar && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { src: config.operatorAvatar, alt: "", class: "pp-avatar" }),
562
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
563
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { class: "pp-header-title", children: config.operatorName ?? "Support" }),
564
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { class: "pp-header-status", children: operatorOnline ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
565
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { class: "pp-status-dot pp-online" }),
566
- " Online"
567
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
568
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { class: "pp-status-dot" }),
569
- " Away"
570
- ] }) })
571
- ] })
572
- ] }),
573
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
574
- "button",
575
- {
576
- class: "pp-close-btn",
577
- onClick: () => client2.setOpen(false),
578
- "aria-label": "Close chat",
579
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CloseIcon, {})
580
- }
581
- )
582
- ] }),
583
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { class: "pp-messages", children: [
584
- config.welcomeMessage && messages.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { class: "pp-welcome", children: config.welcomeMessage }),
585
- messages.map((msg) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
586
- "div",
587
- {
588
- class: `pp-message pp-message-${msg.sender}`,
589
- children: [
590
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { class: "pp-message-content", children: msg.content }),
591
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { class: "pp-message-time", children: [
592
- formatTime(msg.timestamp),
593
- msg.sender === "ai" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { class: "pp-ai-badge", children: "AI" }),
594
- msg.sender === "visitor" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { class: `pp-status pp-status-${msg.status ?? "sent"}`, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StatusIcon, { status: msg.status }) })
1368
+ isOpen && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1369
+ "div",
1370
+ {
1371
+ class: `pp-window pp-${position} pp-theme-${theme} ${isDragging ? "pp-dragging" : ""}`,
1372
+ onDragEnter: handleDragEnter,
1373
+ onDragOver: handleDragOver,
1374
+ onDragLeave: handleDragLeave,
1375
+ onDrop: handleDrop,
1376
+ children: [
1377
+ isDragging && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { class: "pp-drop-overlay", children: [
1378
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { class: "pp-drop-icon", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AttachIcon, {}) }),
1379
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { class: "pp-drop-text", children: "Drop files to upload" })
1380
+ ] }),
1381
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { class: "pp-header", children: [
1382
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { class: "pp-header-info", children: [
1383
+ config.operatorAvatar && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { src: config.operatorAvatar, alt: "", class: "pp-avatar" }),
1384
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
1385
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { class: "pp-header-title", children: config.operatorName ?? "Support" }),
1386
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { class: "pp-header-status", children: operatorOnline ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1387
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { class: "pp-status-dot pp-online" }),
1388
+ " Online"
1389
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1390
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { class: "pp-status-dot" }),
1391
+ " Away"
1392
+ ] }) })
595
1393
  ] })
596
- ]
597
- },
598
- msg.id
599
- )),
600
- isTyping && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { class: "pp-message pp-message-operator pp-typing", children: [
601
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {}),
602
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {}),
603
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {})
604
- ] }),
605
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { ref: messagesEndRef })
606
- ] }),
607
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { class: "pp-input-form", onSubmit: handleSubmit, children: [
608
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
609
- "input",
610
- {
611
- ref: inputRef,
612
- type: "text",
613
- class: "pp-input",
614
- placeholder: config.placeholder ?? "Type a message...",
615
- value: inputValue,
616
- onInput: handleInputChange,
617
- disabled: !isConnected
618
- }
619
- ),
620
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
621
- "button",
622
- {
623
- type: "submit",
624
- class: "pp-send-btn",
625
- disabled: !inputValue.trim() || !isConnected,
626
- "aria-label": "Send message",
627
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SendIcon, {})
628
- }
629
- )
630
- ] }),
631
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { class: "pp-footer", children: [
632
- "Powered by ",
633
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { href: "https://pocketping.io", target: "_blank", rel: "noopener", children: "PocketPing" })
634
- ] })
635
- ] })
1394
+ ] }),
1395
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1396
+ "button",
1397
+ {
1398
+ class: "pp-close-btn",
1399
+ onClick: () => client2.setOpen(false),
1400
+ "aria-label": "Close chat",
1401
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CloseIcon, {})
1402
+ }
1403
+ )
1404
+ ] }),
1405
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { class: "pp-messages", ref: messagesContainerRef, children: [
1406
+ config.welcomeMessage && messages.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { class: "pp-welcome", children: config.welcomeMessage }),
1407
+ messages.map((msg) => {
1408
+ const isDeleted = !!msg.deletedAt;
1409
+ const isEdited = !!msg.editedAt;
1410
+ let replyData = null;
1411
+ if (msg.replyTo) {
1412
+ if (typeof msg.replyTo === "object") {
1413
+ replyData = msg.replyTo;
1414
+ } else {
1415
+ const replyToMsg = messages.find((m) => m.id === msg.replyTo);
1416
+ if (replyToMsg) {
1417
+ replyData = {
1418
+ sender: replyToMsg.sender,
1419
+ content: replyToMsg.content,
1420
+ deleted: !!replyToMsg.deletedAt
1421
+ };
1422
+ }
1423
+ }
1424
+ }
1425
+ const isHovered = hoveredMessageId === msg.id;
1426
+ const showActions = isHovered && !isDeleted;
1427
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1428
+ "div",
1429
+ {
1430
+ class: `pp-message pp-message-${msg.sender} ${isDeleted ? "pp-message-deleted" : ""}`,
1431
+ onContextMenu: (e) => handleMessageContextMenu(e, msg),
1432
+ onMouseEnter: () => setHoveredMessageId(msg.id),
1433
+ onMouseLeave: () => setHoveredMessageId(null),
1434
+ onTouchStart: () => handleTouchStart(msg),
1435
+ onTouchEnd: handleTouchEnd,
1436
+ onTouchCancel: handleTouchEnd,
1437
+ children: [
1438
+ showActions && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { class: `pp-message-actions ${msg.sender === "visitor" ? "pp-actions-left" : "pp-actions-right"}`, children: [
1439
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1440
+ "button",
1441
+ {
1442
+ class: "pp-action-btn",
1443
+ onClick: () => handleReply(msg),
1444
+ title: "Reply",
1445
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ReplyIcon, { color: actionIconColor })
1446
+ }
1447
+ ),
1448
+ msg.sender === "visitor" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1449
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1450
+ "button",
1451
+ {
1452
+ class: "pp-action-btn",
1453
+ onClick: () => handleStartEdit(msg),
1454
+ title: "Edit",
1455
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(EditIcon, { color: actionIconColor })
1456
+ }
1457
+ ),
1458
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1459
+ "button",
1460
+ {
1461
+ class: "pp-action-btn pp-action-delete",
1462
+ onClick: () => handleDelete(msg),
1463
+ title: "Delete",
1464
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DeleteIcon, { color: actionIconColor })
1465
+ }
1466
+ )
1467
+ ] })
1468
+ ] }),
1469
+ replyData && replyData.content && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { class: "pp-reply-quote", children: [
1470
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { class: "pp-reply-sender", children: replyData.sender === "visitor" ? "You" : "Support" }),
1471
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { class: "pp-reply-content", children: [
1472
+ replyData.deleted ? "Message deleted" : (replyData.content || "").slice(0, 50),
1473
+ (replyData.content || "").length > 50 ? "..." : ""
1474
+ ] })
1475
+ ] }),
1476
+ isDeleted ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { class: "pp-message-content pp-deleted-content", children: [
1477
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { class: "pp-deleted-icon", children: "\u{1F5D1}\uFE0F" }),
1478
+ " Message deleted"
1479
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1480
+ msg.content && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { class: "pp-message-content", children: msg.content }),
1481
+ msg.attachments && msg.attachments.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { class: "pp-message-attachments", children: msg.attachments.map((att) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AttachmentDisplay, { attachment: att }, att.id)) })
1482
+ ] }),
1483
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { class: "pp-message-time", children: [
1484
+ formatTime(msg.timestamp),
1485
+ isEdited && !isDeleted && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { class: "pp-edited-badge", children: "edited" }),
1486
+ msg.sender === "ai" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { class: "pp-ai-badge", children: "AI" }),
1487
+ msg.sender === "visitor" && !isDeleted && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { class: `pp-status pp-status-${msg.status ?? "sent"}`, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StatusIcon, { status: msg.status }) })
1488
+ ] })
1489
+ ]
1490
+ },
1491
+ msg.id
1492
+ );
1493
+ }),
1494
+ isTyping && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { class: "pp-message pp-message-operator pp-typing", children: [
1495
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {}),
1496
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {}),
1497
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {})
1498
+ ] }),
1499
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { ref: messagesEndRef })
1500
+ ] }),
1501
+ messageMenu && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1502
+ "div",
1503
+ {
1504
+ class: "pp-message-menu",
1505
+ style: { top: `${messageMenu.y}px`, left: `${messageMenu.x}px` },
1506
+ children: [
1507
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", { onClick: () => handleReply(messageMenu.message), children: [
1508
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ReplyIcon, { color: actionIconColor }),
1509
+ " Reply"
1510
+ ] }),
1511
+ messageMenu.message.sender === "visitor" && !messageMenu.message.deletedAt && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1512
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", { onClick: () => handleStartEdit(messageMenu.message), children: [
1513
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(EditIcon, { color: actionIconColor }),
1514
+ " Edit"
1515
+ ] }),
1516
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", { class: "pp-menu-delete", onClick: () => handleDelete(messageMenu.message), children: [
1517
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DeleteIcon, { color: "#ef4444" }),
1518
+ " Delete"
1519
+ ] })
1520
+ ] })
1521
+ ]
1522
+ }
1523
+ ),
1524
+ editingMessage && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { class: "pp-edit-modal", children: [
1525
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { class: "pp-edit-header", children: [
1526
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "Edit message" }),
1527
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { onClick: handleCancelEdit, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CloseIcon, {}) })
1528
+ ] }),
1529
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1530
+ "textarea",
1531
+ {
1532
+ class: "pp-edit-input",
1533
+ value: editContent,
1534
+ onInput: (e) => setEditContent(e.target.value),
1535
+ autoFocus: true
1536
+ }
1537
+ ),
1538
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { class: "pp-edit-actions", children: [
1539
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { class: "pp-edit-cancel", onClick: handleCancelEdit, children: "Cancel" }),
1540
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { class: "pp-edit-save", onClick: handleSaveEdit, disabled: !editContent.trim(), children: "Save" })
1541
+ ] })
1542
+ ] }),
1543
+ replyingTo && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { class: "pp-reply-preview", children: [
1544
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { class: "pp-reply-preview-content", children: [
1545
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { class: "pp-reply-label", children: "Replying to" }),
1546
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { class: "pp-reply-text", children: [
1547
+ replyingTo.content.slice(0, 50),
1548
+ replyingTo.content.length > 50 ? "..." : ""
1549
+ ] })
1550
+ ] }),
1551
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { class: "pp-reply-cancel", onClick: handleCancelReply, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CloseIcon, {}) })
1552
+ ] }),
1553
+ pendingAttachments.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { class: "pp-attachments-preview", children: pendingAttachments.map((pending) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { class: `pp-attachment-preview pp-attachment-${pending.status}`, children: [
1554
+ pending.preview ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { src: pending.preview, alt: pending.file.name, class: "pp-preview-img" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { class: "pp-preview-file", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FileIcon, { mimeType: pending.file.type }) }),
1555
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1556
+ "button",
1557
+ {
1558
+ class: "pp-remove-attachment",
1559
+ onClick: () => handleRemoveAttachment(pending.id),
1560
+ "aria-label": "Remove attachment",
1561
+ type: "button",
1562
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CloseIcon, {})
1563
+ }
1564
+ ),
1565
+ pending.status === "uploading" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { class: "pp-upload-progress", style: { width: `${pending.progress}%` } }),
1566
+ pending.status === "error" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { class: "pp-upload-error", title: pending.error, children: "!" })
1567
+ ] }, pending.id)) }),
1568
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { class: "pp-input-form", onSubmit: handleSubmit, children: [
1569
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1570
+ "input",
1571
+ {
1572
+ ref: (el) => {
1573
+ fileInputRef.current = el;
1574
+ if (el) {
1575
+ el.onchange = handleFileSelect;
1576
+ }
1577
+ },
1578
+ type: "file",
1579
+ class: "pp-file-input",
1580
+ accept: "image/*,audio/*,video/*,.pdf,.doc,.docx,.xls,.xlsx,.txt",
1581
+ multiple: true
1582
+ }
1583
+ ),
1584
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1585
+ "button",
1586
+ {
1587
+ type: "button",
1588
+ class: "pp-attach-btn",
1589
+ onClick: () => fileInputRef.current?.click(),
1590
+ disabled: !isConnected || isUploading,
1591
+ "aria-label": "Attach file",
1592
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AttachIcon, {})
1593
+ }
1594
+ ),
1595
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1596
+ "input",
1597
+ {
1598
+ ref: inputRef,
1599
+ type: "text",
1600
+ class: "pp-input",
1601
+ placeholder: config.placeholder ?? "Type a message...",
1602
+ value: inputValue,
1603
+ onInput: handleInputChange,
1604
+ disabled: !isConnected
1605
+ }
1606
+ ),
1607
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1608
+ "button",
1609
+ {
1610
+ type: "submit",
1611
+ class: "pp-send-btn",
1612
+ disabled: !inputValue.trim() && pendingAttachments.filter((a) => a.status === "ready").length === 0 || !isConnected || isUploading,
1613
+ "aria-label": "Send message",
1614
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SendIcon, {})
1615
+ }
1616
+ )
1617
+ ] }),
1618
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { class: "pp-footer", children: [
1619
+ "Powered by ",
1620
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { href: "https://pocketping.io", target: "_blank", rel: "noopener", children: "PocketPing" })
1621
+ ] })
1622
+ ]
1623
+ }
1624
+ )
636
1625
  ] });
637
1626
  }
638
1627
  function checkPageVisibility(config) {
@@ -688,6 +1677,89 @@ function StatusIcon({ status }) {
688
1677
  }
689
1678
  return null;
690
1679
  }
1680
+ function AttachIcon() {
1681
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: /* @__PURE__ */ (0, import_jsx_runtime.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" }) });
1682
+ }
1683
+ function ReplyIcon({ color, size = 16 }) {
1684
+ const strokeColor = color || "currentColor";
1685
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { viewBox: "0 0 24 24", fill: "none", "stroke-width": "2", style: { stroke: strokeColor, width: `${size}px`, minWidth: `${size}px`, height: `${size}px`, display: "block", flexShrink: 0 }, children: [
1686
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("polyline", { points: "9 17 4 12 9 7" }),
1687
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M20 18v-2a4 4 0 0 0-4-4H4" })
1688
+ ] });
1689
+ }
1690
+ function EditIcon({ color, size = 16 }) {
1691
+ const strokeColor = color || "currentColor";
1692
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { viewBox: "0 0 24 24", fill: "none", "stroke-width": "2", style: { stroke: strokeColor, width: `${size}px`, minWidth: `${size}px`, height: `${size}px`, display: "block", flexShrink: 0 }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z" }) });
1693
+ }
1694
+ function DeleteIcon({ color, size = 16 }) {
1695
+ const strokeColor = color || "currentColor";
1696
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { viewBox: "0 0 24 24", fill: "none", "stroke-width": "2", style: { stroke: strokeColor, width: `${size}px`, minWidth: `${size}px`, height: `${size}px`, display: "block", flexShrink: 0 }, children: [
1697
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("polyline", { points: "3 6 5 6 21 6" }),
1698
+ /* @__PURE__ */ (0, import_jsx_runtime.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" })
1699
+ ] });
1700
+ }
1701
+ function FileIcon({ mimeType }) {
1702
+ if (mimeType === "application/pdf") {
1703
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: [
1704
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }),
1705
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("polyline", { points: "14 2 14 8 20 8" }),
1706
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M9 15h6" }),
1707
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M9 11h6" })
1708
+ ] });
1709
+ }
1710
+ if (mimeType.startsWith("audio/")) {
1711
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: [
1712
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M9 18V5l12-2v13" }),
1713
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: "6", cy: "18", r: "3" }),
1714
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: "18", cy: "16", r: "3" })
1715
+ ] });
1716
+ }
1717
+ if (mimeType.startsWith("video/")) {
1718
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: [
1719
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", { x: "2", y: "2", width: "20", height: "20", rx: "2.18", ry: "2.18" }),
1720
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "7", y1: "2", x2: "7", y2: "22" }),
1721
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "17", y1: "2", x2: "17", y2: "22" }),
1722
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "2", y1: "12", x2: "22", y2: "12" }),
1723
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "2", y1: "7", x2: "7", y2: "7" }),
1724
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "2", y1: "17", x2: "7", y2: "17" }),
1725
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "17", y1: "17", x2: "22", y2: "17" }),
1726
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "17", y1: "7", x2: "22", y2: "7" })
1727
+ ] });
1728
+ }
1729
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: [
1730
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }),
1731
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("polyline", { points: "14 2 14 8 20 8" })
1732
+ ] });
1733
+ }
1734
+ function AttachmentDisplay({ attachment }) {
1735
+ const isImage = attachment.mimeType.startsWith("image/");
1736
+ const isAudio = attachment.mimeType.startsWith("audio/");
1737
+ const isVideo = attachment.mimeType.startsWith("video/");
1738
+ const formatSize = (bytes) => {
1739
+ if (bytes < 1024) return `${bytes} B`;
1740
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
1741
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1742
+ };
1743
+ if (isImage) {
1744
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { href: attachment.url, target: "_blank", rel: "noopener", class: "pp-attachment pp-attachment-image", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { src: attachment.thumbnailUrl || attachment.url, alt: attachment.filename }) });
1745
+ }
1746
+ if (isAudio) {
1747
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { class: "pp-attachment pp-attachment-audio", children: [
1748
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("audio", { controls: true, preload: "metadata", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("source", { src: attachment.url, type: attachment.mimeType }) }),
1749
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { class: "pp-attachment-name", children: attachment.filename })
1750
+ ] });
1751
+ }
1752
+ if (isVideo) {
1753
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { class: "pp-attachment pp-attachment-video", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("video", { controls: true, preload: "metadata", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("source", { src: attachment.url, type: attachment.mimeType }) }) });
1754
+ }
1755
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("a", { href: attachment.url, target: "_blank", rel: "noopener", class: "pp-attachment pp-attachment-file", children: [
1756
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FileIcon, { mimeType: attachment.mimeType }),
1757
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { class: "pp-attachment-info", children: [
1758
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { class: "pp-attachment-name", children: attachment.filename }),
1759
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { class: "pp-attachment-size", children: formatSize(attachment.size) })
1760
+ ] })
1761
+ ] });
1762
+ }
691
1763
 
692
1764
  // src/version.ts
693
1765
  var VERSION = "0.3.6";
@@ -803,7 +1875,7 @@ var PocketPingClient = class {
803
1875
  this.cleanupTrackedElements();
804
1876
  this.disableInspectorMode();
805
1877
  }
806
- async sendMessage(content) {
1878
+ async sendMessage(content, attachmentIds, replyTo) {
807
1879
  if (!this.session) {
808
1880
  throw new Error("Not connected");
809
1881
  }
@@ -814,7 +1886,8 @@ var PocketPingClient = class {
814
1886
  content,
815
1887
  sender: "visitor",
816
1888
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
817
- status: "sending"
1889
+ status: "sending",
1890
+ replyTo
818
1891
  };
819
1892
  this.session.messages.push(tempMessage);
820
1893
  this.emit("message", tempMessage);
@@ -824,7 +1897,9 @@ var PocketPingClient = class {
824
1897
  body: JSON.stringify({
825
1898
  sessionId: this.session.sessionId,
826
1899
  content,
827
- sender: "visitor"
1900
+ sender: "visitor",
1901
+ attachmentIds: attachmentIds || [],
1902
+ replyTo
828
1903
  })
829
1904
  });
830
1905
  const messageIndex = this.session.messages.findIndex((m) => m.id === tempId);
@@ -832,6 +1907,9 @@ var PocketPingClient = class {
832
1907
  this.session.messages[messageIndex].id = response.messageId;
833
1908
  this.session.messages[messageIndex].timestamp = response.timestamp;
834
1909
  this.session.messages[messageIndex].status = "sent";
1910
+ if (response.attachments && response.attachments.length > 0) {
1911
+ this.session.messages[messageIndex].attachments = response.attachments;
1912
+ }
835
1913
  this.emit("message", this.session.messages[messageIndex]);
836
1914
  }
837
1915
  const message = this.session.messages[messageIndex] || {
@@ -840,7 +1918,8 @@ var PocketPingClient = class {
840
1918
  content,
841
1919
  sender: "visitor",
842
1920
  timestamp: response.timestamp,
843
- status: "sent"
1921
+ status: "sent",
1922
+ attachments: response.attachments
844
1923
  };
845
1924
  this.config.onMessage?.(message);
846
1925
  return message;
@@ -853,6 +1932,110 @@ var PocketPingClient = class {
853
1932
  throw error;
854
1933
  }
855
1934
  }
1935
+ /**
1936
+ * Upload a file attachment
1937
+ * Returns the attachment data after successful upload
1938
+ * @param file - File object to upload
1939
+ * @param onProgress - Optional callback for upload progress (0-100)
1940
+ * @example
1941
+ * const attachment = await PocketPing.uploadFile(file, (progress) => {
1942
+ * console.log(`Upload ${progress}% complete`)
1943
+ * })
1944
+ * await PocketPing.sendMessage('Check this file', [attachment.id])
1945
+ */
1946
+ async uploadFile(file, onProgress) {
1947
+ if (!this.session) {
1948
+ throw new Error("Not connected");
1949
+ }
1950
+ this.emit("uploadStart", { filename: file.name, size: file.size });
1951
+ try {
1952
+ const initResponse = await this.fetch("/upload", {
1953
+ method: "POST",
1954
+ body: JSON.stringify({
1955
+ sessionId: this.session.sessionId,
1956
+ filename: file.name,
1957
+ mimeType: file.type || "application/octet-stream",
1958
+ size: file.size
1959
+ })
1960
+ });
1961
+ onProgress?.(10);
1962
+ this.emit("uploadProgress", { filename: file.name, progress: 10 });
1963
+ await this.uploadToPresignedUrl(initResponse.uploadUrl, file, (progress) => {
1964
+ const mappedProgress = 10 + progress * 0.8;
1965
+ onProgress?.(mappedProgress);
1966
+ this.emit("uploadProgress", { filename: file.name, progress: mappedProgress });
1967
+ });
1968
+ const completeResponse = await this.fetch("/upload/complete", {
1969
+ method: "POST",
1970
+ body: JSON.stringify({
1971
+ sessionId: this.session.sessionId,
1972
+ attachmentId: initResponse.attachmentId
1973
+ })
1974
+ });
1975
+ onProgress?.(100);
1976
+ this.emit("uploadComplete", completeResponse);
1977
+ return {
1978
+ id: completeResponse.id,
1979
+ filename: completeResponse.filename,
1980
+ mimeType: completeResponse.mimeType,
1981
+ size: completeResponse.size,
1982
+ url: completeResponse.url,
1983
+ thumbnailUrl: completeResponse.thumbnailUrl,
1984
+ status: completeResponse.status
1985
+ };
1986
+ } catch (error) {
1987
+ this.emit("uploadError", { filename: file.name, error });
1988
+ throw error;
1989
+ }
1990
+ }
1991
+ /**
1992
+ * Upload multiple files at once
1993
+ * @param files - Array of File objects to upload
1994
+ * @param onProgress - Optional callback for overall progress (0-100)
1995
+ * @returns Array of uploaded attachments
1996
+ */
1997
+ async uploadFiles(files, onProgress) {
1998
+ const attachments = [];
1999
+ const totalFiles = files.length;
2000
+ for (let i = 0; i < totalFiles; i++) {
2001
+ const file = files[i];
2002
+ const baseProgress = i / totalFiles * 100;
2003
+ const fileProgress = 100 / totalFiles;
2004
+ const attachment = await this.uploadFile(file, (progress) => {
2005
+ const totalProgress = baseProgress + progress / 100 * fileProgress;
2006
+ onProgress?.(totalProgress);
2007
+ });
2008
+ attachments.push(attachment);
2009
+ }
2010
+ return attachments;
2011
+ }
2012
+ /**
2013
+ * Upload file to presigned URL with progress tracking
2014
+ */
2015
+ uploadToPresignedUrl(url, file, onProgress) {
2016
+ return new Promise((resolve, reject) => {
2017
+ const xhr = new XMLHttpRequest();
2018
+ xhr.upload.addEventListener("progress", (event) => {
2019
+ if (event.lengthComputable) {
2020
+ const progress = event.loaded / event.total * 100;
2021
+ onProgress?.(progress);
2022
+ }
2023
+ });
2024
+ xhr.addEventListener("load", () => {
2025
+ if (xhr.status >= 200 && xhr.status < 300) {
2026
+ resolve();
2027
+ } else {
2028
+ reject(new Error(`Upload failed with status ${xhr.status}`));
2029
+ }
2030
+ });
2031
+ xhr.addEventListener("error", () => {
2032
+ reject(new Error("Upload failed"));
2033
+ });
2034
+ xhr.open("PUT", url);
2035
+ xhr.setRequestHeader("Content-Type", file.type || "application/octet-stream");
2036
+ xhr.send(file);
2037
+ });
2038
+ }
856
2039
  async fetchMessages(after) {
857
2040
  if (!this.session) {
858
2041
  throw new Error("Not connected");
@@ -906,6 +2089,54 @@ var PocketPingClient = class {
906
2089
  console.error("[PocketPing] Failed to send read status:", err);
907
2090
  }
908
2091
  }
2092
+ /**
2093
+ * Edit a message (visitor can only edit their own messages)
2094
+ * @param messageId - ID of the message to edit
2095
+ * @param content - New content for the message
2096
+ */
2097
+ async editMessage(messageId, content) {
2098
+ if (!this.session) {
2099
+ throw new Error("Not connected");
2100
+ }
2101
+ const response = await this.fetch(`/message/${messageId}`, {
2102
+ method: "PATCH",
2103
+ body: JSON.stringify({
2104
+ sessionId: this.session.sessionId,
2105
+ content
2106
+ })
2107
+ });
2108
+ const messageIndex = this.session.messages.findIndex((m) => m.id === messageId);
2109
+ if (messageIndex >= 0) {
2110
+ this.session.messages[messageIndex].content = response.message.content;
2111
+ this.session.messages[messageIndex].editedAt = response.message.editedAt;
2112
+ this.emit("messageEdited", this.session.messages[messageIndex]);
2113
+ }
2114
+ return this.session.messages[messageIndex];
2115
+ }
2116
+ /**
2117
+ * Delete a message (soft delete - visitor can only delete their own messages)
2118
+ * @param messageId - ID of the message to delete
2119
+ */
2120
+ async deleteMessage(messageId) {
2121
+ if (!this.session) {
2122
+ throw new Error("Not connected");
2123
+ }
2124
+ const response = await this.fetch(`/message/${messageId}`, {
2125
+ method: "DELETE",
2126
+ body: JSON.stringify({
2127
+ sessionId: this.session.sessionId
2128
+ })
2129
+ });
2130
+ if (response.deleted) {
2131
+ const messageIndex = this.session.messages.findIndex((m) => m.id === messageId);
2132
+ if (messageIndex >= 0) {
2133
+ this.session.messages[messageIndex].deletedAt = (/* @__PURE__ */ new Date()).toISOString();
2134
+ this.session.messages[messageIndex].content = "";
2135
+ this.emit("messageDeleted", this.session.messages[messageIndex]);
2136
+ }
2137
+ }
2138
+ return response.deleted;
2139
+ }
909
2140
  async getPresence() {
910
2141
  return this.fetch("/presence", { method: "GET" });
911
2142
  }
@@ -1460,7 +2691,14 @@ var PocketPingClient = class {
1460
2691
  }
1461
2692
  connectSSE() {
1462
2693
  if (!this.session) return;
1463
- const sseUrl = this.config.endpoint.replace(/\/$/, "") + `/stream?sessionId=${this.session.sessionId}`;
2694
+ const params = new URLSearchParams({
2695
+ sessionId: this.session.sessionId
2696
+ });
2697
+ const lastEventTimestamp = this.getLastEventTimestamp();
2698
+ if (lastEventTimestamp) {
2699
+ params.set("after", lastEventTimestamp);
2700
+ }
2701
+ const sseUrl = this.config.endpoint.replace(/\/$/, "") + `/stream?${params.toString()}`;
1464
2702
  try {
1465
2703
  this.sse = new EventSource(sseUrl);
1466
2704
  const connectionTimeout = setTimeout(() => {
@@ -1515,6 +2753,19 @@ var PocketPingClient = class {
1515
2753
  handleRealtimeEvent(event) {
1516
2754
  this.handleWebSocketEvent(event);
1517
2755
  }
2756
+ getLastEventTimestamp() {
2757
+ if (!this.session) return null;
2758
+ let latest = null;
2759
+ for (const msg of this.session.messages) {
2760
+ const candidates = [msg.timestamp, msg.editedAt, msg.deletedAt, msg.deliveredAt, msg.readAt].filter(Boolean).map((value) => new Date(value)).filter((date) => !isNaN(date.getTime()));
2761
+ for (const date of candidates) {
2762
+ if (!latest || date > latest) {
2763
+ latest = date;
2764
+ }
2765
+ }
2766
+ }
2767
+ return latest ? latest.toISOString() : null;
2768
+ }
1518
2769
  handleWebSocketEvent(event) {
1519
2770
  switch (event.type) {
1520
2771
  case "message":
@@ -1537,12 +2788,41 @@ var PocketPingClient = class {
1537
2788
  }
1538
2789
  if (existingIndex >= 0) {
1539
2790
  const existing = this.session.messages[existingIndex];
2791
+ let updated = false;
1540
2792
  if (message.status && message.status !== existing.status) {
1541
2793
  existing.status = message.status;
1542
- if (message.deliveredAt) existing.deliveredAt = message.deliveredAt;
1543
- if (message.readAt) existing.readAt = message.readAt;
2794
+ updated = true;
2795
+ if (message.deliveredAt) {
2796
+ existing.deliveredAt = message.deliveredAt;
2797
+ }
2798
+ if (message.readAt) {
2799
+ existing.readAt = message.readAt;
2800
+ }
1544
2801
  this.emit("read", { messageIds: [message.id], status: message.status });
1545
2802
  }
2803
+ if (message.content !== void 0 && message.content !== existing.content) {
2804
+ existing.content = message.content;
2805
+ updated = true;
2806
+ }
2807
+ if (message.editedAt !== void 0 && message.editedAt !== existing.editedAt) {
2808
+ existing.editedAt = message.editedAt;
2809
+ updated = true;
2810
+ }
2811
+ if (message.deletedAt !== void 0 && message.deletedAt !== existing.deletedAt) {
2812
+ existing.deletedAt = message.deletedAt;
2813
+ updated = true;
2814
+ }
2815
+ if (message.replyTo !== void 0) {
2816
+ existing.replyTo = message.replyTo;
2817
+ updated = true;
2818
+ }
2819
+ if (message.attachments !== void 0) {
2820
+ existing.attachments = message.attachments;
2821
+ updated = true;
2822
+ }
2823
+ if (updated) {
2824
+ this.emit("message", existing);
2825
+ }
1546
2826
  } else {
1547
2827
  this.session.messages.push(message);
1548
2828
  this.emit("message", message);
@@ -1581,6 +2861,29 @@ var PocketPingClient = class {
1581
2861
  }
1582
2862
  this.emit("read", readData);
1583
2863
  break;
2864
+ case "message_edited":
2865
+ if (this.session) {
2866
+ const editData = event.data;
2867
+ const msgIndex = this.session.messages.findIndex((m) => m.id === editData.messageId);
2868
+ if (msgIndex >= 0) {
2869
+ const existing = this.session.messages[msgIndex];
2870
+ existing.content = editData.content;
2871
+ existing.editedAt = editData.editedAt ?? (/* @__PURE__ */ new Date()).toISOString();
2872
+ this.emit("message", existing);
2873
+ }
2874
+ }
2875
+ break;
2876
+ case "message_deleted":
2877
+ if (this.session) {
2878
+ const deleteData = event.data;
2879
+ const msgIndex = this.session.messages.findIndex((m) => m.id === deleteData.messageId);
2880
+ if (msgIndex >= 0) {
2881
+ const existing = this.session.messages[msgIndex];
2882
+ existing.deletedAt = deleteData.deletedAt ?? (/* @__PURE__ */ new Date()).toISOString();
2883
+ this.emit("message", existing);
2884
+ }
2885
+ }
2886
+ break;
1584
2887
  case "event":
1585
2888
  const customEvent = event.data;
1586
2889
  this.emitCustomEvent(customEvent);
@@ -1640,11 +2943,45 @@ var PocketPingClient = class {
1640
2943
  const poll = async () => {
1641
2944
  if (!this.session) return;
1642
2945
  try {
1643
- const lastMessageId = this.session.messages[this.session.messages.length - 1]?.id;
1644
- const newMessages = await this.fetchMessages(lastMessageId);
2946
+ const lastEventTimestamp = this.getLastEventTimestamp();
2947
+ const newMessages = await this.fetchMessages(lastEventTimestamp ?? void 0);
1645
2948
  this.pollingFailures = 0;
1646
2949
  for (const message of newMessages) {
1647
- if (!this.session.messages.find((m) => m.id === message.id)) {
2950
+ const existingIndex = this.session.messages.findIndex((m) => m.id === message.id);
2951
+ if (existingIndex >= 0) {
2952
+ const existing = this.session.messages[existingIndex];
2953
+ let updated = false;
2954
+ if (message.status && message.status !== existing.status) {
2955
+ existing.status = message.status;
2956
+ updated = true;
2957
+ if (message.deliveredAt) existing.deliveredAt = message.deliveredAt;
2958
+ if (message.readAt) existing.readAt = message.readAt;
2959
+ this.emit("read", { messageIds: [message.id], status: message.status });
2960
+ }
2961
+ if (message.content !== void 0 && message.content !== existing.content) {
2962
+ existing.content = message.content;
2963
+ updated = true;
2964
+ }
2965
+ if (message.editedAt !== void 0 && message.editedAt !== existing.editedAt) {
2966
+ existing.editedAt = message.editedAt;
2967
+ updated = true;
2968
+ }
2969
+ if (message.deletedAt !== void 0 && message.deletedAt !== existing.deletedAt) {
2970
+ existing.deletedAt = message.deletedAt;
2971
+ updated = true;
2972
+ }
2973
+ if (message.replyTo !== void 0) {
2974
+ existing.replyTo = message.replyTo;
2975
+ updated = true;
2976
+ }
2977
+ if (message.attachments !== void 0) {
2978
+ existing.attachments = message.attachments;
2979
+ updated = true;
2980
+ }
2981
+ if (updated) {
2982
+ this.emit("message", existing);
2983
+ }
2984
+ } else {
1648
2985
  this.session.messages.push(message);
1649
2986
  this.emit("message", message);
1650
2987
  this.config.onMessage?.(message);
@@ -1809,11 +3146,23 @@ function close() {
1809
3146
  function toggle() {
1810
3147
  client?.toggleOpen();
1811
3148
  }
1812
- function sendMessage(content) {
3149
+ function sendMessage(content, attachmentIds) {
3150
+ if (!client) {
3151
+ throw new Error("[PocketPing] Not initialized");
3152
+ }
3153
+ return client.sendMessage(content, attachmentIds);
3154
+ }
3155
+ async function uploadFile(file, onProgress) {
3156
+ if (!client) {
3157
+ throw new Error("[PocketPing] Not initialized");
3158
+ }
3159
+ return client.uploadFile(file, onProgress);
3160
+ }
3161
+ async function uploadFiles(files, onProgress) {
1813
3162
  if (!client) {
1814
3163
  throw new Error("[PocketPing] Not initialized");
1815
3164
  }
1816
- return client.sendMessage(content);
3165
+ return client.uploadFiles(files, onProgress);
1817
3166
  }
1818
3167
  function trigger(eventName, data, options) {
1819
3168
  if (!client) {
@@ -1882,7 +3231,7 @@ if (typeof document !== "undefined") {
1882
3231
  }
1883
3232
  }
1884
3233
  }
1885
- var index_default = { init, destroy, open, close, toggle, sendMessage, trigger, onEvent, offEvent, on, identify, reset, getIdentity, setupTrackedElements, getTrackedElements };
3234
+ var index_default = { init, destroy, open, close, toggle, sendMessage, uploadFile, uploadFiles, trigger, onEvent, offEvent, on, identify, reset, getIdentity, setupTrackedElements, getTrackedElements };
1886
3235
  // Annotate the CommonJS export names for ESM import in node:
1887
3236
  0 && (module.exports = {
1888
3237
  close,
@@ -1899,5 +3248,7 @@ var index_default = { init, destroy, open, close, toggle, sendMessage, trigger,
1899
3248
  sendMessage,
1900
3249
  setupTrackedElements,
1901
3250
  toggle,
1902
- trigger
3251
+ trigger,
3252
+ uploadFile,
3253
+ uploadFiles
1903
3254
  });