Release Preview 20 shared desktop, Android and Sargo updates

Restore Gaia settings and media workflows, extend native network adapters, and align the APK payload. Generate French/English changelogs and website entries from the simplified JSON source.

Validation: shared JS and Tundra unit tests; desktop workflows and persistence; signed APK payload parity; Sargo RAM and installed startup with boot readback. Android runtime qualification is recorded separately.
This commit is contained in:
OverSu 2026-10-13 00:19:45 +02:00
commit 1311bed5e9
170 changed files with 19698 additions and 2495 deletions

29
services/mms.mjs Normal file
View file

@ -0,0 +1,29 @@
// SPDX-License-Identifier: MPL-2.0
// JSON transport for Gaia attachments; blobs are encoded at the app boundary.
export const MMS_MAX_BYTES = 600 * 1024;
export function mmsPayload(message) {
if (!message || !Array.isArray(message.receivers) || !message.receivers.length || message.receivers.length>10 ||
message.receivers.some(n=>typeof n!=='string' || !/^\+?[0-9]{3,20}$/.test(n))) throw Error('INVALID_NUMBER');
const parts=[];
if(message.smil) parts.push({id:'smil.xml',type:'application/smil',data:utf8Base64(message.smil)});
for(const [i,a] of (message.attachments || []).entries()) {
const content=a.content;
if(!content?.__vulpesBlobV1 || typeof content.base64!=='string' || !/^[A-Za-z0-9+/]*={0,2}$/.test(content.base64)) throw Error('INVALID_MMS');
parts.push({id:a.location || a.id?.replace(/[<>]/g,'') || 'attachment-'+i,type:content.type,data:content.base64});
}
if(!parts.length || parts.length>20 || parts.reduce((n,p)=>n+Math.floor(p.data.length*3/4),0)>MMS_MAX_BYTES) throw Error('MMS_TOO_LARGE');
return {recipients:message.receivers,subject:message.subject || '',parts};
}
function utf8Base64(text) {const bytes=new TextEncoder().encode(text);let s='';for(const byte of bytes)s+=String.fromCharCode(byte);return btoa(s);}
export function receivedMms(row) {
let smil='',body='';const attachments=[];
for(const part of row.parts || []) {
if(['application/smil','text/plain'].includes(part.type)) {
const text=new TextDecoder().decode(Uint8Array.from(atob(part.data),c=>c.charCodeAt(0)));
if(part.type==='application/smil') {smil=text;continue;}
body+=text;
}
attachments.push({id:part.id,location:part.id,content:{__vulpesBlobV1:true,type:part.type,base64:part.data}});
}
return {...row,body,smil,attachments,timestamp:row.timestamp || Date.now()};
}