@pocketping/widget 1.2.0 → 1.4.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
@@ -228,6 +228,94 @@ function styles(primaryColor, theme) {
228
228
  padding: 10px 14px;
229
229
  border-radius: 16px;
230
230
  word-wrap: break-word;
231
+ position: relative;
232
+ user-select: text;
233
+ -webkit-user-select: text;
234
+ }
235
+
236
+ /* Hover actions container - positioned above message (Slack style) */
237
+ .pp-message-actions {
238
+ position: absolute;
239
+ top: -28px;
240
+ display: flex;
241
+ gap: 2px;
242
+ background: ${colors.bg};
243
+ border: 1px solid ${colors.border};
244
+ border-radius: 6px;
245
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
246
+ padding: 2px;
247
+ opacity: 0;
248
+ animation: pp-actions-fade-in 0.12s ease forwards;
249
+ z-index: 10;
250
+ /* Reset color inheritance from message */
251
+ color: ${colors.textSecondary};
252
+ }
253
+
254
+ @keyframes pp-actions-fade-in {
255
+ from { opacity: 0; transform: translateY(4px); }
256
+ to { opacity: 1; transform: translateY(0); }
257
+ }
258
+
259
+ /* Visitor messages: actions aligned right */
260
+ .pp-actions-left {
261
+ right: 0;
262
+ }
263
+
264
+ /* Operator messages: actions aligned left */
265
+ .pp-actions-right {
266
+ left: 0;
267
+ }
268
+
269
+ .pp-message-actions .pp-action-btn {
270
+ width: 24px;
271
+ height: 24px;
272
+ border: none;
273
+ background: transparent;
274
+ border-radius: 4px;
275
+ cursor: pointer;
276
+ display: flex;
277
+ align-items: center;
278
+ justify-content: center;
279
+ color: ${colors.textSecondary} !important;
280
+ transition: background 0.1s, color 0.1s;
281
+ }
282
+
283
+ .pp-message-actions .pp-action-btn:hover {
284
+ background: ${colors.bgSecondary};
285
+ color: ${colors.text} !important;
286
+ }
287
+
288
+ .pp-message-actions .pp-action-btn svg {
289
+ width: 14px;
290
+ height: 14px;
291
+ stroke: ${colors.textSecondary};
292
+ }
293
+
294
+ .pp-message-actions .pp-action-btn:hover svg {
295
+ stroke: ${colors.text};
296
+ }
297
+
298
+ .pp-message-actions .pp-action-delete:hover {
299
+ background: #fef2f2;
300
+ }
301
+
302
+ .pp-message-actions .pp-action-delete:hover svg {
303
+ stroke: #ef4444;
304
+ }
305
+
306
+ .pp-theme-dark .pp-message-actions .pp-action-delete:hover {
307
+ background: #7f1d1d;
308
+ }
309
+
310
+ .pp-theme-dark .pp-message-actions .pp-action-delete:hover svg {
311
+ stroke: #fca5a5;
312
+ }
313
+
314
+ /* Hide hover actions on mobile */
315
+ @media (hover: none) and (pointer: coarse) {
316
+ .pp-message-actions {
317
+ display: none;
318
+ }
231
319
  }
232
320
 
233
321
  .pp-message-visitor {
@@ -609,9 +697,8 @@ function styles(primaryColor, theme) {
609
697
  }
610
698
 
611
699
  /* Drag & Drop */
612
- .pp-dragging {
613
- position: relative;
614
- }
700
+ /* Note: .pp-window already has position: fixed which acts as
701
+ containing block for the absolutely positioned overlay */
615
702
 
616
703
  .pp-drop-overlay {
617
704
  position: absolute;
@@ -817,6 +904,8 @@ function styles(primaryColor, theme) {
817
904
  margin-bottom: 6px;
818
905
  border-radius: 0 4px 4px 0;
819
906
  font-size: 12px;
907
+ position: relative;
908
+ z-index: 1;
820
909
  }
821
910
 
822
911
  .pp-reply-sender {
@@ -834,6 +923,17 @@ function styles(primaryColor, theme) {
834
923
  text-overflow: ellipsis;
835
924
  }
836
925
 
926
+ /* Reply quote in visitor message bubble needs higher contrast */
927
+ .pp-message-visitor .pp-reply-quote {
928
+ background: rgba(255, 255, 255, 0.18);
929
+ border-left-color: rgba(255, 255, 255, 0.7);
930
+ }
931
+
932
+ .pp-message-visitor .pp-reply-sender,
933
+ .pp-message-visitor .pp-reply-content {
934
+ color: rgba(255, 255, 255, 0.9);
935
+ }
936
+
837
937
  /* Deleted Message */
838
938
  .pp-message-deleted {
839
939
  opacity: 0.6;
@@ -878,6 +978,8 @@ function ChatWidget({ client: client2, config: initialConfig }) {
878
978
  const [editContent, setEditContent] = useState("");
879
979
  const [messageMenu, setMessageMenu] = useState(null);
880
980
  const [isDragging, setIsDragging] = useState(false);
981
+ const [hoveredMessageId, setHoveredMessageId] = useState(null);
982
+ const [longPressTimer, setLongPressTimer] = useState(null);
881
983
  const [config, setConfig] = useState(initialConfig);
882
984
  const messagesEndRef = useRef(null);
883
985
  const inputRef = useRef(null);
@@ -1105,6 +1207,25 @@ function ChatWidget({ client: client2, config: initialConfig }) {
1105
1207
  y: mouseEvent.clientY
1106
1208
  });
1107
1209
  };
1210
+ const handleTouchStart = (message) => {
1211
+ const timer = setTimeout(() => {
1212
+ if (navigator.vibrate) navigator.vibrate(50);
1213
+ setMessageMenu({
1214
+ message,
1215
+ x: window.innerWidth / 2 - 60,
1216
+ // Center horizontally
1217
+ y: window.innerHeight / 2 - 50
1218
+ // Center vertically
1219
+ });
1220
+ }, 500);
1221
+ setLongPressTimer(timer);
1222
+ };
1223
+ const handleTouchEnd = () => {
1224
+ if (longPressTimer) {
1225
+ clearTimeout(longPressTimer);
1226
+ setLongPressTimer(null);
1227
+ }
1228
+ };
1108
1229
  useEffect(() => {
1109
1230
  if (!messageMenu) return;
1110
1231
  const handleClickOutside = () => setMessageMenu(null);
@@ -1186,6 +1307,7 @@ function ChatWidget({ client: client2, config: initialConfig }) {
1186
1307
  const position = config.position ?? "bottom-right";
1187
1308
  const theme = getTheme(config.theme ?? "auto");
1188
1309
  const primaryColor = config.primaryColor ?? "#6366f1";
1310
+ const actionIconColor = theme === "dark" ? "#9ca3af" : "#6b7280";
1189
1311
  return /* @__PURE__ */ jsxs(Fragment, { children: [
1190
1312
  /* @__PURE__ */ jsx("style", { children: styles(primaryColor, theme) }),
1191
1313
  /* @__PURE__ */ jsxs(
@@ -1243,18 +1365,70 @@ function ChatWidget({ client: client2, config: initialConfig }) {
1243
1365
  messages.map((msg) => {
1244
1366
  const isDeleted = !!msg.deletedAt;
1245
1367
  const isEdited = !!msg.editedAt;
1246
- const replyToMsg = msg.replyTo ? messages.find((m) => m.id === msg.replyTo) : null;
1368
+ let replyData = null;
1369
+ if (msg.replyTo) {
1370
+ if (typeof msg.replyTo === "object") {
1371
+ replyData = msg.replyTo;
1372
+ } else {
1373
+ const replyToMsg = messages.find((m) => m.id === msg.replyTo);
1374
+ if (replyToMsg) {
1375
+ replyData = {
1376
+ sender: replyToMsg.sender,
1377
+ content: replyToMsg.content,
1378
+ deleted: !!replyToMsg.deletedAt
1379
+ };
1380
+ }
1381
+ }
1382
+ }
1383
+ const isHovered = hoveredMessageId === msg.id;
1384
+ const showActions = isHovered && !isDeleted;
1247
1385
  return /* @__PURE__ */ jsxs(
1248
1386
  "div",
1249
1387
  {
1250
1388
  class: `pp-message pp-message-${msg.sender} ${isDeleted ? "pp-message-deleted" : ""}`,
1251
1389
  onContextMenu: (e) => handleMessageContextMenu(e, msg),
1390
+ onMouseEnter: () => setHoveredMessageId(msg.id),
1391
+ onMouseLeave: () => setHoveredMessageId(null),
1392
+ onTouchStart: () => handleTouchStart(msg),
1393
+ onTouchEnd: handleTouchEnd,
1394
+ onTouchCancel: handleTouchEnd,
1252
1395
  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" }),
1396
+ showActions && /* @__PURE__ */ jsxs("div", { class: `pp-message-actions ${msg.sender === "visitor" ? "pp-actions-left" : "pp-actions-right"}`, children: [
1397
+ /* @__PURE__ */ jsx(
1398
+ "button",
1399
+ {
1400
+ class: "pp-action-btn",
1401
+ onClick: () => handleReply(msg),
1402
+ title: "Reply",
1403
+ children: /* @__PURE__ */ jsx(ReplyIcon, { color: actionIconColor })
1404
+ }
1405
+ ),
1406
+ msg.sender === "visitor" && /* @__PURE__ */ jsxs(Fragment2, { children: [
1407
+ /* @__PURE__ */ jsx(
1408
+ "button",
1409
+ {
1410
+ class: "pp-action-btn",
1411
+ onClick: () => handleStartEdit(msg),
1412
+ title: "Edit",
1413
+ children: /* @__PURE__ */ jsx(EditIcon, { color: actionIconColor })
1414
+ }
1415
+ ),
1416
+ /* @__PURE__ */ jsx(
1417
+ "button",
1418
+ {
1419
+ class: "pp-action-btn pp-action-delete",
1420
+ onClick: () => handleDelete(msg),
1421
+ title: "Delete",
1422
+ children: /* @__PURE__ */ jsx(DeleteIcon, { color: actionIconColor })
1423
+ }
1424
+ )
1425
+ ] })
1426
+ ] }),
1427
+ replyData && replyData.content && /* @__PURE__ */ jsxs("div", { class: "pp-reply-quote", children: [
1428
+ /* @__PURE__ */ jsx("span", { class: "pp-reply-sender", children: replyData.sender === "visitor" ? "You" : "Support" }),
1255
1429
  /* @__PURE__ */ jsxs("span", { class: "pp-reply-content", children: [
1256
- replyToMsg.deletedAt ? "Message deleted" : replyToMsg.content.slice(0, 50),
1257
- replyToMsg.content.length > 50 ? "..." : ""
1430
+ replyData.deleted ? "Message deleted" : (replyData.content || "").slice(0, 50),
1431
+ (replyData.content || "").length > 50 ? "..." : ""
1258
1432
  ] })
1259
1433
  ] }),
1260
1434
  isDeleted ? /* @__PURE__ */ jsxs("div", { class: "pp-message-content pp-deleted-content", children: [
@@ -1289,16 +1463,16 @@ function ChatWidget({ client: client2, config: initialConfig }) {
1289
1463
  style: { top: `${messageMenu.y}px`, left: `${messageMenu.x}px` },
1290
1464
  children: [
1291
1465
  /* @__PURE__ */ jsxs("button", { onClick: () => handleReply(messageMenu.message), children: [
1292
- /* @__PURE__ */ jsx(ReplyIcon, {}),
1466
+ /* @__PURE__ */ jsx(ReplyIcon, { color: actionIconColor }),
1293
1467
  " Reply"
1294
1468
  ] }),
1295
1469
  messageMenu.message.sender === "visitor" && !messageMenu.message.deletedAt && /* @__PURE__ */ jsxs(Fragment2, { children: [
1296
1470
  /* @__PURE__ */ jsxs("button", { onClick: () => handleStartEdit(messageMenu.message), children: [
1297
- /* @__PURE__ */ jsx(EditIcon, {}),
1471
+ /* @__PURE__ */ jsx(EditIcon, { color: actionIconColor }),
1298
1472
  " Edit"
1299
1473
  ] }),
1300
1474
  /* @__PURE__ */ jsxs("button", { class: "pp-menu-delete", onClick: () => handleDelete(messageMenu.message), children: [
1301
- /* @__PURE__ */ jsx(DeleteIcon, {}),
1475
+ /* @__PURE__ */ jsx(DeleteIcon, { color: "#ef4444" }),
1302
1476
  " Delete"
1303
1477
  ] })
1304
1478
  ] })
@@ -1464,17 +1638,20 @@ function StatusIcon({ status }) {
1464
1638
  function AttachIcon() {
1465
1639
  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
1640
  }
1467
- function ReplyIcon() {
1468
- return /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: [
1641
+ function ReplyIcon({ color, size = 16 }) {
1642
+ const strokeColor = color || "currentColor";
1643
+ return /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", "stroke-width": "2", style: { stroke: strokeColor, width: `${size}px`, minWidth: `${size}px`, height: `${size}px`, display: "block", flexShrink: 0 }, children: [
1469
1644
  /* @__PURE__ */ jsx("polyline", { points: "9 17 4 12 9 7" }),
1470
1645
  /* @__PURE__ */ jsx("path", { d: "M20 18v-2a4 4 0 0 0-4-4H4" })
1471
1646
  ] });
1472
1647
  }
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" }) });
1648
+ function EditIcon({ color, size = 16 }) {
1649
+ const strokeColor = color || "currentColor";
1650
+ return /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", "stroke-width": "2", style: { stroke: strokeColor, width: `${size}px`, minWidth: `${size}px`, height: `${size}px`, display: "block", flexShrink: 0 }, children: /* @__PURE__ */ jsx("path", { d: "M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z" }) });
1475
1651
  }
1476
- function DeleteIcon() {
1477
- return /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: [
1652
+ function DeleteIcon({ color, size = 16 }) {
1653
+ const strokeColor = color || "currentColor";
1654
+ return /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", "stroke-width": "2", style: { stroke: strokeColor, width: `${size}px`, minWidth: `${size}px`, height: `${size}px`, display: "block", flexShrink: 0 }, children: [
1478
1655
  /* @__PURE__ */ jsx("polyline", { points: "3 6 5 6 21 6" }),
1479
1656
  /* @__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
1657
  ] });
@@ -2472,7 +2649,14 @@ var PocketPingClient = class {
2472
2649
  }
2473
2650
  connectSSE() {
2474
2651
  if (!this.session) return;
2475
- const sseUrl = this.config.endpoint.replace(/\/$/, "") + `/stream?sessionId=${this.session.sessionId}`;
2652
+ const params = new URLSearchParams({
2653
+ sessionId: this.session.sessionId
2654
+ });
2655
+ const lastEventTimestamp = this.getLastEventTimestamp();
2656
+ if (lastEventTimestamp) {
2657
+ params.set("after", lastEventTimestamp);
2658
+ }
2659
+ const sseUrl = this.config.endpoint.replace(/\/$/, "") + `/stream?${params.toString()}`;
2476
2660
  try {
2477
2661
  this.sse = new EventSource(sseUrl);
2478
2662
  const connectionTimeout = setTimeout(() => {
@@ -2527,6 +2711,19 @@ var PocketPingClient = class {
2527
2711
  handleRealtimeEvent(event) {
2528
2712
  this.handleWebSocketEvent(event);
2529
2713
  }
2714
+ getLastEventTimestamp() {
2715
+ if (!this.session) return null;
2716
+ let latest = null;
2717
+ for (const msg of this.session.messages) {
2718
+ const candidates = [msg.timestamp, msg.editedAt, msg.deletedAt, msg.deliveredAt, msg.readAt].filter(Boolean).map((value) => new Date(value)).filter((date) => !isNaN(date.getTime()));
2719
+ for (const date of candidates) {
2720
+ if (!latest || date > latest) {
2721
+ latest = date;
2722
+ }
2723
+ }
2724
+ }
2725
+ return latest ? latest.toISOString() : null;
2726
+ }
2530
2727
  handleWebSocketEvent(event) {
2531
2728
  switch (event.type) {
2532
2729
  case "message":
@@ -2549,12 +2746,41 @@ var PocketPingClient = class {
2549
2746
  }
2550
2747
  if (existingIndex >= 0) {
2551
2748
  const existing = this.session.messages[existingIndex];
2749
+ let updated = false;
2552
2750
  if (message.status && message.status !== existing.status) {
2553
2751
  existing.status = message.status;
2554
- if (message.deliveredAt) existing.deliveredAt = message.deliveredAt;
2555
- if (message.readAt) existing.readAt = message.readAt;
2752
+ updated = true;
2753
+ if (message.deliveredAt) {
2754
+ existing.deliveredAt = message.deliveredAt;
2755
+ }
2756
+ if (message.readAt) {
2757
+ existing.readAt = message.readAt;
2758
+ }
2556
2759
  this.emit("read", { messageIds: [message.id], status: message.status });
2557
2760
  }
2761
+ if (message.content !== void 0 && message.content !== existing.content) {
2762
+ existing.content = message.content;
2763
+ updated = true;
2764
+ }
2765
+ if (message.editedAt !== void 0 && message.editedAt !== existing.editedAt) {
2766
+ existing.editedAt = message.editedAt;
2767
+ updated = true;
2768
+ }
2769
+ if (message.deletedAt !== void 0 && message.deletedAt !== existing.deletedAt) {
2770
+ existing.deletedAt = message.deletedAt;
2771
+ updated = true;
2772
+ }
2773
+ if (message.replyTo !== void 0) {
2774
+ existing.replyTo = message.replyTo;
2775
+ updated = true;
2776
+ }
2777
+ if (message.attachments !== void 0) {
2778
+ existing.attachments = message.attachments;
2779
+ updated = true;
2780
+ }
2781
+ if (updated) {
2782
+ this.emit("message", existing);
2783
+ }
2558
2784
  } else {
2559
2785
  this.session.messages.push(message);
2560
2786
  this.emit("message", message);
@@ -2593,6 +2819,29 @@ var PocketPingClient = class {
2593
2819
  }
2594
2820
  this.emit("read", readData);
2595
2821
  break;
2822
+ case "message_edited":
2823
+ if (this.session) {
2824
+ const editData = event.data;
2825
+ const msgIndex = this.session.messages.findIndex((m) => m.id === editData.messageId);
2826
+ if (msgIndex >= 0) {
2827
+ const existing = this.session.messages[msgIndex];
2828
+ existing.content = editData.content;
2829
+ existing.editedAt = editData.editedAt ?? (/* @__PURE__ */ new Date()).toISOString();
2830
+ this.emit("message", existing);
2831
+ }
2832
+ }
2833
+ break;
2834
+ case "message_deleted":
2835
+ if (this.session) {
2836
+ const deleteData = event.data;
2837
+ const msgIndex = this.session.messages.findIndex((m) => m.id === deleteData.messageId);
2838
+ if (msgIndex >= 0) {
2839
+ const existing = this.session.messages[msgIndex];
2840
+ existing.deletedAt = deleteData.deletedAt ?? (/* @__PURE__ */ new Date()).toISOString();
2841
+ this.emit("message", existing);
2842
+ }
2843
+ }
2844
+ break;
2596
2845
  case "event":
2597
2846
  const customEvent = event.data;
2598
2847
  this.emitCustomEvent(customEvent);
@@ -2652,11 +2901,45 @@ var PocketPingClient = class {
2652
2901
  const poll = async () => {
2653
2902
  if (!this.session) return;
2654
2903
  try {
2655
- const lastMessageId = this.session.messages[this.session.messages.length - 1]?.id;
2656
- const newMessages = await this.fetchMessages(lastMessageId);
2904
+ const lastEventTimestamp = this.getLastEventTimestamp();
2905
+ const newMessages = await this.fetchMessages(lastEventTimestamp ?? void 0);
2657
2906
  this.pollingFailures = 0;
2658
2907
  for (const message of newMessages) {
2659
- if (!this.session.messages.find((m) => m.id === message.id)) {
2908
+ const existingIndex = this.session.messages.findIndex((m) => m.id === message.id);
2909
+ if (existingIndex >= 0) {
2910
+ const existing = this.session.messages[existingIndex];
2911
+ let updated = false;
2912
+ if (message.status && message.status !== existing.status) {
2913
+ existing.status = message.status;
2914
+ updated = true;
2915
+ if (message.deliveredAt) existing.deliveredAt = message.deliveredAt;
2916
+ if (message.readAt) existing.readAt = message.readAt;
2917
+ this.emit("read", { messageIds: [message.id], status: message.status });
2918
+ }
2919
+ if (message.content !== void 0 && message.content !== existing.content) {
2920
+ existing.content = message.content;
2921
+ updated = true;
2922
+ }
2923
+ if (message.editedAt !== void 0 && message.editedAt !== existing.editedAt) {
2924
+ existing.editedAt = message.editedAt;
2925
+ updated = true;
2926
+ }
2927
+ if (message.deletedAt !== void 0 && message.deletedAt !== existing.deletedAt) {
2928
+ existing.deletedAt = message.deletedAt;
2929
+ updated = true;
2930
+ }
2931
+ if (message.replyTo !== void 0) {
2932
+ existing.replyTo = message.replyTo;
2933
+ updated = true;
2934
+ }
2935
+ if (message.attachments !== void 0) {
2936
+ existing.attachments = message.attachments;
2937
+ updated = true;
2938
+ }
2939
+ if (updated) {
2940
+ this.emit("message", existing);
2941
+ }
2942
+ } else {
2660
2943
  this.session.messages.push(message);
2661
2944
  this.emit("message", message);
2662
2945
  this.config.onMessage?.(message);