@pocketping/widget 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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 {
@@ -817,6 +905,8 @@ function styles(primaryColor, theme) {
817
905
  margin-bottom: 6px;
818
906
  border-radius: 0 4px 4px 0;
819
907
  font-size: 12px;
908
+ position: relative;
909
+ z-index: 1;
820
910
  }
821
911
 
822
912
  .pp-reply-sender {
@@ -834,6 +924,17 @@ function styles(primaryColor, theme) {
834
924
  text-overflow: ellipsis;
835
925
  }
836
926
 
927
+ /* Reply quote in visitor message bubble needs higher contrast */
928
+ .pp-message-visitor .pp-reply-quote {
929
+ background: rgba(255, 255, 255, 0.18);
930
+ border-left-color: rgba(255, 255, 255, 0.7);
931
+ }
932
+
933
+ .pp-message-visitor .pp-reply-sender,
934
+ .pp-message-visitor .pp-reply-content {
935
+ color: rgba(255, 255, 255, 0.9);
936
+ }
937
+
837
938
  /* Deleted Message */
838
939
  .pp-message-deleted {
839
940
  opacity: 0.6;
@@ -878,6 +979,8 @@ function ChatWidget({ client: client2, config: initialConfig }) {
878
979
  const [editContent, setEditContent] = useState("");
879
980
  const [messageMenu, setMessageMenu] = useState(null);
880
981
  const [isDragging, setIsDragging] = useState(false);
982
+ const [hoveredMessageId, setHoveredMessageId] = useState(null);
983
+ const [longPressTimer, setLongPressTimer] = useState(null);
881
984
  const [config, setConfig] = useState(initialConfig);
882
985
  const messagesEndRef = useRef(null);
883
986
  const inputRef = useRef(null);
@@ -1105,6 +1208,25 @@ function ChatWidget({ client: client2, config: initialConfig }) {
1105
1208
  y: mouseEvent.clientY
1106
1209
  });
1107
1210
  };
1211
+ const handleTouchStart = (message) => {
1212
+ const timer = setTimeout(() => {
1213
+ if (navigator.vibrate) navigator.vibrate(50);
1214
+ setMessageMenu({
1215
+ message,
1216
+ x: window.innerWidth / 2 - 60,
1217
+ // Center horizontally
1218
+ y: window.innerHeight / 2 - 50
1219
+ // Center vertically
1220
+ });
1221
+ }, 500);
1222
+ setLongPressTimer(timer);
1223
+ };
1224
+ const handleTouchEnd = () => {
1225
+ if (longPressTimer) {
1226
+ clearTimeout(longPressTimer);
1227
+ setLongPressTimer(null);
1228
+ }
1229
+ };
1108
1230
  useEffect(() => {
1109
1231
  if (!messageMenu) return;
1110
1232
  const handleClickOutside = () => setMessageMenu(null);
@@ -1186,6 +1308,7 @@ function ChatWidget({ client: client2, config: initialConfig }) {
1186
1308
  const position = config.position ?? "bottom-right";
1187
1309
  const theme = getTheme(config.theme ?? "auto");
1188
1310
  const primaryColor = config.primaryColor ?? "#6366f1";
1311
+ const actionIconColor = theme === "dark" ? "#9ca3af" : "#6b7280";
1189
1312
  return /* @__PURE__ */ jsxs(Fragment, { children: [
1190
1313
  /* @__PURE__ */ jsx("style", { children: styles(primaryColor, theme) }),
1191
1314
  /* @__PURE__ */ jsxs(
@@ -1243,18 +1366,70 @@ function ChatWidget({ client: client2, config: initialConfig }) {
1243
1366
  messages.map((msg) => {
1244
1367
  const isDeleted = !!msg.deletedAt;
1245
1368
  const isEdited = !!msg.editedAt;
1246
- const replyToMsg = msg.replyTo ? messages.find((m) => m.id === msg.replyTo) : null;
1369
+ let replyData = null;
1370
+ if (msg.replyTo) {
1371
+ if (typeof msg.replyTo === "object") {
1372
+ replyData = msg.replyTo;
1373
+ } else {
1374
+ const replyToMsg = messages.find((m) => m.id === msg.replyTo);
1375
+ if (replyToMsg) {
1376
+ replyData = {
1377
+ sender: replyToMsg.sender,
1378
+ content: replyToMsg.content,
1379
+ deleted: !!replyToMsg.deletedAt
1380
+ };
1381
+ }
1382
+ }
1383
+ }
1384
+ const isHovered = hoveredMessageId === msg.id;
1385
+ const showActions = isHovered && !isDeleted;
1247
1386
  return /* @__PURE__ */ jsxs(
1248
1387
  "div",
1249
1388
  {
1250
1389
  class: `pp-message pp-message-${msg.sender} ${isDeleted ? "pp-message-deleted" : ""}`,
1251
1390
  onContextMenu: (e) => handleMessageContextMenu(e, msg),
1391
+ onMouseEnter: () => setHoveredMessageId(msg.id),
1392
+ onMouseLeave: () => setHoveredMessageId(null),
1393
+ onTouchStart: () => handleTouchStart(msg),
1394
+ onTouchEnd: handleTouchEnd,
1395
+ onTouchCancel: handleTouchEnd,
1252
1396
  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" }),
1397
+ showActions && /* @__PURE__ */ jsxs("div", { class: `pp-message-actions ${msg.sender === "visitor" ? "pp-actions-left" : "pp-actions-right"}`, children: [
1398
+ /* @__PURE__ */ jsx(
1399
+ "button",
1400
+ {
1401
+ class: "pp-action-btn",
1402
+ onClick: () => handleReply(msg),
1403
+ title: "Reply",
1404
+ children: /* @__PURE__ */ jsx(ReplyIcon, { color: actionIconColor })
1405
+ }
1406
+ ),
1407
+ msg.sender === "visitor" && /* @__PURE__ */ jsxs(Fragment2, { children: [
1408
+ /* @__PURE__ */ jsx(
1409
+ "button",
1410
+ {
1411
+ class: "pp-action-btn",
1412
+ onClick: () => handleStartEdit(msg),
1413
+ title: "Edit",
1414
+ children: /* @__PURE__ */ jsx(EditIcon, { color: actionIconColor })
1415
+ }
1416
+ ),
1417
+ /* @__PURE__ */ jsx(
1418
+ "button",
1419
+ {
1420
+ class: "pp-action-btn pp-action-delete",
1421
+ onClick: () => handleDelete(msg),
1422
+ title: "Delete",
1423
+ children: /* @__PURE__ */ jsx(DeleteIcon, { color: actionIconColor })
1424
+ }
1425
+ )
1426
+ ] })
1427
+ ] }),
1428
+ replyData && replyData.content && /* @__PURE__ */ jsxs("div", { class: "pp-reply-quote", children: [
1429
+ /* @__PURE__ */ jsx("span", { class: "pp-reply-sender", children: replyData.sender === "visitor" ? "You" : "Support" }),
1255
1430
  /* @__PURE__ */ jsxs("span", { class: "pp-reply-content", children: [
1256
- replyToMsg.deletedAt ? "Message deleted" : replyToMsg.content.slice(0, 50),
1257
- replyToMsg.content.length > 50 ? "..." : ""
1431
+ replyData.deleted ? "Message deleted" : (replyData.content || "").slice(0, 50),
1432
+ (replyData.content || "").length > 50 ? "..." : ""
1258
1433
  ] })
1259
1434
  ] }),
1260
1435
  isDeleted ? /* @__PURE__ */ jsxs("div", { class: "pp-message-content pp-deleted-content", children: [
@@ -1289,16 +1464,16 @@ function ChatWidget({ client: client2, config: initialConfig }) {
1289
1464
  style: { top: `${messageMenu.y}px`, left: `${messageMenu.x}px` },
1290
1465
  children: [
1291
1466
  /* @__PURE__ */ jsxs("button", { onClick: () => handleReply(messageMenu.message), children: [
1292
- /* @__PURE__ */ jsx(ReplyIcon, {}),
1467
+ /* @__PURE__ */ jsx(ReplyIcon, { color: actionIconColor }),
1293
1468
  " Reply"
1294
1469
  ] }),
1295
1470
  messageMenu.message.sender === "visitor" && !messageMenu.message.deletedAt && /* @__PURE__ */ jsxs(Fragment2, { children: [
1296
1471
  /* @__PURE__ */ jsxs("button", { onClick: () => handleStartEdit(messageMenu.message), children: [
1297
- /* @__PURE__ */ jsx(EditIcon, {}),
1472
+ /* @__PURE__ */ jsx(EditIcon, { color: actionIconColor }),
1298
1473
  " Edit"
1299
1474
  ] }),
1300
1475
  /* @__PURE__ */ jsxs("button", { class: "pp-menu-delete", onClick: () => handleDelete(messageMenu.message), children: [
1301
- /* @__PURE__ */ jsx(DeleteIcon, {}),
1476
+ /* @__PURE__ */ jsx(DeleteIcon, { color: "#ef4444" }),
1302
1477
  " Delete"
1303
1478
  ] })
1304
1479
  ] })
@@ -1464,17 +1639,20 @@ function StatusIcon({ status }) {
1464
1639
  function AttachIcon() {
1465
1640
  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
1641
  }
1467
- function ReplyIcon() {
1468
- return /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: [
1642
+ function ReplyIcon({ color, size = 16 }) {
1643
+ const strokeColor = color || "currentColor";
1644
+ 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
1645
  /* @__PURE__ */ jsx("polyline", { points: "9 17 4 12 9 7" }),
1470
1646
  /* @__PURE__ */ jsx("path", { d: "M20 18v-2a4 4 0 0 0-4-4H4" })
1471
1647
  ] });
1472
1648
  }
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" }) });
1649
+ function EditIcon({ color, size = 16 }) {
1650
+ const strokeColor = color || "currentColor";
1651
+ 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
1652
  }
1476
- function DeleteIcon() {
1477
- return /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: [
1653
+ function DeleteIcon({ color, size = 16 }) {
1654
+ const strokeColor = color || "currentColor";
1655
+ 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
1656
  /* @__PURE__ */ jsx("polyline", { points: "3 6 5 6 21 6" }),
1479
1657
  /* @__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
1658
  ] });
@@ -2472,7 +2650,14 @@ var PocketPingClient = class {
2472
2650
  }
2473
2651
  connectSSE() {
2474
2652
  if (!this.session) return;
2475
- const sseUrl = this.config.endpoint.replace(/\/$/, "") + `/stream?sessionId=${this.session.sessionId}`;
2653
+ const params = new URLSearchParams({
2654
+ sessionId: this.session.sessionId
2655
+ });
2656
+ const lastEventTimestamp = this.getLastEventTimestamp();
2657
+ if (lastEventTimestamp) {
2658
+ params.set("after", lastEventTimestamp);
2659
+ }
2660
+ const sseUrl = this.config.endpoint.replace(/\/$/, "") + `/stream?${params.toString()}`;
2476
2661
  try {
2477
2662
  this.sse = new EventSource(sseUrl);
2478
2663
  const connectionTimeout = setTimeout(() => {
@@ -2527,6 +2712,19 @@ var PocketPingClient = class {
2527
2712
  handleRealtimeEvent(event) {
2528
2713
  this.handleWebSocketEvent(event);
2529
2714
  }
2715
+ getLastEventTimestamp() {
2716
+ if (!this.session) return null;
2717
+ let latest = null;
2718
+ for (const msg of this.session.messages) {
2719
+ const candidates = [msg.timestamp, msg.editedAt, msg.deletedAt, msg.deliveredAt, msg.readAt].filter(Boolean).map((value) => new Date(value)).filter((date) => !isNaN(date.getTime()));
2720
+ for (const date of candidates) {
2721
+ if (!latest || date > latest) {
2722
+ latest = date;
2723
+ }
2724
+ }
2725
+ }
2726
+ return latest ? latest.toISOString() : null;
2727
+ }
2530
2728
  handleWebSocketEvent(event) {
2531
2729
  switch (event.type) {
2532
2730
  case "message":
@@ -2549,12 +2747,41 @@ var PocketPingClient = class {
2549
2747
  }
2550
2748
  if (existingIndex >= 0) {
2551
2749
  const existing = this.session.messages[existingIndex];
2750
+ let updated = false;
2552
2751
  if (message.status && message.status !== existing.status) {
2553
2752
  existing.status = message.status;
2554
- if (message.deliveredAt) existing.deliveredAt = message.deliveredAt;
2555
- if (message.readAt) existing.readAt = message.readAt;
2753
+ updated = true;
2754
+ if (message.deliveredAt) {
2755
+ existing.deliveredAt = message.deliveredAt;
2756
+ }
2757
+ if (message.readAt) {
2758
+ existing.readAt = message.readAt;
2759
+ }
2556
2760
  this.emit("read", { messageIds: [message.id], status: message.status });
2557
2761
  }
2762
+ if (message.content !== void 0 && message.content !== existing.content) {
2763
+ existing.content = message.content;
2764
+ updated = true;
2765
+ }
2766
+ if (message.editedAt !== void 0 && message.editedAt !== existing.editedAt) {
2767
+ existing.editedAt = message.editedAt;
2768
+ updated = true;
2769
+ }
2770
+ if (message.deletedAt !== void 0 && message.deletedAt !== existing.deletedAt) {
2771
+ existing.deletedAt = message.deletedAt;
2772
+ updated = true;
2773
+ }
2774
+ if (message.replyTo !== void 0) {
2775
+ existing.replyTo = message.replyTo;
2776
+ updated = true;
2777
+ }
2778
+ if (message.attachments !== void 0) {
2779
+ existing.attachments = message.attachments;
2780
+ updated = true;
2781
+ }
2782
+ if (updated) {
2783
+ this.emit("message", existing);
2784
+ }
2558
2785
  } else {
2559
2786
  this.session.messages.push(message);
2560
2787
  this.emit("message", message);
@@ -2593,6 +2820,29 @@ var PocketPingClient = class {
2593
2820
  }
2594
2821
  this.emit("read", readData);
2595
2822
  break;
2823
+ case "message_edited":
2824
+ if (this.session) {
2825
+ const editData = event.data;
2826
+ const msgIndex = this.session.messages.findIndex((m) => m.id === editData.messageId);
2827
+ if (msgIndex >= 0) {
2828
+ const existing = this.session.messages[msgIndex];
2829
+ existing.content = editData.content;
2830
+ existing.editedAt = editData.editedAt ?? (/* @__PURE__ */ new Date()).toISOString();
2831
+ this.emit("message", existing);
2832
+ }
2833
+ }
2834
+ break;
2835
+ case "message_deleted":
2836
+ if (this.session) {
2837
+ const deleteData = event.data;
2838
+ const msgIndex = this.session.messages.findIndex((m) => m.id === deleteData.messageId);
2839
+ if (msgIndex >= 0) {
2840
+ const existing = this.session.messages[msgIndex];
2841
+ existing.deletedAt = deleteData.deletedAt ?? (/* @__PURE__ */ new Date()).toISOString();
2842
+ this.emit("message", existing);
2843
+ }
2844
+ }
2845
+ break;
2596
2846
  case "event":
2597
2847
  const customEvent = event.data;
2598
2848
  this.emitCustomEvent(customEvent);
@@ -2652,11 +2902,45 @@ var PocketPingClient = class {
2652
2902
  const poll = async () => {
2653
2903
  if (!this.session) return;
2654
2904
  try {
2655
- const lastMessageId = this.session.messages[this.session.messages.length - 1]?.id;
2656
- const newMessages = await this.fetchMessages(lastMessageId);
2905
+ const lastEventTimestamp = this.getLastEventTimestamp();
2906
+ const newMessages = await this.fetchMessages(lastEventTimestamp ?? void 0);
2657
2907
  this.pollingFailures = 0;
2658
2908
  for (const message of newMessages) {
2659
- if (!this.session.messages.find((m) => m.id === message.id)) {
2909
+ const existingIndex = this.session.messages.findIndex((m) => m.id === message.id);
2910
+ if (existingIndex >= 0) {
2911
+ const existing = this.session.messages[existingIndex];
2912
+ let updated = false;
2913
+ if (message.status && message.status !== existing.status) {
2914
+ existing.status = message.status;
2915
+ updated = true;
2916
+ if (message.deliveredAt) existing.deliveredAt = message.deliveredAt;
2917
+ if (message.readAt) existing.readAt = message.readAt;
2918
+ this.emit("read", { messageIds: [message.id], status: message.status });
2919
+ }
2920
+ if (message.content !== void 0 && message.content !== existing.content) {
2921
+ existing.content = message.content;
2922
+ updated = true;
2923
+ }
2924
+ if (message.editedAt !== void 0 && message.editedAt !== existing.editedAt) {
2925
+ existing.editedAt = message.editedAt;
2926
+ updated = true;
2927
+ }
2928
+ if (message.deletedAt !== void 0 && message.deletedAt !== existing.deletedAt) {
2929
+ existing.deletedAt = message.deletedAt;
2930
+ updated = true;
2931
+ }
2932
+ if (message.replyTo !== void 0) {
2933
+ existing.replyTo = message.replyTo;
2934
+ updated = true;
2935
+ }
2936
+ if (message.attachments !== void 0) {
2937
+ existing.attachments = message.attachments;
2938
+ updated = true;
2939
+ }
2940
+ if (updated) {
2941
+ this.emit("message", existing);
2942
+ }
2943
+ } else {
2660
2944
  this.session.messages.push(message);
2661
2945
  this.emit("message", message);
2662
2946
  this.config.onMessage?.(message);