@elixpo/lixsketch 5.5.10 → 5.5.12

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.
Files changed (32) hide show
  1. package/dist/react/{AIRenderer-46WIFF2B.js → AIRenderer-PWWLWI6U.js} +84 -16
  2. package/dist/react/AIRenderer-PWWLWI6U.js.map +7 -0
  3. package/dist/react/{MermaidFlowchartRenderer-A26B2SM3.js → MermaidFlowchartRenderer-4YEMAYQC.js} +18 -3
  4. package/dist/react/MermaidFlowchartRenderer-4YEMAYQC.js.map +7 -0
  5. package/dist/react/{MermaidSequenceRenderer-OF4JK77D.js → MermaidSequenceRenderer-KCUHO7UI.js} +106 -4
  6. package/dist/react/MermaidSequenceRenderer-KCUHO7UI.js.map +7 -0
  7. package/dist/react/MermaidStructuredRenderer-GGOOGON5.js +365 -0
  8. package/dist/react/MermaidStructuredRenderer-GGOOGON5.js.map +7 -0
  9. package/dist/react/{SketchEngine-BCRJ62CV.js → SketchEngine-NPF2XN6Z.js} +2 -2
  10. package/dist/react/index.js +61 -127
  11. package/dist/react/index.js.map +3 -3
  12. package/package.json +1 -1
  13. package/src/core/AIRenderer.js +95 -11
  14. package/src/core/MermaidFlowchartRenderer.js +18 -3
  15. package/src/core/MermaidSequenceRenderer.js +116 -7
  16. package/src/core/MermaidStructuredRenderer.js +363 -0
  17. package/src/react/LixSketchCanvas.jsx +1 -1
  18. package/src/react/components/canvas/FindBar.jsx +1 -1
  19. package/src/react/components/modals/CanvasPropertiesModal.jsx +1 -1
  20. package/src/react/components/modals/LixScriptModal.jsx +22 -96
  21. package/src/react/components/sidebars/ArrowSidebar.jsx +6 -6
  22. package/src/react/components/sidebars/CircleSidebar.jsx +3 -3
  23. package/src/react/components/sidebars/FrameSidebar.jsx +1 -1
  24. package/src/react/components/sidebars/LineSidebar.jsx +4 -4
  25. package/src/react/components/sidebars/PaintbrushSidebar.jsx +5 -5
  26. package/src/react/components/sidebars/RectangleSidebar.jsx +4 -4
  27. package/src/react/components/sidebars/TextSidebar.jsx +5 -5
  28. package/src/react/store/useUIStore.js +1 -1
  29. package/dist/react/AIRenderer-46WIFF2B.js.map +0 -7
  30. package/dist/react/MermaidFlowchartRenderer-A26B2SM3.js.map +0 -7
  31. package/dist/react/MermaidSequenceRenderer-OF4JK77D.js.map +0 -7
  32. /package/dist/react/{SketchEngine-BCRJ62CV.js.map → SketchEngine-NPF2XN6Z.js.map} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elixpo/lixsketch",
3
- "version": "5.5.10",
3
+ "version": "5.5.12",
4
4
  "description": "Open-source SVG whiteboard engine + React canvas component with hand-drawn aesthetics — the core of LixSketch",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -43,6 +43,30 @@ export function parseMermaid(src) {
43
43
  const edges = [];
44
44
  const classDefs = new Map(); // classDef name -> { fill, stroke, strokeWidth }
45
45
  const classAssigns = []; // { nodeIds: [...], className }
46
+ const nodeStyles = new Map(); // Mermaid `style A fill:...,stroke:...`
47
+ const palette = [
48
+ { fill: '#e8f3ec', stroke: '#5f836c' },
49
+ { fill: '#f6eadf', stroke: '#a97852' },
50
+ { fill: '#eee9f4', stroke: '#7e6b91' },
51
+ { fill: '#f5efcf', stroke: '#9a8745' },
52
+ { fill: '#e5f0f1', stroke: '#5d7f82' },
53
+ { fill: '#f4e5e3', stroke: '#9a6863' },
54
+ ];
55
+
56
+ function parseStyleProps(value) {
57
+ const props = {};
58
+ for (const part of value.split(',')) {
59
+ const separator = part.indexOf(':');
60
+ if (separator < 0) continue;
61
+ const key = part.slice(0, separator).trim().toLowerCase();
62
+ const val = part.slice(separator + 1).trim();
63
+ if (key === 'fill') props.fill = val;
64
+ else if (key === 'stroke') props.stroke = val;
65
+ else if (key === 'stroke-width') props.strokeWidth = parseFloat(val);
66
+ else if (key === 'color') props.labelColor = val;
67
+ }
68
+ return props;
69
+ }
46
70
 
47
71
  // Clean <br/> and <br> tags in labels → newline marker
48
72
  function cleanLabel(label) {
@@ -91,18 +115,24 @@ export function parseMermaid(src) {
91
115
  const classDefMatch = line.match(/^classDef\s+(\w+)\s+(.+)$/i);
92
116
  if (classDefMatch) {
93
117
  const name = classDefMatch[1];
94
- const propsStr = classDefMatch[2].replace(/;$/, '');
95
- const props = {};
96
- for (const part of propsStr.split(',')) {
97
- const [key, val] = part.split(':').map(s => s.trim());
98
- if (key === 'fill') props.fill = val;
99
- else if (key === 'stroke') props.stroke = val;
100
- else if (key === 'stroke-width') props.strokeWidth = parseFloat(val);
101
- }
102
- classDefs.set(name, props);
118
+ classDefs.set(name, parseStyleProps(classDefMatch[2].replace(/;$/, '')));
119
+ continue;
120
+ }
121
+
122
+ // Direct node styling must be consumed before node/edge parsing.
123
+ // Previously these lines fell through to `parseNodeRef`, producing
124
+ // visible boxes containing the raw `style A fill:...` source.
125
+ const styleMatch = line.match(/^style\s+([\w,-]+)\s+(.+)$/i);
126
+ if (styleMatch) {
127
+ const props = parseStyleProps(styleMatch[2].replace(/;$/, ''));
128
+ styleMatch[1].split(',').map(id => id.trim()).filter(Boolean)
129
+ .forEach(id => nodeStyles.set(id, props));
103
130
  continue;
104
131
  }
105
132
 
133
+ // Presentation-only directives do not represent diagram nodes.
134
+ if (/^(linkStyle|click)\b/i.test(line)) continue;
135
+
106
136
  // class assignment: class sq,e green
107
137
  const classMatch = line.match(/^class\s+(.+?)\s+(\w+)$/i);
108
138
  if (classMatch) {
@@ -214,6 +244,12 @@ export function parseMermaid(src) {
214
244
  }
215
245
  }
216
246
 
247
+
248
+ for (const [nid, style] of nodeStyles) {
249
+ const node = nodesMap.get(nid);
250
+ if (node) Object.assign(node, style);
251
+ }
252
+
217
253
  // Topological BFS layering
218
254
  const nodeIds = Array.from(nodesMap.keys());
219
255
  const children = new Map();
@@ -247,6 +283,7 @@ export function parseMermaid(src) {
247
283
 
248
284
  // Compute dynamic node sizes based on label length
249
285
  const nodes = [];
286
+ let paletteIndex = 0;
250
287
  Array.from(layerGroups.keys()).sort((a, b) => a - b).forEach((layerIdx, li) => {
251
288
  const group = layerGroups.get(layerIdx);
252
289
  const startOffset = -(group.length * H_SPACING) / 2 + H_SPACING / 2;
@@ -262,8 +299,12 @@ export function parseMermaid(src) {
262
299
  nodes.push({
263
300
  id: nd.id, type: nd.type, label: nd.label,
264
301
  x, y, width: nw, height: nh,
265
- fill: nd.fill, stroke: nd.stroke, strokeWidth: nd.strokeWidth,
302
+ fill: nd.fill || palette[paletteIndex % palette.length].fill,
303
+ stroke: nd.stroke || palette[paletteIndex % palette.length].stroke,
304
+ strokeWidth: nd.strokeWidth,
305
+ labelColor: nd.labelColor,
266
306
  });
307
+ paletteIndex += 1;
267
308
  });
268
309
  });
269
310
 
@@ -1327,6 +1368,9 @@ export function initAIRenderer() {
1327
1368
  let _fcPreview = null;
1328
1369
  let _fcCanvas = null;
1329
1370
 
1371
+ // Lazy-load ER / chart renderer
1372
+ let _structured = null;
1373
+
1330
1374
  async function loadSequenceRenderer() {
1331
1375
  if (_seqParser) return;
1332
1376
  const mod = await import('./MermaidSequenceParser.js');
@@ -1343,9 +1387,25 @@ export function initAIRenderer() {
1343
1387
  _fcCanvas = mod.renderFlowchartOnCanvas;
1344
1388
  }
1345
1389
 
1390
+ async function loadStructuredRenderer() {
1391
+ if (_structured) return;
1392
+ _structured = await import('./MermaidStructuredRenderer.js');
1393
+ }
1394
+
1395
+ function mermaidHeader(src) {
1396
+ return src.split('\n').map(line => line.trim()).find(line => line && !line.startsWith('%%'))?.toLowerCase() || '';
1397
+ }
1398
+
1346
1399
  // Detect if source is a sequence diagram
1347
1400
  function isSequenceDiagram(src) {
1348
- return src.trim().split('\n')[0].trim().toLowerCase() === 'sequencediagram';
1401
+ return mermaidHeader(src) === 'sequencediagram';
1402
+ }
1403
+
1404
+ function structuredType(src) {
1405
+ const header = mermaidHeader(src);
1406
+ if (header === 'erdiagram') return 'er';
1407
+ if (/^pie(?:\s|$)/.test(header) || /^xychart(?:-beta)?(?:\s|$)/.test(header)) return 'chart';
1408
+ return null;
1349
1409
  }
1350
1410
 
1351
1411
  window.__aiRenderer = renderAIDiagram;
@@ -1363,6 +1423,15 @@ export function initAIRenderer() {
1363
1423
  return null;
1364
1424
  }
1365
1425
  }
1426
+ if (structuredType(src)) {
1427
+ if (!_structured) {
1428
+ loadStructuredRenderer();
1429
+ return { _pendingStructured: true, src };
1430
+ }
1431
+ return structuredType(src) === 'er'
1432
+ ? _structured.parseERDiagram(src)
1433
+ : _structured.parseChartDiagram(src);
1434
+ }
1366
1435
  return parseMermaid(src);
1367
1436
  };
1368
1437
 
@@ -1374,6 +1443,13 @@ export function initAIRenderer() {
1374
1443
  if (!diagram) return '';
1375
1444
  return _seqPreview(diagram);
1376
1445
  }
1446
+ const type = structuredType(src);
1447
+ if (type) {
1448
+ await loadStructuredRenderer();
1449
+ const diagram = type === 'er' ? _structured.parseERDiagram(src) : _structured.parseChartDiagram(src);
1450
+ if (!diagram) return '';
1451
+ return type === 'er' ? _structured.renderERPreviewSVG(diagram) : _structured.renderChartPreviewSVG(diagram);
1452
+ }
1377
1453
  // Flowchart: use unified flowchart renderer
1378
1454
  await loadFlowchartRenderer();
1379
1455
  const diagram = parseMermaid(src);
@@ -1389,6 +1465,13 @@ export function initAIRenderer() {
1389
1465
  if (!diagram) { console.error('[AIRenderer] Sequence parse failed'); return false; }
1390
1466
  return _seqCanvas(diagram);
1391
1467
  }
1468
+ const type = structuredType(src);
1469
+ if (type) {
1470
+ await loadStructuredRenderer();
1471
+ const diagram = type === 'er' ? _structured.parseERDiagram(src) : _structured.parseChartDiagram(src);
1472
+ if (!diagram) { console.error('[AIRenderer] Structured Mermaid parse failed'); return false; }
1473
+ return type === 'er' ? _structured.renderEROnCanvas(diagram) : _structured.renderChartOnCanvas(diagram);
1474
+ }
1392
1475
  // Flowchart: use unified flowchart renderer (same SVG as preview)
1393
1476
  await loadFlowchartRenderer();
1394
1477
  const diagram = parseMermaid(src);
@@ -1399,4 +1482,5 @@ export function initAIRenderer() {
1399
1482
  // Pre-load renderers so they're ready
1400
1483
  loadSequenceRenderer();
1401
1484
  loadFlowchartRenderer();
1485
+ loadStructuredRenderer();
1402
1486
  }
@@ -27,8 +27,7 @@ function isThemeDark() {
27
27
  function nodeStrokeColor() { return isThemeDark() ? '#fff' : '#1a1a2e'; }
28
28
  function edgeStrokeColor() { return isThemeDark() ? '#888' : '#444'; }
29
29
 
30
- // Theme colors (dark theme — used by the SVG-string preview path only)
31
- const THEME = {
30
+ const DARK_THEME = {
32
31
  bg: '#1e1e28',
33
32
  nodeBg: 'transparent',
34
33
  nodeStroke: '#9090c0',
@@ -40,6 +39,20 @@ const THEME = {
40
39
  subgraphLabel: '#888',
41
40
  };
42
41
 
42
+ const LIGHT_THEME = {
43
+ bg: '#fbfaf6',
44
+ nodeBg: '#f5f2ea',
45
+ nodeStroke: '#6e746c',
46
+ nodeText: '#343832',
47
+ edgeStroke: '#6e746c',
48
+ edgeText: '#4f554e',
49
+ subgraphBg: 'rgba(117, 135, 119, 0.06)',
50
+ subgraphBorder: '#aaa99f',
51
+ subgraphLabel: '#666b64',
52
+ };
53
+
54
+ function previewTheme() { return isThemeDark() ? DARK_THEME : LIGHT_THEME; }
55
+
43
56
  function escapeXml(str) {
44
57
  return String(str)
45
58
  .replace(/&/g, '&amp;')
@@ -62,6 +75,8 @@ function measureText(text, fontSize) {
62
75
  export function renderFlowchartSVG(diagram, opts = {}) {
63
76
  if (!diagram || !diagram.nodes || diagram.nodes.length === 0) return '';
64
77
 
78
+ const THEME = previewTheme();
79
+
65
80
  const nodes = diagram.nodes;
66
81
  const edges = diagram.edges || [];
67
82
  const subgraphs = diagram.subgraphs || [];
@@ -455,7 +470,7 @@ export function renderFlowchartOnCanvas(diagram) {
455
470
  fillStyle: n.fill && n.fill !== 'transparent' ? 'solid' : 'none',
456
471
  roughness: 1,
457
472
  label: n.label || '',
458
- labelColor: n.labelColor || nodeStrokeColor(),
473
+ labelColor: n.labelColor || (n.fill && n.fill !== 'transparent' ? getContrastColor(n.fill) : nodeStrokeColor()),
459
474
  };
460
475
 
461
476
  let shape = null;
@@ -35,7 +35,7 @@ function themeColors() {
35
35
  const isDark = typeof document !== 'undefined'
36
36
  && document.body
37
37
  && document.body.classList.contains('theme-dark');
38
- if (isDark) return THEME;
38
+ if (isDark) return DARK_THEME;
39
39
  return {
40
40
  bg: '#fbfaf6',
41
41
  participantBg: '#ffffff',
@@ -56,7 +56,7 @@ function themeColors() {
56
56
  }
57
57
 
58
58
  // Theme colors (dark theme — preview/SVG-string path).
59
- const THEME = {
59
+ const DARK_THEME = {
60
60
  bg: '#1e1e28',
61
61
  participantBg: '#232329',
62
62
  participantBorder: '#555',
@@ -125,6 +125,8 @@ function wrapText(text, fontSize, maxWidth) {
125
125
  export function renderSequenceSVG(diagram, opts = {}) {
126
126
  if (!diagram || diagram.type !== 'sequenceDiagram') return '';
127
127
 
128
+ const THEME = themeColors();
129
+
128
130
  const participants = diagram.participants;
129
131
  const messages = diagram.messages;
130
132
  const notes = diagram.notes;
@@ -401,9 +403,9 @@ export function parseAndRenderSequence(src) {
401
403
  * Issue #34 bug #3 (follow-up to #24 per-actor split): drops the shared
402
404
  * `groupId` glue so clicking one shape selects only that shape.
403
405
  *
404
- * Notes and block-frames (alt/opt/loop) are skipped for now they'd
405
- * either need their own shape types or a richer label model. Self-
406
- * messages are also skipped (would need a curved arrow).
406
+ * Notes and block frames are ordinary labelled rectangles. Self messages
407
+ * use three line segments plus an arrow, keeping every visible component
408
+ * editable with the same primitives users draw manually.
407
409
  */
408
410
  export function renderSequenceOnCanvas(diagram) {
409
411
  if (!diagram || diagram.type !== 'sequenceDiagram') return false;
@@ -414,6 +416,8 @@ export function renderSequenceOnCanvas(diagram) {
414
416
 
415
417
  const participants = diagram.participants || [];
416
418
  const messages = diagram.messages || [];
419
+ const notes = diagram.notes || [];
420
+ const blocks = diagram.blocks || [];
417
421
  if (participants.length === 0) return false;
418
422
 
419
423
  // Mirror the layout math from renderSequenceSVG so the canvas layout
@@ -425,12 +429,23 @@ export function renderSequenceOnCanvas(diagram) {
425
429
  const pCenters = participants.map((_, i) => startX + i * PARTICIPANT_GAP + PARTICIPANT_W / 2);
426
430
 
427
431
  const topBoxBottom = TOP_MARGIN + PARTICIPANT_H;
432
+ const noteAtMsg = new Map();
433
+ notes.forEach((note) => {
434
+ const lines = wrapText(note.text, 11, NOTE_MAX_W - NOTE_PAD * 2);
435
+ const height = lines.length * 15 + NOTE_PAD * 2;
436
+ if (!noteAtMsg.has(note.atMessage)) noteAtMsg.set(note.atMessage, []);
437
+ noteAtMsg.get(note.atMessage).push({ ...note, lines, height });
438
+ });
428
439
  const msgYPositions = [];
429
440
  let currentY = topBoxBottom + 30;
430
441
  for (let mi = 0; mi < messages.length; mi++) {
442
+ const before = noteAtMsg.get(mi);
443
+ if (before?.length) currentY += Math.max(...before.map(note => note.height)) + 10;
431
444
  msgYPositions.push(currentY);
432
445
  currentY += MSG_ROW_HEIGHT;
433
446
  }
447
+ const trailingNotes = noteAtMsg.get(messages.length);
448
+ if (trailingNotes?.length) currentY += Math.max(...trailingNotes.map(note => note.height)) + 10;
434
449
  const bottomBoxTop = currentY + 20;
435
450
  const totalHeight = bottomBoxTop + PARTICIPANT_H + BOTTOM_MARGIN;
436
451
 
@@ -532,13 +547,43 @@ export function renderSequenceOnCanvas(diagram) {
532
547
  }
533
548
  }
534
549
 
550
+ // ── Blocks: editable containers behind message rows ───────────────
551
+ for (const block of blocks) {
552
+ const startY = block.startMsg < msgYPositions.length ? msgYPositions[block.startMsg] - 22 : topBoxBottom + 15;
553
+ const endY = block.endMsg < msgYPositions.length ? msgYPositions[block.endMsg] - 8 : currentY;
554
+ try {
555
+ const blockShape = new window.Rectangle(
556
+ startX - 18 + ox,
557
+ startY + oy,
558
+ contentWidth + 36,
559
+ Math.max(36, endY - startY),
560
+ {
561
+ stroke: TK.blockBorder,
562
+ strokeWidth: 1,
563
+ strokeDasharray: '5 3',
564
+ fill: 'transparent',
565
+ fillStyle: 'none',
566
+ roughness: .5,
567
+ label: [block.type, block.label].filter(Boolean).join(': '),
568
+ labelColor: TK.blockLabel,
569
+ labelFontSize: 10,
570
+ }
571
+ );
572
+ window.shapes.push(blockShape);
573
+ if (window.pushCreateAction) window.pushCreateAction(blockShape);
574
+ frame.addShapeToFrame(blockShape);
575
+ created.push(blockShape);
576
+ } catch (err) {
577
+ console.warn('[SequenceRenderer] Block creation failed:', block, err);
578
+ }
579
+ }
580
+
535
581
  // ── Messages: arrow (or line for --x style) per row ───────────────
536
582
  for (let mi = 0; mi < messages.length; mi++) {
537
583
  const m = messages[mi];
538
584
  const fromI = pIndex.get(m.from);
539
585
  const toI = pIndex.get(m.to);
540
586
  if (fromI == null || toI == null) continue;
541
- if (fromI === toI) continue; // skip self-messages (v1 limitation)
542
587
 
543
588
  const fromCx = pCenters[fromI] + ox;
544
589
  const toCx = pCenters[toI] + ox;
@@ -562,6 +607,26 @@ export function renderSequenceOnCanvas(diagram) {
562
607
  try {
563
608
  const sp = { x: fromCx, y };
564
609
  const ep = { x: toCx, y };
610
+ if (fromI === toI) {
611
+ const loopWidth = 46;
612
+ const loopHeight = 28;
613
+ const a = { x: fromCx, y };
614
+ const b = { x: fromCx + loopWidth, y };
615
+ const c = { x: fromCx + loopWidth, y: y + loopHeight };
616
+ const d = { x: fromCx + 5, y: y + loopHeight };
617
+ const segments = [
618
+ new window.Line(a, b, opts),
619
+ new window.Line(b, c, opts),
620
+ new window.Arrow(c, d, opts),
621
+ ];
622
+ segments.forEach((segment) => {
623
+ window.shapes.push(segment);
624
+ if (window.pushCreateAction) window.pushCreateAction(segment);
625
+ frame.addShapeToFrame(segment);
626
+ created.push(segment);
627
+ });
628
+ continue;
629
+ }
565
630
  const connector = isCross
566
631
  ? new window.Line(sp, ep, opts)
567
632
  : new window.Arrow(sp, ep, opts);
@@ -574,6 +639,51 @@ export function renderSequenceOnCanvas(diagram) {
574
639
  }
575
640
  }
576
641
 
642
+
643
+ // ── Notes: one independently editable rectangle per note ─────────
644
+ for (const [messageIndex, noteGroup] of noteAtMsg.entries()) {
645
+ const baseY = messageIndex < msgYPositions.length
646
+ ? msgYPositions[messageIndex] - 15
647
+ : (messageIndex > 0 ? msgYPositions[messageIndex - 1] + MSG_ROW_HEIGHT - 15 : topBoxBottom + 30);
648
+ noteGroup.forEach((note, noteIndex) => {
649
+ const indexes = note.targets.map(target => pIndex.get(target)).filter(index => index !== undefined);
650
+ if (!indexes.length) return;
651
+ const noteWidth = NOTE_MAX_W;
652
+ let centerX = pCenters[indexes[0]];
653
+ if (note.position === 'over' && indexes.length > 1) {
654
+ centerX = (pCenters[Math.min(...indexes)] + pCenters[Math.max(...indexes)]) / 2;
655
+ } else if (note.position === 'left of') {
656
+ centerX -= PARTICIPANT_W / 2 + noteWidth / 2 + 8;
657
+ } else if (note.position === 'right of') {
658
+ centerX += PARTICIPANT_W / 2 + noteWidth / 2 + 8;
659
+ }
660
+ try {
661
+ const noteShape = new window.Rectangle(
662
+ centerX - noteWidth / 2 + ox,
663
+ baseY - note.height - noteIndex * (note.height + 6) + oy,
664
+ noteWidth,
665
+ note.height,
666
+ {
667
+ stroke: TK.noteBorder,
668
+ strokeWidth: 1,
669
+ fill: TK.noteBg,
670
+ fillStyle: 'solid',
671
+ roughness: .7,
672
+ label: note.text,
673
+ labelColor: TK.noteText,
674
+ labelFontSize: 11,
675
+ }
676
+ );
677
+ window.shapes.push(noteShape);
678
+ if (window.pushCreateAction) window.pushCreateAction(noteShape);
679
+ frame.addShapeToFrame(noteShape);
680
+ created.push(noteShape);
681
+ } catch (err) {
682
+ console.warn('[SequenceRenderer] Note creation failed:', note, err);
683
+ }
684
+ });
685
+ }
686
+
577
687
  // Auto-select the first node so the user has feedback that the
578
688
  // diagram landed. Selecting a child (not the frame) reinforces the
579
689
  // "independent shapes inside a frame" model from issue #34 bug #3.
@@ -586,4 +696,3 @@ export function renderSequenceOnCanvas(diagram) {
586
696
  console.log(`[SequenceRenderer] Done: ${pCount} participants, ${messages.length} messages`);
587
697
  return true;
588
698
  }
589
-