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.
80 lines
3.4 KiB
JavaScript
80 lines
3.4 KiB
JavaScript
const fs = require('fs');
|
|
const path = require('path');
|
|
const { spawn } = require('child_process');
|
|
const { getConfig } = require('../configs/config-manager.js');
|
|
|
|
const REPEAT_MS = 60000;
|
|
const ICON = path.join(__dirname, '..', 'client', 'assets', 'images', 'snh-oasis.jpg');
|
|
const recent = new Map();
|
|
|
|
const onPath = (bin) => (process.env.PATH || '').split(path.delimiter).some(d => d && fs.existsSync(path.join(d, bin)));
|
|
|
|
const i18nNow = () => {
|
|
const all = require('../client/assets/translations/i18n');
|
|
let lang = 'en';
|
|
try { lang = getConfig().language || 'en'; } catch (_) {}
|
|
return { ...all.en, ...(all[lang] || {}) };
|
|
};
|
|
|
|
const displayName = (id, name) => {
|
|
const n = String(name || '').trim();
|
|
if (!n || n === 'Redacted' || n === String(id || '').replace(/^@/, '').slice(0, 8)) return String(id || '');
|
|
return n.startsWith('@') ? n : `@${n}`;
|
|
};
|
|
|
|
const MOBILE_QUEUE = path.join(__dirname, '..', '..', '.notifications.json');
|
|
const queueForAndroid = (keyword, body, line, key) => {
|
|
try {
|
|
let list = [];
|
|
try { list = JSON.parse(fs.readFileSync(MOBILE_QUEUE, 'utf8')); } catch (_) { list = []; }
|
|
if (!Array.isArray(list)) list = [];
|
|
const t = i18nNow();
|
|
const k = String(keyword || '');
|
|
const route = k === t.phoneTitle ? '/phone' : k === t.inbox ? '/inbox' : k === t.emergenciesTitle ? '/emergencies' : '/';
|
|
const entry = { id: Date.now() + '-' + Math.random().toString(36).slice(2, 8), keyword: k || 'Oasis', body, line, route, key: key ? String(key) : null, ts: Date.now() };
|
|
if (entry.key && entry.key.indexOf('call:') === 0) {
|
|
const ringing = String(t.phoneIsCalling || '');
|
|
entry.caller = ringing && body.endsWith(ringing) ? body.slice(0, body.length - ringing.length).trim() : body;
|
|
entry.accept = String(t.phoneAnswer || '');
|
|
entry.reject = String(t.phoneReject || '');
|
|
}
|
|
list.push(entry);
|
|
while (list.length > 20) list.shift();
|
|
fs.writeFileSync(MOBILE_QUEUE, JSON.stringify(list));
|
|
return true;
|
|
} catch (_) {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
const notify = (keyword, text, key = null, opts = {}) => {
|
|
const onMobile = process.env.OASIS_MOBILE === '1';
|
|
if (!onMobile && !onPath('notify-send')) return false;
|
|
const body = String(text || '').replace(/\s+/g, ' ').trim().replace(/[.。!…]+$/, '').slice(0, 200);
|
|
const line = `${String(keyword || 'Oasis').toUpperCase()}: ${body}...`;
|
|
const now = Date.now();
|
|
const dedupe = key || line;
|
|
for (const [k, t] of recent) if (now - t > REPEAT_MS) recent.delete(k);
|
|
if (recent.has(dedupe)) return false;
|
|
recent.set(dedupe, now);
|
|
if (onMobile) return queueForAndroid(keyword, body, line, key);
|
|
const actions = Array.isArray(opts.actions) ? opts.actions.filter(x => x && x.key && x.label) : [];
|
|
const extra = [
|
|
...(opts.timeoutMs > 0 ? ['-t', String(Math.round(opts.timeoutMs))] : []),
|
|
...actions.flatMap(x => ['-A', `${x.key}=${x.label}`])
|
|
];
|
|
try {
|
|
const p = spawn('notify-send', ['-a', 'Oasis', ...(fs.existsSync(ICON) ? ['-i', ICON] : []), ...extra, line], { stdio: ['ignore', actions.length ? 'pipe' : 'ignore', 'ignore'] });
|
|
p.on('error', () => {});
|
|
if (actions.length && typeof opts.onAction === 'function') {
|
|
let out = '';
|
|
p.stdout.on('data', (d) => { out += d; });
|
|
p.on('close', () => { const chosen = out.trim(); if (actions.some(x => x.key === chosen)) opts.onAction(chosen); });
|
|
}
|
|
return true;
|
|
} catch (_) {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
module.exports = { notify, i18nNow, displayName };
|