@cowwo/dsh-icon-custom 0.12.0 → 0.15.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/README.zh.md CHANGED
@@ -58,7 +58,7 @@ SVG 会在浏览器里先栅格化成 512×512 PNG 再上传,ICO 则由后端提
58
58
  规则:
59
59
 
60
60
  - 数字 = **几个会话**在等你(不是几件事);同一个会话只占一格。
61
- - 只有子代理会话不计。**你正开着的那个会话也算**——窗口可能被最小化、页面可能切到后台,这里无从判断你在不在看,所以不做这个区分;**要清掉它,切走再切回来即可**(你人已经在里面时又结束的那一轮不会自动消,见下一条)。
61
+ - 只有子代理会话不计。**什么算"你在看"**(0.12.1 起):那个会话必须真的显示在主区域的对话里(切到「插件 / 设置」这类页面不算)、浏览器标签页在前台(切到别的标签页或最小化不算)、且屏幕上就是它——残留的"最近打开过"不再算数。**你正开着的那个会话也算**;**要清掉它,切走再切回来即可**(你人已经在里面时又结束的那一轮不会自动消,见下一条)。
62
62
  - **已读记在宿主**(`$DSH_HOME/custom-favicon/unread-seen.json`):进过 = 处理过,刷新/重启不会"复活",**任何一台设备上看过,所有设备都不再提醒**(开着的页面最多约一分钟收敛,刷新则立刻)。浏览器自己也留一份,但只当缓存兼离线队列——所以清站点数据、换浏览器、无痕窗口都不会把"已读"丢回去。装插件之前的历史结束也不算。
63
63
  - **可以要求"待够才算已读"**:设置页填一个秒数(默认 0)。填 0 = 进去就算已读(红点立即消失);填 N = 你要在那个会话里**连续待满 N 秒**它才算看过,中途切走就清零重数,刷新页面也重新数。上限 600 秒。
64
64
  **计时只认"你进入会话时已经存在的那次结束"**:你人已经在会话里的时候它又跑完一轮,那一轮**不算数**——红点会一直亮着,等你**切走再进来**才消。这样"我正看着的会话跑完了"这件事不会被几秒钟后自动抹掉(0.11.1 起;此前是"停留满 N 秒就把最新那次结束也标成已读",红点只闪一下)。
package/client/client.js CHANGED
@@ -80,7 +80,7 @@ window.__ModuleLoader__.load({
80
80
  badgeTestHint: "真实未读 = 有会话发生了上面勾选的情况、而且你还没看过它(打开该会话即视为已读)。你正开着的会话也算——窗口可能被最小化、页面可能切到后台,这里无从判断你在不在看,所以不做这个区分。只有子代理不计。手动模式:自己填数字试看效果。刷新页面后回到真实未读。",
81
81
  clearDelayLabel: "进入会话后多久算已读:",
82
82
  clearDelayUnit: "秒",
83
- clearDelayHint: "填 0 = 进去就算已读(红点立即消失)。大于 0 时,你要在那个会话里连续待满这么多秒它才算看过:中途切走会清零重数,刷新页面也重新数。**计时只认\"你进入会话时已经存在的结束\"**——你人已经在里面的时候又跑完一轮,那一轮不算数:它会一直亮着,等你切走再进来才会清。上限 600 秒。已读记在宿主($DSH_HOME/custom-favicon/unread-seen.json):任何一台设备上看过,所有设备都不再提醒;浏览器那份只是缓存,清站点数据不会把已读丢回去。"
83
+ clearDelayHint: "填 0 = 进去就算已读(红点立即消失)。大于 0 时,你要在那个会话里连续待满这么多秒它才算看过:中途切走会清零重数,刷新页面也重新数。**计时只认\"你进入会话时已经存在的结束\"**——你人已经在里面的时候又跑完一轮,那一轮不算数:它会一直亮着,等你切走再进来才会清。上限 600 秒。**只有它真的显示在屏幕上、且标签页在前台时才算看过**:切到「插件/设置」页或把标签页切到后台都不算。已读记在宿主($DSH_HOME/custom-favicon/unread-seen.json):任何一台设备上看过,所有设备都不再提醒;浏览器那份只是缓存,清站点数据不会把已读丢回去。"
84
84
  };
85
85
  const en = {
86
86
  nav: "Favicon",
@@ -153,7 +153,7 @@ window.__ModuleLoader__.load({
153
153
  badgeTestHint: "Real unread = a session ended for one of the checked reasons and you have not looked at it yet (opening a session marks it read). The session you are viewing counts too — the window may be minimised or the page in the background, so this half cannot tell whether you are looking, and does not pretend to. Only sub-agents never count. Manual = type a number to preview. Resets to Real on reload.",
154
154
  clearDelayLabel: "Mark read after staying:",
155
155
  clearDelayUnit: "seconds",
156
- clearDelayHint: "0 = read as soon as you enter (the dot clears at once). Above 0 you must stay in that session for this many seconds before it counts as read: leaving it resets the clock, and so does a reload. The clock only ever covers **the ending that was already there when you entered** — a turn that finishes while you are sitting in the session does not count: it stays red until you leave and come back. Capped at 600 seconds. Read state lives on the Host ($DSH_HOME/custom-favicon/unread-seen.json): read on any device means read on every device, and the browser's own copy is only a cache — clearing site data no longer throws it away."
156
+ clearDelayHint: "0 = read as soon as you enter (the dot clears at once). Above 0 you must stay in that session for this many seconds before it counts as read: leaving it resets the clock, and so does a reload. The clock only ever covers **the ending that was already there when you entered** — a turn that finishes while you are sitting in the session does not count: it stays red until you leave and come back. Capped at 600 seconds. It only counts while that session is really on screen in the foreground tab: a settings or plugins page, or a tab in the background, is not \"being looked at\". Read state lives on the Host ($DSH_HOME/custom-favicon/unread-seen.json): read on any device means read on every device, and the browser's own copy is only a cache — clearing site data no longer throws it away."
157
157
  };
158
158
  //#endregion
159
159
 
@@ -562,23 +562,62 @@ window.__ModuleLoader__.load({
562
562
  }
563
563
  return changed;
564
564
  }
565
+ /**
566
+ * Whether this page is on screen at all.
567
+ *
568
+ * A hidden tab is not "being looked at": the clock must not run while the page
569
+ * is in the background, or a session left open in another tab would be marked
570
+ * read for every device. Unknown (an exotic embedder) counts as visible, which
571
+ * is the pre-0.12.1 behaviour.
572
+ * @returns whether the page is visible.
573
+ */
574
+ function documentVisible() {
575
+ try { return document.visibilityState !== "hidden"; } catch { return true; }
576
+ }
577
+ /**
578
+ * The session whose conversation the page is rendering right now, from the DOM.
579
+ *
580
+ * This is the only signal that names the session on screen, and it is the one
581
+ * that keeps a stale retention from clearing a dot: `retainedBy.mainView` means
582
+ * "recently opened, not yet released", not "displayed". `undefined` means this
583
+ * build carries no such marker (or the probe failed) and callers must fall back
584
+ * to the retention answer rather than treat it as "nothing is displayed".
585
+ * @returns the session id, or undefined when the page cannot say.
586
+ */
587
+ function displayedSessionId() {
588
+ try {
589
+ const node = document.querySelector("[data-conversation-session]");
590
+ if (node === null) return undefined;
591
+ const id = node.getAttribute("data-conversation-session");
592
+ return typeof id === "string" && id !== "" ? id : undefined;
593
+ } catch { return undefined; }
594
+ }
565
595
  /**
566
596
  * Every session the main view is showing.
567
597
  *
568
- * The official signal is the row's `retainedBy.mainView` retention count, the
569
- * same one ui-layout's DocumentTitle, ui-cordis, ui-open-in-app and ui-session
570
- * read (`list.current` is honoured first for runtimes that expose it).
598
+ * `view` carries what the page itself says about the screen:
599
+ * * `panelActive` — a global panel (settings, plugins, …) replaced the
600
+ * conversation. DSH's own sidebar treats that as "no current session"
601
+ * (`usePanelInfo((info) => info.activePanelId !== null)`), and following it
602
+ * is what fixes the "cleared a dot I never opened" report: the conversation
603
+ * is simply not on screen.
604
+ * * `visible` — whether this page is on screen at all.
605
+ * * `displayed` — the session the conversation is rendering, when the DOM can
606
+ * name it. That is the precise answer and it wins over retention.
571
607
  *
572
- * It returns ALL of them, not one. The pane retains the incoming session
573
- * BEFORE it releases the outgoing one, and a retention that outlives its pane
574
- * can leave an old session looking "current" for good — and picking a single
575
- * id then meant the session you were actually reading was never marked read:
576
- * the dot stayed red however long you stayed in it. Each retained session
577
- * gets its own stay clock instead.
608
+ * Retention (`retainedBy.mainView`) stays as the fallback: it is what shipped
609
+ * before, it keeps working on a build without the newer signals, and its known
610
+ * weakness (a release that lags behind the pane) only costs a false "read" when
611
+ * nothing better is available.
578
612
  * @param list - the session list snapshot (may be absent).
613
+ * @param view - the screen report described above (may be absent).
579
614
  * @returns the ids being viewed, in list order; `[]` when there are none.
580
615
  */
581
- function currentSessionIds(list) {
616
+ function currentSessionIds(list, view) {
617
+ const screen = view !== null && typeof view === "object" ? view : {};
618
+ if (screen.visible === false) return [];
619
+ if (screen.panelActive === true) return [];
620
+ if (typeof screen.displayed === "string" && screen.displayed !== "") return [screen.displayed];
582
621
  if (list === null || typeof list !== "object") return [];
583
622
  if (typeof list.current === "string" && list.current !== "") return [list.current];
584
623
  const byId = list.byId;
@@ -1688,7 +1727,21 @@ window.__ModuleLoader__.load({
1688
1727
  pruneSeenState();
1689
1728
  saveSeenState();
1690
1729
  }, [list]);
1691
- const currentIds = currentSessionIds(list);
1730
+ // What the page says about the screen, refreshed on the two moments that can
1731
+ // change it without the session list changing: a global panel replacing the
1732
+ // conversation (its own standard prop, the same one the shipped sidebar reads)
1733
+ // and the tab going to the background. Both are read here, not inferred, so
1734
+ // "you are looking at it" stops being a guess about stale retention.
1735
+ const panelActive = typeof props.usePanelInfo === "function"
1736
+ ? props.usePanelInfo((info) => info !== null && typeof info === "object" && info.activePanelId !== null)
1737
+ : false;
1738
+ const [visible, setVisible] = React.useState(documentVisible);
1739
+ React.useEffect(() => {
1740
+ const onVisibility = () => setVisible(documentVisible());
1741
+ window.addEventListener("visibilitychange", onVisibility);
1742
+ return () => window.removeEventListener("visibilitychange", onVisibility);
1743
+ }, []);
1744
+ const currentIds = currentSessionIds(list, { panelActive, visible, displayed: displayedSessionId() });
1692
1745
  const items = collectUnread(list, pending, config, seenState.seen);
1693
1746
  const count = items.length;
1694
1747
  // Being in a session is what marks it read — and the clock that does it is
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cowwo/dsh-icon-custom",
3
- "version": "0.12.0",
3
+ "version": "0.15.0",
4
4
  "description": "自定义浏览器标签页图标:在设置页上传 SVG/PNG/ICO,立即生效并持久保存,自动适配安卓/iOS/平板 PWA 图标,并可把未读数字显示为系统应用图标角标(Windows 任务栏)。· Custom browser tab favicon: upload SVG/PNG/ICO from the settings page, applies instantly and persists, auto-sizes PWA icons for Android/iOS/tablets, and projects the unread count onto the system app-icon badge (Windows taskbar).",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",