@gotcos/glasses-server 7.0.27 → 7.0.30
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/CHANGELOG.md +93 -0
- package/docs/web-key-verifier-contract.md +34 -8
- package/package.json +1 -1
- package/server/index.ts +29 -6
- package/server/lib/provider-failure-text.ts +4 -1
- package/server/lib/tailnet-self-name.ts +57 -10
- package/server/lib/web-key-authority.ts +1 -1
- package/server/lib/web-key-registry.ts +1 -1
- package/server/lib/web-key-signer-app.ts +25 -6
- package/server/lib/web-key-signer-tls.ts +800 -0
- package/server/routes/web-key.ts +31 -2
- package/server/web-control/.manifest.json +16 -16
- package/server/web-control/app.css +58 -2
- package/server/web-control/app.js +478 -51
- package/server/web-control/control.css +32 -2
- package/server/web-control/index.html +46 -5
- package/server/web-control/panes/cos-chat.css +5 -1
- package/server/web-control/panes/cos-chat.js +9 -3
- package/server/web-control/panes/meetings.css +4 -0
- package/server/web-control/panes/meetings.js +25 -6
- package/server/web-control/panes/memories.js +2 -2
- package/server/web-control/panes/messages.js +7 -2
- package/server/web-control/panes/settings.js +1 -1
- package/server/web-control/panes/speakers.js +28 -3
- package/server/web-control/panes/today-actions.js +2 -2
- package/server/web-control/panes/work.js +1 -1
- package/server/web-control/shell.js +2 -1
- package/server/web-control/theme.js +169 -2
- package/server/web-signer/.manifest.json +2 -2
- package/server/web-signer/signer-proof.js +18 -3
- package/server/web-signer/signer.js +4 -2
- package/shared/web-turn-envelope.ts +33 -8
|
@@ -245,9 +245,55 @@
|
|
|
245
245
|
clearTimeout(toastTimer); toastTimer = setTimeout(function () { el.classList.add('hidden'); }, 3200);
|
|
246
246
|
}
|
|
247
247
|
function setStatus(el, text, isError) { if (!el) return; el.textContent = text || ''; el.classList.toggle('error', !!isError); }
|
|
248
|
-
|
|
248
|
+
// 7.0.29 (lead F2): an empty session reply box is as tall as its placeholder, so a placeholder that wraps (the rail's
|
|
249
|
+
// width at 382 px wraps "Message this session (not an answer)") is never cut off. Only #session-input; where the
|
|
250
|
+
// placeholder fits on one line the height is the same as before.
|
|
251
|
+
function autoGrow(ta) {
|
|
252
|
+
ta.style.height = 'auto';
|
|
253
|
+
var h = ta.scrollHeight;
|
|
254
|
+
if (!ta.value && ta.id === 'session-input' && ta.placeholder) h = Math.max(h, placeholderHeight(ta, ta.placeholder));
|
|
255
|
+
ta.style.height = Math.min(h, 140) + 'px';
|
|
256
|
+
}
|
|
257
|
+
// 7.0.29 (lead F2): with the rail at 382 px, "Message this session (not an answer)" wraps; with the keyboard up the box
|
|
258
|
+
// is capped under two lines, so the second line would be cut. Where the long words would wrap in the box's width, the
|
|
259
|
+
// box says the short form instead; checked again when the width changes (railSync). Elsewhere the words are as before.
|
|
260
|
+
function sxPlaceholderFit(box) {
|
|
261
|
+
if (!box || !box.setAttribute || !box.phLong) return;
|
|
262
|
+
var ph = box.phLong;
|
|
263
|
+
if (box.phShort) { var one = placeholderHeight(box, 'M'), two = placeholderHeight(box, box.phLong); if (one > 0 && two > one + 2) ph = box.phShort; }
|
|
264
|
+
if (!box.getAttribute || box.getAttribute('placeholder') !== ph) box.setAttribute('placeholder', ph);
|
|
265
|
+
if (!box.value && box.style) autoGrow(box);
|
|
266
|
+
}
|
|
267
|
+
// The placeholder's height in a hidden copy of the box: same width, font, padding and line height. The box's own value
|
|
268
|
+
// is never touched (setting it would drop its undo history and move the caret).
|
|
269
|
+
var PH_PROBE = null;
|
|
270
|
+
function placeholderHeight(ta, text) {
|
|
271
|
+
try {
|
|
272
|
+
if (typeof getComputedStyle !== 'function' || !document.body || !ta.getBoundingClientRect) return 0;
|
|
273
|
+
var cs = getComputedStyle(ta), w = ta.getBoundingClientRect().width;
|
|
274
|
+
if (!(w > 0)) return 0;
|
|
275
|
+
var c = PH_PROBE;
|
|
276
|
+
if (!c) { c = PH_PROBE = document.createElement('textarea'); c.setAttribute('aria-hidden', 'true'); c.tabIndex = -1; c.rows = 1; }
|
|
277
|
+
c.style.cssText = 'position:absolute;left:-9999px;top:0;visibility:hidden;height:auto;min-height:0;max-height:none;overflow:hidden;resize:none;border:0;' +
|
|
278
|
+
'box-sizing:' + cs.boxSizing + ';width:' + w + 'px;padding:' + cs.paddingTop + ' ' + cs.paddingRight + ' ' + cs.paddingBottom + ' ' + cs.paddingLeft + ';' +
|
|
279
|
+
'font:' + cs.font + ';letter-spacing:' + cs.letterSpacing + ';line-height:' + cs.lineHeight + ';white-space:' + cs.whiteSpace + ';word-break:' + cs.wordBreak + ';';
|
|
280
|
+
c.value = text;
|
|
281
|
+
if (c.parentNode !== document.body) document.body.appendChild(c);
|
|
282
|
+
return c.scrollHeight;
|
|
283
|
+
} catch (_e) { return 0; }
|
|
284
|
+
}
|
|
249
285
|
|
|
250
286
|
// ── API ─────────────────────────────────────────────────
|
|
287
|
+
// 7.0.29 R1 (Q1 FIX 1): a write on its way to the Mac (a send, a save). theme.js's restyle check asks window.CosBusy
|
|
288
|
+
// before it reloads the page, so a reload never lands on a message in flight.
|
|
289
|
+
var WRITES = { n: 0 };
|
|
290
|
+
function writeStart(method) {
|
|
291
|
+
if (!method || method === 'GET') return function () {};
|
|
292
|
+
WRITES.n++;
|
|
293
|
+
var once = false;
|
|
294
|
+
return function () { if (!once) { once = true; WRITES.n = Math.max(0, WRITES.n - 1); } };
|
|
295
|
+
}
|
|
296
|
+
if (typeof window === 'object' && window) window.CosBusy = function () { return WRITES.n > 0 || !!(typeof state === 'object' && state && state.reading && state.reading.sending); };
|
|
251
297
|
function api(path, opts) {
|
|
252
298
|
var o = opts || {};
|
|
253
299
|
var headers = { 'X-COS-Web': '1' };
|
|
@@ -260,7 +306,7 @@
|
|
|
260
306
|
var limit = o.timeoutMs != null ? o.timeoutMs : (method === 'GET' ? API_READ_TIMEOUT_MS : 0);
|
|
261
307
|
var ctrl = limit && window.AbortController ? new AbortController() : null;
|
|
262
308
|
var timer = ctrl ? setTimeout(function () { ctrl.abort(); }, limit) : null;
|
|
263
|
-
var sentAt = Date.now();
|
|
309
|
+
var sentAt = Date.now(), writeDone = typeof writeStart === 'function' ? writeStart(method) : function () {};
|
|
264
310
|
return fetch(path, { method: method, headers: headers, credentials: 'same-origin', body: body, signal: ctrl ? ctrl.signal : undefined }).then(function (res) {
|
|
265
311
|
// Blob bodies keep their deadline after headers: a stalled download must release its queue slot.
|
|
266
312
|
if (timer && o.responseType !== 'blob') clearTimeout(timer);
|
|
@@ -288,9 +334,11 @@
|
|
|
288
334
|
});
|
|
289
335
|
}).then(function (value) {
|
|
290
336
|
if (timer) clearTimeout(timer);
|
|
337
|
+
writeDone();
|
|
291
338
|
return value;
|
|
292
339
|
}, function (err) {
|
|
293
340
|
if (timer) clearTimeout(timer);
|
|
341
|
+
writeDone();
|
|
294
342
|
if (err && err.name === 'AbortError' && ctrl && ctrl.signal.aborted) {
|
|
295
343
|
// 6.99.32 (QA 6.99.30 canary W1): a read the Mac never answered in time means it is not answering. A write has
|
|
296
344
|
// no deadline unless its caller set one, and a slow save is not a Mac that went away, so only reads say so.
|
|
@@ -525,10 +573,187 @@
|
|
|
525
573
|
var hash = '#/' + [pane].concat(args || []).map(encodeURIComponent).join('/');
|
|
526
574
|
if (location.hash === hash) route(); else location.hash = hash;
|
|
527
575
|
}
|
|
576
|
+
// ── Back by path (7.0.30, item 1: Anand on the 11-12 HC call, Miles's pick) ──
|
|
577
|
+
// Back goes where the person came from. Each address this page makes is marked in its own history entry
|
|
578
|
+
// (history.state.cosNav): this page load's id, the entry's position, and the address it came from. Back is then the
|
|
579
|
+
// browser's own Back (history.back), so the system Back and ours agree and Back adds no entry. With no mark (a deep
|
|
580
|
+
// link, a reload, the first page, or an entry an overlay took over), Back keeps the parent chain it always had, and it
|
|
581
|
+
// REPLACES the entry rather than adding one, so the two can never bounce between each other. The image viewer and
|
|
582
|
+
// Today's sheet push entries of their own (cosImageViewer, cosTodayOverlay): those are never marked, never stepped into,
|
|
583
|
+
// and an address made while one of them still sat behind gets no mark, because the entry behind it is not an address
|
|
584
|
+
// (its position is not the last address's position plus one).
|
|
585
|
+
var NAV = { doc: Math.random().toString(36).slice(2, 10), last: null, replace: null };
|
|
586
|
+
function navState() { try { return history.state; } catch (_e) { return null; } }
|
|
587
|
+
function navOverlay(st) { return !!(st && (st.cosImageViewer || st.cosTodayOverlay)); }
|
|
588
|
+
function navStamp() {
|
|
589
|
+
if (typeof history === 'undefined' || !history || typeof location === 'undefined') return; // a page without them: the parent chain
|
|
590
|
+
var hash = location.hash || '', st = navState(), pos;
|
|
591
|
+
if (navOverlay(st)) { NAV.last = null; NAV.replace = null; return; }
|
|
592
|
+
if (st && st.cosNav && st.cosNav.doc === NAV.doc) { NAV.last = { hash: hash, pos: st.cosNav.pos, back: st.cosNav.back, trail: st.cosNav.trail || null, label: st.cosNav.label || '' }; NAV.replace = null; return; }
|
|
593
|
+
if (st && st.cosNav) {
|
|
594
|
+
// Marked by an earlier load of this page (a reload): what lies behind it belongs to that load, so no path.
|
|
595
|
+
pos = st.cosNav.pos;
|
|
596
|
+
} else if (st) { NAV.last = null; NAV.replace = null; return; } // someone else's state: leave it alone
|
|
597
|
+
else pos = NAV.replace ? NAV.replace.pos : history.length - 1;
|
|
598
|
+
var back = null;
|
|
599
|
+
if (!st && !NAV.replace && NAV.last && typeof NAV.last.pos === 'number' && pos === NAV.last.pos + 1) back = NAV.last.hash;
|
|
600
|
+
NAV.replace = null;
|
|
601
|
+
// 7.0.30, Miles's pick B: the steps before this one ride in the same mark, the last of them being where Back goes.
|
|
602
|
+
var trail = back ? (NAV.last.trail || []).concat([{ h: NAV.last.hash, t: NAV.last.label || '' }]).slice(-NAV_TRAIL_MAX) : null;
|
|
603
|
+
var mark = { doc: NAV.doc, pos: pos, back: back };
|
|
604
|
+
if (trail) mark.trail = trail;
|
|
605
|
+
try { history.replaceState(Object.assign({}, st || {}, { cosNav: mark }), ''); } catch (_e) { /* no history API: the parent chain */ }
|
|
606
|
+
NAV.last = { hash: hash, pos: pos, back: back, trail: trail, label: '' };
|
|
607
|
+
}
|
|
608
|
+
var NAV_TRAIL_MAX = 12;
|
|
609
|
+
// The page's own name, kept in its mark, so the next page's trail can name it. setCrumb calls this.
|
|
610
|
+
function navLabel(text) {
|
|
611
|
+
var st = navState();
|
|
612
|
+
if (navOverlay(st) || !st || !st.cosNav || st.cosNav.doc !== NAV.doc || (st.cosNav.label || '') === text) return;
|
|
613
|
+
try { history.replaceState(Object.assign({}, st, { cosNav: Object.assign({}, st.cosNav, { label: text }) }), ''); } catch (_e) { return; }
|
|
614
|
+
if (NAV.last && NAV.last.pos === st.cosNav.pos) NAV.last.label = text;
|
|
615
|
+
}
|
|
616
|
+
// The way he came, oldest first, from this entry's mark: [{ label, hash, back }], back being how many steps back that
|
|
617
|
+
// one is. Null when Back has no path (a deep link, a reload, the first page, an overlay).
|
|
618
|
+
function navTrail() {
|
|
619
|
+
var st = navState();
|
|
620
|
+
if (navOverlay(st) || !st || !st.cosNav || !st.cosNav.back || !st.cosNav.trail || !st.cosNav.trail.length) return null;
|
|
621
|
+
var t = st.cosNav.trail;
|
|
622
|
+
return t.map(function (s, i) { return { label: trailName(s.h, s.t), hash: s.h, back: t.length - i }; });
|
|
623
|
+
}
|
|
624
|
+
// A step is named by what kind of page it is, not its title (the title is the heading under the row): "Meeting",
|
|
625
|
+
// "Session", "Naming voices". A list is its pane. A message, a day and a chat keep their own short name.
|
|
626
|
+
function trailName(hash, crumb) {
|
|
627
|
+
var a = String(hash || '').replace(/^#\/?/, '').split('/').filter(function (x) { return x !== ''; }), p = ROOT_PANES.indexOf(a[0]) >= 0 ? a[0] : 'today';
|
|
628
|
+
if (a.length < 2 || isTabRoute(a.slice(1))) return TITLES[p] || 'Today';
|
|
629
|
+
if (p === 'meetings') return 'Meeting';
|
|
630
|
+
if (p === 'sessions') return 'Session';
|
|
631
|
+
if (p === 'speakers' && a[1] === 'review') return 'Naming voices';
|
|
632
|
+
return crumb || TITLES[p] || 'Today';
|
|
633
|
+
}
|
|
634
|
+
// One line that never wraps. When it is too wide, steps leave from the middle (the one after the first, then the next),
|
|
635
|
+
// never the first, the one before this page, or this page; "…" stands where they were. widths: each step's, then this
|
|
636
|
+
// page's; more: the width "…" adds. Returns the indexes to leave out.
|
|
637
|
+
function trailCut(widths, more, avail) {
|
|
638
|
+
var n = widths.length - 1, total = widths.reduce(function (a, b) { return a + b; }, 0), cut = [];
|
|
639
|
+
if (total <= avail) return cut;
|
|
640
|
+
total += more;
|
|
641
|
+
for (var i = 1; i < n - 1 && total > avail; i++) { cut.push(i); total -= widths[i]; }
|
|
642
|
+
return cut;
|
|
643
|
+
}
|
|
644
|
+
// The row under the top bar, on a phone (below 600 px; wider screens have the navigation bar's crumbs). Only on a page
|
|
645
|
+
// you opened (a reader, a meeting, a session), and only when there is a path.
|
|
646
|
+
function paintTrail(r) {
|
|
647
|
+
var el = typeof document !== 'undefined' && document.getElementById('nav-trail');
|
|
648
|
+
if (!el) return;
|
|
649
|
+
r = r || parseRoute();
|
|
650
|
+
var steps = r.args.length && !isTabRoute(r.args) ? navTrail() : null;
|
|
651
|
+
if (!steps) { el.hidden = true; el.innerHTML = ''; return; }
|
|
652
|
+
var here = trailName(location.hash, crumbDetail);
|
|
653
|
+
el.innerHTML = '<ol>' + steps.map(function (s, i) {
|
|
654
|
+
return '<li class="trail-step"><a href="' + esc(s.hash) + '" data-trail-back="' + s.back + '">' + esc(s.label) + '</a></li>' +
|
|
655
|
+
(i === 0 ? '<li class="trail-more" aria-hidden="true" hidden>…</li>' : '');
|
|
656
|
+
}).join('') + '<li class="trail-here" aria-current="page"><span>' + esc(here) + '</span></li></ol>';
|
|
657
|
+
el.hidden = false;
|
|
658
|
+
var items = Array.prototype.slice.call(el.querySelectorAll('.trail-step,.trail-here')), more = el.querySelector('.trail-more');
|
|
659
|
+
if (!more || !el.clientWidth) return;
|
|
660
|
+
el.classList.add('trail-fit'); // each step at its full width, to measure
|
|
661
|
+
more.hidden = false;
|
|
662
|
+
var moreW = more.offsetWidth, widths = items.map(function (li) { return li.offsetWidth; });
|
|
663
|
+
more.hidden = true;
|
|
664
|
+
el.classList.remove('trail-fit');
|
|
665
|
+
var cut = trailCut(widths, moreW, el.clientWidth);
|
|
666
|
+
// A left-out step stays for a screen reader but takes no keyboard focus (Q1 L1): it is not on screen.
|
|
667
|
+
cut.forEach(function (i) { items[i].classList.add('trail-cut'); var a = items[i].querySelector('a'); if (a) a.setAttribute('tabindex', '-1'); });
|
|
668
|
+
more.hidden = !cut.length;
|
|
669
|
+
}
|
|
670
|
+
// Where Back goes from here: { pane, path } (path true: the way he came; false: the parent chain).
|
|
671
|
+
function navBackTo() {
|
|
672
|
+
var st = navState();
|
|
673
|
+
// navStamp has run on this entry (every landing routes), so its mark is this load's: an earlier load's was re-marked
|
|
674
|
+
// with no path there.
|
|
675
|
+
if (!navOverlay(st) && st && st.cosNav && st.cosNav.back) {
|
|
676
|
+
var p = String(st.cosNav.back).replace(/^#\/?/, '').split('/')[0];
|
|
677
|
+
return { pane: ROOT_PANES.indexOf(p) >= 0 ? p : 'today', path: true };
|
|
678
|
+
}
|
|
679
|
+
var r = parseRoute(), up = PANES[r.pane] && PANES[r.pane].up ? PANES[r.pane].up(r.args) : null;
|
|
680
|
+
return { pane: up || (r.args.length && !isTabRoute(r.args)) ? r.pane : 'today', path: false };
|
|
681
|
+
}
|
|
682
|
+
// The one way this page replaces an address (W.replace for the panes; Q1 7.0.30 H1). It tells navStamp the entry was
|
|
683
|
+
// replaced, never pushed, so a replaced entry never gets a path: a pane switching a tab with location.replace after a
|
|
684
|
+
// Back (a forward entry left) was stamped as a push at history.length - 1, and Back then called history.back() from
|
|
685
|
+
// the first entry, out of the app. Its position is this entry's mark, or one above the last mark when an overlay's
|
|
686
|
+
// entry (Today's sheet, the image viewer) is the one replaced. Returns false, doing nothing, when the address is the
|
|
687
|
+
// same (location.replace would not route, and the flag would wrongly mark the next address).
|
|
688
|
+
function navReplace(hash) {
|
|
689
|
+
if (location.hash === hash) return false;
|
|
690
|
+
var st = navState(), pos;
|
|
691
|
+
if (navOverlay(st)) pos = NAV.last && typeof NAV.last.pos === 'number' ? NAV.last.pos + 1 : history.length - 1;
|
|
692
|
+
else pos = st && st.cosNav && typeof st.cosNav.pos === 'number' ? st.cosNav.pos : history.length - 1;
|
|
693
|
+
NAV.replace = { pos: pos };
|
|
694
|
+
try { location.replace(location.pathname + location.search + hash); } catch (_e) { NAV.replace = null; location.hash = hash; }
|
|
695
|
+
return true;
|
|
696
|
+
}
|
|
697
|
+
// The parent chain, replacing this entry (never adding one).
|
|
698
|
+
function goReplace(pane, args) {
|
|
699
|
+
var hash = '#/' + [pane].concat(args || []).map(encodeURIComponent).join('/');
|
|
700
|
+
if (!navReplace(hash)) route();
|
|
701
|
+
}
|
|
702
|
+
// ── end back by path
|
|
528
703
|
// 7.0.2 (A8): a phone is under 600 px wide, or a short touch screen at any width (the unfolded Duo is 951x539 in
|
|
529
704
|
// Comet): height is its scarce axis, so it gets the phone Ask sheet and the 44 px tap floor (app.css), as folded does.
|
|
530
705
|
var SHORT_TOUCH_MQ = '(pointer: coarse) and (max-height: 720px)';
|
|
531
706
|
function isPhone() { return !window.matchMedia('(min-width: 600px)').matches || window.matchMedia(SHORT_TOUCH_MQ).matches; }
|
|
707
|
+
// 7.0.29: the folded Duo's side rail (app.css "the folded Duo's side rail"). theme.js decides from the screen's shape
|
|
708
|
+
// (html.short-screen); this media query is app.css's own, so the two never disagree. When it is on, the rail is the one
|
|
709
|
+
// way to switch panes: the top bar's section button keeps the name and loses its menu (inert), and its Back moves to
|
|
710
|
+
// the rail's foot (#rail-back, the same goBack).
|
|
711
|
+
var SIDE_RAIL_MQ = '(max-width: 599px) and (pointer: coarse)';
|
|
712
|
+
function sideRail() {
|
|
713
|
+
var de = document.documentElement;
|
|
714
|
+
return !!(de && de.classList && de.classList.contains('short-screen') && window.matchMedia && window.matchMedia(SIDE_RAIL_MQ).matches);
|
|
715
|
+
}
|
|
716
|
+
// 44 points on the phone. Safari's Page Zoom lays the page out wider and draws it at visualViewport.scale (his Duo: 449 px
|
|
717
|
+
// at 0.85), so 44 px would be 37 points: the rail's buttons are ceil(44 / scale) px: 44 at 100 percent, 52 at 85, 88 at 50 (the lowest Page Zoom). Read at boot and when the
|
|
718
|
+
// layout width changes (rotation), never on a pinch: a pinch must not resize the rail.
|
|
719
|
+
var RAIL = { hit: 0, cw: -1, hooked: false, on: null };
|
|
720
|
+
function railHitSync() {
|
|
721
|
+
var de = document.documentElement, cw = de ? de.clientWidth : 0;
|
|
722
|
+
if (RAIL.hit && cw === RAIL.cw) return RAIL.hit;
|
|
723
|
+
var vv = window.visualViewport, s = vv && typeof vv.scale === 'number' ? vv.scale : 1;
|
|
724
|
+
if (!(s >= 0.5 && s <= 1)) s = 1;
|
|
725
|
+
RAIL.hit = Math.ceil(44 / s - 0.01); // s is 0.5 to 1 here, so 44 to 88
|
|
726
|
+
RAIL.cw = cw;
|
|
727
|
+
if (de && de.style) de.style.setProperty('--rail-hit', RAIL.hit + 'px');
|
|
728
|
+
return RAIL.hit;
|
|
729
|
+
}
|
|
730
|
+
function railSync() {
|
|
731
|
+
var on = false;
|
|
732
|
+
try {
|
|
733
|
+
// theme.js's one screen test first (its own resize listener ran too; this keeps the order certain), then the page.
|
|
734
|
+
if (typeof window.CosShortScreen === 'function') window.CosShortScreen();
|
|
735
|
+
on = sideRail();
|
|
736
|
+
var btn = typeof $ === 'function' ? $('section-btn') : null;
|
|
737
|
+
if (btn) {
|
|
738
|
+
btn.inert = on;
|
|
739
|
+
if (on) btn.removeAttribute('aria-haspopup'); else btn.setAttribute('aria-haspopup', 'dialog');
|
|
740
|
+
}
|
|
741
|
+
if (on) { railHitSync(); closeMenu(); }
|
|
742
|
+
// The rail changes the reply box's width: an empty box re-fits its placeholder (autoGrow, F2).
|
|
743
|
+
var si = typeof $ === 'function' ? $('session-input') : null; if (si && typeof sxPlaceholderFit === 'function' && si.phLong) sxPlaceholderFit(si); else if (si && !si.value && typeof autoGrow === 'function') autoGrow(si);
|
|
744
|
+
// A change after boot (a fold, a rotation, a new screen) is reported once, with the screen's own numbers.
|
|
745
|
+
if (RAIL.on !== null && RAIL.on !== on && typeof layoutCrumb === 'function') { var sc = window.screen || {}; layoutCrumb('rail', { sw: sc.width, sh: sc.height, rail: on, railHit: RAIL.hit }); }
|
|
746
|
+
RAIL.on = on;
|
|
747
|
+
if (!RAIL.hooked && typeof window.matchMedia === 'function' && typeof window.addEventListener === 'function') {
|
|
748
|
+
RAIL.hooked = true;
|
|
749
|
+
var mq = window.matchMedia(SIDE_RAIL_MQ);
|
|
750
|
+
if (mq && mq.addEventListener) mq.addEventListener('change', railSync); else if (mq && mq.addListener) mq.addListener(railSync);
|
|
751
|
+
window.addEventListener('resize', function () { railSync(); });
|
|
752
|
+
window.addEventListener('orientationchange', function () { railSync(); });
|
|
753
|
+
}
|
|
754
|
+
} catch (_e) { /* a harness without a page: no rail */ }
|
|
755
|
+
return on;
|
|
756
|
+
}
|
|
532
757
|
/** A keyboard and a fine pointer are the way in (a Mac, a PC): Return sends. Touch screens type a new line. */
|
|
533
758
|
function keyboardFirst() { return !!(window.matchMedia && window.matchMedia('(hover: hover) and (pointer: fine)').matches); }
|
|
534
759
|
// Return in a text box sends only where a keyboard and a pointer are the way in (cos-chat.js keyboardFirst): never on
|
|
@@ -541,6 +766,7 @@
|
|
|
541
766
|
return !!(e.metaKey || e.ctrlKey || keyboardFirst());
|
|
542
767
|
}
|
|
543
768
|
function route(forceChanged) {
|
|
769
|
+
if (typeof navStamp === 'function') navStamp();
|
|
544
770
|
var r = parseRoute();
|
|
545
771
|
var changedPane = forceChanged === true || r.pane !== state.pane || !document.querySelector('#pane-' + r.pane + ':not(.hidden)');
|
|
546
772
|
if (state.pane !== r.pane && PANES[state.pane] && PANES[state.pane].leave) { try { PANES[state.pane].leave(); } catch (err) { console.error(err); } }
|
|
@@ -563,9 +789,21 @@
|
|
|
563
789
|
|
|
564
790
|
// Top bar (phone): the section switcher at a list, a back button on a reader.
|
|
565
791
|
$('topbar-back').classList.toggle('hidden', !reading);
|
|
792
|
+
var railBack = $('rail-back');
|
|
793
|
+
if (railBack) railBack.hidden = !reading;
|
|
566
794
|
$('section-btn').classList.toggle('hidden', reading);
|
|
567
|
-
|
|
795
|
+
// 7.0.30: Back names where it goes, the way he came or the parent (navBackTo).
|
|
796
|
+
var backTo = typeof navBackTo === 'function' ? navBackTo() : { pane: r.pane };
|
|
797
|
+
$('topbar-back-label').textContent = TITLES[backTo.pane] || 'Back';
|
|
798
|
+
var topBack = $('topbar-back');
|
|
799
|
+
if (topBack && topBack.setAttribute) topBack.setAttribute('aria-label', 'Back to ' + (TITLES[backTo.pane] || 'the list'));
|
|
800
|
+
if (railBack && railBack.setAttribute) railBack.setAttribute('aria-label', 'Back to ' + (TITLES[backTo.pane] || 'the list'));
|
|
801
|
+
// From 600 px Back is the navigation bar's (shell.js): its word stays "Back", as on the Mac's bar, and its name and
|
|
802
|
+
// tooltip say where it goes, as the rail's do (7.0.30 counted run, items_check B1 and B2 at the open Duo).
|
|
803
|
+
var navBack = $('nav-back');
|
|
804
|
+
if (navBack && navBack.setAttribute) { navBack.setAttribute('aria-label', 'Back to ' + (TITLES[backTo.pane] || 'the list')); navBack.setAttribute('title', 'Back to ' + (TITLES[backTo.pane] || 'the list')); }
|
|
568
805
|
$('section-name').textContent = TITLES[r.pane] || 'Today';
|
|
806
|
+
if (typeof paintTrail === 'function') paintTrail(r);
|
|
569
807
|
closeMenu();
|
|
570
808
|
if (changedPane) window.scrollTo(0, 0);
|
|
571
809
|
$('topbar').classList.remove('tucked');
|
|
@@ -601,30 +839,36 @@
|
|
|
601
839
|
runHooks(routeHooks, r);
|
|
602
840
|
}
|
|
603
841
|
function runHooks(list, arg) { list.forEach(function (fn) { try { fn(arg); } catch (err) { console.error(err); } }); }
|
|
604
|
-
// Back
|
|
605
|
-
// nested reader (an archive chat goes back to its day,
|
|
842
|
+
// Back (7.0.30): the way he came, when this page made the entry behind (navBackTo). Otherwise one step up, the way
|
|
843
|
+
// Control's Back does: a pane module may name the step above a nested reader (an archive chat goes back to its day,
|
|
844
|
+
// not to the list), replacing this entry.
|
|
606
845
|
function goBack() {
|
|
846
|
+
if (typeof navBackTo === 'function' && navBackTo().path) { history.back(); return; }
|
|
607
847
|
var r = parseRoute();
|
|
608
848
|
var up = PANES[r.pane] && PANES[r.pane].up ? PANES[r.pane].up(r.args) : null;
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
else
|
|
849
|
+
var step = typeof goReplace === 'function' ? goReplace : go;
|
|
850
|
+
if (up) step(r.pane, up);
|
|
851
|
+
else if (r.args.length && !isTabRoute(r.args)) step(r.pane);
|
|
852
|
+
else step('today');
|
|
612
853
|
}
|
|
613
854
|
// '#/<pane>/t/<tab>' is the pane's list on one of its tabs, not a reader: the phone keeps the section switcher and Back
|
|
614
855
|
// leaves the pane. Memories keeps its tab in the address this way, so a reload or a new browser lands on the same tab.
|
|
615
856
|
function isTabRoute(args) { return args.length === 2 && args[0] === 't'; }
|
|
616
|
-
// On
|
|
617
|
-
//
|
|
857
|
+
// On a phone every vertical pixel counts: the top bar slides away while you scroll down a list and comes back the
|
|
858
|
+
// moment you scroll up. 7.0.30 (item 1): never on a reader, where the top bar holds Back, so Back stays in reach on a
|
|
859
|
+
// tall phone however far down he reads.
|
|
618
860
|
var lastY = 0;
|
|
619
861
|
function onScroll() {
|
|
620
862
|
var y = Math.max(0, window.scrollY || 0);
|
|
621
|
-
|
|
863
|
+
// On a reader the bar stays (its Back is there). The folded Duo's Back is on the side rail (the bar's is hidden by CSS,
|
|
864
|
+
// not the class), so there the bar tucks as before (Q1 7.0.30 M1).
|
|
865
|
+
var bar = $('topbar'), back = $('topbar-back'), onReader = !!(back && !back.classList.contains('hidden')) && !(typeof sideRail === 'function' && sideRail());
|
|
622
866
|
bar.classList.toggle('scrolled', y > 8);
|
|
623
|
-
if (y < 60 || y < lastY - 4) bar.classList.remove('tucked');
|
|
867
|
+
if (y < 60 || y < lastY - 4 || onReader) bar.classList.remove('tucked');
|
|
624
868
|
else if (y > lastY + 4 && isPhone()) bar.classList.add('tucked');
|
|
625
869
|
lastY = y;
|
|
626
870
|
}
|
|
627
|
-
function openMenu() { $('section-menu').classList.remove('hidden'); var cur = document.querySelector('#section-menu [aria-current="page"]') || document.querySelector('#section-menu [data-goto]'); if (cur) cur.focus(); }
|
|
871
|
+
function openMenu() { if (sideRail()) return; $('section-menu').classList.remove('hidden'); var cur = document.querySelector('#section-menu [aria-current="page"]') || document.querySelector('#section-menu [data-goto]'); if (cur) cur.focus(); }
|
|
628
872
|
function closeMenu() { $('section-menu').classList.add('hidden'); }
|
|
629
873
|
|
|
630
874
|
// ── auth ────────────────────────────────────────────────
|
|
@@ -660,7 +904,7 @@
|
|
|
660
904
|
// 7.0.23 regression (lapse_check, 08:2x): a frame class left on keeps html and body at one screen with overflow
|
|
661
905
|
// hidden, so the gate could not scroll and Connect sat below a keyboard-height screen. The gate is a normal page;
|
|
662
906
|
// showShell's route() puts the frame back (sxFrameSync) when the shell returns.
|
|
663
|
-
document.documentElement.classList.remove('sx-frame'
|
|
907
|
+
document.documentElement.classList.remove('sx-frame');
|
|
664
908
|
// A later 401 from a pane still polling (W.api passes no message) must not blank what the gate already says:
|
|
665
909
|
// the reason it opened, or "That code did not match" while the person retypes.
|
|
666
910
|
if (!message && alreadyOpen) return;
|
|
@@ -2068,7 +2312,11 @@
|
|
|
2068
2312
|
var qs1 = sessionQs(), open = (!!html && !te) || !!(qs1 && ((qs1.itemsFor && qs1.itemsFor(r.id).length) || (qs1.atMacFor && qs1.atMacFor(r.id).length)));
|
|
2069
2313
|
var box = $('session-input');
|
|
2070
2314
|
// 7.0.18: here a reply IS the answer (sent with the contract's attribution), so the box says so.
|
|
2071
|
-
if (box && box.setAttribute)
|
|
2315
|
+
if (box && box.setAttribute) {
|
|
2316
|
+
box.phLong = te ? 'Or write your own answer' : open ? 'Message this session (not an answer)' : 'Continue this session…';
|
|
2317
|
+
box.phShort = open && !te ? 'Message (not an answer)' : '';
|
|
2318
|
+
if (typeof sxPlaceholderFit === 'function') sxPlaceholderFit(box); else box.setAttribute('placeholder', box.phLong);
|
|
2319
|
+
}
|
|
2072
2320
|
}
|
|
2073
2321
|
// ── end at the Mac's prompt
|
|
2074
2322
|
|
|
@@ -2484,7 +2732,7 @@
|
|
|
2484
2732
|
var dest = session ? ['sessions', s[0], s.slice(1).join('|')] : kind === 'pet' ? ['sessions'] : ['work'].concat(kind === 'need' ? [] : ['sort']);
|
|
2485
2733
|
console.error('[today] the overlay did not open (' + kind + '); opening ' + dest.join('/') + ' instead.', err);
|
|
2486
2734
|
toast(session ? 'Could not open that here. Opening the session.' : kind === 'pet' ? 'Could not open that here. Opening Sessions.' : 'Could not open that here. Opening it in Work.');
|
|
2487
|
-
if (history.state && history.state.cosTodayOverlay)
|
|
2735
|
+
if (history.state && history.state.cosTodayOverlay) navReplace('#/' + dest.map(encodeURIComponent).join('/'));
|
|
2488
2736
|
else go(dest[0], dest.slice(1));
|
|
2489
2737
|
});
|
|
2490
2738
|
}
|
|
@@ -2759,6 +3007,8 @@
|
|
|
2759
3007
|
var touched = function () { SX_PIN.userAt = Date.now(); };
|
|
2760
3008
|
['wheel', 'touchstart', 'pointerdown', 'keydown'].forEach(function (t) { body.addEventListener(t, touched, { passive: true }); });
|
|
2761
3009
|
body.addEventListener('scroll', sxGoQuestionPaint, { passive: true });
|
|
3010
|
+
// 7.0.29 ADD 3: reaching the end shows him what is there.
|
|
3011
|
+
body.addEventListener('scroll', function () { if (typeof sxMarkShown === 'function' && state.reading) sxMarkShown(state.reading); }, { passive: true });
|
|
2762
3012
|
body.addEventListener('scroll', function () {
|
|
2763
3013
|
if (SX_PIN.body !== body || Date.now() - SX_PIN.userAt > SX_PIN_USER_MS) return;
|
|
2764
3014
|
if (Math.abs(body.scrollTop - SX_PIN.put) > 4 && body.scrollTop < body.scrollHeight - body.clientHeight - 4) sxUnpin();
|
|
@@ -2907,12 +3157,25 @@
|
|
|
2907
3157
|
// Earlier this month as compact rows, folded shut with a count. Days are counted back from today (2 to 6 days ago is
|
|
2908
3158
|
// Earlier this week, 7 to 13 Last week, 14 to 30 Earlier this month), so no group is empty only because of the
|
|
2909
3159
|
// calendar.
|
|
2910
|
-
var SX_GROUPS = [['needs', 'Needs you', 'state'], ['working', 'Working now', 'state'], ['pinned', 'Pinned', ''], ['today', 'Today', ''],
|
|
3160
|
+
var SX_GROUPS = [['needs', 'Needs you', 'state'], ['working', 'Working now', 'state'], ['pinned', 'Pinned', 'pinned'], ['today', 'Today', ''],
|
|
2911
3161
|
['yesterday', 'Yesterday', ''], ['week', 'Earlier this week', ''], ['lastweek', 'Last week', 'compact'], ['month', 'Earlier this month', 'compact']];
|
|
2912
3162
|
var SX_DAY_MS = 86400000, SX_REST_MS = 2000, SX_EMPTY_HOLD_MS = 5000;
|
|
2913
3163
|
// place: each row's group as last drawn at rest; seen: when Needs you and Working now last had rows; open: unfolded
|
|
2914
3164
|
// compact groups; older: /older's short rows (olderOk false: an older server, 404).
|
|
2915
3165
|
var SXL = { place: null, down: false, touchAt: 0, scrollAt: 0, timer: 0, seen: {}, open: {}, older: [], olderN: null, olderOk: null, olderNext: null, olderAt: 0, olderBusy: false };
|
|
3166
|
+
// 7.0.29 ADD 1 (Miles 23:53: thirteen pinned rows sat between Working now and Today): Pinned folds like a folder, with
|
|
3167
|
+
// its count, and this browser remembers the choice (localStorage). Open by default, so no list changes until he folds
|
|
3168
|
+
// it. Placement is unchanged: sxGroupFor tests state before pin, so a pinned session that is running is drawn in
|
|
3169
|
+
// Working now, one that waits or failed in Needs you, folded or not, and it goes back under Pinned when idle.
|
|
3170
|
+
var SX_PIN_FOLD_KEY = 'cos_sx_pinned_folded';
|
|
3171
|
+
function sxPinnedOpen() {
|
|
3172
|
+
if (typeof SXL.pinOpen !== 'boolean') { var v = null; try { v = typeof localStorage === 'object' && localStorage ? localStorage.getItem(SX_PIN_FOLD_KEY) : null; } catch (_e) { v = null; } SXL.pinOpen = v !== '1'; }
|
|
3173
|
+
return SXL.pinOpen;
|
|
3174
|
+
}
|
|
3175
|
+
function sxPinnedSet(open) {
|
|
3176
|
+
SXL.pinOpen = !!open;
|
|
3177
|
+
try { if (typeof localStorage === 'object' && localStorage) { if (open) localStorage.removeItem(SX_PIN_FOLD_KEY); else localStorage.setItem(SX_PIN_FOLD_KEY, '1'); } } catch (_e) { /* storage off: this visit only */ }
|
|
3178
|
+
}
|
|
2916
3179
|
function sxRowKey(s) { var k = splitSessionId(s.provider, s.session_id || s.id); return k.provider + '|' + String(k.id).toLowerCase(); }
|
|
2917
3180
|
function sxDayStart(t) { var d = new Date(t); d.setHours(0, 0, 0, 0); return d.getTime(); }
|
|
2918
3181
|
function sxSortTime(s) { return SX.clock === 'opened' ? (sxTime(s.created) || sxTime(s.modified)) : sxTime(s.modified); }
|
|
@@ -3017,6 +3280,13 @@
|
|
|
3017
3280
|
return;
|
|
3018
3281
|
}
|
|
3019
3282
|
if (!n) return;
|
|
3283
|
+
if (g[2] === 'pinned') {
|
|
3284
|
+
var popen = sxPinnedOpen();
|
|
3285
|
+
html += '<section class="sx-g" data-g="pinned"><button type="button" class="sx-gh" data-sx="fold" data-g="pinned" aria-expanded="' + popen + '">' +
|
|
3286
|
+
'<span class="fold"><span class="chev" aria-hidden="true"></span>' + g[1] + '</span><span class="n">' + n + '</span></button>' +
|
|
3287
|
+
(popen ? '<div class="sx-rows">' + rows.map(function (s) { return sxRow(s, null, amb, s.form === 'short'); }).join('') + '</div>' : '') + '</section>';
|
|
3288
|
+
return;
|
|
3289
|
+
}
|
|
3020
3290
|
if (g[2] === 'compact') {
|
|
3021
3291
|
var open = !!SXL.open[id];
|
|
3022
3292
|
html += '<section class="sx-g" data-g="' + id + '"><button type="button" class="sx-gh" data-sx="fold" data-g="' + id + '" aria-expanded="' + open + '">' +
|
|
@@ -3188,6 +3458,9 @@
|
|
|
3188
3458
|
(s.cos_pinned ? '<button type="button" role="menuitem" data-sx="org-pin" data-key="' + esc(key) + '">Unpin in COS</button>' : '')
|
|
3189
3459
|
: '<button type="button" role="menuitem" data-sx="org-pin" data-key="' + esc(key) + '">' + (s.cos_pinned ? 'Unpin' : 'Pin') + '</button>') +
|
|
3190
3460
|
'<button type="button" role="menuitem" data-sx="org-move-menu" data-key="' + esc(key) + '">Move to folder</button>' +
|
|
3461
|
+
// 7.0.29 ADD 2 (Miles 23:53: he could not find how to take a session out of a folder; "No folder" sat at the foot
|
|
3462
|
+
// of Move to folder): a row in a folder gets Remove from folder, the same write as No folder (folder_id null).
|
|
3463
|
+
(s.folder_id && SXF.byId[s.folder_id] ? '<button type="button" role="menuitem" data-sx="org-move" data-key="' + esc(key) + '" data-folder="">Remove from folder</button>' : '') +
|
|
3191
3464
|
'<button type="button" role="menuitem" data-sx="org-rename" data-key="' + esc(key) + '">Rename</button>';
|
|
3192
3465
|
}
|
|
3193
3466
|
function sxFolderMenuHtml(id) {
|
|
@@ -3491,6 +3764,7 @@
|
|
|
3491
3764
|
if (act === 'view') { sxViewMenu(false); return setSessionsView(t.getAttribute('data-view')); }
|
|
3492
3765
|
if (act === 'view-menu') return sxViewMenu();
|
|
3493
3766
|
if (act === 'goto-question') { sxToNewest(); sxGoQuestionPaint(); var gq = $('session-questions'), first = gq && gq.querySelector('button, input, [tabindex]'); if (first && first.focus) first.focus({ preventScroll: true }); return; }
|
|
3767
|
+
if (act === 'fold' && t.getAttribute('data-g') === 'pinned') { sxPinnedSet(!sxPinnedOpen()); return renderSessionGroups(); }
|
|
3494
3768
|
if (act === 'fold') { var g = t.getAttribute('data-g'), isF = g.indexOf('f:') === 0; SXL.open[g] = isF ? SXL.open[g] === false : !SXL.open[g]; return renderSessionGroups(); }
|
|
3495
3769
|
if (/^(row-menu|org-|folder-|new-folder|rename-clear|inline-cancel)/.test(act)) { e.stopPropagation(); return sxOrgClick(act, t); }
|
|
3496
3770
|
if (act === 'older-more') return sxLoadOlder(false, true);
|
|
@@ -3507,7 +3781,6 @@
|
|
|
3507
3781
|
if (act === 'clear') { var input = $('sessions-q'); input.value = ''; searchSessions(); input.focus(); return; }
|
|
3508
3782
|
if (act === 'copy-session') return copySessionBrief();
|
|
3509
3783
|
if (act === 'full') { if (SX_SPLIT) SX_SPLIT.toggleFull(); return; }
|
|
3510
|
-
if (act === 'refresh-turns' && rd) { rd.refusal = null; renderVerdict(rd); refreshTurns(rd.provider, rd.id); return; }
|
|
3511
3784
|
if (act === 'retry' && rd) { rd.refusal = null; renderVerdict(rd); return sendSessionTurn(!!rd.retryFork); }
|
|
3512
3785
|
if (act === 'caution-send' && rd) { rd.cautionAck = true; rd.cautionAsk = false; renderVerdict(rd); return sendSessionTurn(false); }
|
|
3513
3786
|
if (act === 'caution-cancel' && rd) { rd.cautionAsk = false; renderVerdict(rd); return; }
|
|
@@ -3803,7 +4076,7 @@
|
|
|
3803
4076
|
var timer = ctrl && o.timeoutMs ? setTimeout(function () { ctrl.abort(); }, o.timeoutMs) : null;
|
|
3804
4077
|
var headers = { 'X-COS-Web': '1' };
|
|
3805
4078
|
if (o.body !== undefined) headers['Content-Type'] = 'application/json';
|
|
3806
|
-
var sentAt = Date.now();
|
|
4079
|
+
var sentAt = Date.now(), writeDone = typeof writeStart === 'function' ? writeStart(o.method || 'GET') : function () {};
|
|
3807
4080
|
return fetch(path, { method: o.method || 'GET', headers: headers, credentials: 'same-origin', body: o.body !== undefined ? JSON.stringify(o.body) : undefined, signal: ctrl ? ctrl.signal : undefined })
|
|
3808
4081
|
.then(function (res) {
|
|
3809
4082
|
sxNoteClock(res, sentAt);
|
|
@@ -3816,7 +4089,7 @@
|
|
|
3816
4089
|
noteServer(false);
|
|
3817
4090
|
throw unreachableError(err);
|
|
3818
4091
|
})
|
|
3819
|
-
.finally(function () { if (timer) clearTimeout(timer); });
|
|
4092
|
+
.finally(function () { if (timer) clearTimeout(timer); writeDone(); });
|
|
3820
4093
|
}
|
|
3821
4094
|
|
|
3822
4095
|
// Per-browser memory for one session: messages held until a reply ends, and the queue keys
|
|
@@ -3925,6 +4198,8 @@
|
|
|
3925
4198
|
// edge says one waits. It sits in the reader, not the dock, because the dock scrolls by itself as a last resort.
|
|
3926
4199
|
'<p class="sx-goq" id="session-goq" hidden><span>A question is waiting at the end.</span><button type="button" class="cx-btn quiet" data-sx="goto-question">Go to the question</button></p>';
|
|
3927
4200
|
readerImagesMount($('session-turns'), r);
|
|
4201
|
+
if (typeof unsentRestore === 'function') unsentRestore(r);
|
|
4202
|
+
var si0 = $('session-input'); if (si0 && !si0.value && typeof autoGrow === 'function') autoGrow(si0);
|
|
3928
4203
|
sxNoteList(r, d);
|
|
3929
4204
|
setLive({});
|
|
3930
4205
|
if (typeof readerMacPaint === 'function') readerMacPaint(r, sxState(d) === 'waiting');
|
|
@@ -3942,6 +4217,8 @@
|
|
|
3942
4217
|
startHeldTimer(state.reading);
|
|
3943
4218
|
}
|
|
3944
4219
|
if (turns.length) requestAnimationFrame(function () { if (typeof sxToNewest === 'function') sxToNewest(); else window.scrollTo(0, document.documentElement.scrollHeight); });
|
|
4220
|
+
// 7.0.29 ADD 3: the newest turns he was just taken to count as shown (this frame runs after the one above).
|
|
4221
|
+
if (turns.length) requestAnimationFrame(function () { if (typeof sxMarkShown === 'function') sxMarkShown(r); });
|
|
3945
4222
|
requestAnimationFrame(function () { if (typeof sxFrameCrumb === 'function') sxFrameCrumb('open'); });
|
|
3946
4223
|
watchSession(provider, id);
|
|
3947
4224
|
}).catch(function (err) {
|
|
@@ -4230,6 +4507,53 @@
|
|
|
4230
4507
|
paintActivity(r);
|
|
4231
4508
|
if (moved) renderVerdict(r);
|
|
4232
4509
|
}
|
|
4510
|
+
// ── 7.0.29 ADD 3 (Miles 00:01 "we have to hit continue to steer the session"; 00:03 "Refresh doesn't seem to do
|
|
4511
|
+
// anything"; the lead's ruling 00:02 and A1 to A3 00:47): the changed-thread stop. The server guard is unchanged; the
|
|
4512
|
+
// page does the acknowledging. Shown, not just drawn (A1): a turn counts as shown only while the reader is at the
|
|
4513
|
+
// newest turn (following it, or scrolled to the end since). Read from the reader's own pin and scroll, no observer per
|
|
4514
|
+
// turn. Only turns with words count; tool steps are not turns here.
|
|
4515
|
+
function sxTextTurns(turns) { return (turns || []).filter(function (t) { return t && String(t.text || t.content || '').trim(); }); }
|
|
4516
|
+
function sxAtEnd() {
|
|
4517
|
+
try {
|
|
4518
|
+
var body = $('session-body');
|
|
4519
|
+
if (body && document.documentElement.classList.contains('sx-frame')) return SX_PIN.body === body || body.scrollTop >= body.scrollHeight - body.clientHeight - 4;
|
|
4520
|
+
var de = document.documentElement;
|
|
4521
|
+
return (window.scrollY || 0) + window.innerHeight >= de.scrollHeight - 4;
|
|
4522
|
+
} catch (_e) { return false; } // a page without these (a test harness): nothing counts as shown, so nothing resends
|
|
4523
|
+
}
|
|
4524
|
+
function sxMarkShown(r) {
|
|
4525
|
+
if (!r || state.reading !== r || !sxAtEnd()) return;
|
|
4526
|
+
r.shown = r.shown || {};
|
|
4527
|
+
sxTextTurns(r.turns).forEach(function (t) { r.shown[sxTurnSig(t)] = 1; });
|
|
4528
|
+
}
|
|
4529
|
+
// At Send: what he had been shown, and whether the session was already waiting on him.
|
|
4530
|
+
function sxPressSeen(r) {
|
|
4531
|
+
sxMarkShown(r);
|
|
4532
|
+
return { shown: Object.assign({}, r.shown || {}), waiting: !!r.waitingNow };
|
|
4533
|
+
}
|
|
4534
|
+
// After a changed-thread refusal: fetch the turns (the refreshTurns request), draw them, and say what is new since Send:
|
|
4535
|
+
// turns with words he had not been shown, or a question or permission now waiting that was not (A2). null: the fetch
|
|
4536
|
+
// failed, so nothing can be said and the box shows.
|
|
4537
|
+
function sxChangedLook(r, seen) {
|
|
4538
|
+
return api(sessionPath(r.provider, r.id, '?turns=' + SX_TURNS + '&cards=1')).then(function (d) {
|
|
4539
|
+
if (state.reading !== r) return null;
|
|
4540
|
+
var turns = Array.isArray(d && d.recent_turns) ? d.recent_turns : [];
|
|
4541
|
+
var box = $('session-turns'); if (box) { sxMergeTurns(box, r.turns || [], turns, d); readerImagesMount(box, r); }
|
|
4542
|
+
r.detail = d; r.turns = turns; r.more = d.recent_turns_more === true;
|
|
4543
|
+
var fresh = sxTextTurns(turns).filter(function (t) { return !seen.shown[sxTurnSig(t)]; });
|
|
4544
|
+
// Waiting by the reader's own rule, or by the detail's own word (a session read as stale may still say it waits):
|
|
4545
|
+
// either way stopping is the safe side.
|
|
4546
|
+
return { fresh: fresh, newWait: (sxState(d) === 'waiting' || (d && d.agent_state === 'waiting')) && !seen.waiting };
|
|
4547
|
+
}, function () { return null; });
|
|
4548
|
+
}
|
|
4549
|
+
// Take him to the first new turn: it is drawn, and the stop is about it.
|
|
4550
|
+
function sxShowFresh(fresh) {
|
|
4551
|
+
if (!fresh || !fresh.length) return;
|
|
4552
|
+
var sig = sxTurnSig(fresh[0]), el = null;
|
|
4553
|
+
document.querySelectorAll('#session-turns .turn[data-sig]').forEach(function (n) { if (!el && n.getAttribute('data-sig') === sig) el = n; });
|
|
4554
|
+
sxUnpin();
|
|
4555
|
+
if (el && el.scrollIntoView) el.scrollIntoView({ block: 'start' }); else sxToNewest();
|
|
4556
|
+
}
|
|
4233
4557
|
function refreshTurns(provider, id) {
|
|
4234
4558
|
var reading = state.reading;
|
|
4235
4559
|
api(sessionPath(provider, id, '?turns=' + SX_TURNS + '&cards=1')).then(function (d) {
|
|
@@ -4239,6 +4563,7 @@
|
|
|
4239
4563
|
var box = $('session-turns'); if (box) { sxMergeTurns(box, r.turns || [], turns, d); readerImagesMount(box, r); }
|
|
4240
4564
|
// The header keeps the subtitle it opened with: a rewrap above the turns would move them.
|
|
4241
4565
|
r.detail = d; r.turns = turns; r.more = d.recent_turns_more === true;
|
|
4566
|
+
sxMarkShown(r);
|
|
4242
4567
|
renderHeld(r);
|
|
4243
4568
|
loadQueue(provider, id);
|
|
4244
4569
|
sxProbeAttach(r);
|
|
@@ -4362,9 +4687,10 @@
|
|
|
4362
4687
|
} else if (r.refusal) {
|
|
4363
4688
|
var f = r.refusal;
|
|
4364
4689
|
html = refusalBox(f.copy, f.supplement,
|
|
4365
|
-
|
|
4690
|
+
// 7.0.29 ADD 3: a changed thread says what each button does. The page has already refreshed, so no Refresh.
|
|
4691
|
+
(f.changed ? '<button type="button" class="cx-btn quiet" data-action="send-anyway">Send it anyway</button>' : '') +
|
|
4366
4692
|
(f.retry ? '<button type="button" class="cx-btn quiet" data-sx="retry">Retry</button>' : '') +
|
|
4367
|
-
(f.fork ? forkBtn : ''));
|
|
4693
|
+
(f.fork ? (f.changed ? '<button type="button" class="cx-btn quiet" data-action="fork-session" title="Runs your message in a copy of this thread. The original is untouched.">Send to a copy</button>' : forkBtn) : ''));
|
|
4368
4694
|
} else if (v && v.attachable === false && /^(native_thread_working|native_target_busy)$/.test(v.reason)) {
|
|
4369
4695
|
// 6.99.29: "Working right now" only while the one state says so.
|
|
4370
4696
|
if (sxParkNoteShows(r)) html = '<div class="sx-park"><p>' + esc(sxParkLine(r)) + '</p>' + (/fork/i.test(v.reasonCopy) ? '<div class="sx-box-actions">' + forkBtn + '</div>' : '') + '</div>';
|
|
@@ -4399,18 +4725,51 @@
|
|
|
4399
4725
|
if (!r.draft || r.draft.text !== text || (r.draft.ids || []).join(',') !== files.join(',')) r.draft = { text: text, key: newTurnKey(), ids: files };
|
|
4400
4726
|
return r.draft;
|
|
4401
4727
|
}
|
|
4402
|
-
//
|
|
4403
|
-
|
|
4728
|
+
// 7.0.29 (lead F3: the rail's Back sits level with Send, so a mis-tap is likely): words typed in a session's reply
|
|
4729
|
+
// box and not sent come back when that session opens again in this tab, after Back, the rail or another pane. Kept in
|
|
4730
|
+
// memory, per session, as he types; dropped when the box is empty or when those words are sent.
|
|
4731
|
+
var SX_UNSENT = {};
|
|
4732
|
+
function unsentKey(provider, id) { return String(provider || '') + '|' + String(id || '').toLowerCase(); }
|
|
4733
|
+
function unsentSave() {
|
|
4734
|
+
var r = state.reading, input = $('session-input');
|
|
4735
|
+
if (!r || !input) return;
|
|
4736
|
+
var k = unsentKey(r.provider, r.id);
|
|
4737
|
+
if (input.value.trim()) SX_UNSENT[k] = input.value; else delete SX_UNSENT[k];
|
|
4738
|
+
}
|
|
4739
|
+
function unsentRestore(r) {
|
|
4740
|
+
var input = $('session-input'), v = r ? SX_UNSENT[unsentKey(r.provider, r.id)] : '';
|
|
4741
|
+
if (input && v && !input.value) { input.value = v; autoGrow(input); }
|
|
4742
|
+
}
|
|
4743
|
+
// 7.0.29 R4 (Q1 Note 1): the saved words of the session the message was sent FROM only, never another session's
|
|
4744
|
+
// words that happen to match. No session named, nothing is dropped.
|
|
4745
|
+
function unsentDrop(text, r) {
|
|
4746
|
+
if (!r || !r.provider || !r.id) return;
|
|
4747
|
+
var k = unsentKey(r.provider, r.id), v = SX_UNSENT[k];
|
|
4748
|
+
if (v == null) return;
|
|
4749
|
+
var t = String(text).trim(), bare = typeof turnEndBare === 'function' ? turnEndBare(text) : t;
|
|
4750
|
+
v = String(v).trim();
|
|
4751
|
+
if (v === t || v === bare) delete SX_UNSENT[k];
|
|
4752
|
+
}
|
|
4753
|
+
// The box is cleared only of the very words that were sent; anything typed since stays. `r` is the session they were
|
|
4754
|
+
// sent from: another session's box is never touched (R4).
|
|
4755
|
+
function clearBoxOf(text, r) {
|
|
4756
|
+
if (typeof unsentDrop === 'function') unsentDrop(text, r || state.reading);
|
|
4757
|
+
if (r && state.reading !== r) return;
|
|
4404
4758
|
var input = $('session-input');
|
|
4405
4759
|
// 7.0.18: an answer to a turn-end ask was sent with the attribution in front; the box holds his words without it.
|
|
4406
4760
|
if (input && (input.value.trim() === String(text).trim() || input.value.trim() === turnEndBare(text))) { input.value = ''; autoGrow(input); }
|
|
4407
4761
|
if (state.reading && state.reading.draft && state.reading.draft.text === text) state.reading.draft = null;
|
|
4408
4762
|
}
|
|
4409
4763
|
// Words go back in the box only if the box is empty; otherwise the status line keeps them. `files` says what became
|
|
4410
|
-
// of the message's files (heldFilesBack).
|
|
4411
|
-
|
|
4764
|
+
// of the message's files (heldFilesBack). 7.0.29 R4: when `r`, the session they came from, is not the one open, they
|
|
4765
|
+
// go to that session's saved words (back in its box when it opens), never into the open session's box.
|
|
4766
|
+
function returnToBox(text, why, files, r) {
|
|
4767
|
+
if (r && r.provider && r.id && state.reading !== r) {
|
|
4768
|
+
if (typeof unsentKey === 'function') { var k = unsentKey(r.provider, r.id), had = SX_UNSENT[k]; SX_UNSENT[k] = had && String(had).trim() ? had + '\n\n' + text : text; }
|
|
4769
|
+
return;
|
|
4770
|
+
}
|
|
4412
4771
|
var input = $('session-input');
|
|
4413
|
-
if (input && !input.value.trim()) { input.value = text; autoGrow(input); sessionStatus(why + ' Your message is back in the box.' + (files || ''), 'error'); }
|
|
4772
|
+
if (input && !input.value.trim()) { input.value = text; autoGrow(input); if (typeof unsentSave === 'function') unsentSave(); sessionStatus(why + ' Your message is back in the box.' + (files || ''), 'error'); }
|
|
4414
4773
|
else sessionStatus(why + ' Not sent: “' + String(text).slice(0, 80) + (text.length > 80 ? '…' : '') + '”' + (files || ''), 'error');
|
|
4415
4774
|
}
|
|
4416
4775
|
// Your words show in the conversation the moment the Mac accepts them; the next refresh of
|
|
@@ -4437,7 +4796,7 @@
|
|
|
4437
4796
|
var READER_VIEW = {
|
|
4438
4797
|
status: function (r, text, kind, actionHtml) { sessionStatus(text, kind, actionHtml); },
|
|
4439
4798
|
pending: function (r, text, note) { showPendingTurn(text, note); },
|
|
4440
|
-
clear: function (r, text) { clearBoxOf(text); },
|
|
4799
|
+
clear: function (r, text) { clearBoxOf(text, r); },
|
|
4441
4800
|
verdict: function (r) { renderVerdict(r); },
|
|
4442
4801
|
hold: function (r, draft) { holdUntilReplyEnds(r, draft); },
|
|
4443
4802
|
watch: function (r, bindingId, key) { watchTurn(bindingId, key, r.provider, r.id); },
|
|
@@ -4552,7 +4911,11 @@
|
|
|
4552
4911
|
if (r.refusal || r.cautionAsk) { r.refusal = null; r.cautionAsk = false; renderVerdict(r); }
|
|
4553
4912
|
// 7.0.18: while a turn-end ask is open, the box's words are his own answer ('Other' in the contract): sent with the
|
|
4554
4913
|
// same attribution as a choice. A fork goes to a copy and is not an answer, so it is sent as written.
|
|
4555
|
-
|
|
4914
|
+
var answering = !!(!fork && r.turnEnd && turnEndOf(readerRowOf(r)));
|
|
4915
|
+
if (answering) text = turnEndPrompt(text);
|
|
4916
|
+
// 7.0.29 ADD 3: what he had been shown when he pressed Send. A turn-end answer never resends by itself; R6 (Q1's nit):
|
|
4917
|
+
// the same test as the answer's prefix (r.turnEnd AND the ask still open), so a message after a closed ask is a message.
|
|
4918
|
+
if (!fork && typeof sxPressSeen === 'function') opts = Object.assign({}, opts || {}, { seen: answering ? null : sxPressSeen(r) });
|
|
4556
4919
|
r.sending = true;
|
|
4557
4920
|
var btns = form.querySelectorAll('button'); btns.forEach(function (b) { b.disabled = true; });
|
|
4558
4921
|
var done = function () { r.sending = false; btns.forEach(function (b) { b.disabled = false; }); paintReaderFiles(); };
|
|
@@ -4633,14 +4996,25 @@
|
|
|
4633
4996
|
// it under the same key and sends it the moment the reply ends.
|
|
4634
4997
|
if (reason === 'native_turn_in_progress') { if (!opts.held) view.hold(r, draft); return 'held'; }
|
|
4635
4998
|
if (reason === 'native_thread_changed' && b.revision) {
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4999
|
+
// The box. Send it anyway always carries the revision of THIS refusal, the newest (A3).
|
|
5000
|
+
var changedBox = function (copy) { r.sendAnyway = b.revision; view.status(r, ''); r.refusal = { copy: copy, changed: true, fork: true }; view.verdict(r); };
|
|
5001
|
+
if (opts.held) { settleHeld(r, opts.held, 'refused', 'This session changed on your Mac while your message waited.'); return 'changed'; }
|
|
5002
|
+
// 7.0.29 ADD 3, the session reader only (opts.seen; Today's panel keeps its own path): never for a held or
|
|
5003
|
+
// queued message, a turn-end answer (seen is null), or a send that already carries a revision, which the one
|
|
5004
|
+
// automatic resend always does. So at most one resend, and a second refusal shows the box.
|
|
5005
|
+
if (opts.seen && !opts.acknowledgedRevision && state.reading === r) {
|
|
5006
|
+
return sxChangedLook(r, opts.seen).then(function (look) {
|
|
5007
|
+
if (look && !look.fresh.length && !look.newWait && state.reading === r) return continueSession(r, text, Object.assign({}, opts, { acknowledgedRevision: b.revision }));
|
|
5008
|
+
changedBox(!look ? 'This session changed on your Mac, and COS could not load what changed. Your message is not sent yet.'
|
|
5009
|
+
: look.fresh.length ? 'A new reply arrived while you were writing. Your message is not sent yet.'
|
|
5010
|
+
: 'The session is now waiting on a question. Your message is not sent yet.');
|
|
5011
|
+
if (look) sxShowFresh(look.fresh);
|
|
5012
|
+
return 'changed';
|
|
5013
|
+
});
|
|
4643
5014
|
}
|
|
5015
|
+
changedBox(opts.seen === undefined ? (b.reasonCopy || 'This session changed on your Mac since you opened it.')
|
|
5016
|
+
: opts.seen === null ? 'This session changed on your Mac. Your answer is not sent yet.'
|
|
5017
|
+
: 'This session changed on your Mac again. Your message is not sent yet.');
|
|
4644
5018
|
return 'changed';
|
|
4645
5019
|
}
|
|
4646
5020
|
if (/^(stale_epoch|binding_expired|binding_not_active|binding_unusable|unknown_binding)$/.test(reason) && !opts.reattached) {
|
|
@@ -4861,7 +5235,7 @@
|
|
|
4861
5235
|
showPendingTurn(draft.text, 'sends after this reply', draft.key);
|
|
4862
5236
|
saveHeld(r);
|
|
4863
5237
|
}
|
|
4864
|
-
clearBoxOf(draft.text);
|
|
5238
|
+
clearBoxOf(draft.text, r);
|
|
4865
5239
|
sessionStatus('COS is still answering. ' + (r.held.length > 1 ? r.held.length + ' messages send' : 'Yours sends') + ' as soon as it finishes. ', null, '<button type="button" class="link-btn" data-action="drop-after">Don’t send</button>');
|
|
4866
5240
|
startHeldTimer(r);
|
|
4867
5241
|
}
|
|
@@ -4894,7 +5268,7 @@
|
|
|
4894
5268
|
} else {
|
|
4895
5269
|
if (bubble) bubble.remove();
|
|
4896
5270
|
// The whole message comes back: its words, and its files where the Mac still keeps them.
|
|
4897
|
-
returnToBox(h.text, how === 'cancelled' ? 'Not sent.' : copy || 'Not sent.', heldFilesBack(r, h));
|
|
5271
|
+
returnToBox(h.text, how === 'cancelled' ? 'Not sent.' : copy || 'Not sent.', heldFilesBack(r, h), r);
|
|
4898
5272
|
}
|
|
4899
5273
|
if (!r.held.some(function (x) { return !x.cancelled; })) clearInterval(r.heldTimer);
|
|
4900
5274
|
}
|
|
@@ -4966,7 +5340,7 @@
|
|
|
4966
5340
|
sessionStatus('Copying the session. This can take up to a minute.');
|
|
4967
5341
|
var settle = function (b) {
|
|
4968
5342
|
if (b.forked === true) {
|
|
4969
|
-
r.forkDraft = null; clearBoxOf(text); readerFilesSent(readerFilesOn() ? readerBox(r) : null, ids);
|
|
5343
|
+
r.forkDraft = null; clearBoxOf(text, r); readerFilesSent(readerFilesOn() ? readerBox(r) : null, ids);
|
|
4970
5344
|
sessionStatus('Forked. The copy has your message and shows in Sessions in a moment.');
|
|
4971
5345
|
setTimeout(function () { if (state.pane === 'sessions') loadSessions(true); }, 4000);
|
|
4972
5346
|
return true;
|
|
@@ -5361,7 +5735,7 @@
|
|
|
5361
5735
|
// estimates into them, and its rows keep Control's category titles.
|
|
5362
5736
|
function loadUsage() {
|
|
5363
5737
|
SX.view = 'usage'; sxSave();
|
|
5364
|
-
|
|
5738
|
+
navReplace('#/sessions');
|
|
5365
5739
|
}
|
|
5366
5740
|
function renderUsageView(force) {
|
|
5367
5741
|
var box = $('sessions-usage'); if (!box) return;
|
|
@@ -7136,10 +7510,13 @@
|
|
|
7136
7510
|
|
|
7137
7511
|
// ── events (one delegated listener) ─────────────────────
|
|
7138
7512
|
document.addEventListener('click', function (e) {
|
|
7139
|
-
var t = e.target.closest('[data-today-next],[data-next-prep],[data-next-prep-cancel],[data-next-prep-remove],[data-today-needs],[data-today],[data-goto],[data-pane],[data-session],[data-meeting],[data-speakers],[data-action],[data-open-ask],[data-theme-choice],#topbar-back,#section-btn,#sign-out,#copy-link,#ask-close,#auth-switch,#auth-token-show,#auth-retry,#device-code-btn');
|
|
7513
|
+
var t = e.target.closest('[data-today-next],[data-next-prep],[data-next-prep-cancel],[data-next-prep-remove],[data-today-needs],[data-today],[data-goto],[data-pane],[data-session],[data-meeting],[data-speakers],[data-action],[data-open-ask],[data-theme-choice],[data-trail-back],#topbar-back,#rail-back,#section-btn,#sign-out,#copy-link,#ask-close,#auth-switch,#auth-token-show,#auth-retry,#device-code-btn');
|
|
7140
7514
|
if (!t) { if (e.target.id === 'ask-sheet') closeAsk(); if (e.target.id === 'section-menu') closeMenu(); return; }
|
|
7141
7515
|
var inSheet = !!t.closest('#ask-sheet');
|
|
7142
7516
|
if (t.id === 'topbar-back') return goBack();
|
|
7517
|
+
if (t.id === 'rail-back') return goBack();
|
|
7518
|
+
// 7.0.30 (pick B): a trail step is the browser's own Back, that many steps.
|
|
7519
|
+
if (t.hasAttribute('data-trail-back')) { e.preventDefault(); history.go(-Number(t.getAttribute('data-trail-back'))); return; }
|
|
7143
7520
|
if (t.id === 'section-btn') return openMenu();
|
|
7144
7521
|
if (t.id === 'auth-retry') { t.disabled = true; t.textContent = 'Checking'; bootStatus().then(function () { t.disabled = false; t.textContent = 'Try again'; }); return; }
|
|
7145
7522
|
if (t.id === 'auth-switch') {
|
|
@@ -7284,6 +7661,7 @@
|
|
|
7284
7661
|
});
|
|
7285
7662
|
document.addEventListener('input', function (e) {
|
|
7286
7663
|
if (e.target.tagName === 'TEXTAREA') autoGrow(e.target);
|
|
7664
|
+
if (e.target.id === 'session-input' && typeof unsentSave === 'function') unsentSave();
|
|
7287
7665
|
if (e.target.id === 'ask-q') renderAsk();
|
|
7288
7666
|
if (e.target.hasAttribute && e.target.hasAttribute('data-next-prep-steer')) nextPrepInput(e.target);
|
|
7289
7667
|
if (e.target.id === 'sessions-q' && !e.target.value.trim()) renderSessionGroups(state.sessions);
|
|
@@ -7507,13 +7885,54 @@
|
|
|
7507
7885
|
} catch (_e) { return false; }
|
|
7508
7886
|
}
|
|
7509
7887
|
|
|
7888
|
+
// Page errors (lead 08:31, 11/11): Control Web reported none, so "no errors in his rows" proved nothing. A script error
|
|
7889
|
+
// or an unhandled rejection becomes one bounded row, event web.error: what kind it was, the error's class (TypeError),
|
|
7890
|
+
// its code and HTTP status when it has them, the script file's own name, line and column, and the pane. Never the
|
|
7891
|
+
// message, a stack or any text: those can carry what a person typed. At most WEB_ERR_MAX rows a page load, one per
|
|
7892
|
+
// kind, class, code and place.
|
|
7893
|
+
var WEB_ERR_MAX = 10, WEB_ERR = { n: 0, seen: {} };
|
|
7894
|
+
function webErrorRow(kind, err, file, line, col) {
|
|
7895
|
+
try {
|
|
7896
|
+
var name = err && typeof err.name === 'string' ? err.name : '';
|
|
7897
|
+
var cls = /^[A-Za-z][A-Za-z0-9]{0,39}$/.test(name) ? name : (err === null || err === undefined ? 'none' : typeof err === 'object' ? 'object' : typeof err);
|
|
7898
|
+
var code = err && (typeof err.code === 'string' || typeof err.code === 'number') && /^[A-Za-z0-9_.-]{1,40}$/.test(String(err.code)) ? String(err.code) : '';
|
|
7899
|
+
var status = err && typeof err.status === 'number' && isFinite(err.status) ? err.status : null;
|
|
7900
|
+
var src = String(file || '').split(/[?#]/)[0].split('/').pop();
|
|
7901
|
+
src = /^[A-Za-z0-9_.-]{1,40}$/.test(src) ? src : '';
|
|
7902
|
+
var ln = Number(line) > 0 ? Math.floor(Number(line)) : 0, cn = Number(col) > 0 ? Math.floor(Number(col)) : 0;
|
|
7903
|
+
var key = [kind, cls, code, status, src, ln].join('|');
|
|
7904
|
+
if (WEB_ERR.seen[key] || WEB_ERR.n >= WEB_ERR_MAX) return false;
|
|
7905
|
+
WEB_ERR.seen[key] = 1; WEB_ERR.n++;
|
|
7906
|
+
var pane = typeof state === 'object' && state && typeof state.pane === 'string' && /^[a-z-]{1,24}$/.test(state.pane) ? state.pane : '';
|
|
7907
|
+
api('/api/diag/client', { method: 'POST', body: { ts: Date.now(), level: 'error', event: 'web.error',
|
|
7908
|
+
data: { kind: kind, cls: cls, code: code, status: status, src: src, line: ln, col: cn, pane: pane, n: WEB_ERR.n } }, timeoutMs: 5000 }).catch(function () {});
|
|
7909
|
+
return true;
|
|
7910
|
+
} catch (_e) { return false; }
|
|
7911
|
+
}
|
|
7912
|
+
if (window.addEventListener) {
|
|
7913
|
+
// Only script errors: a failed image or file load fires 'error' on its element, which does not reach the window.
|
|
7914
|
+
window.addEventListener('error', function (e) { if (e && (e.error || e.filename)) webErrorRow('error', e.error || null, e.filename, e.lineno, e.colno); });
|
|
7915
|
+
window.addEventListener('unhandledrejection', function (e) { webErrorRow('rejection', e ? e.reason : null, '', 0, 0); });
|
|
7916
|
+
}
|
|
7917
|
+
|
|
7918
|
+
// 7.0.28: when theme.js had to bring back a stylesheet or a script (the page opened before it could reach the Mac), one
|
|
7919
|
+
// numbers-only row says so: how ('sheets', 'reload' or 'gave-up'), how many sheets and scripts had failed, how many
|
|
7920
|
+
// checks it took and how long. A reload's report comes back from sessionStorage once.
|
|
7921
|
+
(function restyleRow() {
|
|
7922
|
+
var R = window.CosRestyle;
|
|
7923
|
+
if (!R) return;
|
|
7924
|
+
var row = function (d) { if (d && typeof d === 'object') layoutCrumb('restyle', { how: String(d.how || '').slice(0, 24), sheets: +d.sheets || 0, scripts: +d.scripts || 0, tries: +d.tries || 0, ms: +d.ms || 0 }); };
|
|
7925
|
+
row(R.takeSaved && R.takeSaved());
|
|
7926
|
+
window.addEventListener('cos-restyle', function (e) { row(e && e.detail); });
|
|
7927
|
+
})();
|
|
7928
|
+
|
|
7510
7929
|
// ── the shared kit for pane modules (panes/*.js) ───────
|
|
7511
7930
|
window.COSWeb = {
|
|
7512
7931
|
$: $, esc: esc, md: md, plain: plain, structureLensAnswer: structureLensAnswer, tableFit: tableFit, api: api, toast: toast, ago: ago, clock: clock, toDate: toDate,
|
|
7513
7932
|
plural: plural, countWord: countWord, dayWord: dayWord, plainDay: plainDay, providerLabel: providerLabel, domainLabel: domainLabel,
|
|
7514
7933
|
skeletonRows: skeletonRows, emptyBox: emptyBox, setStatus: setStatus, autoGrow: autoGrow,
|
|
7515
7934
|
readStore: readStore, writeStore: writeStore, copyText: copyText, isPhone: isPhone,
|
|
7516
|
-
state: state, titles: TITLES, go: go, parseRoute: parseRoute, goBack: goBack,
|
|
7935
|
+
state: state, titles: TITLES, go: go, replace: navReplace, parseRoute: parseRoute, goBack: goBack,
|
|
7517
7936
|
registerPane: function (name, def) { PANES[name] = def; },
|
|
7518
7937
|
sessionList: sessionList,
|
|
7519
7938
|
petPrefs: petPrefs,
|
|
@@ -7523,7 +7942,7 @@
|
|
|
7523
7942
|
onShell: function (fn) { shellHooks.push(fn); },
|
|
7524
7943
|
// The detail title after "COS Control > Section" (Control's breadcrumb). Pane modules set it
|
|
7525
7944
|
// once a detail has loaded; route() clears it on every navigation.
|
|
7526
|
-
setCrumb: function (text) { crumbDetail = String(text || ''); runHooks(routeHooks, parseRoute()); },
|
|
7945
|
+
setCrumb: function (text) { crumbDetail = String(text || ''); navLabel(crumbDetail); paintTrail(); runHooks(routeHooks, parseRoute()); },
|
|
7527
7946
|
crumb: function () { return crumbDetail; },
|
|
7528
7947
|
// Panes that read with their own fetch call this on a 401 so the sign-in screen shows.
|
|
7529
7948
|
showAuth: function (message) { showAuth(message); },
|
|
@@ -7679,7 +8098,7 @@
|
|
|
7679
8098
|
var sc = window.screen || {}, w = lrWide();
|
|
7680
8099
|
layoutCrumb('device', { sw: sc.width, sh: sc.height, sah: sc.availHeight, oh: window.outerHeight, ow: window.outerWidth, dpr: window.devicePixelRatio,
|
|
7681
8100
|
sat: lrProbe('padding-top:env(safe-area-inset-top)', 'paddingTop'), sab: lrProbe('padding-bottom:env(safe-area-inset-bottom)', 'paddingBottom'),
|
|
7682
|
-
anyCoarse: lrMatch('(any-pointer: coarse)'), shortTouch: lrMatch(SHORT_TOUCH_MQ), phone: isPhone(), cw: w.cw, dw: w.dw, scw: w.scw, wide: w.wide });
|
|
8101
|
+
anyCoarse: lrMatch('(any-pointer: coarse)'), shortTouch: lrMatch(SHORT_TOUCH_MQ), rail: typeof sideRail === 'function' ? sideRail() : false, railHit: typeof RAIL === 'object' ? RAIL.hit : 0, phone: isPhone(), cw: w.cw, dw: w.dw, scw: w.scw, wide: w.wide });
|
|
7683
8102
|
}
|
|
7684
8103
|
// The focus line is for touch screens only (QA W3): on a Mac it would only spend the budget.
|
|
7685
8104
|
function lrFocus() {
|
|
@@ -7774,6 +8193,7 @@
|
|
|
7774
8193
|
|
|
7775
8194
|
// Every pane module is a plain script after this one; boot waits for all of them.
|
|
7776
8195
|
function boot() {
|
|
8196
|
+
if (typeof railSync === 'function') railSync(); // 7.0.29: before the shell shows, so the rail never moves the page
|
|
7777
8197
|
// The open-time report's clock (ms since navigation start, -1 where there is none) and the two numbers boot gives it:
|
|
7778
8198
|
// when boot started (every script had run) and the sign-in check's round trip. Kept inside boot, and FIRST_PAINT
|
|
7779
8199
|
// looked up, never assumed, because the harnesses that run boot alone have neither.
|
|
@@ -7822,20 +8242,27 @@
|
|
|
7822
8242
|
return api('/api/web-session/status').then(function (st) { return bootAnswer(st); }, function (err) { return bootFail(err); });
|
|
7823
8243
|
}
|
|
7824
8244
|
function bootAnswer(st) {
|
|
8245
|
+
AUTH_RETRY.n = 0; // the Mac answered: a later not-answering card starts its quick checks again
|
|
7825
8246
|
if (st && st.authenticated) return showShell(st);
|
|
7826
8247
|
// Q1 S2: a browser that was signed in before learns why it is not now (the 30 days, no use in its last 2 days, a sign-out).
|
|
7827
8248
|
showAuth(typeof signinKnown === 'function' && signinKnown() ? signinLapsedCopy() : undefined);
|
|
7828
8249
|
}
|
|
7829
8250
|
function bootFail(err) {
|
|
7830
8251
|
// F23: a Mac that did not answer is not a sign-in problem. Say so, offer Try again, keep the form out of the way.
|
|
7831
|
-
|
|
7832
|
-
|
|
7833
|
-
|
|
7834
|
-
|
|
8252
|
+
// showAuth turns the card off first (authOffline(false)); that must not restart the 1, 2, 4, 8 s steps, so only a Mac
|
|
8253
|
+
// that answered (bootAnswer, or an error with a status here) resets them.
|
|
8254
|
+
if (err && err.status) { AUTH_RETRY.n = 0; showAuth(err.message); } else { showAuth(); authOffline(true); }
|
|
8255
|
+
}
|
|
8256
|
+
// Q1 M2: the not-answering card checks again while the page is in front, and stops while it is hidden. 7.0.28 (Miles
|
|
8257
|
+
// 19:28): the first checks come after 1, 2, 4 and 8 s, because a phone that opened the page before its tunnel was up
|
|
8258
|
+
// usually reaches the Mac within seconds; then every 30 s. The steps start over once the Mac has answered.
|
|
8259
|
+
var AUTH_RETRY = { timer: null, ms: 30000, steps: [1000, 2000, 4000, 8000], n: 0 };
|
|
7835
8260
|
function authRetrySchedule(on) {
|
|
7836
8261
|
if (AUTH_RETRY.timer) { clearTimeout(AUTH_RETRY.timer); AUTH_RETRY.timer = null; }
|
|
7837
8262
|
if (!on || document.hidden) return;
|
|
7838
|
-
|
|
8263
|
+
var ms = AUTH_RETRY.n < AUTH_RETRY.steps.length ? AUTH_RETRY.steps[AUTH_RETRY.n] : AUTH_RETRY.ms;
|
|
8264
|
+
AUTH_RETRY.n++;
|
|
8265
|
+
AUTH_RETRY.timer = setTimeout(function () { AUTH_RETRY.timer = null; if (!document.hidden) bootStatus(); }, ms);
|
|
7839
8266
|
}
|
|
7840
8267
|
document.addEventListener('visibilitychange', function () {
|
|
7841
8268
|
var box = $('auth-offline');
|
|
@@ -7853,8 +8280,8 @@
|
|
|
7853
8280
|
if (title) title.textContent = on ? (offline ? 'This device is offline' : 'COS on your Mac is not answering') : 'Connect this browser';
|
|
7854
8281
|
if (intro) intro.classList.toggle('hidden', !!on);
|
|
7855
8282
|
var text = $('auth-offline-text');
|
|
7856
|
-
if (text && on) text.textContent = offline ? 'Connect to the internet and Tailscale. This page checks again every 30 seconds.'
|
|
7857
|
-
: 'Your Mac may be asleep, or this device may be offline or off Tailscale. This page checks again every 30 seconds.';
|
|
8283
|
+
if (text && on) text.textContent = offline ? 'Connect to the internet and Tailscale. This page checks again in a few seconds, then every 30 seconds.'
|
|
8284
|
+
: 'Your Mac may be asleep, or this device may be offline or off Tailscale. This page checks again in a few seconds, then every 30 seconds.';
|
|
7858
8285
|
authRetrySchedule(!!on);
|
|
7859
8286
|
if (on) { var err = $('auth-error'); if (err) { setStatus(err, '', true); err.classList.add('hidden'); } }
|
|
7860
8287
|
}
|