@pocketping/widget 1.1.0 → 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 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 {
@@ -411,6 +419,486 @@ function styles(primaryColor, theme) {
411
419
  .pp-footer a:hover {
412
420
  text-decoration: underline;
413
421
  }
422
+
423
+ /* Attachment Styles */
424
+ .pp-file-input {
425
+ /* Use offscreen positioning instead of display:none for better browser compatibility */
426
+ position: absolute;
427
+ width: 1px;
428
+ height: 1px;
429
+ padding: 0;
430
+ margin: -1px;
431
+ overflow: hidden;
432
+ clip: rect(0, 0, 0, 0);
433
+ white-space: nowrap;
434
+ border: 0;
435
+ }
436
+
437
+ .pp-attach-btn {
438
+ width: 40px;
439
+ height: 40px;
440
+ border-radius: 50%;
441
+ background: transparent;
442
+ color: ${colors.textSecondary};
443
+ border: 1px solid ${colors.border};
444
+ cursor: pointer;
445
+ display: flex;
446
+ align-items: center;
447
+ justify-content: center;
448
+ transition: color 0.2s, border-color 0.2s;
449
+ flex-shrink: 0;
450
+ }
451
+
452
+ .pp-attach-btn:hover:not(:disabled) {
453
+ color: ${primaryColor};
454
+ border-color: ${primaryColor};
455
+ }
456
+
457
+ .pp-attach-btn:disabled {
458
+ opacity: 0.5;
459
+ cursor: not-allowed;
460
+ }
461
+
462
+ .pp-attach-btn svg {
463
+ width: 18px;
464
+ height: 18px;
465
+ }
466
+
467
+ .pp-attachments-preview {
468
+ display: flex;
469
+ gap: 8px;
470
+ padding: 8px 12px;
471
+ border-top: 1px solid ${colors.border};
472
+ overflow-x: auto;
473
+ background: ${colors.bgSecondary};
474
+ }
475
+
476
+ .pp-attachment-preview {
477
+ position: relative;
478
+ width: 60px;
479
+ height: 60px;
480
+ border-radius: 8px;
481
+ overflow: hidden;
482
+ flex-shrink: 0;
483
+ background: ${colors.bg};
484
+ border: 1px solid ${colors.border};
485
+ }
486
+
487
+ .pp-preview-img {
488
+ width: 100%;
489
+ height: 100%;
490
+ object-fit: cover;
491
+ }
492
+
493
+ .pp-preview-file {
494
+ width: 100%;
495
+ height: 100%;
496
+ display: flex;
497
+ align-items: center;
498
+ justify-content: center;
499
+ color: ${colors.textSecondary};
500
+ }
501
+
502
+ .pp-preview-file svg {
503
+ width: 24px;
504
+ height: 24px;
505
+ }
506
+
507
+ .pp-remove-attachment {
508
+ position: absolute;
509
+ top: 2px;
510
+ right: 2px;
511
+ width: 18px;
512
+ height: 18px;
513
+ border-radius: 50%;
514
+ background: rgba(0, 0, 0, 0.6);
515
+ color: white;
516
+ border: none;
517
+ cursor: pointer;
518
+ display: flex;
519
+ align-items: center;
520
+ justify-content: center;
521
+ padding: 0;
522
+ }
523
+
524
+ .pp-remove-attachment svg {
525
+ width: 10px;
526
+ height: 10px;
527
+ }
528
+
529
+ .pp-upload-progress {
530
+ position: absolute;
531
+ bottom: 0;
532
+ left: 0;
533
+ height: 3px;
534
+ background: ${primaryColor};
535
+ transition: width 0.1s;
536
+ }
537
+
538
+ .pp-upload-error {
539
+ position: absolute;
540
+ top: 50%;
541
+ left: 50%;
542
+ transform: translate(-50%, -50%);
543
+ width: 24px;
544
+ height: 24px;
545
+ border-radius: 50%;
546
+ background: #ef4444;
547
+ color: white;
548
+ font-weight: bold;
549
+ display: flex;
550
+ align-items: center;
551
+ justify-content: center;
552
+ font-size: 14px;
553
+ }
554
+
555
+ .pp-attachment-uploading {
556
+ opacity: 0.7;
557
+ }
558
+
559
+ .pp-attachment-error {
560
+ border-color: #ef4444;
561
+ }
562
+
563
+ /* Message Attachments */
564
+ .pp-message-attachments {
565
+ display: flex;
566
+ flex-direction: column;
567
+ gap: 8px;
568
+ margin-top: 4px;
569
+ }
570
+
571
+ .pp-attachment {
572
+ display: block;
573
+ text-decoration: none;
574
+ color: inherit;
575
+ border-radius: 8px;
576
+ overflow: hidden;
577
+ }
578
+
579
+ .pp-attachment-image img {
580
+ max-width: 200px;
581
+ max-height: 200px;
582
+ border-radius: 8px;
583
+ display: block;
584
+ }
585
+
586
+ .pp-attachment-audio {
587
+ display: flex;
588
+ flex-direction: column;
589
+ gap: 4px;
590
+ }
591
+
592
+ .pp-attachment-audio audio {
593
+ width: 200px;
594
+ height: 36px;
595
+ }
596
+
597
+ .pp-attachment-audio .pp-attachment-name {
598
+ font-size: 11px;
599
+ opacity: 0.7;
600
+ white-space: nowrap;
601
+ overflow: hidden;
602
+ text-overflow: ellipsis;
603
+ max-width: 200px;
604
+ }
605
+
606
+ .pp-attachment-video video {
607
+ max-width: 200px;
608
+ max-height: 200px;
609
+ border-radius: 8px;
610
+ display: block;
611
+ }
612
+
613
+ .pp-attachment-file {
614
+ display: flex;
615
+ align-items: center;
616
+ gap: 8px;
617
+ padding: 8px 12px;
618
+ background: ${isDark ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.05)"};
619
+ border-radius: 8px;
620
+ transition: background 0.2s;
621
+ }
622
+
623
+ .pp-attachment-file:hover {
624
+ background: ${isDark ? "rgba(255,255,255,0.15)" : "rgba(0,0,0,0.08)"};
625
+ }
626
+
627
+ .pp-attachment-file svg {
628
+ width: 24px;
629
+ height: 24px;
630
+ flex-shrink: 0;
631
+ }
632
+
633
+ .pp-attachment-info {
634
+ display: flex;
635
+ flex-direction: column;
636
+ min-width: 0;
637
+ }
638
+
639
+ .pp-attachment-name {
640
+ font-size: 13px;
641
+ font-weight: 500;
642
+ white-space: nowrap;
643
+ overflow: hidden;
644
+ text-overflow: ellipsis;
645
+ }
646
+
647
+ .pp-attachment-size {
648
+ font-size: 11px;
649
+ opacity: 0.7;
650
+ }
651
+
652
+ /* Drag & Drop */
653
+ .pp-dragging {
654
+ position: relative;
655
+ }
656
+
657
+ .pp-drop-overlay {
658
+ position: absolute;
659
+ inset: 0;
660
+ background: ${isDark ? "rgba(0,0,0,0.9)" : "rgba(255,255,255,0.95)"};
661
+ display: flex;
662
+ flex-direction: column;
663
+ align-items: center;
664
+ justify-content: center;
665
+ gap: 12px;
666
+ z-index: 100;
667
+ border: 3px dashed ${primaryColor};
668
+ border-radius: 16px;
669
+ margin: 4px;
670
+ pointer-events: none;
671
+ }
672
+
673
+ .pp-drop-icon svg {
674
+ width: 48px;
675
+ height: 48px;
676
+ color: ${primaryColor};
677
+ }
678
+
679
+ .pp-drop-text {
680
+ font-size: 16px;
681
+ font-weight: 500;
682
+ color: ${colors.text};
683
+ }
684
+
685
+ /* Message Context Menu */
686
+ .pp-message-menu {
687
+ position: fixed;
688
+ background: ${colors.bg};
689
+ border: 1px solid ${colors.border};
690
+ border-radius: 8px;
691
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
692
+ padding: 4px;
693
+ z-index: 200;
694
+ min-width: 120px;
695
+ }
696
+
697
+ .pp-message-menu button {
698
+ display: flex;
699
+ align-items: center;
700
+ gap: 8px;
701
+ width: 100%;
702
+ padding: 8px 12px;
703
+ border: none;
704
+ background: transparent;
705
+ color: ${colors.text};
706
+ font-size: 13px;
707
+ cursor: pointer;
708
+ border-radius: 4px;
709
+ text-align: left;
710
+ }
711
+
712
+ .pp-message-menu button:hover {
713
+ background: ${isDark ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.05)"};
714
+ }
715
+
716
+ .pp-message-menu button svg {
717
+ width: 16px;
718
+ height: 16px;
719
+ }
720
+
721
+ .pp-menu-delete {
722
+ color: #ef4444 !important;
723
+ }
724
+
725
+ /* Edit Modal */
726
+ .pp-edit-modal {
727
+ position: absolute;
728
+ bottom: 80px;
729
+ left: 12px;
730
+ right: 12px;
731
+ background: ${colors.bg};
732
+ border: 1px solid ${colors.border};
733
+ border-radius: 12px;
734
+ box-shadow: 0 4px 20px rgba(0,0,0,0.15);
735
+ z-index: 150;
736
+ overflow: hidden;
737
+ }
738
+
739
+ .pp-edit-header {
740
+ display: flex;
741
+ justify-content: space-between;
742
+ align-items: center;
743
+ padding: 12px 16px;
744
+ border-bottom: 1px solid ${colors.border};
745
+ font-weight: 500;
746
+ }
747
+
748
+ .pp-edit-header button {
749
+ background: transparent;
750
+ border: none;
751
+ color: ${colors.textSecondary};
752
+ cursor: pointer;
753
+ padding: 4px;
754
+ }
755
+
756
+ .pp-edit-header button svg {
757
+ width: 18px;
758
+ height: 18px;
759
+ }
760
+
761
+ .pp-edit-input {
762
+ width: 100%;
763
+ padding: 12px 16px;
764
+ border: none;
765
+ background: transparent;
766
+ color: ${colors.text};
767
+ font-size: 14px;
768
+ resize: none;
769
+ min-height: 80px;
770
+ outline: none;
771
+ }
772
+
773
+ .pp-edit-actions {
774
+ display: flex;
775
+ justify-content: flex-end;
776
+ gap: 8px;
777
+ padding: 12px 16px;
778
+ border-top: 1px solid ${colors.border};
779
+ }
780
+
781
+ .pp-edit-cancel {
782
+ padding: 8px 16px;
783
+ border: 1px solid ${colors.border};
784
+ border-radius: 6px;
785
+ background: transparent;
786
+ color: ${colors.text};
787
+ font-size: 13px;
788
+ cursor: pointer;
789
+ }
790
+
791
+ .pp-edit-save {
792
+ padding: 8px 16px;
793
+ border: none;
794
+ border-radius: 6px;
795
+ background: ${primaryColor};
796
+ color: white;
797
+ font-size: 13px;
798
+ cursor: pointer;
799
+ }
800
+
801
+ .pp-edit-save:disabled {
802
+ opacity: 0.5;
803
+ cursor: not-allowed;
804
+ }
805
+
806
+ /* Reply Preview */
807
+ .pp-reply-preview {
808
+ display: flex;
809
+ align-items: center;
810
+ gap: 8px;
811
+ padding: 8px 12px;
812
+ background: ${isDark ? "rgba(255,255,255,0.05)" : "rgba(0,0,0,0.03)"};
813
+ border-top: 1px solid ${colors.border};
814
+ border-left: 3px solid ${primaryColor};
815
+ }
816
+
817
+ .pp-reply-preview-content {
818
+ flex: 1;
819
+ min-width: 0;
820
+ }
821
+
822
+ .pp-reply-label {
823
+ display: block;
824
+ font-size: 11px;
825
+ color: ${primaryColor};
826
+ font-weight: 500;
827
+ margin-bottom: 2px;
828
+ }
829
+
830
+ .pp-reply-text {
831
+ display: block;
832
+ font-size: 12px;
833
+ color: ${colors.textSecondary};
834
+ white-space: nowrap;
835
+ overflow: hidden;
836
+ text-overflow: ellipsis;
837
+ }
838
+
839
+ .pp-reply-cancel {
840
+ background: transparent;
841
+ border: none;
842
+ color: ${colors.textSecondary};
843
+ cursor: pointer;
844
+ padding: 4px;
845
+ flex-shrink: 0;
846
+ }
847
+
848
+ .pp-reply-cancel svg {
849
+ width: 16px;
850
+ height: 16px;
851
+ }
852
+
853
+ /* Reply Quote in Message */
854
+ .pp-reply-quote {
855
+ background: ${isDark ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.05)"};
856
+ border-left: 2px solid ${primaryColor};
857
+ padding: 4px 8px;
858
+ margin-bottom: 6px;
859
+ border-radius: 0 4px 4px 0;
860
+ font-size: 12px;
861
+ }
862
+
863
+ .pp-reply-sender {
864
+ display: block;
865
+ font-weight: 500;
866
+ color: ${primaryColor};
867
+ margin-bottom: 2px;
868
+ }
869
+
870
+ .pp-reply-content {
871
+ display: block;
872
+ color: ${colors.textSecondary};
873
+ white-space: nowrap;
874
+ overflow: hidden;
875
+ text-overflow: ellipsis;
876
+ }
877
+
878
+ /* Deleted Message */
879
+ .pp-message-deleted {
880
+ opacity: 0.6;
881
+ }
882
+
883
+ .pp-deleted-content {
884
+ font-style: italic;
885
+ color: ${colors.textSecondary};
886
+ display: flex;
887
+ align-items: center;
888
+ gap: 4px;
889
+ }
890
+
891
+ .pp-deleted-icon {
892
+ font-size: 12px;
893
+ }
894
+
895
+ /* Edited Badge */
896
+ .pp-edited-badge {
897
+ font-size: 10px;
898
+ color: ${colors.textSecondary};
899
+ margin-left: 4px;
900
+ font-style: italic;
901
+ }
414
902
  `;
415
903
  }
416
904
 
@@ -424,9 +912,18 @@ function ChatWidget({ client: client2, config: initialConfig }) {
424
912
  const [operatorOnline, setOperatorOnline] = (0, import_hooks.useState)(false);
425
913
  const [isConnected, setIsConnected] = (0, import_hooks.useState)(false);
426
914
  const [unreadCount, setUnreadCount] = (0, import_hooks.useState)(0);
915
+ const [pendingAttachments, setPendingAttachments] = (0, import_hooks.useState)([]);
916
+ const [isUploading, setIsUploading] = (0, import_hooks.useState)(false);
917
+ const [replyingTo, setReplyingTo] = (0, import_hooks.useState)(null);
918
+ const [editingMessage, setEditingMessage] = (0, import_hooks.useState)(null);
919
+ const [editContent, setEditContent] = (0, import_hooks.useState)("");
920
+ const [messageMenu, setMessageMenu] = (0, import_hooks.useState)(null);
921
+ const [isDragging, setIsDragging] = (0, import_hooks.useState)(false);
427
922
  const [config, setConfig] = (0, import_hooks.useState)(initialConfig);
428
923
  const messagesEndRef = (0, import_hooks.useRef)(null);
429
924
  const inputRef = (0, import_hooks.useRef)(null);
925
+ const fileInputRef = (0, import_hooks.useRef)(null);
926
+ const messagesContainerRef = (0, import_hooks.useRef)(null);
430
927
  (0, import_hooks.useEffect)(() => {
431
928
  const unsubOpen = client2.on("openChange", setIsOpen);
432
929
  const unsubMessage = client2.on("message", () => {
@@ -523,11 +1020,17 @@ function ChatWidget({ client: client2, config: initialConfig }) {
523
1020
  if (!shouldShow) return null;
524
1021
  const handleSubmit = async (e) => {
525
1022
  e.preventDefault();
526
- if (!inputValue.trim()) return;
1023
+ const hasContent = inputValue.trim().length > 0;
1024
+ const readyAttachments = pendingAttachments.filter((a) => a.status === "ready" && a.attachment);
1025
+ if (!hasContent && readyAttachments.length === 0) return;
527
1026
  const content = inputValue;
1027
+ const attachmentIds = readyAttachments.map((a) => a.attachment.id);
1028
+ const replyToId = replyingTo?.id;
528
1029
  setInputValue("");
1030
+ setPendingAttachments([]);
1031
+ setReplyingTo(null);
529
1032
  try {
530
- await client2.sendMessage(content);
1033
+ await client2.sendMessage(content, attachmentIds, replyToId);
531
1034
  } catch (err) {
532
1035
  console.error("[PocketPing] Failed to send message:", err);
533
1036
  }
@@ -537,6 +1040,190 @@ function ChatWidget({ client: client2, config: initialConfig }) {
537
1040
  setInputValue(target.value);
538
1041
  client2.sendTyping(true);
539
1042
  };
1043
+ const handleFileSelect = async (e) => {
1044
+ const target = e.target;
1045
+ const files = target.files;
1046
+ if (!files || files.length === 0) return;
1047
+ const newPending = [];
1048
+ for (let i = 0; i < files.length; i++) {
1049
+ const file = files[i];
1050
+ const id = `pending-${Date.now()}-${i}`;
1051
+ let preview;
1052
+ if (file.type.startsWith("image/")) {
1053
+ preview = URL.createObjectURL(file);
1054
+ }
1055
+ newPending.push({
1056
+ id,
1057
+ file,
1058
+ preview,
1059
+ progress: 0,
1060
+ status: "pending"
1061
+ });
1062
+ }
1063
+ setPendingAttachments((prev) => [...prev, ...newPending]);
1064
+ target.value = "";
1065
+ setIsUploading(true);
1066
+ for (const pending of newPending) {
1067
+ try {
1068
+ setPendingAttachments(
1069
+ (prev) => prev.map((a) => a.id === pending.id ? { ...a, status: "uploading" } : a)
1070
+ );
1071
+ const attachment = await client2.uploadFile(pending.file, (progress) => {
1072
+ setPendingAttachments(
1073
+ (prev) => prev.map((a) => a.id === pending.id ? { ...a, progress } : a)
1074
+ );
1075
+ });
1076
+ setPendingAttachments(
1077
+ (prev) => prev.map(
1078
+ (a) => a.id === pending.id ? { ...a, status: "ready", progress: 100, attachment } : a
1079
+ )
1080
+ );
1081
+ } catch (err) {
1082
+ console.error("[PocketPing] Failed to upload file:", err);
1083
+ setPendingAttachments(
1084
+ (prev) => prev.map(
1085
+ (a) => a.id === pending.id ? { ...a, status: "error", error: "Upload failed" } : a
1086
+ )
1087
+ );
1088
+ }
1089
+ }
1090
+ setIsUploading(false);
1091
+ };
1092
+ const handleRemoveAttachment = (id) => {
1093
+ setPendingAttachments((prev) => {
1094
+ const removed = prev.find((a) => a.id === id);
1095
+ if (removed?.preview) {
1096
+ URL.revokeObjectURL(removed.preview);
1097
+ }
1098
+ return prev.filter((a) => a.id !== id);
1099
+ });
1100
+ };
1101
+ const handleReply = (message) => {
1102
+ setReplyingTo(message);
1103
+ setMessageMenu(null);
1104
+ inputRef.current?.focus();
1105
+ };
1106
+ const handleCancelReply = () => {
1107
+ setReplyingTo(null);
1108
+ };
1109
+ const handleStartEdit = (message) => {
1110
+ if (message.sender !== "visitor") return;
1111
+ setEditingMessage(message);
1112
+ setEditContent(message.content);
1113
+ setMessageMenu(null);
1114
+ };
1115
+ const handleCancelEdit = () => {
1116
+ setEditingMessage(null);
1117
+ setEditContent("");
1118
+ };
1119
+ const handleSaveEdit = async () => {
1120
+ if (!editingMessage || !editContent.trim()) return;
1121
+ try {
1122
+ await client2.editMessage(editingMessage.id, editContent.trim());
1123
+ setEditingMessage(null);
1124
+ setEditContent("");
1125
+ } catch (err) {
1126
+ console.error("[PocketPing] Failed to edit message:", err);
1127
+ }
1128
+ };
1129
+ const handleDelete = async (message) => {
1130
+ if (message.sender !== "visitor") return;
1131
+ setMessageMenu(null);
1132
+ if (confirm("Delete this message?")) {
1133
+ try {
1134
+ await client2.deleteMessage(message.id);
1135
+ } catch (err) {
1136
+ console.error("[PocketPing] Failed to delete message:", err);
1137
+ }
1138
+ }
1139
+ };
1140
+ const handleMessageContextMenu = (e, message) => {
1141
+ e.preventDefault();
1142
+ const mouseEvent = e;
1143
+ setMessageMenu({
1144
+ message,
1145
+ x: mouseEvent.clientX,
1146
+ y: mouseEvent.clientY
1147
+ });
1148
+ };
1149
+ (0, import_hooks.useEffect)(() => {
1150
+ if (!messageMenu) return;
1151
+ const handleClickOutside = () => setMessageMenu(null);
1152
+ document.addEventListener("click", handleClickOutside);
1153
+ return () => document.removeEventListener("click", handleClickOutside);
1154
+ }, [messageMenu]);
1155
+ const dragCounterRef = (0, import_hooks.useRef)(0);
1156
+ const handleDragEnter = (e) => {
1157
+ e.preventDefault();
1158
+ e.stopPropagation();
1159
+ dragCounterRef.current++;
1160
+ if (dragCounterRef.current === 1) {
1161
+ setIsDragging(true);
1162
+ }
1163
+ };
1164
+ const handleDragOver = (e) => {
1165
+ e.preventDefault();
1166
+ e.stopPropagation();
1167
+ };
1168
+ const handleDragLeave = (e) => {
1169
+ e.preventDefault();
1170
+ e.stopPropagation();
1171
+ dragCounterRef.current--;
1172
+ if (dragCounterRef.current === 0) {
1173
+ setIsDragging(false);
1174
+ }
1175
+ };
1176
+ const handleDrop = async (e) => {
1177
+ e.preventDefault();
1178
+ e.stopPropagation();
1179
+ dragCounterRef.current = 0;
1180
+ setIsDragging(false);
1181
+ const files = e.dataTransfer?.files;
1182
+ if (!files || files.length === 0) return;
1183
+ const newPending = [];
1184
+ for (let i = 0; i < files.length; i++) {
1185
+ const file = files[i];
1186
+ const id = `pending-${Date.now()}-${i}`;
1187
+ let preview;
1188
+ if (file.type.startsWith("image/")) {
1189
+ preview = URL.createObjectURL(file);
1190
+ }
1191
+ newPending.push({
1192
+ id,
1193
+ file,
1194
+ preview,
1195
+ progress: 0,
1196
+ status: "pending"
1197
+ });
1198
+ }
1199
+ setPendingAttachments((prev) => [...prev, ...newPending]);
1200
+ setIsUploading(true);
1201
+ for (const pending of newPending) {
1202
+ try {
1203
+ setPendingAttachments(
1204
+ (prev) => prev.map((a) => a.id === pending.id ? { ...a, status: "uploading" } : a)
1205
+ );
1206
+ const attachment = await client2.uploadFile(pending.file, (progress) => {
1207
+ setPendingAttachments(
1208
+ (prev) => prev.map((a) => a.id === pending.id ? { ...a, progress } : a)
1209
+ );
1210
+ });
1211
+ setPendingAttachments(
1212
+ (prev) => prev.map(
1213
+ (a) => a.id === pending.id ? { ...a, status: "ready", progress: 100, attachment } : a
1214
+ )
1215
+ );
1216
+ } catch (err) {
1217
+ console.error("[PocketPing] Failed to upload dropped file:", err);
1218
+ setPendingAttachments(
1219
+ (prev) => prev.map(
1220
+ (a) => a.id === pending.id ? { ...a, status: "error", error: "Upload failed" } : a
1221
+ )
1222
+ );
1223
+ }
1224
+ }
1225
+ setIsUploading(false);
1226
+ };
540
1227
  const position = config.position ?? "bottom-right";
541
1228
  const theme = getTheme(config.theme ?? "auto");
542
1229
  const primaryColor = config.primaryColor ?? "#6366f1";
@@ -555,84 +1242,211 @@ function ChatWidget({ client: client2, config: initialConfig }) {
555
1242
  ]
556
1243
  }
557
1244
  ),
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 }) })
1245
+ isOpen && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1246
+ "div",
1247
+ {
1248
+ class: `pp-window pp-${position} pp-theme-${theme} ${isDragging ? "pp-dragging" : ""}`,
1249
+ onDragEnter: handleDragEnter,
1250
+ onDragOver: handleDragOver,
1251
+ onDragLeave: handleDragLeave,
1252
+ onDrop: handleDrop,
1253
+ children: [
1254
+ isDragging && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { class: "pp-drop-overlay", children: [
1255
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { class: "pp-drop-icon", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AttachIcon, {}) }),
1256
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { class: "pp-drop-text", children: "Drop files to upload" })
1257
+ ] }),
1258
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { class: "pp-header", children: [
1259
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { class: "pp-header-info", children: [
1260
+ config.operatorAvatar && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { src: config.operatorAvatar, alt: "", class: "pp-avatar" }),
1261
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
1262
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { class: "pp-header-title", children: config.operatorName ?? "Support" }),
1263
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { class: "pp-header-status", children: operatorOnline ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1264
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { class: "pp-status-dot pp-online" }),
1265
+ " Online"
1266
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1267
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { class: "pp-status-dot" }),
1268
+ " Away"
1269
+ ] }) })
595
1270
  ] })
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
- ] })
1271
+ ] }),
1272
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1273
+ "button",
1274
+ {
1275
+ class: "pp-close-btn",
1276
+ onClick: () => client2.setOpen(false),
1277
+ "aria-label": "Close chat",
1278
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CloseIcon, {})
1279
+ }
1280
+ )
1281
+ ] }),
1282
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { class: "pp-messages", ref: messagesContainerRef, children: [
1283
+ config.welcomeMessage && messages.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { class: "pp-welcome", children: config.welcomeMessage }),
1284
+ messages.map((msg) => {
1285
+ const isDeleted = !!msg.deletedAt;
1286
+ const isEdited = !!msg.editedAt;
1287
+ const replyToMsg = msg.replyTo ? messages.find((m) => m.id === msg.replyTo) : null;
1288
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1289
+ "div",
1290
+ {
1291
+ class: `pp-message pp-message-${msg.sender} ${isDeleted ? "pp-message-deleted" : ""}`,
1292
+ onContextMenu: (e) => handleMessageContextMenu(e, msg),
1293
+ children: [
1294
+ replyToMsg && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { class: "pp-reply-quote", children: [
1295
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { class: "pp-reply-sender", children: replyToMsg.sender === "visitor" ? "You" : "Support" }),
1296
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { class: "pp-reply-content", children: [
1297
+ replyToMsg.deletedAt ? "Message deleted" : replyToMsg.content.slice(0, 50),
1298
+ replyToMsg.content.length > 50 ? "..." : ""
1299
+ ] })
1300
+ ] }),
1301
+ isDeleted ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { class: "pp-message-content pp-deleted-content", children: [
1302
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { class: "pp-deleted-icon", children: "\u{1F5D1}\uFE0F" }),
1303
+ " Message deleted"
1304
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1305
+ msg.content && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { class: "pp-message-content", children: msg.content }),
1306
+ 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)) })
1307
+ ] }),
1308
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { class: "pp-message-time", children: [
1309
+ formatTime(msg.timestamp),
1310
+ isEdited && !isDeleted && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { class: "pp-edited-badge", children: "edited" }),
1311
+ msg.sender === "ai" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { class: "pp-ai-badge", children: "AI" }),
1312
+ 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 }) })
1313
+ ] })
1314
+ ]
1315
+ },
1316
+ msg.id
1317
+ );
1318
+ }),
1319
+ isTyping && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { class: "pp-message pp-message-operator pp-typing", children: [
1320
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {}),
1321
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {}),
1322
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {})
1323
+ ] }),
1324
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { ref: messagesEndRef })
1325
+ ] }),
1326
+ messageMenu && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1327
+ "div",
1328
+ {
1329
+ class: "pp-message-menu",
1330
+ style: { top: `${messageMenu.y}px`, left: `${messageMenu.x}px` },
1331
+ children: [
1332
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", { onClick: () => handleReply(messageMenu.message), children: [
1333
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ReplyIcon, {}),
1334
+ " Reply"
1335
+ ] }),
1336
+ messageMenu.message.sender === "visitor" && !messageMenu.message.deletedAt && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1337
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", { onClick: () => handleStartEdit(messageMenu.message), children: [
1338
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(EditIcon, {}),
1339
+ " Edit"
1340
+ ] }),
1341
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", { class: "pp-menu-delete", onClick: () => handleDelete(messageMenu.message), children: [
1342
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DeleteIcon, {}),
1343
+ " Delete"
1344
+ ] })
1345
+ ] })
1346
+ ]
1347
+ }
1348
+ ),
1349
+ editingMessage && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { class: "pp-edit-modal", children: [
1350
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { class: "pp-edit-header", children: [
1351
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "Edit message" }),
1352
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { onClick: handleCancelEdit, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CloseIcon, {}) })
1353
+ ] }),
1354
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1355
+ "textarea",
1356
+ {
1357
+ class: "pp-edit-input",
1358
+ value: editContent,
1359
+ onInput: (e) => setEditContent(e.target.value),
1360
+ autoFocus: true
1361
+ }
1362
+ ),
1363
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { class: "pp-edit-actions", children: [
1364
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { class: "pp-edit-cancel", onClick: handleCancelEdit, children: "Cancel" }),
1365
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { class: "pp-edit-save", onClick: handleSaveEdit, disabled: !editContent.trim(), children: "Save" })
1366
+ ] })
1367
+ ] }),
1368
+ replyingTo && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { class: "pp-reply-preview", children: [
1369
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { class: "pp-reply-preview-content", children: [
1370
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { class: "pp-reply-label", children: "Replying to" }),
1371
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { class: "pp-reply-text", children: [
1372
+ replyingTo.content.slice(0, 50),
1373
+ replyingTo.content.length > 50 ? "..." : ""
1374
+ ] })
1375
+ ] }),
1376
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { class: "pp-reply-cancel", onClick: handleCancelReply, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CloseIcon, {}) })
1377
+ ] }),
1378
+ 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: [
1379
+ 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 }) }),
1380
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1381
+ "button",
1382
+ {
1383
+ class: "pp-remove-attachment",
1384
+ onClick: () => handleRemoveAttachment(pending.id),
1385
+ "aria-label": "Remove attachment",
1386
+ type: "button",
1387
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CloseIcon, {})
1388
+ }
1389
+ ),
1390
+ pending.status === "uploading" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { class: "pp-upload-progress", style: { width: `${pending.progress}%` } }),
1391
+ pending.status === "error" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { class: "pp-upload-error", title: pending.error, children: "!" })
1392
+ ] }, pending.id)) }),
1393
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { class: "pp-input-form", onSubmit: handleSubmit, children: [
1394
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1395
+ "input",
1396
+ {
1397
+ ref: (el) => {
1398
+ fileInputRef.current = el;
1399
+ if (el) {
1400
+ el.onchange = handleFileSelect;
1401
+ }
1402
+ },
1403
+ type: "file",
1404
+ class: "pp-file-input",
1405
+ accept: "image/*,audio/*,video/*,.pdf,.doc,.docx,.xls,.xlsx,.txt",
1406
+ multiple: true
1407
+ }
1408
+ ),
1409
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1410
+ "button",
1411
+ {
1412
+ type: "button",
1413
+ class: "pp-attach-btn",
1414
+ onClick: () => fileInputRef.current?.click(),
1415
+ disabled: !isConnected || isUploading,
1416
+ "aria-label": "Attach file",
1417
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AttachIcon, {})
1418
+ }
1419
+ ),
1420
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1421
+ "input",
1422
+ {
1423
+ ref: inputRef,
1424
+ type: "text",
1425
+ class: "pp-input",
1426
+ placeholder: config.placeholder ?? "Type a message...",
1427
+ value: inputValue,
1428
+ onInput: handleInputChange,
1429
+ disabled: !isConnected
1430
+ }
1431
+ ),
1432
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1433
+ "button",
1434
+ {
1435
+ type: "submit",
1436
+ class: "pp-send-btn",
1437
+ disabled: !inputValue.trim() && pendingAttachments.filter((a) => a.status === "ready").length === 0 || !isConnected || isUploading,
1438
+ "aria-label": "Send message",
1439
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SendIcon, {})
1440
+ }
1441
+ )
1442
+ ] }),
1443
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { class: "pp-footer", children: [
1444
+ "Powered by ",
1445
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { href: "https://pocketping.io", target: "_blank", rel: "noopener", children: "PocketPing" })
1446
+ ] })
1447
+ ]
1448
+ }
1449
+ )
636
1450
  ] });
637
1451
  }
638
1452
  function checkPageVisibility(config) {
@@ -688,6 +1502,86 @@ function StatusIcon({ status }) {
688
1502
  }
689
1503
  return null;
690
1504
  }
1505
+ function AttachIcon() {
1506
+ 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" }) });
1507
+ }
1508
+ function ReplyIcon() {
1509
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: [
1510
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("polyline", { points: "9 17 4 12 9 7" }),
1511
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M20 18v-2a4 4 0 0 0-4-4H4" })
1512
+ ] });
1513
+ }
1514
+ function EditIcon() {
1515
+ 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: "M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z" }) });
1516
+ }
1517
+ function DeleteIcon() {
1518
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: [
1519
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("polyline", { points: "3 6 5 6 21 6" }),
1520
+ /* @__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" })
1521
+ ] });
1522
+ }
1523
+ function FileIcon({ mimeType }) {
1524
+ if (mimeType === "application/pdf") {
1525
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: [
1526
+ /* @__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" }),
1527
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("polyline", { points: "14 2 14 8 20 8" }),
1528
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M9 15h6" }),
1529
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M9 11h6" })
1530
+ ] });
1531
+ }
1532
+ if (mimeType.startsWith("audio/")) {
1533
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: [
1534
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M9 18V5l12-2v13" }),
1535
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: "6", cy: "18", r: "3" }),
1536
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: "18", cy: "16", r: "3" })
1537
+ ] });
1538
+ }
1539
+ if (mimeType.startsWith("video/")) {
1540
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: [
1541
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", { x: "2", y: "2", width: "20", height: "20", rx: "2.18", ry: "2.18" }),
1542
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "7", y1: "2", x2: "7", y2: "22" }),
1543
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "17", y1: "2", x2: "17", y2: "22" }),
1544
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "2", y1: "12", x2: "22", y2: "12" }),
1545
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "2", y1: "7", x2: "7", y2: "7" }),
1546
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "2", y1: "17", x2: "7", y2: "17" }),
1547
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "17", y1: "17", x2: "22", y2: "17" }),
1548
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "17", y1: "7", x2: "22", y2: "7" })
1549
+ ] });
1550
+ }
1551
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: [
1552
+ /* @__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" }),
1553
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("polyline", { points: "14 2 14 8 20 8" })
1554
+ ] });
1555
+ }
1556
+ function AttachmentDisplay({ attachment }) {
1557
+ const isImage = attachment.mimeType.startsWith("image/");
1558
+ const isAudio = attachment.mimeType.startsWith("audio/");
1559
+ const isVideo = attachment.mimeType.startsWith("video/");
1560
+ const formatSize = (bytes) => {
1561
+ if (bytes < 1024) return `${bytes} B`;
1562
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
1563
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1564
+ };
1565
+ if (isImage) {
1566
+ 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 }) });
1567
+ }
1568
+ if (isAudio) {
1569
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { class: "pp-attachment pp-attachment-audio", children: [
1570
+ /* @__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 }) }),
1571
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { class: "pp-attachment-name", children: attachment.filename })
1572
+ ] });
1573
+ }
1574
+ if (isVideo) {
1575
+ 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 }) }) });
1576
+ }
1577
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("a", { href: attachment.url, target: "_blank", rel: "noopener", class: "pp-attachment pp-attachment-file", children: [
1578
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FileIcon, { mimeType: attachment.mimeType }),
1579
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { class: "pp-attachment-info", children: [
1580
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { class: "pp-attachment-name", children: attachment.filename }),
1581
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { class: "pp-attachment-size", children: formatSize(attachment.size) })
1582
+ ] })
1583
+ ] });
1584
+ }
691
1585
 
692
1586
  // src/version.ts
693
1587
  var VERSION = "0.3.6";
@@ -803,7 +1697,7 @@ var PocketPingClient = class {
803
1697
  this.cleanupTrackedElements();
804
1698
  this.disableInspectorMode();
805
1699
  }
806
- async sendMessage(content) {
1700
+ async sendMessage(content, attachmentIds, replyTo) {
807
1701
  if (!this.session) {
808
1702
  throw new Error("Not connected");
809
1703
  }
@@ -814,7 +1708,8 @@ var PocketPingClient = class {
814
1708
  content,
815
1709
  sender: "visitor",
816
1710
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
817
- status: "sending"
1711
+ status: "sending",
1712
+ replyTo
818
1713
  };
819
1714
  this.session.messages.push(tempMessage);
820
1715
  this.emit("message", tempMessage);
@@ -824,7 +1719,9 @@ var PocketPingClient = class {
824
1719
  body: JSON.stringify({
825
1720
  sessionId: this.session.sessionId,
826
1721
  content,
827
- sender: "visitor"
1722
+ sender: "visitor",
1723
+ attachmentIds: attachmentIds || [],
1724
+ replyTo
828
1725
  })
829
1726
  });
830
1727
  const messageIndex = this.session.messages.findIndex((m) => m.id === tempId);
@@ -832,6 +1729,9 @@ var PocketPingClient = class {
832
1729
  this.session.messages[messageIndex].id = response.messageId;
833
1730
  this.session.messages[messageIndex].timestamp = response.timestamp;
834
1731
  this.session.messages[messageIndex].status = "sent";
1732
+ if (response.attachments && response.attachments.length > 0) {
1733
+ this.session.messages[messageIndex].attachments = response.attachments;
1734
+ }
835
1735
  this.emit("message", this.session.messages[messageIndex]);
836
1736
  }
837
1737
  const message = this.session.messages[messageIndex] || {
@@ -840,7 +1740,8 @@ var PocketPingClient = class {
840
1740
  content,
841
1741
  sender: "visitor",
842
1742
  timestamp: response.timestamp,
843
- status: "sent"
1743
+ status: "sent",
1744
+ attachments: response.attachments
844
1745
  };
845
1746
  this.config.onMessage?.(message);
846
1747
  return message;
@@ -853,6 +1754,110 @@ var PocketPingClient = class {
853
1754
  throw error;
854
1755
  }
855
1756
  }
1757
+ /**
1758
+ * Upload a file attachment
1759
+ * Returns the attachment data after successful upload
1760
+ * @param file - File object to upload
1761
+ * @param onProgress - Optional callback for upload progress (0-100)
1762
+ * @example
1763
+ * const attachment = await PocketPing.uploadFile(file, (progress) => {
1764
+ * console.log(`Upload ${progress}% complete`)
1765
+ * })
1766
+ * await PocketPing.sendMessage('Check this file', [attachment.id])
1767
+ */
1768
+ async uploadFile(file, onProgress) {
1769
+ if (!this.session) {
1770
+ throw new Error("Not connected");
1771
+ }
1772
+ this.emit("uploadStart", { filename: file.name, size: file.size });
1773
+ try {
1774
+ const initResponse = await this.fetch("/upload", {
1775
+ method: "POST",
1776
+ body: JSON.stringify({
1777
+ sessionId: this.session.sessionId,
1778
+ filename: file.name,
1779
+ mimeType: file.type || "application/octet-stream",
1780
+ size: file.size
1781
+ })
1782
+ });
1783
+ onProgress?.(10);
1784
+ this.emit("uploadProgress", { filename: file.name, progress: 10 });
1785
+ await this.uploadToPresignedUrl(initResponse.uploadUrl, file, (progress) => {
1786
+ const mappedProgress = 10 + progress * 0.8;
1787
+ onProgress?.(mappedProgress);
1788
+ this.emit("uploadProgress", { filename: file.name, progress: mappedProgress });
1789
+ });
1790
+ const completeResponse = await this.fetch("/upload/complete", {
1791
+ method: "POST",
1792
+ body: JSON.stringify({
1793
+ sessionId: this.session.sessionId,
1794
+ attachmentId: initResponse.attachmentId
1795
+ })
1796
+ });
1797
+ onProgress?.(100);
1798
+ this.emit("uploadComplete", completeResponse);
1799
+ return {
1800
+ id: completeResponse.id,
1801
+ filename: completeResponse.filename,
1802
+ mimeType: completeResponse.mimeType,
1803
+ size: completeResponse.size,
1804
+ url: completeResponse.url,
1805
+ thumbnailUrl: completeResponse.thumbnailUrl,
1806
+ status: completeResponse.status
1807
+ };
1808
+ } catch (error) {
1809
+ this.emit("uploadError", { filename: file.name, error });
1810
+ throw error;
1811
+ }
1812
+ }
1813
+ /**
1814
+ * Upload multiple files at once
1815
+ * @param files - Array of File objects to upload
1816
+ * @param onProgress - Optional callback for overall progress (0-100)
1817
+ * @returns Array of uploaded attachments
1818
+ */
1819
+ async uploadFiles(files, onProgress) {
1820
+ const attachments = [];
1821
+ const totalFiles = files.length;
1822
+ for (let i = 0; i < totalFiles; i++) {
1823
+ const file = files[i];
1824
+ const baseProgress = i / totalFiles * 100;
1825
+ const fileProgress = 100 / totalFiles;
1826
+ const attachment = await this.uploadFile(file, (progress) => {
1827
+ const totalProgress = baseProgress + progress / 100 * fileProgress;
1828
+ onProgress?.(totalProgress);
1829
+ });
1830
+ attachments.push(attachment);
1831
+ }
1832
+ return attachments;
1833
+ }
1834
+ /**
1835
+ * Upload file to presigned URL with progress tracking
1836
+ */
1837
+ uploadToPresignedUrl(url, file, onProgress) {
1838
+ return new Promise((resolve, reject) => {
1839
+ const xhr = new XMLHttpRequest();
1840
+ xhr.upload.addEventListener("progress", (event) => {
1841
+ if (event.lengthComputable) {
1842
+ const progress = event.loaded / event.total * 100;
1843
+ onProgress?.(progress);
1844
+ }
1845
+ });
1846
+ xhr.addEventListener("load", () => {
1847
+ if (xhr.status >= 200 && xhr.status < 300) {
1848
+ resolve();
1849
+ } else {
1850
+ reject(new Error(`Upload failed with status ${xhr.status}`));
1851
+ }
1852
+ });
1853
+ xhr.addEventListener("error", () => {
1854
+ reject(new Error("Upload failed"));
1855
+ });
1856
+ xhr.open("PUT", url);
1857
+ xhr.setRequestHeader("Content-Type", file.type || "application/octet-stream");
1858
+ xhr.send(file);
1859
+ });
1860
+ }
856
1861
  async fetchMessages(after) {
857
1862
  if (!this.session) {
858
1863
  throw new Error("Not connected");
@@ -906,6 +1911,54 @@ var PocketPingClient = class {
906
1911
  console.error("[PocketPing] Failed to send read status:", err);
907
1912
  }
908
1913
  }
1914
+ /**
1915
+ * Edit a message (visitor can only edit their own messages)
1916
+ * @param messageId - ID of the message to edit
1917
+ * @param content - New content for the message
1918
+ */
1919
+ async editMessage(messageId, content) {
1920
+ if (!this.session) {
1921
+ throw new Error("Not connected");
1922
+ }
1923
+ const response = await this.fetch(`/message/${messageId}`, {
1924
+ method: "PATCH",
1925
+ body: JSON.stringify({
1926
+ sessionId: this.session.sessionId,
1927
+ content
1928
+ })
1929
+ });
1930
+ const messageIndex = this.session.messages.findIndex((m) => m.id === messageId);
1931
+ if (messageIndex >= 0) {
1932
+ this.session.messages[messageIndex].content = response.message.content;
1933
+ this.session.messages[messageIndex].editedAt = response.message.editedAt;
1934
+ this.emit("messageEdited", this.session.messages[messageIndex]);
1935
+ }
1936
+ return this.session.messages[messageIndex];
1937
+ }
1938
+ /**
1939
+ * Delete a message (soft delete - visitor can only delete their own messages)
1940
+ * @param messageId - ID of the message to delete
1941
+ */
1942
+ async deleteMessage(messageId) {
1943
+ if (!this.session) {
1944
+ throw new Error("Not connected");
1945
+ }
1946
+ const response = await this.fetch(`/message/${messageId}`, {
1947
+ method: "DELETE",
1948
+ body: JSON.stringify({
1949
+ sessionId: this.session.sessionId
1950
+ })
1951
+ });
1952
+ if (response.deleted) {
1953
+ const messageIndex = this.session.messages.findIndex((m) => m.id === messageId);
1954
+ if (messageIndex >= 0) {
1955
+ this.session.messages[messageIndex].deletedAt = (/* @__PURE__ */ new Date()).toISOString();
1956
+ this.session.messages[messageIndex].content = "";
1957
+ this.emit("messageDeleted", this.session.messages[messageIndex]);
1958
+ }
1959
+ }
1960
+ return response.deleted;
1961
+ }
909
1962
  async getPresence() {
910
1963
  return this.fetch("/presence", { method: "GET" });
911
1964
  }
@@ -1809,11 +2862,23 @@ function close() {
1809
2862
  function toggle() {
1810
2863
  client?.toggleOpen();
1811
2864
  }
1812
- function sendMessage(content) {
2865
+ function sendMessage(content, attachmentIds) {
2866
+ if (!client) {
2867
+ throw new Error("[PocketPing] Not initialized");
2868
+ }
2869
+ return client.sendMessage(content, attachmentIds);
2870
+ }
2871
+ async function uploadFile(file, onProgress) {
2872
+ if (!client) {
2873
+ throw new Error("[PocketPing] Not initialized");
2874
+ }
2875
+ return client.uploadFile(file, onProgress);
2876
+ }
2877
+ async function uploadFiles(files, onProgress) {
1813
2878
  if (!client) {
1814
2879
  throw new Error("[PocketPing] Not initialized");
1815
2880
  }
1816
- return client.sendMessage(content);
2881
+ return client.uploadFiles(files, onProgress);
1817
2882
  }
1818
2883
  function trigger(eventName, data, options) {
1819
2884
  if (!client) {
@@ -1882,7 +2947,7 @@ if (typeof document !== "undefined") {
1882
2947
  }
1883
2948
  }
1884
2949
  }
1885
- var index_default = { init, destroy, open, close, toggle, sendMessage, trigger, onEvent, offEvent, on, identify, reset, getIdentity, setupTrackedElements, getTrackedElements };
2950
+ var index_default = { init, destroy, open, close, toggle, sendMessage, uploadFile, uploadFiles, trigger, onEvent, offEvent, on, identify, reset, getIdentity, setupTrackedElements, getTrackedElements };
1886
2951
  // Annotate the CommonJS export names for ESM import in node:
1887
2952
  0 && (module.exports = {
1888
2953
  close,
@@ -1899,5 +2964,7 @@ var index_default = { init, destroy, open, close, toggle, sendMessage, trigger,
1899
2964
  sendMessage,
1900
2965
  setupTrackedElements,
1901
2966
  toggle,
1902
- trigger
2967
+ trigger,
2968
+ uploadFile,
2969
+ uploadFiles
1903
2970
  });