Base del merge a tres bandas: git 0.9.5 de epsylon, esta rama y el src de la
APK oficial 1.3.1 (la variante movil: OASIS_MOBILE, regex sin \p{}, audio y
video de Android). Corre en el mismo nodejs-mobile 18.20.4 que ya usabamos.
Se conserva de la rama: Karvan, identidades y multicuenta, topbar y barra
inferior, borrado de EXIF sin sharp, filtro hot del foro, RTC sin STUN y los
modulos apagados en movil (wallet, banking, torrents).
Se toma de upstream lo que ya hace de serie: respuesta citada en chats, ssb-lan
en movil, configuracion en ~/.ssb/oasis (sobrevive a reinstalar el backend).
El asistente de bienvenida deja elegir workflow tambien en movil.
218 lines
7.8 KiB
JavaScript
218 lines
7.8 KiB
JavaScript
const fs = require('fs');
|
|
const path = require('path');
|
|
const pull = require('../server/node_modules/pull-stream');
|
|
|
|
const STEPS = ['profile', 'ux', 'workflow', 'theme', 'community', 'greeting'];
|
|
const MARKED_STEPS = ['profile', 'ux', 'workflow', 'theme', 'community'];
|
|
const FLAG = 'oasis-first-contact';
|
|
const PENDING = 'welcome=pending';
|
|
const DISMISSED = 'welcome=dismissed';
|
|
const DONE = 'welcome=done';
|
|
|
|
const ssbPathOf = (given) => {
|
|
if (given) return given;
|
|
try { return require('../server/ssb_config').path; } catch (_) { return null; }
|
|
};
|
|
|
|
const flagPath = (dir) => {
|
|
try {
|
|
const state = require('../configs/state-manager');
|
|
if (!dir || path.resolve(dir) === state.ssbDir()) return state.statePath(FLAG);
|
|
} catch (_) {}
|
|
return path.join(dir, FLAG);
|
|
};
|
|
|
|
function readFlag(dir) {
|
|
if (!dir) return { exists: false, id: '', markers: new Set() };
|
|
let raw = '';
|
|
try { raw = fs.readFileSync(flagPath(dir), 'utf8'); } catch (_) { return { exists: false, id: '', markers: new Set() }; }
|
|
const lines = raw.split('\n').map(l => l.trim()).filter(Boolean);
|
|
const isMarker = (l) => /^(welcome|step)=/.test(l);
|
|
const markers = new Set(lines.filter(isMarker));
|
|
return { exists: true, id: lines.find(l => !isMarker(l)) || '', markers };
|
|
}
|
|
|
|
function appendMarker(dir, marker, createIfMissing) {
|
|
if (!dir) return false;
|
|
const flag = readFlag(dir);
|
|
if (flag.markers.has(marker)) return true;
|
|
if (!flag.exists && !createIfMissing) return false;
|
|
try {
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
fs.appendFileSync(flagPath(dir), `${marker}\n`);
|
|
return true;
|
|
} catch (_) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
const isPending = (dir) => {
|
|
if (!dir) return false;
|
|
const flag = readFlag(dir);
|
|
if (!flag.exists) return true;
|
|
if (flag.markers.has(DISMISSED) || flag.markers.has(DONE)) return false;
|
|
return flag.markers.has(PENDING);
|
|
};
|
|
|
|
const bannerVisible = (ssbPath) => isPending(ssbPathOf(ssbPath));
|
|
|
|
const DEFAULT_LANGUAGE = 'en';
|
|
|
|
const browserLanguage = (header, supported) => {
|
|
const list = Array.isArray(supported) ? supported : [];
|
|
const wanted = String(header || '').slice(0, 1024).split(',').map((part, i) => {
|
|
const [tag, ...params] = part.trim().split(';');
|
|
const qParam = params.map(p => p.trim()).find(p => /^q=/i.test(p));
|
|
const q = qParam ? Number(qParam.slice(2)) : 1;
|
|
return { tag: String(tag || '').trim().toLowerCase().replace(/_/g, '-'), q: Number.isFinite(q) ? q : 0, i };
|
|
}).filter(w => w.tag && w.tag !== '*' && w.q > 0).sort((a, b) => b.q - a.q || a.i - b.i);
|
|
for (const w of wanted) {
|
|
if (list.includes(w.tag)) return w.tag;
|
|
const base = w.tag.split('-')[0];
|
|
if (list.includes(base)) return base;
|
|
}
|
|
return '';
|
|
};
|
|
|
|
const firstRunLanguage = ({ fresh, isPublic, configured, cookie, header, supported } = {}) => {
|
|
const list = Array.isArray(supported) ? supported : [];
|
|
if (!fresh || isPublic) return { settled: true, language: '' };
|
|
if (list.includes(configured) && configured !== DEFAULT_LANGUAGE) return { settled: true, language: '' };
|
|
const picked = (list.includes(cookie) ? cookie : '') || browserLanguage(header, list);
|
|
if (!picked && !String(header || '').trim()) return { settled: false, language: '' };
|
|
return { settled: true, language: picked };
|
|
};
|
|
|
|
module.exports = ({ cooler, ssbPath } = {}) => {
|
|
const dir = () => ssbPathOf(ssbPath);
|
|
|
|
const ownMessages = async () => {
|
|
const ssb = await cooler.open();
|
|
const mine = ssb.id;
|
|
const all = await new Promise((resolve, reject) =>
|
|
pull(ssb.createUserStream({ id: mine, reverse: true, limit: 2000 }), pull.collect((err, msgs) => err ? reject(err) : resolve(msgs || [])))
|
|
);
|
|
return { mine, messages: all.filter(m => m && m.value && m.value.author === mine) };
|
|
};
|
|
|
|
const hasProfile = (mine, messages) => messages.some(m => {
|
|
const c = m.value.content;
|
|
if (!c || typeof c !== 'object' || c.type !== 'about' || c.about !== mine) return false;
|
|
return String(c.name || '').trim().length > 0 || String(c.image || '').trim().length > 0;
|
|
});
|
|
|
|
const hasGreeted = (messages) => messages.some(m => {
|
|
const c = m.value.content;
|
|
return c && typeof c === 'object' && c.type === 'feed' && String(c.text || '').trim().length > 0;
|
|
});
|
|
|
|
const hasFederated = (mine, messages) => {
|
|
try {
|
|
const gossip = JSON.parse(fs.readFileSync(path.join(dir(), 'gossip.json'), 'utf8'));
|
|
if (Array.isArray(gossip) && gossip.length > 0) return true;
|
|
} catch (_) {}
|
|
return messages.some(m => {
|
|
const c = m.value.content;
|
|
return c && typeof c === 'object' && c.type === 'contact' && c.following === true && c.contact && c.contact !== mine;
|
|
});
|
|
};
|
|
|
|
return {
|
|
STEPS,
|
|
|
|
flagPath() {
|
|
return flagPath(dir());
|
|
},
|
|
|
|
firstContactSeen(feedId) {
|
|
const flag = readFlag(dir());
|
|
return flag.exists && (!feedId || flag.id === String(feedId));
|
|
},
|
|
|
|
begin(feedId) {
|
|
const target = dir();
|
|
if (!target) return false;
|
|
const flag = readFlag(target);
|
|
const closed = flag.markers.has(DISMISSED) || flag.markers.has(DONE);
|
|
const markers = closed ? [...flag.markers] : [...new Set([PENDING, ...flag.markers])];
|
|
try {
|
|
fs.mkdirSync(target, { recursive: true });
|
|
fs.writeFileSync(flagPath(target), `${String(feedId || '')}\n${new Date().toISOString()}\n${markers.join('\n')}\n`);
|
|
return true;
|
|
} catch (_) {
|
|
return false;
|
|
}
|
|
},
|
|
|
|
adopt(feedId) {
|
|
const target = dir();
|
|
if (!target || !feedId) return false;
|
|
try {
|
|
fs.mkdirSync(target, { recursive: true });
|
|
fs.writeFileSync(flagPath(target), `${String(feedId)}\n${new Date().toISOString()}\n${DONE}\n`);
|
|
return true;
|
|
} catch (_) {
|
|
return false;
|
|
}
|
|
},
|
|
|
|
markStep(step) {
|
|
if (!MARKED_STEPS.includes(step)) return false;
|
|
if (!isPending(dir())) return true;
|
|
return appendMarker(dir(), `step=${step}`);
|
|
},
|
|
|
|
dismiss() {
|
|
return appendMarker(dir(), DISMISSED, true);
|
|
},
|
|
|
|
isVisible() {
|
|
return isPending(dir());
|
|
},
|
|
|
|
async status(available) {
|
|
const target = dir();
|
|
const flag = readFlag(target);
|
|
const usable = STEPS.filter(s => !available || available[s] !== false);
|
|
let mine = '';
|
|
let messages = [];
|
|
try { ({ mine, messages } = await ownMessages()); } catch (_) { mine = ''; messages = []; }
|
|
const steps = {
|
|
profile: hasProfile(mine, messages) || flag.markers.has('step=profile'),
|
|
ux: flag.markers.has('step=ux'),
|
|
workflow: flag.markers.has('step=workflow'),
|
|
theme: flag.markers.has('step=theme'),
|
|
community: hasFederated(mine, messages) || flag.markers.has('step=community'),
|
|
greeting: hasGreeted(messages)
|
|
};
|
|
const profile = { id: mine, name: '', description: '', image: '' };
|
|
for (const m of messages) {
|
|
const c = m.value.content;
|
|
if (!c || typeof c !== 'object' || c.type !== 'about' || c.about !== mine) continue;
|
|
if (!profile.name && String(c.name || '').trim()) profile.name = String(c.name).trim();
|
|
if (!profile.description && String(c.description || '').trim()) profile.description = String(c.description).trim();
|
|
if (!profile.image && String(c.image || '').trim()) profile.image = String(c.image).trim();
|
|
if (profile.name && profile.description && profile.image) break;
|
|
}
|
|
const done = usable.filter(s => steps[s]).length;
|
|
const complete = done === usable.length;
|
|
if (complete && isPending(target)) appendMarker(target, DONE);
|
|
return {
|
|
id: flag.id,
|
|
pending: isPending(target),
|
|
dismissed: flag.markers.has(DISMISSED),
|
|
steps,
|
|
profile,
|
|
usable,
|
|
done,
|
|
total: usable.length,
|
|
complete
|
|
};
|
|
}
|
|
};
|
|
};
|
|
|
|
module.exports.bannerVisible = bannerVisible;
|
|
module.exports.STEPS = STEPS;
|
|
module.exports.browserLanguage = browserLanguage;
|
|
module.exports.firstRunLanguage = firstRunLanguage;
|