diff --git a/addon/chrome/content/icons/in-link-16.svg b/addon/chrome/content/icons/in-link-16.svg new file mode 100644 index 0000000..2faf4cb --- /dev/null +++ b/addon/chrome/content/icons/in-link-16.svg @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/addon/chrome/content/icons/in-link-20.svg b/addon/chrome/content/icons/in-link-20.svg new file mode 100644 index 0000000..375aa39 --- /dev/null +++ b/addon/chrome/content/icons/in-link-20.svg @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/addon/chrome/content/icons/out-link-16.svg b/addon/chrome/content/icons/out-link-16.svg new file mode 100644 index 0000000..81b1dff --- /dev/null +++ b/addon/chrome/content/icons/out-link-16.svg @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/addon/chrome/content/icons/out-link-20.svg b/addon/chrome/content/icons/out-link-20.svg new file mode 100644 index 0000000..d9a993c --- /dev/null +++ b/addon/chrome/content/icons/out-link-20.svg @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/addon/chrome/content/relationGraph.html b/addon/chrome/content/relationGraph.html index 84d2a81..1296dcc 100644 --- a/addon/chrome/content/relationGraph.html +++ b/addon/chrome/content/relationGraph.html @@ -12,13 +12,6 @@ background: var(--material-background); } - .tooltip { - position: absolute; - left: 0; - top: 0; - margin: 10px; - } - .node:hover { cursor: pointer; } @@ -54,13 +47,13 @@ } // Specify the color scale. - const color = d3.scaleOrdinal([1, 2], ["grey", "#e8af59"]); + const color = d3.scaleOrdinal([1, 2], ["grey", "#A88F6A"]); // The force simulation mutates links and nodes, so create a copy // so that re-evaluating this cell produces the same result. const links = data.links.map((d) => ({ ...d })); const nodes = data.nodes.map((d) => ({ ...d })); - + const linkColor = "#e8af59"; // Create a simulation with several forces. const simulation = d3 .forceSimulation(nodes) @@ -83,15 +76,34 @@ .attr("viewBox", [-width / 2, -height / 2, width, height]) .attr("style", "max-width: 100%; height: auto;"); - // Add a line for each link, and a circle for each node. + svg + .append("defs") + .append("marker") + .attr("id", "arrowhead") + .attr("refX", 25) + .attr("refY", 6) + .attr("orient", "auto-start-reverse") + .attr("markerWidth", 20) + .attr("markerHeight", 12) + .attr("markerUnits", "userSpaceOnUse") + .append("path") + .attr("d", "M 1 1 L 18 6 L 1 11 Z") + .attr("fill", linkColor) + .attr("stroke", linkColor) + .attr("class", "arrow-head"); + const link = svg .append("g") - .attr("stroke", "#999") - .attr("stroke-opacity", 0.6) - .selectAll("line") + .attr("fill", "none") + .selectAll("path") .data(links) .join("line") - .attr("stroke-width", (d) => Math.sqrt(d.value)); + .attr("stroke-width", (d) => Math.sqrt(d.value)) + .attr("stroke", linkColor) + .attr("marker-start", (d) => + d.type === "both" ? "url(#arrowhead)" : "", + ) + .attr("marker-end", "url(#arrowhead)"); const node = svg .append("g") @@ -114,7 +126,7 @@ .attr("text-anchor", "middle") // Ensure the text is centered .attr("fill", "var(--text-color)") .attr("stroke", "none") - .text((d) => d.title); + .text((d) => (d.group === 1 ? "" : d.shortTitle)); node.append("title").text((d) => d.title); @@ -168,7 +180,9 @@ .transition() .duration(200) .attr("r", 10); // New, larger radius - d3.select(".tooltip").style("display", "").html(d.title); + d3.select(this) + .select("text") + .text((d) => d.title); }) .on("mouseout", function (event, d) { // Shrink the node circle back to original size @@ -177,7 +191,9 @@ .transition() .duration(500) .attr("r", 7); // Original radius - d3.select(".tooltip").style("display", "none"); + d3.select(this) + .select("text") + .text((d) => (d.group === 1 ? "" : d.shortTitle)); }) .on("click", function (event, d) { window.postMessage( @@ -186,14 +202,7 @@ ); }); - document.body.replaceChildren( - svg.node(), - d3 - .create("div") - .attr("class", "tooltip") - .style("display", "none") - .node(), - ); + document.body.replaceChildren(svg.node()); } d3.select(window).on("resize", function () { diff --git a/addon/chrome/content/styles/related.css b/addon/chrome/content/styles/related.css index ee3abdf..132b974 100644 --- a/addon/chrome/content/styles/related.css +++ b/addon/chrome/content/styles/related.css @@ -2,60 +2,76 @@ bn-related-box { display: flex; flex-direction: column; gap: 2px; + + &[hidden] { + display: none; + } + + &[readonly] { + .add { + display: none; + } + } } -bn-related-box[hidden] { - display: none; -} -bn-related-box[readonly] .add { - display: none; -} -bn-related-box .body { + +bn-related-box .body, +item-pane-custom-section .bn-link-body { display: flex; flex-direction: column; padding-inline-start: 12px; -} -bn-related-box .body .row { - display: flex; - gap: 4px; - align-items: flex-start; -} -[zoteroUIDensity="comfortable"] bn-related-box .body .row { - padding-block: 2px; -} -bn-related-box .body .row .box { - display: flex; - align-items: flex-start; - gap: 4px; - padding-inline-start: 4px; - overflow: hidden; - border-radius: 5px; - flex: 1; -} -bn-related-box .body .row .box:not([disabled]):hover { - background-color: var(--fill-quinary); -} -bn-related-box .body .row .box:not([disabled]):active { - background-color: var(--fill-quarternary); -} -bn-related-box .body .row .box .icon { - height: calc(1.3333333333 * var(--zotero-font-size)); -} -bn-related-box .body .row .box .label { - display: -webkit-box; - -webkit-box-orient: vertical; - -webkit-line-clamp: 10; - width: 0; - flex: 1; - overflow: hidden; -} -bn-related-box .body .row .box .icon, -bn-related-box .body .row .box .label { - padding-block: 2px; -} -bn-related-box .body .row toolbarbutton { - margin-inline-start: auto; - visibility: hidden; -} -bn-related-box .body .row:is(:hover, :focus-within) toolbarbutton { - visibility: visible; + + .row { + display: flex; + gap: 4px; + align-items: flex-start; + + [zoteroUIDensity="comfortable"] & { + padding-block: 2px; + } + + .box { + display: flex; + align-items: flex-start; + gap: 4px; + padding-inline-start: 4px; + overflow: hidden; + border-radius: 5px; + + &:not([disabled]):hover { + background-color: var(--fill-quinary); + } + + &:not([disabled]):active { + background-color: var(--fill-quarternary); + } + + .icon { + height: calc(1.3333333333 * var(--zotero-font-size)); + } + + .label { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 10; + width: 0; + flex: 1; + overflow: hidden; + } + + .icon, + .label { + padding-block: 2px; + } + flex: 1; + } + + toolbarbutton { + margin-inline-start: auto; + visibility: hidden; + } + + &:is(:hover, :focus-within) toolbarbutton { + visibility: visible; + } + } } diff --git a/addon/locale/en-US/noteRelation.ftl b/addon/locale/en-US/noteRelation.ftl index 8efeb8f..6b3705c 100644 --- a/addon/locale/en-US/noteRelation.ftl +++ b/addon/locale/en-US/noteRelation.ftl @@ -4,3 +4,25 @@ note-relation-sidenav = .tooltiptext = Relation Graph note-relation-refresh = .tooltiptext = Refresh + +note-inlink-header = + .label = + { $count -> + [one] { $count } Inbound Link + *[other] { $count } Inbound Links + } +note-inlink-sidenav = + .tooltiptext = Inbound Links +note-inlink-refresh = + .tooltiptext = Refresh + +note-outlink-header = + .label = + { $count -> + [one] { $count } Outbound Link + *[other] { $count } Outbound Links + } +note-outlink-sidenav = + .tooltiptext = Outbound Links +note-outlink-refresh = + .tooltiptext = Refresh diff --git a/addon/locale/it-IT/noteRelation.ftl b/addon/locale/it-IT/noteRelation.ftl index 8efeb8f..6b3705c 100644 --- a/addon/locale/it-IT/noteRelation.ftl +++ b/addon/locale/it-IT/noteRelation.ftl @@ -4,3 +4,25 @@ note-relation-sidenav = .tooltiptext = Relation Graph note-relation-refresh = .tooltiptext = Refresh + +note-inlink-header = + .label = + { $count -> + [one] { $count } Inbound Link + *[other] { $count } Inbound Links + } +note-inlink-sidenav = + .tooltiptext = Inbound Links +note-inlink-refresh = + .tooltiptext = Refresh + +note-outlink-header = + .label = + { $count -> + [one] { $count } Outbound Link + *[other] { $count } Outbound Links + } +note-outlink-sidenav = + .tooltiptext = Outbound Links +note-outlink-refresh = + .tooltiptext = Refresh diff --git a/addon/locale/ru-RU/noteRelation.ftl b/addon/locale/ru-RU/noteRelation.ftl index 8efeb8f..6b3705c 100644 --- a/addon/locale/ru-RU/noteRelation.ftl +++ b/addon/locale/ru-RU/noteRelation.ftl @@ -4,3 +4,25 @@ note-relation-sidenav = .tooltiptext = Relation Graph note-relation-refresh = .tooltiptext = Refresh + +note-inlink-header = + .label = + { $count -> + [one] { $count } Inbound Link + *[other] { $count } Inbound Links + } +note-inlink-sidenav = + .tooltiptext = Inbound Links +note-inlink-refresh = + .tooltiptext = Refresh + +note-outlink-header = + .label = + { $count -> + [one] { $count } Outbound Link + *[other] { $count } Outbound Links + } +note-outlink-sidenav = + .tooltiptext = Outbound Links +note-outlink-refresh = + .tooltiptext = Refresh diff --git a/addon/locale/tr-TR/noteRelation.ftl b/addon/locale/tr-TR/noteRelation.ftl index 8efeb8f..6b3705c 100644 --- a/addon/locale/tr-TR/noteRelation.ftl +++ b/addon/locale/tr-TR/noteRelation.ftl @@ -4,3 +4,25 @@ note-relation-sidenav = .tooltiptext = Relation Graph note-relation-refresh = .tooltiptext = Refresh + +note-inlink-header = + .label = + { $count -> + [one] { $count } Inbound Link + *[other] { $count } Inbound Links + } +note-inlink-sidenav = + .tooltiptext = Inbound Links +note-inlink-refresh = + .tooltiptext = Refresh + +note-outlink-header = + .label = + { $count -> + [one] { $count } Outbound Link + *[other] { $count } Outbound Links + } +note-outlink-sidenav = + .tooltiptext = Outbound Links +note-outlink-refresh = + .tooltiptext = Refresh diff --git a/addon/locale/zh-CN/noteRelation.ftl b/addon/locale/zh-CN/noteRelation.ftl index cd201ad..534952e 100644 --- a/addon/locale/zh-CN/noteRelation.ftl +++ b/addon/locale/zh-CN/noteRelation.ftl @@ -4,3 +4,25 @@ note-relation-sidenav = .tooltiptext = 关系图 note-relation-refresh = .tooltiptext = 刷新 + +note-inlink-header = + .label = + { $count -> + [one] { $count }条入链 + *[other] { $count }条入链 + } +note-inlink-sidenav = + .tooltiptext = 入链 +note-inlink-refresh = + .tooltiptext = 刷新 + +note-outlink-header = + .label = + { $count -> + [one] { $count }条出链 + *[other] { $count }条出链 + } +note-outlink-sidenav = + .tooltiptext = 出链 +note-outlink-refresh = + .tooltiptext = 刷新 diff --git a/src/elements/related.ts b/src/elements/related.ts index a0ca49c..64e32e4 100644 --- a/src/elements/related.ts +++ b/src/elements/related.ts @@ -1,6 +1,5 @@ // @ts-nocheck import { config } from "../../package.json"; -import { getPref } from "../utils/prefs"; const RelatedBox = customElements.get("related-box")! as typeof XULElementBase; @@ -68,14 +67,14 @@ export class NoteRelatedBox extends RelatedBox { // Extra button to open note if (relatedItem.isNote()) { - const note = document.createXULElement("toolbarbutton"); - note.addEventListener("command", (event) => { + const openNote = document.createXULElement("toolbarbutton"); + openNote.addEventListener("command", (event) => { const position = event.shiftKey ? "window" : "tab"; Zotero[config.addonRef].hooks.onOpenNote(id, position); }); - note.className = "zotero-clicky zotero-clicky-open-link"; - note.setAttribute("tabindex", "0"); - row.append(note); + openNote.className = "zotero-clicky zotero-clicky-open-link"; + openNote.setAttribute("tabindex", "0"); + row.append(openNote); } if (this.editable) { diff --git a/src/hooks.ts b/src/hooks.ts index 6f127a1..9ab87c3 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -37,8 +37,10 @@ import { initSyncList } from "./modules/sync/api"; import { patchViewItems } from "./modules/viewItems"; import { getFocusedWindow } from "./utils/window"; import { registerNoteRelation } from "./modules/workspace/relation"; -import { getPref } from "./utils/prefs"; +import { getPref, setPref } from "./utils/prefs"; import { closeRelationWorker } from "./utils/relation"; +import { registerNoteInboundLink } from "./modules/workspace/inLink"; +import { registerNoteOutboundLink } from "./modules/workspace/outLink"; async function onStartup() { await Promise.all([ @@ -46,6 +48,7 @@ async function onStartup() { Zotero.unlockPromise, Zotero.uiReadyPromise, ]); + Zotero.Prefs.set("layout.css.nesting.enabled", true, true); initLocale(); ztoolkit.ProgressWindow.setIconURI( "default", @@ -62,6 +65,10 @@ async function onStartup() { registerNoteRelation(); + registerNoteOutboundLink(); + + registerNoteInboundLink(); + initSyncList(); setSyncing(); diff --git a/src/modules/workspace/inLink.ts b/src/modules/workspace/inLink.ts new file mode 100644 index 0000000..c8c896c --- /dev/null +++ b/src/modules/workspace/inLink.ts @@ -0,0 +1,125 @@ +import { config } from "../../../package.json"; + +export function registerNoteInboundLink() { + const key = Zotero.ItemPaneManager.registerSection({ + paneID: `bn-note-inbound-link`, + pluginID: config.addonID, + header: { + icon: `chrome://${config.addonRef}/content/icons/in-link-16.svg`, + l10nID: `${config.addonRef}-note-inlink-header`, + }, + sidenav: { + icon: `chrome://${config.addonRef}/content/icons/in-link-20.svg`, + l10nID: `${config.addonRef}-note-inlink-sidenav`, + }, + sectionButtons: [ + { + type: "refreshGraph", + icon: "chrome://zotero/skin/16/universal/sync.svg", + l10nID: `${config.addonRef}-note-inlink-refresh`, + onClick: ({ body, item, setL10nArgs }) => { + renderSection(body, item, makeSetCount(setL10nArgs)); + }, + }, + ], + onInit({ body, refresh }) { + const notifierKey = Zotero.Notifier.registerObserver( + { + notify: (event, type, ids, extraData) => { + const item = Zotero.Items.get(body.dataset.itemID || ""); + if ( + item && + // @ts-ignore + event === "updateBNRelation" && + type === "item" && + (ids as number[]).includes(item.id) + ) { + ztoolkit.log("relation notify refresh", item.id); + refresh(); + } + }, + }, + ["item"], + ); + body.classList.add("bn-link-body"); + body.dataset.notifierKey = notifierKey; + }, + onDestroy({ body }) { + const notifierKey = body.dataset.notifierKey; + if (notifierKey) { + Zotero.Notifier.unregisterObserver(notifierKey); + } + }, + onItemChange: ({ body, item, setEnabled }) => { + if (body.closest("bn-workspace") as HTMLElement | undefined) { + setEnabled(true); + body.dataset.itemID = String(item.id); + return; + } + setEnabled(false); + }, + onRender: () => {}, + onAsyncRender: async ({ body, item, setL10nArgs }) => { + await renderSection(body, item, makeSetCount(setL10nArgs)); + }, + }); +} + +async function renderSection( + body: HTMLElement, + item: Zotero.Item, + setCount: (count: number) => void, +) { + body.replaceChildren(); + const doc = body.ownerDocument; + const inLinks = await addon.api.relation.getNoteLinkInboundRelation(item.id); + for (const linkData of inLinks) { + const fromItem = (await Zotero.Items.getByLibraryAndKeyAsync( + linkData.fromLibID, + linkData.fromKey, + )) as Zotero.Item; + + const row = doc.createElement("div"); + row.className = "row"; + + const icon = ztoolkit + .getGlobal("require")("components/icons") + .getCSSItemTypeIcon("note"); + + const label = doc.createElement("span"); + label.className = "label"; + label.append(fromItem.getNoteTitle()); + + const box = doc.createElement("div"); + box.addEventListener("click", () => handleShowItem(fromItem.id)); + box.className = "box keyboard-clickable"; + box.setAttribute("tabindex", "0"); + box.append(icon, label); + + row.append(box); + + const note = (doc as any).createXULElement("toolbarbutton"); + note.addEventListener("command", (event: MouseEvent) => { + const position = event.shiftKey ? "window" : "tab"; + addon.hooks.onOpenNote(fromItem.id, position); + }); + note.className = "zotero-clicky zotero-clicky-open-link"; + note.setAttribute("tabindex", "0"); + row.append(note); + + body.append(row); + } + + const count = inLinks.length; + setCount(count); +} + +function handleShowItem(id: number) { + ZoteroPane.selectItem(id); +} + +function makeSetCount(setL10nArgs: (str: string) => void) { + return (count: number) => { + setL10nArgs(`{"count": "${count}"}`); + }; +} diff --git a/src/modules/workspace/outLink.ts b/src/modules/workspace/outLink.ts new file mode 100644 index 0000000..69a9f73 --- /dev/null +++ b/src/modules/workspace/outLink.ts @@ -0,0 +1,125 @@ +import { config } from "../../../package.json"; + +export function registerNoteOutboundLink() { + const key = Zotero.ItemPaneManager.registerSection({ + paneID: `bn-note-outbound-link`, + pluginID: config.addonID, + header: { + icon: `chrome://${config.addonRef}/content/icons/out-link-16.svg`, + l10nID: `${config.addonRef}-note-outlink-header`, + }, + sidenav: { + icon: `chrome://${config.addonRef}/content/icons/out-link-20.svg`, + l10nID: `${config.addonRef}-note-outlink-sidenav`, + }, + sectionButtons: [ + { + type: "refreshGraph", + icon: "chrome://zotero/skin/16/universal/sync.svg", + l10nID: `${config.addonRef}-note-outlink-refresh`, + onClick: ({ body, item, setL10nArgs }) => { + renderSection(body, item, makeSetCount(setL10nArgs)); + }, + }, + ], + onInit({ body, refresh, getData }) { + const notifierKey = Zotero.Notifier.registerObserver( + { + notify: (event, type, ids, extraData) => { + const item = Zotero.Items.get(body.dataset.itemID || ""); + if ( + item && + // @ts-ignore + event === "updateBNRelation" && + type === "item" && + (ids as number[]).includes(item.id) + ) { + ztoolkit.log("relation notify refresh", item.id); + refresh(); + } + }, + }, + ["item"], + ); + body.classList.add("bn-link-body"); + body.dataset.notifierKey = notifierKey; + }, + onDestroy({ body }) { + const notifierKey = body.dataset.notifierKey; + if (notifierKey) { + Zotero.Notifier.unregisterObserver(notifierKey); + } + }, + onItemChange: ({ body, item, setEnabled }) => { + if (body.closest("bn-workspace") as HTMLElement | undefined) { + setEnabled(true); + body.dataset.itemID = String(item.id); + return; + } + setEnabled(false); + }, + onRender: () => {}, + onAsyncRender: async ({ body, item, setL10nArgs }) => { + await renderSection(body, item, makeSetCount(setL10nArgs)); + }, + }); +} + +async function renderSection( + body: HTMLElement, + item: Zotero.Item, + setCount: (count: number) => void, +) { + body.replaceChildren(); + const doc = body.ownerDocument; + const inLinks = await addon.api.relation.getNoteLinkOutboundRelation(item.id); + for (const linkData of inLinks) { + const toItem = (await Zotero.Items.getByLibraryAndKeyAsync( + linkData.toLibID, + linkData.toKey, + )) as Zotero.Item; + + const row = doc.createElement("div"); + row.className = "row"; + + const icon = ztoolkit + .getGlobal("require")("components/icons") + .getCSSItemTypeIcon("note"); + + const label = doc.createElement("span"); + label.className = "label"; + label.append(toItem.getNoteTitle()); + + const box = doc.createElement("div"); + box.addEventListener("click", () => handleShowItem(toItem.id)); + box.className = "box keyboard-clickable"; + box.setAttribute("tabindex", "0"); + box.append(icon, label); + + row.append(box); + + const note = (doc as any).createXULElement("toolbarbutton"); + note.addEventListener("command", (event: MouseEvent) => { + const position = event.shiftKey ? "window" : "tab"; + addon.hooks.onOpenNote(toItem.id, position); + }); + note.className = "zotero-clicky zotero-clicky-open-link"; + note.setAttribute("tabindex", "0"); + row.append(note); + + body.append(row); + } + + const count = inLinks.length; + setCount(count); +} + +function handleShowItem(id: number) { + ZoteroPane.selectItem(id); +} + +function makeSetCount(setL10nArgs: (str: string) => void) { + return (count: number) => { + setL10nArgs(`{"count": "${count}"}`); + }; +} diff --git a/src/modules/workspace/relation.ts b/src/modules/workspace/relation.ts index 5fc4a44..b2d47af 100644 --- a/src/modules/workspace/relation.ts +++ b/src/modules/workspace/relation.ts @@ -106,9 +106,10 @@ async function getRelationData(note: Zotero.Item) { const inLink = await addon.api.relation.getNoteLinkInboundRelation(note.id); const outLink = await addon.api.relation.getNoteLinkOutboundRelation(note.id); - const links = []; const noteSet: Set = new Set(); + const linkModels: Record = {}; + for (const linkData of inLink) { const noteItem = await Zotero.Items.getByLibraryAndKeyAsync( linkData.fromLibID, @@ -116,11 +117,18 @@ async function getRelationData(note: Zotero.Item) { ); if (!noteItem) continue; noteSet.add(noteItem.id); - links.push({ - source: noteItem.id, - target: note.id, - value: 1, - }); + let noteLinks = linkModels[noteItem.id]; + if (!noteLinks) { + noteLinks = { + source: noteItem.id, + target: note.id, + value: 1, + type: "in", + }; + linkModels[noteItem.id] = noteLinks; + } else { + noteLinks.value++; + } } for (const linkData of outLink) { @@ -130,28 +138,52 @@ async function getRelationData(note: Zotero.Item) { ); if (!noteItem) continue; noteSet.add(noteItem.id); - links.push({ - source: note.id, - target: noteItem.id, - value: 1, - }); + let noteLinks = linkModels[noteItem.id]; + if (!noteLinks) { + noteLinks = { + source: note.id, + target: noteItem.id, + value: 1, + type: "out", + }; + linkModels[noteItem.id] = noteLinks; + } else { + noteLinks.value++; + if (noteLinks.type === "in") { + noteLinks.type = "both"; + } + } } noteSet.delete(note.id); const nodes = Array.from(noteSet).map((id) => { const item = Zotero.Items.get(id); + const title = item.getNoteTitle(); return { id: item.id, - title: slice(item.getNoteTitle(), 15), + title, + shortTitle: slice(title, 15), group: 2, }; }); + const title = note.getNoteTitle(); nodes.push({ id: note.id, - title: slice(note.getNoteTitle(), 15), + title, + shortTitle: slice(title, 15), group: 1, }); - return { nodes, links }; + return { + nodes, + links: Object.values(linkModels), + }; +} + +interface NoteLinkModal { + source: number; + target: number; + value: number; + type: "in" | "out" | "both"; } diff --git a/src/utils/relation.ts b/src/utils/relation.ts index df26bdf..6ee4188 100644 --- a/src/utils/relation.ts +++ b/src/utils/relation.ts @@ -80,7 +80,7 @@ async function updateNoteLinkRelation(noteID: number) { for (const link of linkMatches) { const { noteItem, libraryID, noteKey, lineIndex, sectionName } = getNoteLinkParams(link); - if (noteItem && noteItem.isNote()) { + if (noteItem && noteItem.isNote() && noteItem.id !== note.id) { affectedNoteIDs.add(noteItem.id); linkToData.push({ fromLibID,