/** * comment-anonymous —— 「悄悄话」开关(内容仅博主可见的评论) * * 用户拍板的语义:**只藏内容,不藏身份**。勾选后发表的评论 * - 头像 / 昵称 / 头衔 / 等级 / IP 归属照常显示; * - 内容被服务端从公开接口剥空(F12 / 网络面板拿不到原文), * 前端识别空内容后画一条「仅博主可见」占位; * - 博主自己(后台 / 登录态 / 邮件通知)看得到全文。 * 回复悄悄话时勾不勾都行 —— 想公开回还是悄悄回自己定,不做默认勾选。 * * 实现要点: * - 开关放在底栏左侧(.atk-bottom-left)里、表情/预览按钮**右边**, * 和它们同一行(用户指定位置);短文案 + 小号 checkbox,移动端不挤。 * - Artalk 是 editorTravel 单实例游走,全站只有一个活的 .atk-main-editor, * fetch 拦截读它的开关状态即可。 * - 借已有的 fetch 拦截链给 POST /api/v2/comments 的 body 注入 anonymous:true * (字段名沿用 is_anonymous 列,语义已升级为悄悄话)。 * - 占位识别:服务端剥空后 .atk-content 是空的 —— 正常评论内容永不为空 * (后端拒绝空评论),空内容即悄悄话,不需要解析 API 也能对上 DOM。 */ (function () { if (window.__atkAnonReady) return; // PJAX 重复执行防抖 window.__atkAnonReady = true; var TIP = '勾选后,内容只有博主能看到(头像和昵称照常显示)'; // 取「当前活着的」编辑器里的开关:优先可见的(editorTravel 游走的那个) function currentToggle() { var eds = document.querySelectorAll('.atk-main-editor'); var fallback = null; for (var i = 0; i < eds.length; i++) { var cb = eds[i].querySelector('.atk-whisper-toggle input'); if (!cb) continue; if (eds[i].offsetParent !== null) return cb; // 可见编辑器优先 if (!fallback) fallback = cb; } return fallback; } function inject(editor) { if (!editor || !editor.querySelector) return; if (editor.querySelector('.atk-whisper-toggle')) return; // 已注入 var left = editor.querySelector('.atk-bottom-left'); if (!left) return; // 底栏没就绪,等下轮扫描 var label = document.createElement('label'); label.className = 'atk-whisper-toggle'; label.title = TIP; var cb = document.createElement('input'); cb.type = 'checkbox'; var txt = document.createElement('span'); txt.textContent = '悄悄话'; label.appendChild(cb); label.appendChild(txt); // 插在表情/预览按钮组(.atk-plug-btn-wrap)的右边 var plugWrap = left.querySelector('.atk-plug-btn-wrap'); if (plugWrap && plugWrap.nextSibling) { left.insertBefore(label, plugWrap.nextSibling); } else { left.appendChild(label); } } // ── 悄悄话占位:服务端已把内容剥空,识别 = 内容区为空 ──────────────── function contentElOf(comment) { var main = comment.querySelector('.atk-main'); if (!main) return null; // 评论正文是 .atk-main 的直接子级;跳过编辑器回复预览里的 .atk-content var el = main.querySelector(':scope > .atk-content'); if (!el) { var all = main.querySelectorAll('.atk-content'); for (var i = 0; i < all.length; i++) { if (!all[i].closest('.atk-reply-to') && !all[i].closest('.atk-main-editor')) { el = all[i]; break; } } } return el; } function isEmptyContent(el) { if (!el) return true; // 有图/视频/占位的一律不算空(纯表情评论也有 ,不会误伤) if (el.querySelector('img, video, iframe, .atk-whisper-note')) return false; return el.textContent.trim() === '' && el.children.length === 0; } function maskComment(node) { if (node.__atkWhisperMasked) return; var content = contentElOf(node); if (!isEmptyContent(content)) return; // 有内容 → 不是悄悄话 node.__atkWhisperMasked = true; node.classList.add('atk-whisper-comment'); if (!content) { content = document.createElement('div'); content.className = 'atk-content'; var main = node.querySelector('.atk-main'); if (main) main.appendChild(content); } if (!content.querySelector('.atk-whisper-note')) { var note = document.createElement('div'); note.className = 'atk-whisper-note'; note.textContent = '悄悄话 · 内容仅博主可见'; while (content.firstChild) content.removeChild(content.firstChild); content.appendChild(note); } } function fullScan() { try { var comments = document.querySelectorAll('.atk-comment'); for (var i = 0; i < comments.length; i++) maskComment(comments[i]); } catch (e) {} } var scanTimer = null; function scheduleScan() { if (scanTimer) return; scanTimer = setTimeout(function () { scanTimer = null; try { var eds = document.querySelectorAll('.atk-main-editor'); for (var i = 0; i < eds.length; i++) inject(eds[i]); fullScan(); // 悄悄话占位,共用这一个扫描节奏 } catch (e) {} }, 120); } try { // 编辑器由 Artalk 动态创建(PJAX / 点回复时游走重建位置),观察兜底 new MutationObserver(scheduleScan).observe(document.body, { childList: true, subtree: true }); } catch (e) {} // MutationObserver 不监听「隐藏节点变可见」,用点击/聚焦兜底扫一遍 document.addEventListener('click', scheduleScan, true); document.addEventListener('focusin', scheduleScan, true); scheduleScan(); // ── fetch 拦截:给新建评论请求注入 anonymous 标志 ────────────────────── // Artalk 的提交走 fetch + JSON body;链上已有别的拦截器(提交反馈/失败 // 翻译),这里只改 init.body,然后原样放行,互不干扰。 var origFetch = window.fetch; if (typeof origFetch === 'function') { window.fetch = function (input, init) { try { var url = String(typeof input === 'string' ? input : (input && input.url) || ''); var method = String((init && init.method) || (input && input.method) || 'GET').toUpperCase(); if (method === 'POST' && /\/api\/v2\/comments(\?|$)/.test(url) && init && typeof init.body === 'string') { var cb = currentToggle(); if (cb && cb.checked) { var data = JSON.parse(init.body); if (data && typeof data === 'object' && !data.anonymous) { data.anonymous = true; init = Object.assign({}, init, { body: JSON.stringify(data) }); } } } } catch (e) {} // 解析失败就按原样发,绝不能因为悄悄话开关挂掉评论 return origFetch.apply(this, [input, init]); }; } })();