@mikitasazan/notify 1.12.1 → 1.14.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.
Files changed (2) hide show
  1. package/dist/render.js +106 -28
  2. package/package.json +1 -1
package/dist/render.js CHANGED
@@ -252,13 +252,18 @@ const groupItem = (it, index, numbered) => {
252
252
  // part of the heading. Longer than ~400 characters and the quote collapses
253
253
  // on its own (`expandable`, Bot API), otherwise a stack trace or a log dump
254
254
  // stretches the card across the whole screen.
255
+ // A quote collapses when it would take more than a screenful — by length OR
256
+ // by line count, because five short lines eat as much screen as one long
257
+ // paragraph and the old length-only rule let them through (v2.1).
255
258
  const EXPAND_AT = 400;
259
+ const EXPAND_LINES = 5;
256
260
  const note = (text) => {
257
261
  if (!text) {
258
262
  return null;
259
263
  }
260
264
  const body = esc(text);
261
- return body.length > EXPAND_AT ? `<blockquote expandable>${body}</blockquote>` : `<blockquote>${body}</blockquote>`;
265
+ const long = body.length > EXPAND_AT || body.split('\n').length > EXPAND_LINES;
266
+ return long ? `<blockquote expandable>${body}</blockquote>` : `<blockquote>${body}</blockquote>`;
262
267
  };
263
268
  /**
264
269
  * A quote with a caption. A bare quote reads as a continuation of the
@@ -349,12 +354,17 @@ const renderGroup = (g) => [
349
354
  * was the same row the issue card had already lost for the same reason: you
350
355
  * could not read what the card was about without reading two lines.
351
356
  */
352
- const commitRow = (hash, url, title) => {
353
- const linked = fieldLink('Commit', url, hash);
354
- if (linked === null || !title) {
355
- return linked ?? field('Commit', title);
356
- }
357
- return `${linked} ${esc(firstLine(title))}`;
357
+ const commitRow = (hash, title) => {
358
+ // The title is the content — what the commit DID — so it is what the row
359
+ // says. The hash is a pointer, and pointers live in the last block with
360
+ // `Log`, `Check` and `Source`; the owner caught the hash still carrying the
361
+ // link from the middle of the card (31.08.2026): "source, это hash", and on
362
+ // a manual deploy that buried link was the card's ONLY way back to its
363
+ // source while the pointer block sat empty.
364
+ //
365
+ // With no title there is nothing to say but the hash, so it stands here as
366
+ // plain text — the link to it is in the pointer block either way.
367
+ return field('Commit', title ? firstLine(title) : hash);
358
368
  };
359
369
  const bodyQuote = (body) => body ? note(body) : null;
360
370
  /**
@@ -512,7 +522,7 @@ const renderDeploy = (e) => {
512
522
  // The run URL is gone from this line too — it is the `Source:` row now.
513
523
  typeLine(icon, 'Deploy', mechanism(e.workflowName, e.via), undefined, e.status === 'fail' ? 'Fail' : 'OK'),
514
524
  ...twoBlocks([field('Target', e.target), reason('Reason', e.note), field('Still red', e.stillRed ? `day ${e.stillRed}` : null)], [
515
- commitRow(e.commit, e.commitUrl, e.commitTitle),
525
+ commitRow(e.commit, e.commitTitle),
516
526
  fieldPerson('Author', e.commitAuthor),
517
527
  bodyQuote(e.commitBody)
518
528
  ])
@@ -609,7 +619,7 @@ const renderCi = (e) => {
609
619
  // on a nightly card, the same person everywhere else.
610
620
  ...twoBlocks([reason('Reason', e.note), field('Still red', e.stillRed ? `day ${e.stillRed}` : null)], [
611
621
  fieldTelegram('Actor', e.actor),
612
- commitRow(e.commit, e.commitUrl, e.commitTitle),
622
+ commitRow(e.commit, e.commitTitle),
613
623
  fieldPerson('Author', e.commitAuthor),
614
624
  bodyQuote(e.commitBody)
615
625
  ])
@@ -621,7 +631,26 @@ const renderCi = (e) => {
621
631
  // the thing the card is about could not be read without reading three lines.
622
632
  // The action is not repeated in words: the icon carries it, and no two actions
623
633
  // of one type share an icon.
624
- const named = (number, title) => title ? `#${number} ${title}` : `#${number}`;
634
+ // The number left line 2 on 31.08.2026, by the same rule that moved the commit
635
+ // hash the same day: `#347` is a pointer, and every pointer lives in the last
636
+ // block. Line 2 keeps the title, which is what the thing IS; the number comes
637
+ // back as the text of the `Source` link, where it names exactly what opens.
638
+ //
639
+ // With no title there is nothing left to say, so line 2 falls back to the bare
640
+ // type word — `typeLine` already does that on an empty name. The number is not
641
+ // put back here: it would then appear twice on a titled card and once on an
642
+ // untitled one, which is the inconsistency this move exists to remove.
643
+ //
644
+ // With NO url the number stays put, because then the `Source` row does not
645
+ // exist and dropping it here would erase the number from everything a person
646
+ // reads — the card would name a task without saying which. Same law as
647
+ // `fieldLink`: an identifier must not vanish just because the caller passed no
648
+ // address for it.
649
+ const named = (number, title, url) => {
650
+ if (!title)
651
+ return url ? '' : `#${number}`;
652
+ return url ? title : `#${number} ${title}`;
653
+ };
625
654
  // The people come BEFORE the text, and the text comes only when it is the
626
655
  // news. An `assigned` card carries one new fact — who took it — and it used to
627
656
  // sit dead last, under the issue's entire description: the owner read a card
@@ -636,32 +665,35 @@ const named = (number, title) => title ? `#${number} ${title}` : `#${number}`;
636
665
  // REVIEWER'S OWN comment there, which is new text he has not seen. "Verdict:
637
666
  // changes_requested, then nothing" was the owner's complaint: a verdict with
638
667
  // no comment attached said less than the review itself did.
668
+ // Same rule as the issue card since 31.08.2026: the body prints on every
669
+ // action, collapsed when long. `VERDICT` stays because it changes what the
670
+ // body MEANS — on approved/changes_requested the sender puts the reviewer's
671
+ // own comment there, not the PR description — and that is what the caption
672
+ // has to say.
639
673
  const VERDICT = new Set(['approved', 'changes_requested']);
640
- const opening = (action, body) => action === 'opened' || VERDICT.has(action) ? bodyQuote(body) : null;
674
+ const prBody = (action, body) => VERDICT.has(action) ? quoted('Review', body) : bodyQuote(body);
641
675
  // The body is what the title stands for — it sits directly under the name,
642
676
  // with nothing between them. The people come after, consolidated in one
643
677
  // place, never splitting the title from what it names: the owner on the
644
678
  // old order, title then Author then Assignee then finally the body — "why
645
679
  // does the assignee cut apart what should be inseparable?"
646
680
  const renderPr = (e) => join([
647
- typeLine(iconFor(e), 'PR', named(e.number, e.title)),
648
- opening(e.action, e.body),
649
- (e.action === 'opened' || VERDICT.has(e.action)) && e.body ? '' : null,
681
+ typeLine(iconFor(e), 'PR', named(e.number, e.title, e.url)),
682
+ prBody(e.action, e.body),
683
+ e.body ? '' : null,
650
684
  fieldPerson('Author', e.author),
651
685
  fieldPerson('Reviewer', e.reviewer)
652
686
  ]);
653
- // The issue's body prints ONLY on `opened` the same law the PR card
654
- // follows. History of this line: hidden shown everywhere (the owner's call
655
- // of 26.08.2026, "inconsistent with the same card showing it moments
656
- // earlier") hidden again on 31.08.2026, when the owner, shown the live
657
- // duplicate ("the full body arrives a second time in one day"), delegated
658
- // the call ("сделай как лучше по оптимизации и простоте") and approved the
659
- // short assigned card in the v2.1 mockups. On assigned/closed the card is
660
- // the one new fact plus the `Source:` link to the full text.
687
+ // The body prints on EVERY action, as a quote that collapses when it is
688
+ // long. The owner settled this on 31.08.2026, after the short assigned card
689
+ // shipped: "если это ишью, то видеть тело ишью… везде, где это может быть
690
+ // полезно, не переходя по ссылке на источник." A collapsed quote costs four
691
+ // lines, which is what the earlier "he already saw it" reasoning was trying
692
+ // to save the expandable quote buys the context back without the cost.
661
693
  const renderIssue = (e) => join([
662
- typeLine(iconFor(e), 'Issue', named(e.number, e.title)),
663
- e.action === 'opened' ? bodyQuote(e.body) : null,
664
- e.action === 'opened' && e.body ? '' : null,
694
+ typeLine(iconFor(e), 'Issue', named(e.number, e.title, e.url)),
695
+ bodyQuote(e.body),
696
+ e.body ? '' : null,
665
697
  fieldPerson('Author', e.author),
666
698
  fieldPerson('Assignee', e.assignee)
667
699
  ]);
@@ -847,13 +879,44 @@ const sourceUrl = (e) => {
847
879
  const url = 'url' in e ? e.url : undefined;
848
880
  return wf ?? url;
849
881
  };
882
+ const link = (url, text) => `<a href="${esc(firstLine(url))}">${esc(text)}</a>`;
883
+ /**
884
+ * Everything the card can be traced back to, as one `Source:` row.
885
+ *
886
+ * A card can have more than one honest source — a deploy through Actions has
887
+ * both a run and the commit that triggered it — and one label with two links
888
+ * beside each other reads better than two rows both called `Source`. They are
889
+ * ordered widest first: the run contains the commit, not the other way round.
890
+ */
891
+ const sourceLinks = (e) => {
892
+ const out = [];
893
+ const run = sourceUrl(e);
894
+ const commitUrl = 'commitUrl' in e ? e.commitUrl : undefined;
895
+ const commit = 'commit' in e ? e.commit : undefined;
896
+ // A pull request and an issue are named by their number and by NOTHING else.
897
+ // `pull request #118` says the type a second time — line 2 already opens with
898
+ // `PR:` — and the owner cut it the hour it shipped: "сверху написано PR,
899
+ // зачем здесь ещё писать PR? Здесь можно просто номер и всё."
900
+ //
901
+ // Only these two types have a number, and only for them is the type word
902
+ // redundant: `workflow run` and `commit 9b1fc68` name things line 2 does NOT,
903
+ // so they keep their nouns.
904
+ const numbered = 'number' in e && typeof e.number === 'number' ? `#${e.number}` : null;
905
+ if (run)
906
+ out.push(link(run, numbered ?? SOURCE_NAME[e.type] ?? 'source'));
907
+ // Without a hash there is no text to put on the link but the word itself,
908
+ // and `commit` alone next to `workflow run` says nothing a reader can use.
909
+ if (commitUrl && commit)
910
+ out.push(link(commitUrl, `commit ${firstLine(commit)}`));
911
+ return out;
912
+ };
850
913
  const pointerBlock = (e) => {
851
- const url = sourceUrl(e);
914
+ const sources = sourceLinks(e);
852
915
  const logs = 'logs' in e ? e.logs : undefined;
853
916
  const rows = [
854
917
  fieldCode('Log', logs),
855
918
  fieldCode('Check', e.check),
856
- url ? `<b>Source:</b> <a href="${esc(url)}">${esc(SOURCE_NAME[e.type] ?? 'source')}</a>` : null
919
+ sources.length > 0 ? `<b>Source:</b> ${sources.join(' · ')}` : null
857
920
  ].filter((r) => r !== null);
858
921
  return rows.length > 0 ? `\n\n${rows.join('\n')}` : '';
859
922
  };
@@ -868,8 +931,23 @@ const cutMarker = (e) => {
868
931
  if (e.path) {
869
932
  return '⋯ cut, full text attached';
870
933
  }
934
+ // A bare `⋯ cut` announces a loss and then says nothing about it. The owner
935
+ // met one on a live PR card (31.08.2026) — a description long enough to be
936
+ // clamped — and asked what the three dots even referred to. The card had a
937
+ // `Source` link the whole time: the rest was one tap away and the marker
938
+ // never said so.
939
+ //
940
+ // So the marker NAMES the row that holds the rest, and never repeats its
941
+ // value: it used to print the log path in full, and the pointer block then
942
+ // printed the same path again on the very next line.
943
+ //
944
+ // `below` is literal, not a figure of speech: the marker closes the clamped
945
+ // body and the pointer block is appended after it.
871
946
  const logs = 'logs' in e ? e.logs : undefined;
872
- return logs ? `⋯ cut, full: <code>${esc(logs)}</code>` : '⋯ cut';
947
+ if (logs) {
948
+ return '⋯ cut, full text at Log below';
949
+ }
950
+ return sourceLinks(e).length > 0 ? '⋯ cut, full text at Source below' : '⋯ cut';
873
951
  };
874
952
  /**
875
953
  * Renders an event into finished HTML text, cut to Telegram's limit.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikitasazan/notify",
3
- "version": "1.12.1",
3
+ "version": "1.14.0",
4
4
  "description": "Единая типизированная отправка Telegram-уведомлений (форум-темы, маршрутизация, ретраи) для всех проектов",
5
5
  "type": "module",
6
6
  "license": "MIT",