Reader settings per login session
Each sign-in keeps its own reader mode, zoom, page tone and skipping once it changes one; sessions that never changed follow the latest set, and a new sign-in starts from the session used last. Settings -> Reading shows every session's state, lets a session follow the latest again or become the latest, and turns the feature off for one shared set. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
03767d5704
commit
e3e57b60bd
9 files changed
+326
-27
No files matched your search
@@ -0,0 +1,88 @@
|
||||
'use strict';
|
||||
/* Reader settings per login session (docs/architecture.md, "Reader settings per session").
|
||||
*
|
||||
* The user's own reader settings (`user.settings`) are the "latest" set. A session that never changed a
|
||||
* reader setting follows the latest set; the first change copies the latest set into the session and
|
||||
* locks it there, and also makes it the new latest set. Using a locked session makes its set the latest
|
||||
* again, so a new login starts from the last active session. With `readerPerSession: false` there is
|
||||
* one set per user, as before; locked sets are kept and return when the switch is turned on again. */
|
||||
const crypto = require('crypto');
|
||||
|
||||
const READER_KEYS = ['startMode', 'lastMode', 'adapt', 'skipExtras', 'skipCovers', 'flowPageGap', 'pageTone', 'zoom'];
|
||||
|
||||
const perSession = settings => settings.readerPerSession !== false;
|
||||
const publicId = key => crypto.createHash('sha256').update('reader-session:' + key).digest('hex').slice(0, 16);
|
||||
|
||||
function pick(settings) {
|
||||
const out = {};
|
||||
for (const k of READER_KEYS) if (settings[k] !== undefined) out[k] = k === 'zoom' ? { ...settings[k] } : settings[k];
|
||||
return out;
|
||||
}
|
||||
|
||||
// `base` is the user's merged settings (defaults + user.settings); the session's locked set wins over it.
|
||||
function effective(base, session) {
|
||||
if (!perSession(base) || !session?.reader) return base;
|
||||
return { ...base, ...pick(session.reader), zoom: { ...session.reader.zoom } };
|
||||
}
|
||||
|
||||
// Splits a validated settings patch: reader keys go to the session (locking it) and to the latest set.
|
||||
function applyPatch({ user, base, session, key, patch }) {
|
||||
const s = (user.settings = user.settings || {});
|
||||
const reader = pick(patch), rest = Object.fromEntries(Object.entries(patch).filter(([k]) => !READER_KEYS.includes(k)));
|
||||
Object.assign(s, rest);
|
||||
if (!Object.keys(reader).length) return;
|
||||
if (perSession({ ...base, ...rest }) && session) {
|
||||
session.reader = { ...pick(effective(base, session)), ...reader };
|
||||
Object.assign(s, pick(session.reader));
|
||||
user.readerFrom = publicId(key);
|
||||
} else {
|
||||
Object.assign(s, reader);
|
||||
user.readerFrom = null;
|
||||
}
|
||||
}
|
||||
|
||||
// A request from a locked session makes its set the latest one. Returns true when something changed.
|
||||
function noteActive(user, base, session, key) {
|
||||
if (!perSession(base) || !session?.reader) return false;
|
||||
const id = publicId(key);
|
||||
if (user.readerFrom === id) return false;
|
||||
user.settings = { ...(user.settings || {}), ...pick(session.reader) };
|
||||
user.readerFrom = id;
|
||||
return true;
|
||||
}
|
||||
|
||||
function deviceLabel(agent) {
|
||||
const a = String(agent || '');
|
||||
if (!a) return 'Unknown device';
|
||||
const os = /iPhone/.test(a) ? 'iPhone' : /iPad/.test(a) ? 'iPad' : /Android/.test(a) ? 'Android'
|
||||
: /Windows/.test(a) ? 'Windows' : /Mac OS X|Macintosh/.test(a) ? 'Mac' : /CrOS/.test(a) ? 'ChromeOS' : /Linux/.test(a) ? 'Linux' : '';
|
||||
const browser = /Edg\//.test(a) ? 'Edge' : /OPR\//.test(a) ? 'Opera' : /Firefox\//.test(a) ? 'Firefox'
|
||||
: /Chrome\//.test(a) ? 'Chrome' : /Safari\//.test(a) ? 'Safari' : '';
|
||||
return [browser, os].filter(Boolean).join(' on ') || a.slice(0, 60);
|
||||
}
|
||||
|
||||
// What Settings shows: every login session of the user with its state and effective reader set.
|
||||
function overview(user, base, sessions, currentKey) {
|
||||
const list = Object.entries(sessions).filter(([, s]) => s.u === user.name).map(([key, s]) => ({
|
||||
id: publicId(key), device: deviceLabel(s.agent), created: s.t, seen: s.seen || s.t,
|
||||
current: key === currentKey, locked: !!s.reader, reader: pick(effective(base, s)),
|
||||
})).sort((a, b) => b.current - a.current || b.seen - a.seen);
|
||||
return { enabled: perSession(base), current: currentKey ? publicId(currentKey) : null,
|
||||
latestFrom: user.readerFrom || null, latest: pick(base), sessions: list };
|
||||
}
|
||||
|
||||
// Settings actions on one of the user's sessions: 'follow' drops its lock, 'latest' makes its set the latest.
|
||||
function act(user, base, sessions, id, action) {
|
||||
const entry = Object.entries(sessions).find(([key, s]) => s.u === user.name && publicId(key) === id);
|
||||
if (!entry) return false;
|
||||
const [key, session] = entry;
|
||||
if (action === 'follow') { delete session.reader; if (user.readerFrom === id) user.readerFrom = null; return true; }
|
||||
if (action === 'latest') {
|
||||
user.settings = { ...(user.settings || {}), ...pick(effective(base, session)) };
|
||||
user.readerFrom = session.reader ? publicId(key) : null;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
module.exports = { READER_KEYS, publicId, pick, effective, applyPatch, noteActive, deviceLabel, overview, act };
|
||||
Reference in new issue
Block a user