v32 第 32 批(4 条):资源共享弹框真落库 / 消息通知覆盖全部共享入口 / 浏览器通知(站内消息+日程到期提醒) / 首页任务随看板色
用户原话: ①「资源共享功能有问题,弹框中,点击共享给某个人后,弹框消失,但是并没有共享成功。」 ②「消息通知,在所有引用自己的地方,都通知,包括别人分享了日历本给自己, 分享了笔记给自己,分享了资源给自己等等。」 ③「当有消息通知时,还有日历中日程到期提醒时,比如一个日程,设置了10点开始, 提前十分钟提醒,则需要在 9:50 时浏览器中发提醒通知。」 ④「首页,近期任务,任务的文字和前面的圆点,颜色并没有跟看板颜色一致,这个需要修正。」 四条真根因: ① v28 把资源编辑改成三页签后,每个页签各有自己的 form 与 <input name=id>, 而设 id 的代码是 panel.querySelector() → 只命中文档序第一个(info 的 update), ⇒ share/att 表单的 id 恒空 ⇒ 提交 id=0 ⇒ Resource::find(familyId,0,me) 返回 null ⇒ 静默不共享。修:表单打 data-res-idform 标记 + querySelectorAll 逐个填。 ② v30 只在 saveAttendees 后调 syncForTarget,6 个共享入口一个都没接。 新增 Notification::syncShares(家庭,类型,宿主id,动作人) —— 照共享表现状重建差额 (写表后调用;漏调只会没通知,不会状态不一致)。SHARE_KINDS 抽常量。 ③ 完全没有浏览器通知。新增 Calendar::remindersBetween()(窗口留 -2 分钟防后台节流) + ?r=notifications/poll JSON 路由 + assets/js/notify.js(轮询 + Notification API + id@at 去重)+ 顶栏铃铛(权限只能用户点一下才要,自动申请会被 Chrome 静默拒绝)。 ④ v30 把用户那句和主题颜色一样理解成了**任务模块主题色**,用户要的是**看板色**。 新增 Task::boardColorById(家庭,看板id)(没设色按看板名派生稳定色),home.php 两处改 $tkColor。 验证:诊断探针 18/0、真机 25/0(真点共享→库里真的多一行;三类共享真发通知; 9:50 窗口真算出 10:00 那条;首页每条任务色 == 其看板色)、反向 5/0、 e2e 1502/0、升级 153/0、几何 428+680+92、城市 32/0。 部署脚本静态断言段本地空跑通过;线上站点自检 19 条全过,notifications/poll 未登录 302。
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* 浏览器通知(v32 第 3 条)。
|
||||
*
|
||||
* 用户原话:「当有消息通知时,还有日历中日程到期提醒时,比如一个日程,设置了10点开始,
|
||||
* 提前十分钟提醒,则需要在 9:50 时浏览器中发提醒通知。」
|
||||
*
|
||||
* 做两件事:
|
||||
* ① 定期问服务端 `?r=notifications/poll`(**到点与否由 PHP 算**,见 index.php 那段);
|
||||
* ② 把该响的用 Notification API 弹出来(站内消息 + 日程提醒)。
|
||||
*
|
||||
* ────────────────────────────────────────────────────────────────────
|
||||
* 五条设计判断(每条都对应一个"不这么做就会出的问题"):
|
||||
*
|
||||
* ① **权限只在用户"动手"时要,绝不在页面加载时自动弹**。
|
||||
* 浏览器对"未经交互就 requestPermission"的做法会直接**静默拒绝**
|
||||
* (Chrome 的 abusiveness 规则),而且会把本域名拉进"用户不喜欢"的名单。
|
||||
* ⇒ 页头放一个小铃铛按钮(`.fb-notify-enable`),用户点了才要权限。
|
||||
* 用户没开权限时轮询**照常跑**(站内未读数还得更新),只是不弹系统通知。
|
||||
*
|
||||
* ② **同一个提醒只弹一次**。轮询窗口故意留了"过去 2 分钟"(见后端注释:
|
||||
* 防标签页节流导致整条错过),代价是同一档提醒可能连续两轮都落进窗口。
|
||||
* ⇒ 用 `已弹过的 key 集合`(`事件id@提醒时刻`)去重,且集合只在**内存**里 ——
|
||||
* 刷新页面会重弹一次"刚刚那条",这反而是对的(用户刷新说明他还没看到)。
|
||||
*
|
||||
* ③ **点通知要能回到那个东西**。`new Notification(...).onclick` 里
|
||||
* `window.focus()` + `location.href = 链接`。
|
||||
* ⚠️ 用 `tag` 而不是 `new Notification` 的自定义字段:部分浏览器只认 tag,
|
||||
* 不认自定义属性(用 tag 还能让同一件事的通知**合并**而不是堆一列)。
|
||||
*
|
||||
* ④ **后台标签页也要能弹**。`setInterval` 在后台标签页会被浏览器节流到
|
||||
* 1~2 分钟一次(甚至更慢)。所以**轮询间隔取 60 秒**(与节流后的粒度相当),
|
||||
* 并把窗口开到"过去 2 分钟"把这层抖动吸收掉 —— 而不是去和浏览器对抗。
|
||||
*
|
||||
* ⑤ **不用 Service Worker / 推送服务**。那要 HTTPS + 服务端推送通道,
|
||||
* 而用户要的是"页面开着时能提醒"。页面关掉还提醒属于 Web Push 的范畴,
|
||||
* 是另一件事(也超出"站内"的边界,与 Notification 头部第 ⑤条同一取舍)。
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var API = (window.FB_NOTIFY && window.FB_NOTIFY.poll) || '';
|
||||
if (!API) { return; }
|
||||
|
||||
var POLL_MS = 60000; // 见头部第 ④条
|
||||
var seen = {}; // 已弹过的提醒 key(见头部第 ②条)
|
||||
var lastUnread = null; // 上一轮的未读数:只在**变多**时提醒,避免反复骚扰
|
||||
var timer = null;
|
||||
|
||||
function supported() {
|
||||
return (typeof window.Notification === 'function');
|
||||
}
|
||||
|
||||
function enabled() {
|
||||
return supported() && window.Notification.permission === 'granted';
|
||||
}
|
||||
|
||||
/** 弹一条系统通知;点了就回到对应的页面 */
|
||||
function fire(title, body, href, tag, icon) {
|
||||
try {
|
||||
var n = new window.Notification(title, {
|
||||
body: body || '',
|
||||
tag: tag || '', // 同 tag 合并(见头部第 ③条)
|
||||
icon: icon || ''
|
||||
});
|
||||
n.onclick = function () {
|
||||
try { window.focus(); } catch (e) { /* 有些浏览器不允许抢焦点 */ }
|
||||
if (href) { window.location.href = href; }
|
||||
try { n.close(); } catch (e) { /* 已关闭 */ }
|
||||
};
|
||||
// 8 秒后自动收起:提醒条一直在会挡住下一个
|
||||
setTimeout(function () { try { n.close(); } catch (e) { /* 已关 */ } }, 8000);
|
||||
} catch (e) {
|
||||
// 某些浏览器在"页面不可见"时抛(旧实现)—— 忽略,别让它影响轮询
|
||||
}
|
||||
}
|
||||
|
||||
function poll() {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', API, true);
|
||||
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
|
||||
xhr.onload = function () {
|
||||
if (xhr.status !== 200) { return; }
|
||||
var d;
|
||||
try { d = JSON.parse(xhr.responseText); } catch (e) { return; }
|
||||
handle(d);
|
||||
};
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
function handle(d) {
|
||||
if (!d) { return; }
|
||||
|
||||
// ---- 站内消息:只在**未读数变多**时弹一次(否则每轮都弹同样的几条)----
|
||||
var unread = parseInt(d.unread || 0, 10);
|
||||
if (enabled() && unread > 0 && lastUnread !== null && unread > lastUnread) {
|
||||
var first = (d.items && d.items[0]) || null;
|
||||
fire('家书 · 新消息',
|
||||
first ? (first.actor + first.body) : ('你有 ' + unread + ' 条新消息'),
|
||||
(window.FB_NOTIFY && window.FB_NOTIFY.list) || '',
|
||||
'fb-notify', (window.FB_NOTIFY && window.FB_NOTIFY.icon) || '');
|
||||
}
|
||||
lastUnread = unread;
|
||||
|
||||
// ---- 日程提醒:按 `id@at` 去重(见头部第 ②条)----
|
||||
var rs = d.reminders || [];
|
||||
for (var i = 0; i < rs.length; i++) {
|
||||
var r = rs[i];
|
||||
var key = r.id + '@' + r.at + '@' + (r.lead || '');
|
||||
if (seen[key]) { continue; }
|
||||
seen[key] = true;
|
||||
if (!enabled()) { continue; }
|
||||
var t = String(r.starts_at || '').substr(11, 5);
|
||||
fire('日程提醒:' + (r.title || '(无标题)'),
|
||||
(r.lead ? ('还有 ' + r.lead + ' 分钟开始') : '马上开始')
|
||||
+ (t ? ('(' + t + ')') : '') + (r.cal ? (' · ' + r.cal) : ''),
|
||||
r.href || '', 'fb-remind-' + r.id + '-' + (r.lead || ''), '');
|
||||
}
|
||||
}
|
||||
|
||||
/** 页头那个小铃铛:点一下要权限(见头部第 ①条) */
|
||||
function bindButton() {
|
||||
var btn = document.querySelector('.fb-notify-enable');
|
||||
if (!btn) { return; }
|
||||
var paint = function () {
|
||||
var on = enabled();
|
||||
btn.hidden = on || !supported();
|
||||
btn.title = '开启浏览器提醒';
|
||||
btn.setAttribute('aria-label', '开启浏览器提醒');
|
||||
};
|
||||
btn.hidden = enabled() || !supported();
|
||||
btn.addEventListener('click', function () {
|
||||
if (!supported()) { return; }
|
||||
window.Notification.requestPermission().then(function () {
|
||||
paint();
|
||||
if (enabled()) {
|
||||
fire('家书 · 提醒已开启', '有新的消息或日程提醒时,我会在这里通知你。', '', 'fb-on', '');
|
||||
}
|
||||
});
|
||||
});
|
||||
paint();
|
||||
}
|
||||
|
||||
function start() {
|
||||
bindButton();
|
||||
poll(); // 先拉一次(页面刚打开时可能有未读)
|
||||
timer = setInterval(poll, POLL_MS);
|
||||
// 页面重新可见时立刻补一次:标签页在后台被节流,回来时可能已经错过窗口
|
||||
document.addEventListener('visibilitychange', function () {
|
||||
if (!document.hidden) { poll(); }
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', start);
|
||||
} else {
|
||||
start();
|
||||
}
|
||||
|
||||
// 供探针/调试:手动触发一次
|
||||
window.fbNotifyPoll = poll;
|
||||
})();
|
||||
Reference in New Issue
Block a user