@pocketping/widget 1.0.2 → 1.2.0

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